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

Improve hasUserReadEvent and getUserReadUpTo realibility with threads #3031

Merged
merged 29 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d1db4db
Add failing test scenario when multiple receipts are in the same event
germain-gg Jan 5, 2023
03968c3
Fix cached read receipts
germain-gg Jan 4, 2023
a776ff8
Improve hasUserReadEvent and getUserReadUpTo realibility with threads
germain-gg Jan 5, 2023
23cd620
Reword code comments and improve readibility
Jan 5, 2023
1f3760f
Optimise code paths
Jan 5, 2023
be7fb61
fix getEventReadUpTo logic with unthreaded receipts
germain-gg Jan 5, 2023
0d4da28
Re-introduce optional chaining
germain-gg Jan 5, 2023
1ccf954
fixes
germain-gg Jan 5, 2023
c24f027
mend
germain-gg Jan 6, 2023
435f485
Add tests for getEventReadUpTo and hasUserReadEvent
germain-gg Jan 6, 2023
3fe9d8a
Merge branch 'develop' into gsouquet/better-getreadupto
germain-gg Jan 6, 2023
2f8328d
Reword code comments and improve readibility
Jan 6, 2023
e634807
Add comments to methods
germain-gg Jan 6, 2023
2d147d4
Make properties private and provide accessors
germain-gg Jan 6, 2023
c9827de
Remove unwanted change
germain-gg Jan 6, 2023
d1ab2de
Improve thread spec
germain-gg Jan 6, 2023
02f5a6c
Explain the unthreaded receipt logic in comments
germain-gg Jan 6, 2023
420d0c3
Merge branch 'develop' into gsouquet/better-getreadupto
Jan 6, 2023
feb8f5e
Apply comments readibility suggestions
Jan 9, 2023
5c5cbbf
Clarify code comments based on PR feedback
germain-gg Jan 9, 2023
0ee16d3
Remove unneeded nullish coalescing check
germain-gg Jan 9, 2023
889c0b4
Merge branch 'develop' into gsouquet/better-getreadupto
Jan 9, 2023
83e0b71
Merge branch 'develop' into gsouquet/better-getreadupto
Jan 10, 2023
253427d
Improve comments wording
Jan 10, 2023
4efb717
Clarify comments
germain-gg Jan 10, 2023
d2935d8
fix tests
germain-gg Jan 10, 2023
1d88910
lint fix
germain-gg Jan 10, 2023
dae36b7
Final comment wording updates
Jan 11, 2023
64ab5a0
Merge branch 'develop' into gsouquet/better-getreadupto
Jan 11, 2023
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
7 changes: 3 additions & 4 deletions spec/unit/models/thread.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe("Thread", () => {
// last threaded receipt
"$event2:localhost": {
[ReceiptType.Read]: {
[client.getUserId()!]: { ts: 300 },
[client.getUserId()!]: { ts: 300, thread_id: "$threadId" },
},
},
},
Expand All @@ -137,9 +137,9 @@ describe("Thread", () => {
authorId: myUserId,
participantUserIds: [myUserId],
length: 2,
ts: 201,
});

// The event is automatically considered read as the current user is the sender
expect(thread.hasUserReadEvent(myUserId, events.at(-1)!.getId() ?? "")).toBeTruthy();
});

Expand Down Expand Up @@ -267,8 +267,7 @@ describe("Thread", () => {
ts: 10,
});

// The 10th event has been read, as alice's last unthreaded receipt is at ts 200
// and `mkThread` increment every thread response by 1ms.
// This is marked as read as it is before alice's first threaded receipt...
expect(thread.getEventReadUpTo(myUserId)).toBe(events.at(-1)!.getId());

const { thread: thread2 } = mkThread({
Expand Down
4 changes: 2 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,9 @@ interface IJoinRequestBody {
third_party_signed?: IThirdPartySigned;
}

export interface ITagMetadata {
interface ITagMetadata {
[key: string]: any;
order?: number;
order: number;
}

interface IMessagesResponse {
Expand Down
27 changes: 24 additions & 3 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,12 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
private readonly threadNotifications = new Map<string, NotificationCount>();
public readonly cachedThreadReadReceipts = new Map<string, CachedReceiptStructure[]>();
// Useful to know at what point the current user has started using threads in this room
public oldestThreadedReceiptTs = Infinity;
// Important to compute `hasUserReadEvent` and similar methods correctly.
public unthreadedReceipts = new Map<string, Receipt>();
private oldestThreadedReceiptTs = Infinity;
/**
* Keeping a record of the lastest unthread receipts per user
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
* This is useful in determining whether a user has read a thread or not
*/
private unthreadedReceipts = new Map<string, Receipt>();
private readonly timelineSets: EventTimelineSet[];
public readonly threadsTimelineSets: EventTimelineSet[] = [];
// any filtered timeline sets we're maintaining for this room
Expand Down Expand Up @@ -3292,6 +3295,24 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}
event.applyVisibilityEvent(visibilityChange);
}

/**
* Find when a client has gained thread capabilities by inspecting the oldest
* threaded receipt
* @returns the timestamp of the oldest threaded receipt
*/
public getOldestThreadedReceiptTs(): number {
return this.oldestThreadedReceiptTs;
}

/**
* Returns the most receipt unthreaded receipt for a given user
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
* @param userId - the MxID of the User
* @returns an unthreaded Receipt
richvdh marked this conversation as resolved.
Show resolved Hide resolved
*/
public getLastUnthreadedReceiptFor(userId: string): Receipt | undefined {
return this.unthreadedReceipts.get(userId);
}
}

// a map from current event status to a list of allowed next statuses
Expand Down
46 changes: 37 additions & 9 deletions src/models/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,41 +504,69 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
throw new Error("Unsupported function on the thread model");
}

