You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-29 16:43:09 +03:00
Inform the client that historical messages were imported in the timeline and they should refresh the timeline in order to see the new events. Companion `matrix-react-sdk` PR: https://github.com/matrix-org/matrix-react-sdk/pull/8354 The `marker` events are being used as state now because this way they can't be lost in a timeline gap. Regardless of when they were sent, we will still have the latest version of the state to compare against. Any time we see our latest state value change for marker events, prompt the user that the timeline needs to refresh. > In a [sync meeting with @ara4n](https://docs.google.com/document/d/1KCEmpnGr4J-I8EeaVQ8QJZKBDu53ViI7V62y5BzfXr0/edit#bookmark=id.67nio1ka8znc), we came up with the idea to make the `marker` events as state events. When the client sees that the `m.room.marker` state changed to a different event ID, it can throw away all of the timeline and re-fetch as needed. > > For homeservers where the [same problem](https://github.com/matrix-org/matrix-doc/pull/2716#discussion_r782499674) can happen, we probably don't want to throw away the whole timeline but it can go up the `unsigned.replaces_state` chain of the `m.room.marker` state events to get them all. > > In terms of state performance, there could be thousands of `marker` events in a room but it's no different than room members joining and leaving over and over like an IRC room. > > *-- https://github.com/matrix-org/matrix-spec-proposals/pull/2716#discussion_r782629097* ### Why are we just setting `timlineNeedsRefresh` (and [prompting the user](https://github.com/matrix-org/matrix-react-sdk/pull/8354)) instead of automatically refreshing the timeline for the user? If we refreshed the timeline automatically, someone could cause your Element client to constantly refresh the timeline by just sending marker events over and over. Granted, you probably want to leave a room like this 🤷. Perhaps also some sort of DOS vector since everyone will be refreshing and hitting the server at the exact same time. In order to avoid the timeline maybe going blank during the refresh, we could re-fetch the new events first, then replace the timeline. But the points above still stand on why we shouldn't.
378 lines
15 KiB
JavaScript
378 lines
15 KiB
JavaScript
import * as utils from "../test-utils/test-utils";
|
|
import { EventTimeline } from "../../src/models/event-timeline";
|
|
import { RoomState } from "../../src/models/room-state";
|
|
|
|
function mockRoomStates(timeline) {
|
|
timeline.startState = utils.mock(RoomState, "startState");
|
|
timeline.endState = utils.mock(RoomState, "endState");
|
|
}
|
|
|
|
describe("EventTimeline", function() {
|
|
const roomId = "!foo:bar";
|
|
const userA = "@alice:bar";
|
|
const userB = "@bertha:bar";
|
|
let timeline;
|
|
|
|
beforeEach(function() {
|
|
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
|
const timelineSet = { room: { roomId: roomId } };
|
|
timelineSet.room.getUnfilteredTimelineSet = function() {
|
|
return timelineSet;
|
|
};
|
|
|
|
timeline = new EventTimeline(timelineSet);
|
|
});
|
|
|
|
describe("construction", function() {
|
|
it("getRoomId should get room id", function() {
|
|
const v = timeline.getRoomId();
|
|
expect(v).toEqual(roomId);
|
|
});
|
|
});
|
|
|
|
describe("initialiseState", function() {
|
|
beforeEach(function() {
|
|
mockRoomStates(timeline);
|
|
});
|
|
|
|
it("should copy state events to start and end state", function() {
|
|
const events = [
|
|
utils.mkMembership({
|
|
room: roomId, mship: "invite", user: userB, skey: userA,
|
|
event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
type: "m.room.name", room: roomId, user: userB,
|
|
event: true,
|
|
content: { name: "New room" },
|
|
}),
|
|
];
|
|
timeline.initialiseState(events);
|
|
expect(timeline.startState.setStateEvents).toHaveBeenCalledWith(
|
|
events,
|
|
{ timelineWasEmpty: undefined },
|
|
);
|
|
expect(timeline.endState.setStateEvents).toHaveBeenCalledWith(
|
|
events,
|
|
{ timelineWasEmpty: undefined },
|
|
);
|
|
});
|
|
|
|
it("should raise an exception if called after events are added", function() {
|
|
const event =
|
|
utils.mkMessage({
|
|
room: roomId, user: userA, msg: "Adam stole the plushies",
|
|
event: true,
|
|
});
|
|
|
|
const state = [
|
|
utils.mkMembership({
|
|
room: roomId, mship: "invite", user: userB, skey: userA,
|
|
event: true,
|
|
}),
|
|
];
|
|
|
|
expect(function() {
|
|
timeline.initialiseState(state);
|
|
}).not.toThrow();
|
|
timeline.addEvent(event, { toStartOfTimeline: false });
|
|
expect(function() {
|
|
timeline.initialiseState(state);
|
|
}).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("paginationTokens", function() {
|
|
it("pagination tokens should start null", function() {
|
|
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
|
|
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
|
|
});
|
|
|
|
it("setPaginationToken should set token", function() {
|
|
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
|
|
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
|
|
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
|
|
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
|
|
});
|
|
});
|
|
|
|
describe("neighbouringTimelines", function() {
|
|
it("neighbouring timelines should start null", function() {
|
|
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
|
|
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(null);
|
|
});
|
|
|
|
it("setNeighbouringTimeline should set neighbour", function() {
|
|
const prev = { a: "a" };
|
|
const next = { b: "b" };
|
|
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
|
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
|
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
|
|
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
|
|
});
|
|
|
|
it("setNeighbouringTimeline should throw if called twice", function() {
|
|
const prev = { a: "a" };
|
|
const next = { b: "b" };
|
|
expect(function() {
|
|
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
|
}).not.toThrow();
|
|
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
|
.toBe(prev);
|
|
expect(function() {
|
|
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
|
}).toThrow();
|
|
|
|
expect(function() {
|
|
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
|
}).not.toThrow();
|
|
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
|
.toBe(next);
|
|
expect(function() {
|
|
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
|
}).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("addEvent", function() {
|
|
beforeEach(function() {
|
|
mockRoomStates(timeline);
|
|
});
|
|
|
|
const events = [
|
|
utils.mkMessage({
|
|
room: roomId, user: userA, msg: "hungry hungry hungry",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId, user: userB, msg: "nom nom nom",
|
|
event: true,
|
|
}),
|
|
];
|
|
|
|
it("should be able to add events to the end", function() {
|
|
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
|
const initialIndex = timeline.getBaseIndex();
|
|
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
|
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
|
expect(timeline.getEvents().length).toEqual(2);
|
|
expect(timeline.getEvents()[0]).toEqual(events[0]);
|
|
expect(timeline.getEvents()[1]).toEqual(events[1]);
|
|
});
|
|
|
|
it("should be able to add events to the start", function() {
|
|
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
|
const initialIndex = timeline.getBaseIndex();
|
|
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
|
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
|
|
expect(timeline.getEvents().length).toEqual(2);
|
|
expect(timeline.getEvents()[0]).toEqual(events[1]);
|
|
expect(timeline.getEvents()[1]).toEqual(events[0]);
|
|
});
|
|
|
|
it("should set event.sender for new and old events", function() {
|
|
const sentinel = {
|
|
userId: userA,
|
|
membership: "join",
|
|
name: "Alice",
|
|
};
|
|
const oldSentinel = {
|
|
userId: userA,
|
|
membership: "join",
|
|
name: "Old Alice",
|
|
};
|
|
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
|
.mockImplementation(function(uid) {
|
|
if (uid === userA) {
|
|
return sentinel;
|
|
}
|
|
return null;
|
|
});
|
|
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
|
.mockImplementation(function(uid) {
|
|
if (uid === userA) {
|
|
return oldSentinel;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const newEv = utils.mkEvent({
|
|
type: "m.room.name", room: roomId, user: userA, event: true,
|
|
content: { name: "New Room Name" },
|
|
});
|
|
const oldEv = utils.mkEvent({
|
|
type: "m.room.name", room: roomId, user: userA, event: true,
|
|
content: { name: "Old Room Name" },
|
|
});
|
|
|
|
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
|
expect(newEv.sender).toEqual(sentinel);
|
|
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
|
expect(oldEv.sender).toEqual(oldSentinel);
|
|
});
|
|
|
|
it("should set event.target for new and old m.room.member events",
|
|
function() {
|
|
const sentinel = {
|
|
userId: userA,
|
|
membership: "join",
|
|
name: "Alice",
|
|
};
|
|
const oldSentinel = {
|
|
userId: userA,
|
|
membership: "join",
|
|
name: "Old Alice",
|
|
};
|
|
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
|
.mockImplementation(function(uid) {
|
|
if (uid === userA) {
|
|
return sentinel;
|
|
}
|
|
return null;
|
|
});
|
|
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
|
.mockImplementation(function(uid) {
|
|
if (uid === userA) {
|
|
return oldSentinel;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const newEv = utils.mkMembership({
|
|
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
|
});
|
|
const oldEv = utils.mkMembership({
|
|
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
|
|
});
|
|
timeline.addEvent(newEv, { toStartOfTimeline: false });
|
|
expect(newEv.target).toEqual(sentinel);
|
|
timeline.addEvent(oldEv, { toStartOfTimeline: true });
|
|
expect(oldEv.target).toEqual(oldSentinel);
|
|
});
|
|
|
|
it("should call setStateEvents on the right RoomState with the right " +
|
|
"forwardLooking value for new events", function() {
|
|
const events = [
|
|
utils.mkMembership({
|
|
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
type: "m.room.name", room: roomId, user: userB, event: true,
|
|
content: {
|
|
name: "New room",
|
|
},
|
|
}),
|
|
];
|
|
|
|
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
|
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
|
|
|
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
|
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
|
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
|
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
|
|
|
expect(events[0].forwardLooking).toBe(true);
|
|
expect(events[1].forwardLooking).toBe(true);
|
|
|
|
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
|
not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should call setStateEvents on the right RoomState with the right " +
|
|
"forwardLooking value for old events", function() {
|
|
const events = [
|
|
utils.mkMembership({
|
|
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
type: "m.room.name", room: roomId, user: userB, event: true,
|
|
content: {
|
|
name: "New room",
|
|
},
|
|
}),
|
|
];
|
|
|
|
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
|
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
|
|
|
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
|
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
|
|
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
|
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
|
|
|
|
expect(events[0].forwardLooking).toBe(false);
|
|
expect(events[1].forwardLooking).toBe(false);
|
|
|
|
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
|
not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("Make sure legacy overload passing options directly as parameters still works", () => {
|
|
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
|
|
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState() })).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("removeEvent", function() {
|
|
const events = [
|
|
utils.mkMessage({
|
|
room: roomId, user: userA, msg: "hungry hungry hungry",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId, user: userB, msg: "nom nom nom",
|
|
event: true,
|
|
}),
|
|
utils.mkMessage({
|
|
room: roomId, user: userB, msg: "piiie",
|
|
event: true,
|
|
}),
|
|
];
|
|
|
|
it("should remove events", function() {
|
|
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
|
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
|
expect(timeline.getEvents().length).toEqual(2);
|
|
|
|
let ev = timeline.removeEvent(events[0].getId());
|
|
expect(ev).toBe(events[0]);
|
|
expect(timeline.getEvents().length).toEqual(1);
|
|
|
|
ev = timeline.removeEvent(events[1].getId());
|
|
expect(ev).toBe(events[1]);
|
|
expect(timeline.getEvents().length).toEqual(0);
|
|
});
|
|
|
|
it("should update baseIndex", function() {
|
|
timeline.addEvent(events[0], { toStartOfTimeline: false });
|
|
timeline.addEvent(events[1], { toStartOfTimeline: true });
|
|
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
|
expect(timeline.getEvents().length).toEqual(3);
|
|
expect(timeline.getBaseIndex()).toEqual(1);
|
|
|
|
timeline.removeEvent(events[2].getId());
|
|
expect(timeline.getEvents().length).toEqual(2);
|
|
expect(timeline.getBaseIndex()).toEqual(1);
|
|
|
|
timeline.removeEvent(events[1].getId());
|
|
expect(timeline.getEvents().length).toEqual(1);
|
|
expect(timeline.getBaseIndex()).toEqual(0);
|
|
});
|
|
|
|
// this is basically https://github.com/vector-im/vector-web/issues/937
|
|
// - removing the last event got baseIndex into such a state that
|
|
// further addEvent(ev, false) calls made the index increase.
|
|
it("should not make baseIndex assplode when removing the last event",
|
|
function() {
|
|
timeline.addEvent(events[0], { toStartOfTimeline: true });
|
|
timeline.removeEvent(events[0].getId());
|
|
const initialIndex = timeline.getBaseIndex();
|
|
timeline.addEvent(events[1], { toStartOfTimeline: false });
|
|
timeline.addEvent(events[2], { toStartOfTimeline: false });
|
|
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
|
expect(timeline.getEvents().length).toEqual(2);
|
|
});
|
|
});
|
|
});
|