Skip to content

Commit c1c5984

Browse files
author
Germain Souquet
committed
Merge branch 'gsouquet/threads-rr-poc' into gsouquet/threads-notifications-poc
2 parents d5116d1 + e2fd2de commit c1c5984

File tree

10 files changed

+411
-184
lines changed

10 files changed

+411
-184
lines changed

spec/unit/room.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ import {
3232
RoomEvent,
3333
} from "../../src";
3434
import { EventTimeline } from "../../src/models/event-timeline";
35-
import { IWrappedReceipt, Room } from "../../src/models/room";
35+
import { Room } from "../../src/models/room";
3636
import { RoomState } from "../../src/models/room-state";
3737
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
3838
import { TestClient } from "../TestClient";
3939
import { emitPromise } from "../test-utils/test-utils";
4040
import { ReceiptType } from "../../src/@types/read_receipts";
4141
import { Thread, ThreadEvent } from "../../src/models/thread";
42+
import { WrappedReceipt } from "../../src/models/timeline-receipts";
4243

4344
describe("Room", function() {
4445
const roomId = "!foo:bar";
@@ -2429,7 +2430,7 @@ describe("Room", function() {
24292430

24302431
it("handles missing receipt type", () => {
24312432
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
2432-
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null;
2433+
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as WrappedReceipt : null;
24332434
};
24342435

24352436
expect(room.getEventReadUpTo(userA)).toEqual("eventId");

src/client.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4584,11 +4584,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
45844584
return Promise.resolve({}); // guests cannot send receipts so don't bother.
45854585
}
45864586

4587-
const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
4587+
let path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
45884588
$roomId: event.getRoomId(),
45894589
$receiptType: receiptType,
45904590
$eventId: event.getId(),
45914591
});
4592+
4593+
const isThread = !!event.threadRootId;
4594+
if (isThread) {
4595+
path += utils.encodeUri("/$threadId", {
4596+
$threadId: event.threadRootId,
4597+
});
4598+
}
4599+
45924600
const promise = this.http.authedRequest(callback, Method.Post, path, undefined, body || {});
45934601

45944602
const room = this.getRoom(event.getRoomId());
@@ -4607,6 +4615,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
46074615
* @return {module:http-api.MatrixError} Rejects: with an error response.
46084616
*/
46094617
public async sendReadReceipt(event: MatrixEvent, receiptType = ReceiptType.Read, callback?: Callback): Promise<{}> {
4618+
if (!event) return;
46104619
const eventId = event.getId();
46114620
const room = this.getRoom(event.getRoomId());
46124621
if (room && room.hasPendingEvent(eventId)) {

src/http-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ export class MatrixHttpApi {
390390
};
391391

392392
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
393-
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
393+
let timeoutTimer = callbacks.setTimeout(timeoutFn, 60000);
394394

395395
xhr.onreadystatechange = function() {
396396
let resp: string;
@@ -421,7 +421,7 @@ export class MatrixHttpApi {
421421
callbacks.clearTimeout(timeoutTimer);
422422
upload.loaded = ev.loaded;
423423
upload.total = ev.total;
424-
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
424+
timeoutTimer = callbacks.setTimeout(timeoutFn, 60000);
425425
if (opts.progressHandler) {
426426
opts.progressHandler({
427427
loaded: ev.loaded,

src/models/room-member.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
221221
* @fires module:client~MatrixClient#event:"RoomMember.typing"
222222
*/
223223
public setTypingEvent(event: MatrixEvent): void {
224-
if (event.getType() !== "m.typing") {
224+
if (event.getType() !== EventType.Typing) {
225225
return;
226226
}
227227
const oldTyping = this.typing;

src/models/room.ts

Lines changed: 19 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ import {
4646
FILTER_RELATED_BY_SENDERS,
4747
ThreadFilterType,
4848
} from "./thread";
49-
import { TypedEventEmitter } from "./typed-event-emitter";
5049
import { ReceiptType } from "../@types/read_receipts";
5150
import { IStateEventWithRoomId } from "../@types/search";
5251
import { RelationsContainer } from "./relations-container";
52+
import { ReceiptContent, synthesizeReceipt, TimelineReceipts } from "./timeline-receipts";
5353

5454
// These constants are used as sane defaults when the homeserver doesn't support
5555
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -60,23 +60,6 @@ import { RelationsContainer } from "./relations-container";
6060
export const KNOWN_SAFE_ROOM_VERSION = '9';
6161
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
6262

63-
function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
64-
// console.log("synthesizing receipt for "+event.getId());
65-
return new MatrixEvent({
66-
content: {
67-
[event.getId()]: {
68-
[receiptType]: {
69-
[userId]: {
70-
ts: event.getTs(),
71-
},
72-
},
73-
},
74-
},
75-
type: EventType.Receipt,
76-
room_id: event.getRoomId(),
77-
});
78-
}
79-
8063
interface IOpts {
8164
storageToken?: string;
8265
pendingEventOrdering?: PendingEventOrdering;
@@ -90,40 +73,6 @@ export interface IRecommendedVersion {
9073
urgent: boolean;
9174
}
9275

93-
interface IReceipt {
94-
ts: number;
95-
}
96-
97-
export interface IWrappedReceipt {
98-
eventId: string;
99-
data: IReceipt;
100-
}
101-
102-
interface ICachedReceipt {
103-
type: ReceiptType;
104-
userId: string;
105-
data: IReceipt;
106-
}
107-
108-
type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
109-
110-
interface IReceiptContent {
111-
[eventId: string]: {
112-
[key in ReceiptType]: {
113-
[userId: string]: IReceipt;
114-
};
115-
};
116-
}
117-
118-
const ReceiptPairRealIndex = 0;
119-
const ReceiptPairSyntheticIndex = 1;
120-
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
121-
type Receipts = {
122-
[receiptType: string]: {
123-
[userId: string]: [IWrappedReceipt, IWrappedReceipt]; // Pair<real receipt, synthetic receipt> (both nullable)
124-
};
125-
};
126-
12776
// When inserting a visibility event affecting event `eventId`, we
12877
// need to scan through existing visibility events for `eventId`.
12978
// In theory, this could take an unlimited amount of time if:
@@ -212,12 +161,6 @@ type NotificationCount = Partial<Record<NotificationCountType, number>>;
212161
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
213162
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
214163
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
215-
// receipts should clobber based on receipt_type and user_id pairs hence
216-
// the form of this structure. This is sub-optimal for the exposed APIs
217-
// which pass in an event ID and get back some receipts, so we also store
218-
// a pre-cached list for this purpose.
219-
private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } }
220-
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] }
221164
private notificationCounts: NotificationCount = {};
222165
private threadNotifications: Record<string, NotificationCount> = {};
223166
private readonly timelineSets: EventTimelineSet[];
@@ -2622,7 +2565,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
26222565

26232566
let latest = privateReadReceipt;
26242567
[unstablePrivateReadReceipt, publicReadReceipt].forEach((receipt) => {
2625-
if (receipt?.data?.ts > latest?.data?.ts || !latest) {
2568+
if (receipt?.data?.ts > latest?.data?.ts) {
26262569
latest = receipt;
26272570
}
26282571
});
@@ -2688,123 +2631,28 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
26882631
* @param {Boolean} synthetic True if this event is implicit.
26892632
*/
26902633
public addReceipt(event: MatrixEvent, synthetic = false): void {
2691-
this.addReceiptsToStructure(event, synthetic);
2692-
// send events after we've regenerated the structure & cache, otherwise things that
2693-
// listened for the event would read stale data.
2694-
this.emit(RoomEvent.Receipt, event, this);
2695-
}
2696-
2697-
/**
2698-
* Add a receipt event to the room.
2699-
* @param {MatrixEvent} event The m.receipt event.
2700-
* @param {Boolean} synthetic True if this event is implicit.
2701-
*/
2702-
private addReceiptsToStructure(event: MatrixEvent, synthetic: boolean): void {
2703-
const content = event.getContent<IReceiptContent>();
2704-
Object.keys(content).forEach((eventId) => {
2705-
Object.keys(content[eventId]).forEach((receiptType) => {
2706-
Object.keys(content[eventId][receiptType]).forEach((userId) => {
2707-
const receipt = content[eventId][receiptType][userId];
2708-
2709-
if (!this.receipts[receiptType]) {
2710-
this.receipts[receiptType] = {};
2711-
}
2712-
if (!this.receipts[receiptType][userId]) {
2713-
this.receipts[receiptType][userId] = [null, null];
2714-
}
2715-
2716-
const pair = this.receipts[receiptType][userId];
2717-
2718-
let existingReceipt = pair[ReceiptPairRealIndex];
2719-
if (synthetic) {
2720-
existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
2721-
}
2722-
2723-
if (existingReceipt) {
2724-
// we only want to add this receipt if we think it is later than the one we already have.
2725-
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
2726-
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
2727-
existingReceipt.eventId,
2728-
eventId,
2729-
);
2730-
if (ordering !== null && ordering >= 0) {
2731-
return;
2732-
}
2733-
}
2734-
2735-
const wrappedReceipt: IWrappedReceipt = {
2634+
const content = event.getContent<ReceiptContent>();
2635+
Object.keys(content).forEach((eventId: string) => {
2636+
Object.keys(content[eventId]).forEach((receiptType: ReceiptType) => {
2637+
Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
2638+
// hack, threadId should be thread_id
2639+
const receipt = content[eventId][receiptType][userId] as any;
2640+
2641+
const receiptDestination = this.threads.get(receipt.thread_id) ?? this;
2642+
receiptDestination.addReceiptToStructure(
27362643
eventId,
2737-
data: receipt,
2738-
};
2739-
2740-
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
2741-
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
2742-
2743-
let ordering: number | null = null;
2744-
if (realReceipt && syntheticReceipt) {
2745-
ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
2746-
realReceipt.eventId,
2747-
syntheticReceipt.eventId,
2748-
);
2749-
}
2750-
2751-
const preferSynthetic = ordering === null || ordering < 0;
2752-
2753-
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
2754-
// Take the current cached receipt before we overwrite the pair elements.
2755-
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
2756-
2757-
if (synthetic && preferSynthetic) {
2758-
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
2759-
} else if (!synthetic) {
2760-
pair[ReceiptPairRealIndex] = wrappedReceipt;
2761-
2762-
if (!preferSynthetic) {
2763-
pair[ReceiptPairSyntheticIndex] = null;
2764-
}
2765-
}
2766-
2767-
const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
2768-
if (cachedReceipt === newCachedReceipt) return;
2769-
2770-
// clean up any previous cache entry
2771-
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
2772-
const previousEventId = cachedReceipt.eventId;
2773-
// Remove the receipt we're about to clobber out of existence from the cache
2774-
this.receiptCacheByEventId[previousEventId] = (
2775-
this.receiptCacheByEventId[previousEventId].filter(r => {
2776-
return r.type !== receiptType || r.userId !== userId;
2777-
})
2778-
);
2779-
2780-
if (this.receiptCacheByEventId[previousEventId].length < 1) {
2781-
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
2782-
}
2783-
}
2784-
2785-
// cache the new one
2786-
if (!this.receiptCacheByEventId[eventId]) {
2787-
this.receiptCacheByEventId[eventId] = [];
2788-
}
2789-
this.receiptCacheByEventId[eventId].push({
2790-
userId: userId,
2791-
type: receiptType as ReceiptType,
2792-
data: receipt,
2793-
});
2644+
receiptType,
2645+
userId,
2646+
receipt,
2647+
synthetic,
2648+
);
27942649
});
27952650
});
27962651
});
2797-
}
27982652

2799-
/**
2800-
* Add a temporary local-echo receipt to the room to reflect in the
2801-
* client the fact that we've sent one.
2802-
* @param {string} userId The user ID if the receipt sender
2803-
* @param {MatrixEvent} e The event that is to be acknowledged
2804-
* @param {ReceiptType} receiptType The type of receipt
2805-
*/
2806-
public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void {
2807-
this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
2653+
// send events after we've regenerated the structure & cache, otherwise things that
2654+
// listened for the event would read stale data.
2655+
this.emit(RoomEvent.Receipt, event, this);
28082656
}
28092657

28102658
/**

src/models/thread.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ import { IThreadBundledRelationship, MatrixEvent } from "./event";
2323
import { Direction, EventTimeline } from "./event-timeline";
2424
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
2525
import { Room } from './room';
26-
import { TypedEventEmitter } from "./typed-event-emitter";
2726
import { RoomState } from "./room-state";
2827
import { ServerControlledNamespacedValue } from "../NamespacedValue";
2928
import { logger } from "../logger";
29+
import { TimelineReceipts } from "./timeline-receipts";
3030

3131
export enum ThreadEvent {
3232
New = "Thread.new",
@@ -54,7 +54,7 @@ interface IThreadOpts {
5454
/**
5555
* @experimental
5656
*/
57-
export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
57+
export class Thread extends TimelineReceipts<EmittedEvents, EventHandlerMap> {
5858
public static hasServerSideSupport: boolean;
5959

6060
/**
@@ -429,6 +429,18 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
429429
nextBatch,
430430
};
431431
}
432+
433+
public getUnfilteredTimelineSet(): EventTimelineSet {
434+
return this.timelineSet;
435+
}
436+
437+
public get timeline(): MatrixEvent[] {
438+
return this.events;
439+
}
440+
441+
public addReceipt(event: MatrixEvent, synthetic: boolean): void {
442+
throw new Error("Unsupported function on the thread model");
443+
}
432444
}
433445

434446
export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue(

0 commit comments

Comments
 (0)