/**
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
* @param userId - The user ID to get read receipt event ID for
* @param ignoreSynthesized - If true, return only receipts that have been
* sent by the server, not implicit ones generated
* by the JS SDK.
* @returns ID of the latest event that the given user has read, or null.
*/
public getEventReadUpTo(userId: string, ignoreSynthesized?: boolean): string | null {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
const isCurrentUser = userId === this.client.getUserId();
if (isCurrentUser && this.lastReply()) {
const lastReply = this.lastReply();
if (isCurrentUser && lastReply) {
// If the last activity in a thread is prior to the first threaded read receipt
// sent in the room we want to consider this thread as read.
const beforeFirstThreadedReceipt = (this.lastReply()?.getTs() ?? 0) < this.room.oldestThreadedReceiptTs;
// sent in the room (suggesting that it was sent before the user started
// using a client that supported threaded read receipts), we want to
// consider this thread as read.
const beforeFirstThreadedReceipt = (lastReply.getTs() ?? 0) < this.room.getOldestThreadedReceiptTs();
richvdh marked this conversation as resolved.
Show resolved Hide resolved
if (beforeFirstThreadedReceipt) {
return this.timeline.at(-1)?.getId() ?? null;
richvdh marked this conversation as resolved.
Show resolved Hide resolved
}
}

const readUpToId = super.getEventReadUpTo(userId, ignoreSynthesized);

// Checking whether the unthreaded read receipt for that user is more recent
// Check whether the unthreaded read receipt for that user is more recent
// than the read receipt inside that thread.
if (this.lastReply()) {
if (!this.room.unthreadedReceipts.has(userId)) {
const unthreadedReceipt = this.room.getLastUnthreadedReceiptFor(userId);
if (!unthreadedReceipt) {
return readUpToId;
}

const unthreadedReceiptTs = this.room.unthreadedReceipts.get(userId)!.ts;
for (let i = this.timeline?.length - 1; i >= 0; --i) {
const ev = this.timeline[i];
// If we encounter the `readUpToId` we do not need to look further
// there is no "more recent" unthreaded read receipt
if (ev.getId() === readUpToId) return readUpToId;
if (ev.getTs() < unthreadedReceiptTs) return ev.getId() ?? readUpToId;

// Inspecting events from most recent to oldest, we're checking
// whether an unthreaded read receipt is more recent that the current event.
// We usually prefer relying on the order of the DAG but in this scenario
// it is not possible and we have to rely on timestamp
if (ev.getTs() < unthreadedReceipt.ts) return ev.getId() ?? readUpToId;
}
}

return readUpToId;
}

/**
* Determines if the given user has read a particular event ID with the known
* history of the room. This is not a definitive check as it relies only on
* what is available to the thread at the time of execution.
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* @param userId - The user ID to check the read state of.
* @param eventId - The event ID to check if the user read.
* @returns True if the user has read the event, false otherwise.
*/
public hasUserReadEvent(userId: string, eventId: string): boolean {
if (userId === this.client.getUserId()) {
const beforeFirstThreadedReceipt = (this.lastReply()?.getTs() ?? 0) < this.room.oldestThreadedReceiptTs;
const unthreadedReceiptTs = this.room.unthreadedReceipts.get(userId)?.ts ?? 0;
const beforeFirstThreadedReceipt =
(this.lastReply()?.getTs() ?? 0) < this.room.getOldestThreadedReceiptTs();
const unthreadedReceiptTs = this.room.getLastUnthreadedReceiptFor(userId)?.ts ?? 0;
const beforeLastUnthreadedReceipt = (this?.lastReply()?.getTs() ?? 0) < unthreadedReceiptTs;
if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) {
return true;
Expand Down