Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix getLatestTimeline not working when the latest event in the room is a threaded message #2521

Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules
/*.log
package-lock.json
.lock-wscript
.DS_Store
build/Release
coverage
lib-cov
Expand Down
50 changes: 40 additions & 10 deletions spec/integ/matrix-client-event-timeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ describe("MatrixClient event timelines", function() {
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy();
});

it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
it("should not include main timeline event when timelineSet is representing a thread", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true, false);
Expand All @@ -649,13 +649,40 @@ describe("MatrixClient event timelines", function() {
};
});

return Promise.all([
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id)).resolves.toBeUndefined(),
httpBackend.flushAllExpected(),
]);
// getEventTimeline -> thread.fetchInitialEvents
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
Copy link
Contributor Author

@MadLittleMods MadLittleMods Aug 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some lints in CI: https://github.com/matrix-org/matrix-js-sdk/runs/8077711103?check_suite_focus=true

But I don't see them locally even after re-installing node_modules to ensure correct versions.

$ yarn lint
yarn run v1.22.18
$ yarn lint:types && yarn lint:js
$ tsc --noEmit
$ eslint --max-warnings 0 src spec
✨  Done in 21.60s.

(on the correct branch, madlittlemods/refresh-timeline-when-we-see-msc2716-marker-events-v2)


This is also the same pattern we use in the existing tests here but don't appear because they're not in the diff so will need to refactor this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are shown if you use tsc --strict - hence being reported by the Typescript Strict Error Checker

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably merge develop to resolve all of these unrelated errors now that #2835 fixed them up

encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20&direction=b")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
// no next batch as this is the oldest end of the timeline
};
});

// getEventTimeline -> thread.fetchEvents
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?direction=b&limit=50")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
// no next batch as this is the oldest end of the timeline
};
});

const timelinePromise = client.getEventTimeline(timelineSet, EVENTS[0].event_id);
await httpBackend.flushAllExpected();

const timeline = await timelinePromise;

// The main timeline event should not be in the timelineSet representing a thread
expect(timeline.getEvents().find(e => e.getId() === EVENTS[0].event_id)).toBeFalsy();
});

it("should return undefined when event is within a thread but timelineSet is not", () => {
it("should not include threaded reply when timelineSet is representing the main room", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true, false);
Expand All @@ -675,10 +702,13 @@ describe("MatrixClient event timelines", function() {
};
});

return Promise.all([
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id)).resolves.toBeUndefined(),
httpBackend.flushAllExpected(),
]);
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
await httpBackend.flushAllExpected();

const timeline = await timelinePromise;

// The threaded reply should not be in a main room timeline
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeFalsy();
});

it("should should add lazy loading filter when requested", async () => {
Expand Down
2 changes: 1 addition & 1 deletion spec/integ/matrix-client-room-timeline.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ describe("MatrixClient room timelines", function() {
});

// Wait for the timeline to reset(when it goes blank) which means
// it's in the middle of the refrsh logic right before the
// it's in the middle of the refresh logic right before the
// `getEventTimeline()` -> `/context`. Then simulate a racey `/sync`
// to happen in the middle of all of this refresh timeline logic. We
// want to make sure the sync pagination still works as expected
Expand Down
76 changes: 59 additions & 17 deletions spec/unit/event-timeline-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ describe('EventTimelineSet', () => {
});
};

const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}, room.client);

beforeEach(() => {
client = utils.mock(MatrixClient, 'MatrixClient');
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
Expand Down Expand Up @@ -116,6 +133,13 @@ describe('EventTimelineSet', () => {
});

describe('addEventToTimeline', () => {
let thread: Thread;

beforeEach(() => {
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});

it("Adds event to timeline", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
Expand Down Expand Up @@ -143,6 +167,41 @@ describe('EventTimelineSet', () => {
);
}).not.toThrow();
});

it("should not add an event to a timeline that does not belong to the timelineSet", () => {
const eventTimelineSet2 = new EventTimelineSet(room);
const liveTimeline2 = eventTimelineSet2.getLiveTimeline();
expect(liveTimeline2.getEvents().length).toStrictEqual(0);

expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
toStartOfTimeline: true,
});
}).toThrowError();
});

it("should not add a threaded reply to the main room timeline", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);

const threadedReplyEvent = mkThreadResponse(messageEvent);

eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});

it("should not add a normal message to the timelineSet representing a thread", () => {
const eventTimelineSetForThread = new EventTimelineSet(room, {}, client, thread);
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);

eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
Comment on lines +184 to +205
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tests the two main mixing scenarios. We could add tests for the other mixing scenarios but we're using canContain which already has it's own set of tests,

describe("canContain", () => {
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}, room.client);
let thread: Thread;
beforeEach(() => {
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
it("should throw if timeline set has no room", () => {
const eventTimelineSet = new EventTimelineSet(undefined, {}, client);
expect(() => eventTimelineSet.canContain(messageEvent)).toThrowError();
});
it("should return false if timeline set is for thread but event is not threaded", () => {
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
expect(eventTimelineSet.canContain(replyEvent)).toBeFalsy();
});
it("should return false if timeline set it for thread but event it for a different thread", () => {
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
const event = mkThreadResponse(replyEvent);
expect(eventTimelineSet.canContain(event)).toBeFalsy();
});
it("should return false if timeline set is not for a thread but event is a thread response", () => {
const eventTimelineSet = new EventTimelineSet(room, {}, client);
const event = mkThreadResponse(replyEvent);
expect(eventTimelineSet.canContain(event)).toBeFalsy();
});
it("should return true if the timeline set is not for a thread and the event is a thread root", () => {
const eventTimelineSet = new EventTimelineSet(room, {}, client);
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
});
it("should return true if the timeline set is for a thread and the event is its thread root", () => {
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
messageEvent.setThread(thread);
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
});
it("should return true if the timeline set is for a thread and the event is a response to it", () => {
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
messageEvent.setThread(thread);
const event = mkThreadResponse(messageEvent);
expect(eventTimelineSet.canContain(event)).toBeTruthy();
});
});

});

describe('aggregateRelations', () => {
Expand Down Expand Up @@ -225,23 +284,6 @@ describe('EventTimelineSet', () => {
});

describe("canContain", () => {
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}, room.client);

let thread: Thread;

beforeEach(() => {
Expand Down
4 changes: 0 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5288,10 +5288,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
];

if (this.supportsExperimentalThreads()) {
if (!timelineSet.canContain(event)) {
return undefined;
}
Comment on lines -5213 to -5215
Copy link
Contributor Author

@MadLittleMods MadLittleMods Jul 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of returning undefined, we now just ignore the event and don't add it to the given timelineSet.

Did returning undefined have some other special meaning? The getEventTimeline usage didn't stand out to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did returning undefined have some other special meaning? The getEventTimeline usage didn't stand out to me.

I don't think it has any special meaning, see #2521 (comment)

Comment on lines -5213 to -5215
Copy link
Contributor Author

@MadLittleMods MadLittleMods Jul 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to make sure we're not regressing #2444 and #2454 for sure, do we have a specific testing strategy in the Element app to reproduce previously (main timeline events in a thread for example)? How do we know that those previous PR's fixed the problem besides reasoning about it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@t3chguy Any hints here?


// Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
// functions contiguously, so we have to jump through some hoops to get our target event in it.
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
Expand Down
12 changes: 12 additions & 0 deletions src/models/event-timeline-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,18 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
);
}

if (timeline.getTimelineSet() !== this) {
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
"in timelineSet(threadId=${this.thread?.id})`);
}

// Make sure events don't get mixed in timelines they shouldn't be in
// (e.g. a threaded message should not be in the main timeline).
if (!this.canContain(event)) {
logger.warn(`EventTimelineSet.addEventToTimeline: Ignoring event=${event.getId()} that does not belong in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`);
return;
}

const eventId = event.getId();
timeline.addEvent(event, {
toStartOfTimeline,
Expand Down
4 changes: 4 additions & 0 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId());
}

if (!newTimeline) {
throw new Error(`[refreshLiveTimeline for ${this.roomId}] No new timeline created`);
}

// If a racing `/sync` beat us to creating a new timeline, use that
// instead because it's the latest in the room and any new messages in
// the scrollback will include the history.
Expand Down