Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
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;
};
31 changes: 28 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,9 @@ 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[]>();
public oldestRecordedThreadedReceiptTs = Infinity;
public newestRecordedUnthreadedReceiptTs = 0;
private readonly timelineSets: EventTimelineSet[];
public readonly threadsTimelineSets: EventTimelineSet[] = [];
// any filtered timeline sets we're maintaining for this room
Expand Down Expand Up @@ -2721,9 +2729,26 @@ 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 },
]);
}

// Some threads were created before MSC3771 landed. Those threads
// do not have read receipts, and this will be problematic in encrypted
// rooms where clients rely on receipts to compute highlight notifications
Comment on lines +2735 to +2738
Copy link
Member

Choose a reason for hiding this comment

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

Is this actually necessary for more than the "bold" case of thread activity?

// Having a ref to the oldest known thread receipt in this room
// means that we can opt-out of that logic for threads that have their last
// reply before
const me = this.client.getSafeUserId();
if (userId === me) {
if (!receiptForMainTimeline && receipt.ts < this.oldestRecordedThreadedReceiptTs) {
this.oldestRecordedThreadedReceiptTs = receipt.ts;
}

if (!receipt.thread_id && receipt.ts > this.newestRecordedUnthreadedReceiptTs) {
this.newestRecordedUnthreadedReceiptTs = receipt.ts;
}
}
});
});
});
Expand Down
54 changes: 40 additions & 14 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 @@ -102,6 +102,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
* with server suppport.
*/
public replayEvents: MatrixEvent[] | null = [];
public replayReceipts: CachedReceiptStructure[] = [];

public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) {
super();
Expand Down Expand Up @@ -133,7 +134,9 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.room.on(RoomEvent.LocalEchoUpdated, this.onLocalEcho);
this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent);

this.processReceipts(opts.receipts);
if (opts.receipts) {
this.replayReceipts = opts.receipts;
}

// even if this thread is thought to be originating from this client, we initialise it as we may be in a
// gappy sync and a thread around this event may already exist.
Expand Down Expand Up @@ -317,17 +320,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 @@ -398,6 +393,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.addEvent(event, false);
}
this.replayEvents = null;
this.processReceipts(this.replayReceipts);
// just to make sure that, if we've created a timeline window for this thread before the thread itself
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
Expand Down Expand Up @@ -512,8 +508,38 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
throw new Error("Unsupported function on the thread model");
}

public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
if (userId === this.client.getUserId()) {
if (this.lastReply()) {
const beforeFirstThreadedReceipt =
this?.lastReply()?.getTs() ?? 0 < this.room.oldestRecordedThreadedReceiptTs;
const beforeLastUnthreadedReceipt =
this?.lastReply()?.getTs() ?? 0 < this.room.newestRecordedUnthreadedReceiptTs;

if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) {
return this.lastReply()?.getId() ?? null;
}
}
}

return super.getEventReadUpTo(userId, ignoreSynthesized);
}

public hasUserReadEvent(userId: string, eventId: string): boolean {
if (userId === this.client.getUserId()) {
// We consider all threaded events read if they are part of a thread
// that has no activity since the first ever threaded event recorded in that room
// This prevents rooms to generated unwanted notifications for threads
// created before MSC3771
const beforeFirstThreadedReceipt =
this?.lastReply()?.getTs() ?? 0 < this.room.oldestRecordedThreadedReceiptTs;
const beforeLastUnthreadedReceipt =
this?.lastReply()?.getTs() ?? 0 < this.room.newestRecordedUnthreadedReceiptTs;

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