Skip to content

Commit 286500e

Browse files
authored
Fix issues around echo & redaction handling in threads (#2286)
1 parent 5937e6a commit 286500e

File tree

5 files changed

+175
-29
lines changed

5 files changed

+175
-29
lines changed

spec/test-utils/test-utils.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { logger } from '../../src/logger';
88
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
99
import { ClientEvent, EventType, MatrixClient } from "../../src";
1010
import { SyncState } from "../../src/sync";
11+
import { eventMapperFor } from "../../src/event-mapper";
1112

1213
/**
1314
* Return a promise that is resolved when the client next emits a
@@ -79,6 +80,7 @@ interface IEventOpts {
7980
redacts?: string;
8081
}
8182

83+
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
8284
/**
8385
* Create an Event.
8486
* @param {Object} opts Values for the event.
@@ -88,9 +90,10 @@ interface IEventOpts {
8890
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
8991
* @param {Object} opts.content The event.content
9092
* @param {boolean} opts.event True to make a MatrixEvent.
93+
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
9194
* @return {Object} a JSON object representing this event.
9295
*/
93-
export function mkEvent(opts: IEventOpts): object | MatrixEvent {
96+
export function mkEvent(opts: IEventOpts, client?: MatrixClient): object | MatrixEvent {
9497
if (!opts.type || !opts.content) {
9598
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
9699
}
@@ -100,7 +103,7 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
100103
sender: opts.sender || opts.user, // opts.user for backwards-compat
101104
content: opts.content,
102105
unsigned: opts.unsigned || {},
103-
event_id: "$" + Math.random() + "-" + Math.random(),
106+
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
104107
txn_id: "~" + Math.random(),
105108
redacts: opts.redacts,
106109
};
@@ -117,6 +120,11 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
117120
].includes(opts.type)) {
118121
event.state_key = "";
119122
}
123+
124+
if (opts.event && client) {
125+
return eventMapperFor(client, {})(event);
126+
}
127+
120128
return opts.event ? new MatrixEvent(event) : event;
121129
}
122130

