Skip to content

Commit 1e041a2

Browse files
authored
Add Room.getLastLiveEvent and Room.getLastThread (#3321)
* Add room.getLastLiveEvent and remove room.lastThread * Deprecate Room.lastThread * Add comments about timestamps * Improve lastThread prop doc * Simplify test structure
1 parent 1631d6f commit 1e041a2

File tree

2 files changed

+182
-3
lines changed

2 files changed

+182
-3
lines changed

spec/unit/room.spec.ts

+129-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ limitations under the License.
1919
*/
2020

2121
import { mocked } from "jest-mock";
22-
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, PollStartEvent } from "matrix-events-sdk";
22+
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, Optional, PollStartEvent } from "matrix-events-sdk";
2323

2424
import * as utils from "../test-utils/test-utils";
2525
import { emitPromise } from "../test-utils/test-utils";
@@ -54,6 +54,7 @@ import { Crypto } from "../../src/crypto";
5454
import { mkThread } from "../test-utils/thread";
5555
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
5656
import { logger } from "../../src/logger";
57+
import { IMessageOpts } from "../test-utils/test-utils";
5758

5859
describe("Room", function () {
5960
const roomId = "!foo:bar";
@@ -63,9 +64,10 @@ describe("Room", function () {
6364
const userD = "@dorothy:bar";
6465
let room: Room;
6566

66-
const mkMessage = () =>
67+
const mkMessage = (opts?: Partial<IMessageOpts>) =>
6768
utils.mkMessage(
6869
{
70+
...opts,
6971
event: true,
7072
user: userA,
7173
room: roomId,
@@ -113,9 +115,10 @@ describe("Room", function () {
113115
room.client,
114116
);
115117

116-
const mkThreadResponse = (root: MatrixEvent) =>
118+
const mkThreadResponse = (root: MatrixEvent, opts?: Partial<IMessageOpts>) =>
117119
utils.mkEvent(
118120
{
121+
...opts,
119122
event: true,
120123
type: EventType.RoomMessage,
121124
user: userA,
@@ -165,6 +168,66 @@ describe("Room", function () {
165168
room.client,
166169
);
167170

171+
const addRoomMainAndThreadMessages = (
172+
room: Room,
173+
tsMain?: number,
174+
tsThread?: number,
175+
): { mainEvent?: MatrixEvent; threadEvent?: MatrixEvent } => {
176+
const result: { mainEvent?: MatrixEvent; threadEvent?: MatrixEvent } = {};
177+
178+
if (tsMain) {
179+
result.mainEvent = mkMessage({ ts: tsMain });
180+
room.addLiveEvents([result.mainEvent]);
181+
}
182+
183+
if (tsThread) {
184+
const { rootEvent, thread } = mkThread({
185+
room,
186+
client: new TestClient().client,
187+
authorId: "@bob:example.org",
188+
participantUserIds: ["@bob:example.org"],
189+
});
190+
result.threadEvent = mkThreadResponse(rootEvent, { ts: tsThread });
191+
thread.liveTimeline.addEvent(result.threadEvent, { toStartOfTimeline: true });
192+
}
193+
194+
return result;
195+
};
196+
197+
const addRoomThreads = (
198+
room: Room,
199+
thread1EventTs: Optional<number>,
200+
thread2EventTs: Optional<number>,
201+
): { thread1?: Thread; thread2?: Thread } => {
202+
const result: { thread1?: Thread; thread2?: Thread } = {};
203+
204+
if (thread1EventTs !== null) {
205+
const { rootEvent: thread1RootEvent, thread: thread1 } = mkThread({
206+
room,
207+
client: new TestClient().client,
208+
authorId: "@bob:example.org",
209+
participantUserIds: ["@bob:example.org"],
210+
});
211+
const thread1Event = mkThreadResponse(thread1RootEvent, { ts: thread1EventTs });
212+
thread1.liveTimeline.addEvent(thread1Event, { toStartOfTimeline: true });
213+
result.thread1 = thread1;
214+
}
215+
216+
if (thread2EventTs !== null) {
217+
const { rootEvent: thread2RootEvent, thread: thread2 } = mkThread({
218+
room,
219+
client: new TestClient().client,
220+
authorId: "@bob:example.org",
221+
participantUserIds: ["@bob:example.org"],
222+
});
223+
const thread2Event = mkThreadResponse(thread2RootEvent, { ts: thread2EventTs });
224+
thread2.liveTimeline.addEvent(thread2Event, { toStartOfTimeline: true });
225+
result.thread2 = thread2;
226+
}
227+
228+
return result;
229+
};
230+
168231
beforeEach(function () {
169232
room = new Room(roomId, new TestClient(userA, "device").client, userA);
170233
// mock RoomStates
@@ -3475,4 +3538,67 @@ describe("Room", function () {
34753538
expect(room.findPredecessor()).toBeNull();
34763539
});
34773540
});
3541+
3542+
describe("getLastLiveEvent", () => {
3543+
let lastEventInMainTimeline: MatrixEvent;
3544+
let lastEventInThread: MatrixEvent;
3545+
3546+
it("when there are no events, it should return undefined", () => {
3547+
expect(room.getLastLiveEvent()).toBeUndefined();
3548+
});
3549+
3550+
it("when there is only an event in the main timeline and there are no threads, it should return the last event from the main timeline", () => {
3551+
lastEventInMainTimeline = addRoomMainAndThreadMessages(room, 23).mainEvent!;
3552+
room.addLiveEvents([lastEventInMainTimeline]);
3553+
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
3554+
});
3555+
3556+
it("when there is no event in the room live timeline but in a thread, it should return the last event from the thread", () => {
3557+
lastEventInThread = addRoomMainAndThreadMessages(room, undefined, 42).threadEvent!;
3558+
expect(room.getLastLiveEvent()).toBe(lastEventInThread);
3559+
});
3560+
3561+
describe("when there are events in both, the main timeline and threads", () => {
3562+
it("and the last event is in a thread, it should return the last event from the thread", () => {
3563+
lastEventInThread = addRoomMainAndThreadMessages(room, 23, 42).threadEvent!;
3564+
expect(room.getLastLiveEvent()).toBe(lastEventInThread);
3565+
});
3566+
3567+
it("and the last event is in the main timeline, it should return the last event from the main timeline", () => {
3568+
lastEventInMainTimeline = addRoomMainAndThreadMessages(room, 42, 23).mainEvent!;
3569+
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
3570+
});
3571+
});
3572+
});
3573+
3574+
describe("getLastThread", () => {
3575+
it("when there is no thread, it should return undefined", () => {
3576+
expect(room.getLastThread()).toBeUndefined();
3577+
});
3578+
3579+
it("when there is only one thread, it should return this one", () => {
3580+
const { thread1 } = addRoomThreads(room, 23, null);
3581+
expect(room.getLastThread()).toBe(thread1);
3582+
});
3583+
3584+
it("when there are tho threads, it should return the one with the recent event I", () => {
3585+
const { thread2 } = addRoomThreads(room, 23, 42);
3586+
expect(room.getLastThread()).toBe(thread2);
3587+
});
3588+
3589+
it("when there are tho threads, it should return the one with the recent event II", () => {
3590+
const { thread1 } = addRoomThreads(room, 42, 23);
3591+
expect(room.getLastThread()).toBe(thread1);
3592+
});
3593+
3594+
it("when there is a thread with the last event ts undefined, it should return the thread with the defined event ts", () => {
3595+
const { thread2 } = addRoomThreads(room, undefined, 23);
3596+
expect(room.getLastThread()).toBe(thread2);
3597+
});
3598+
3599+
it("when the last event ts of all threads is undefined, it should return the last added thread", () => {
3600+
const { thread2 } = addRoomThreads(room, undefined, undefined);
3601+
expect(room.getLastThread()).toBe(thread2);
3602+
});
3603+
});
34783604
});

src/models/room.ts

+53
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
396396
* This is not a comprehensive list of the threads that exist in this room
397397
*/
398398
private threads = new Map<string, Thread>();
399+
400+
/**
401+
* @deprecated This value is unreliable. It may not contain the last thread.
402+
* Use {@link Room.getLastThread} instead.
403+
*/
399404
public lastThread?: Thread;
400405

401406
/**
@@ -785,6 +790,54 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
785790
}
786791
}
787792

793+
/**
794+
* Returns the last live event of this room.
795+
* "last" means latest timestamp.
796+
* Instead of using timestamps, it would be better to do the comparison based on the order of the homeserver DAG.
797+
* Unfortunately, this information is currently not available in the client.
798+
* See {@link https://github.com/matrix-org/matrix-js-sdk/issues/3325}.
799+
* "live of this room" means from all live timelines: the room and the threads.
800+
*
801+
* @returns MatrixEvent if there is a last event; else undefined.
802+
*/
803+
public getLastLiveEvent(): MatrixEvent | undefined {
804+
const roomEvents = this.getLiveTimeline().getEvents();
805+
const lastRoomEvent = roomEvents[roomEvents.length - 1] as MatrixEvent | undefined;
806+
const lastThread = this.getLastThread();
807+
808+
if (!lastThread) return lastRoomEvent;
809+
810+
const lastThreadEvent = lastThread.events[lastThread.events.length - 1];
811+
812+
return (lastRoomEvent?.getTs() ?? 0) > (lastThreadEvent.getTs() ?? 0) ? lastRoomEvent : lastThreadEvent;
813+
}
814+
815+
/**
816+
* Returns the last thread of this room.
817+
* "last" means latest timestamp of the last thread event.
818+
* Instead of using timestamps, it would be better to do the comparison based on the order of the homeserver DAG.
819+
* Unfortunately, this information is currently not available in the client.
820+
* See {@link https://github.com/matrix-org/matrix-js-sdk/issues/3325}.
821+
*
822+
* @returns the thread with the most recent event in its live time line. undefined if there is no thread.
823+
*/
824+
public getLastThread(): Thread | undefined {
825+
return this.getThreads().reduce<Thread | undefined>((lastThread: Thread | undefined, thread: Thread) => {
826+
if (!lastThread) return thread;
827+
828+
const threadEvent = thread.events[thread.events.length - 1];
829+
const lastThreadEvent = lastThread.events[lastThread.events.length - 1];
830+
831+
if ((threadEvent?.getTs() ?? 0) >= (lastThreadEvent?.getTs() ?? 0)) {
832+
// Last message of current thread is newer → new last thread.
833+
// Equal also means newer, because it was added to the thread map later.
834+
return thread;
835+
}
836+
837+
return lastThread;
838+
}, undefined);
839+
}
840+
788841
/**
789842
* @returns the membership type (join | leave | invite) for the logged in user
790843
*/

0 commit comments

Comments
 (0)