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 3 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
46 changes: 46 additions & 0 deletions spec/integ/matrix-client-syncing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,52 @@ describe("MatrixClient syncing", () => {
});
});
});

it("only replays receipts relevant to the current context", async () => {
const THREAD_ID = "$unknownthread:localhost";

const receipt = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 666, thread_id: THREAD_ID },
},
},
"$otherevent:localhost": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 999, thread_id: "$otherthread:localhost" },
},
},
},
};
syncData.rooms.join[roomOne].ephemeral.events = [receipt];

httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();

return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
const room = client?.getRoom(roomOne);
expect(room).toBeInstanceOf(Room);

expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true);

const thread = room!.createThread(THREAD_ID, undefined, [], true);

expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false);

const receipt = thread.getReadReceiptForUserId("@alice:localhost");

expect(receipt).toStrictEqual({
data: {
thread_id: "$unknownthread:localhost",
ts: 666,
},
eventId: "$event1:localhost",
});
});
});
});

describe("of a room", () => {
Expand Down
8 changes: 8 additions & 0 deletions src/@types/read_receipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,11 @@ export type Receipts = {
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
};
};

export type CachedReceiptStructure = {
eventId: string;
receiptType: string | ReceiptType;
userId: string;
receipt: Receipt;
synthetic: boolean;
};
9 changes: 2 additions & 7 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4840,13 +4840,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$eventId: event.getId()!,
});

const supportsThreadRR = this.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
if (supportsThreadRR && !unthreaded) {
const isThread = !!event.threadRootId;
body = {
...body,
thread_id: isThread ? event.threadRootId : MAIN_ROOM_TIMELINE,
};
if (this.supportsExperimentalThreads() && !unthreaded) {
body.thread_id = event.threadRootId ? event.threadRootId : MAIN_ROOM_TIMELINE;
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
}

const promise = this.http.authedRequest<{}>(Method.Post, path, undefined, body || {});
Expand Down
25 changes: 22 additions & 3 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ import {
FILTER_RELATED_BY_SENDERS,
ThreadFilterType,
} from "./thread";
import { MAIN_ROOM_TIMELINE, Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts";
import {
CachedReceiptStructure,
MAIN_ROOM_TIMELINE,
Receipt,
ReceiptContent,
ReceiptType,
} from "../@types/read_receipts";
import { IStateEventWithRoomId } from "../@types/search";
import { RelationsContainer } from "./relations-container";
import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
Expand Down Expand Up @@ -302,7 +308,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
private notificationCounts: NotificationCount = {};
private readonly threadNotifications = new Map<string, NotificationCount>();
public readonly cachedThreadReadReceipts = new Map<string, { event: MatrixEvent; synthetic: boolean }[]>();
public readonly cachedThreadReadReceipts = new Map<string, CachedReceiptStructure[]>();
// Useful to know at what point the current user has started using thread in this room
public oldestThreadedReceiptTs = Infinity;
// Important to compute compute `hasUserReadEvent` and similar method correctly
public unthreadedReceipts = new Map<string, Receipt>();
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
private readonly timelineSets: EventTimelineSet[];
public readonly threadsTimelineSets: EventTimelineSet[] = [];
// any filtered timeline sets we're maintaining for this room
Expand Down Expand Up @@ -2721,9 +2731,18 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// when the thread is created
this.cachedThreadReadReceipts.set(receipt.thread_id!, [
...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []),
{ event, synthetic },
{ eventId, receiptType, userId, receipt, synthetic },
]);
}

const me = this.client.getSafeUserId();
if (userId === me && !receiptForMainTimeline && receipt.ts < this.oldestThreadedReceiptTs) {
this.oldestThreadedReceiptTs = receipt.ts;
}
germain-gg marked this conversation as resolved.
Show resolved Hide resolved

if (!receipt.thread_id && receipt.ts > (this.unthreadedReceipts.get(userId)?.ts ?? 0)) {
this.unthreadedReceipts.set(userId, receipt);
}
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
Expand Down
52 changes: 39 additions & 13 deletions src/models/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { RoomState } from "./room-state";
import { ServerControlledNamespacedValue } from "../NamespacedValue";
import { logger } from "../logger";
import { ReadReceipt } from "./read-receipt";
import { Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts";
import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";

export enum ThreadEvent {
New = "Thread.new",
Expand All @@ -50,7 +50,7 @@ interface IThreadOpts {
room: Room;
client: MatrixClient;
pendingEventOrdering?: PendingEventOrdering;
receipts?: { event: MatrixEvent; synthetic: boolean }[];
receipts?: CachedReceiptStructure[];
}

export enum FeatureSupport {
Expand Down Expand Up @@ -317,17 +317,9 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
* and apply them to the current thread
* @param receipts - A collection of the receipts cached from initial sync
*/
private processReceipts(receipts: { event: MatrixEvent; synthetic: boolean }[] = []): void {
for (const { event, synthetic } of receipts) {
const content = event.getContent<ReceiptContent>();
Object.keys(content).forEach((eventId: string) => {
Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => {
Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
const receipt = content[eventId][receiptType][userId] as Receipt;
this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic);
});
});
});
private processReceipts(receipts: CachedReceiptStructure[] = []): void {
for (const { eventId, receiptType, userId, receipt, synthetic } of receipts) {
this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic);
}
}

Expand Down Expand Up @@ -512,8 +504,42 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
throw new Error("Unsupported function on the thread model");
}

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()) {
// If a thread last activity is prior the first read receipt sent in a thread
// we want to consider that this thread has been read
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
const beforeFirstThreadedReceipt = (this.lastReply()?.getTs() ?? 0) < this.room.oldestThreadedReceiptTs;
germain-gg 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
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
// than the read receipt inside that thread.
if (isCurrentUser && this.lastReply()) {
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
const unthreadedReceiptTs = this.room.unthreadedReceipts.get(userId)?.ts ?? Infinity;
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
for (let i = this.timeline?.length - 1; i >= 0; --i) {
const ev = this.timeline[i];
if (ev.getTs() > unthreadedReceiptTs) return ev.getId() ?? readUpToId;
if (ev.getId() === readUpToId) return readUpToId;
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
}
}

return readUpToId;
}

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 beforeLastUnthreadedReceipt = (this?.lastReply()?.getTs() ?? 0) < unthreadedReceiptTs;
richvdh marked this conversation as resolved.
Show resolved Hide resolved
if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) {
return true;
}

const publicReadReceipt = this.getReadReceiptForUserId(userId, false, ReceiptType.Read);
const privateReadReceipt = this.getReadReceiptForUserId(userId, false, ReceiptType.ReadPrivate);
const hasUnreads = this.room.getThreadUnreadNotificationCount(this.id, NotificationCountType.Total) > 0;
Expand Down