@@ -209,9 +217,10 @@ interface IMessageOpts {
209217
* @param {string} opts.user The user ID for the event.
210218
* @param {string} opts.msg Optional. The content.body for the event.
211219
* @param {boolean} opts.event True to make a MatrixEvent.
220+
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
212221
* @return {Object|MatrixEvent} The event
213222
*/
214-
export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
223+
export function mkMessage(opts: IMessageOpts, client?: MatrixClient): object | MatrixEvent {
215224
const eventOpts: IEventOpts = {
216225
...opts,
217226
type: EventType.RoomMessage,
@@ -224,7 +233,7 @@ export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
224233
if (!eventOpts.content.body) {
225234
eventOpts.content.body = "Random->" + Math.random();
226235
}
227-
return mkEvent(eventOpts);
236+
return mkEvent(eventOpts, client);
228237
}
229238

230239
/**

spec/unit/room.spec.ts

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe("Room", function() {
5050
event: true,
5151
user: userA,
5252
room: roomId,
53-
}) as MatrixEvent;
53+
}, room.client) as MatrixEvent;
5454

5555
const mkReply = (target: MatrixEvent) => utils.mkEvent({
5656
event: true,
@@ -65,7 +65,7 @@ describe("Room", function() {
6565
},
6666
},
6767
},
68-
}) as MatrixEvent;
68+
}, room.client) as MatrixEvent;
6969

7070
const mkEdit = (target: MatrixEvent, salt = Math.random()) => utils.mkEvent({
7171
event: true,
@@ -82,7 +82,7 @@ describe("Room", function() {
8282
event_id: target.getId(),
8383
},
8484
},
85-
}) as MatrixEvent;
85+
}, room.client) as MatrixEvent;
8686

8787
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
8888
event: true,
@@ -99,7 +99,7 @@ describe("Room", function() {
9999
"rel_type": "m.thread",
100100
},
101101
},
102-
}) as MatrixEvent;
102+
}, room.client) as MatrixEvent;
103103

104104
const mkReaction = (target: MatrixEvent) => utils.mkEvent({
105105
event: true,
@@ -113,7 +113,7 @@ describe("Room", function() {
113113
"key": Math.random().toString(),
114114
},
115115
},
116-
}) as MatrixEvent;
116+
}, room.client) as MatrixEvent;
117117

118118
const mkRedaction = (target: MatrixEvent) => utils.mkEvent({
119119
event: true,
@@ -122,7 +122,7 @@ describe("Room", function() {
122122
room: roomId,
123123
redacts: target.getId(),
124124
content: {},
125-
}) as MatrixEvent;
125+
}, room.client) as MatrixEvent;
126126

127127
beforeEach(function() {
128128
room = new Room(roomId, new TestClient(userA, "device").client, userA);
@@ -1899,6 +1899,7 @@ describe("Room", function() {
18991899
"@alice:example.com", "alicedevice",
19001900
)).client;
19011901
room = new Room(roomId, client, userA);
1902+
client.getRoom = () => room;
19021903
});
19031904

19041905
it("allow create threads without a root event", function() {
@@ -1938,11 +1939,7 @@ describe("Room", function() {
19381939
});
19391940

19401941
it("Edits update the lastReply event", async () => {
1941-
const client = (new TestClient(
1942-
"@alice:example.com", "alicedevice",
1943-
)).client;
1944-
client.supportsExperimentalThreads = () => true;
1945-
room = new Room(roomId, client, userA);
1942+
room.client.supportsExperimentalThreads = () => true;
19461943

19471944
const randomMessage = mkMessage();
19481945
const threadRoot = mkMessage();
@@ -1951,7 +1948,7 @@ describe("Room", function() {
19511948
const threadResponseEdit = mkEdit(threadResponse);
19521949
threadResponseEdit.localTimestamp += 2000;
19531950

1954-
client.fetchRoomEvent = (eventId: string) => Promise.resolve({
1951+
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
19551952
...threadRoot.event,
19561953
unsigned: {
19571954
"age": 123,
@@ -1975,6 +1972,121 @@ describe("Room", function() {
19751972
await emitPromise(thread, ThreadEvent.Update);
19761973
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
19771974
});
1975+
1976+
it("Redactions to thread responses decrement the length", async () => {
1977+
room.client.supportsExperimentalThreads = () => true;
1978+
1979+
const threadRoot = mkMessage();
1980+
const threadResponse1 = mkThreadResponse(threadRoot);
1981+
threadResponse1.localTimestamp += 1000;
1982+
const threadResponse2 = mkThreadResponse(threadRoot);
1983+
threadResponse2.localTimestamp += 2000;
1984+
1985+
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
1986+
...threadRoot.event,
1987+
unsigned: {
1988+
"age": 123,
1989+
"m.relations": {
1990+
"m.thread": {
1991+
latest_event: threadResponse2.event,
1992+
count: 2,
1993+
current_user_participated: true,
1994+
},
1995+
},
1996+
},
1997+
});
1998+
1999+
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
2000+
const thread = await emitPromise(room, ThreadEvent.New);
2001+
2002+
expect(thread).toHaveLength(2);
2003+
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
2004+
2005+
const threadResponse1Redaction = mkRedaction(threadResponse1);
2006+
room.addLiveEvents([threadResponse1Redaction]);
2007+
await emitPromise(thread, ThreadEvent.Update);
2008+
expect(thread).toHaveLength(1);
2009+
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
2010+
});
2011+
2012+
it("Redactions to reactions in threads do not decrement the length", async () => {
2013+
room.client.supportsExperimentalThreads = () => true;
2014+
2015+
const threadRoot = mkMessage();
2016+
const threadResponse1 = mkThreadResponse(threadRoot);
2017+
threadResponse1.localTimestamp += 1000;
2018+
const threadResponse2 = mkThreadResponse(threadRoot);
2019+
threadResponse2.localTimestamp += 2000;
2020+
const threadResponse2Reaction = mkReaction(threadResponse2);
2021+
2022+
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
2023+
...threadRoot.event,
2024+
unsigned: {
2025+
"age": 123,
2026+
"m.relations": {
2027+
"m.thread": {
2028+
latest_event: threadResponse2.event,
2029+
count: 2,
2030+
current_user_participated: true,
2031+
},
2032+
},
2033+
},
2034+
});
2035+
2036+
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
2037+
const thread = await emitPromise(room, ThreadEvent.New);
2038+
2039+
expect(thread).toHaveLength(2);
2040+
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
2041+
2042+
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
2043+
room.addLiveEvents([threadResponse2ReactionRedaction]);
2044+
await emitPromise(thread, ThreadEvent.Update);
2045+
expect(thread).toHaveLength(2);
2046+
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
2047+
});
2048+
2049+
it("Redacting the lastEvent finds a new lastEvent", async () => {
2050+
room.client.supportsExperimentalThreads = () => true;
2051+
2052+
const threadRoot = mkMessage();
2053+
const threadResponse1 = mkThreadResponse(threadRoot);
2054+
threadResponse1.localTimestamp += 1000;
2055+
const threadResponse2 = mkThreadResponse(threadRoot);
2056+
threadResponse2.localTimestamp += 2000;
2057+
2058+
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
2059+
...threadRoot.event,
2060+
unsigned: {
2061+
"age": 123,
2062+
"m.relations": {
2063+
"m.thread": {
2064+
latest_event: threadResponse2.event,
2065+
count: 2,
2066+
current_user_participated: true,
2067+
},
2068+
},
2069+
},
2070+
});
2071+
2072+
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
2073+
const thread = await emitPromise(room, ThreadEvent.New);
2074+
2075+
expect(thread).toHaveLength(2);
2076+
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
2077+
2078+
const threadResponse2Redaction = mkRedaction(threadResponse2);
2079+
room.addLiveEvents([threadResponse2Redaction]);
2080+
await emitPromise(thread, ThreadEvent.Update);
2081+
expect(thread).toHaveLength(1);
2082+
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
2083+
2084+
const threadResponse1Redaction = mkRedaction(threadResponse1);
2085+
room.addLiveEvents([threadResponse1Redaction]);
2086+
await emitPromise(thread, ThreadEvent.Update);
2087+
expect(thread).toHaveLength(0);
2088+
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
2089+
});
19782090
});
19792091

19802092
describe("eventShouldLiveIn", () => {

src/event-mapper.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
6666
MatrixEventEvent.Replaced,
6767
MatrixEventEvent.VisibilityChange,
6868
]);
69+
room?.reEmitter.reEmit(event, [
70+
MatrixEventEvent.BeforeRedaction,
71+
]);
6972
}
7073
return event;
7174
}

src/models/room.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { Direction, EventTimeline } from "./event-timeline";
2323
import { getHttpUriForMxc } from "../content-repo";
2424
import * as utils from "../utils";
2525
import { defer, normalize } from "../utils";
26-
import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event";
26+
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
2727
import { EventStatus } from "./event-status";
2828
import { RoomMember } from "./room-member";
2929
import { IRoomSummary, RoomSummary } from "./room-summary";
@@ -171,7 +171,8 @@ type EmittedEvents = RoomEvent
171171
| ThreadEvent.Update
172172
| ThreadEvent.NewReply
173173
| RoomEvent.Timeline
174-
| RoomEvent.TimelineReset;
174+
| RoomEvent.TimelineReset
175+
| MatrixEventEvent.BeforeRedaction;
175176

176177
export type RoomEventHandlerMap = {
177178
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
@@ -188,10 +189,10 @@ export type RoomEventHandlerMap = {
188189
oldStatus?: EventStatus,
189190
) => void;
190191
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
191-
} & ThreadHandlerMap;
192+
} & ThreadHandlerMap & MatrixEventHandlerMap;
192193

193194
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
194-
private readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
195+
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
195196
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
196197
// receipts should clobber based on receipt_type and user_id pairs hence
197198
// the form of this structure. This is sub-optimal for the exposed APIs
@@ -383,7 +384,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
383384
return this.threadTimelineSetsPromise;
384385
}
385386

386-
if (this.client?.supportsExperimentalThreads) {
387+
if (this.client?.supportsExperimentalThreads()) {
387388
try {
388389
this.threadTimelineSetsPromise = Promise.all([
389390
this.createThreadTimelineSet(),
@@ -1676,6 +1677,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
16761677
thread = await this.threadPromises.get(threadId);
16771678
}
16781679

1680+
events = events.filter(e => e.getId() !== threadId); // filter out any root events
1681+
16791682
if (thread) {
16801683
for (const event of events) {
16811684
await thread.addEvent(event, toStartOfTimeline);
@@ -1810,7 +1813,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
18101813
}
18111814
};
18121815

1813-
private processLiveEvent(event: MatrixEvent): Promise<void> {
1816+
private processLiveEvent(event: MatrixEvent): void {
18141817
this.applyRedaction(event);
18151818

18161819
// Implement MSC3531: hiding messages.
@@ -1827,7 +1830,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
18271830
if (existingEvent) {
18281831
// remote echo of an event we sent earlier
18291832
this.handleRemoteEcho(event, existingEvent);
1830-
return;
18311833
}
18321834
}
18331835
}

0 commit comments

Comments
 (0)