Skip to content

Commit

Permalink
Implement MSC 3981 (#3248)
Browse files Browse the repository at this point in the history
* Implement MSC 3891

* Add necessary mocks to tests

* Only set recurse parameter if supported

* fix: address review comments

* task: unify unstable prefix code between client and tests

* Add test for relations recursion

* Make prettier happier :)

* Revert "task: unify unstable prefix code between client and tests"

This reverts commit f7401e0

* Fix broken tests
  • Loading branch information
justjanne authored Apr 27, 2023
1 parent 1e041a2 commit e10db6f
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 25 deletions.
119 changes: 118 additions & 1 deletion spec/integ/matrix-client-event-timeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@ import {
EventStatus,
EventTimeline,
EventTimelineSet,
EventType,
Filter,
IEvent,
MatrixClient,
MatrixEvent,
PendingEventOrdering,
RelationType,
Room,
} from "../../src/matrix";
import { logger } from "../../src/logger";
import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/utils";
import { TestClient } from "../TestClient";
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
import { emitPromise } from "../test-utils/test-utils";
import { Feature, ServerSupport } from "../../src/feature";

const userId = "@alice:localhost";
const userName = "Alice";
Expand Down Expand Up @@ -1164,6 +1167,117 @@ describe("MatrixClient event timelines", function () {
]);
});

it("should ensure thread events don't get reordered with recursive relations", async () => {
// Test data for a second reply to the first thread
const THREAD_REPLY2 = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply 2",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
event: true,
});
THREAD_REPLY2.localTimestamp += 1000;
const THREAD_ROOT_REACTION = utils.mkEvent({
event: true,
type: EventType.Reaction,
user: userId,
room: roomId,
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: THREAD_ROOT.event_id!,
key: Math.random().toString(),
},
},
});
THREAD_ROOT_REACTION.localTimestamp += 2000;

// Test data for a second reply to the first thread
const THREAD_REPLY3 = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply 3",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
event: true,
});
THREAD_REPLY3.localTimestamp += 3000;

// Test data for the first thread, with the second reply
const THREAD_ROOT_UPDATED = {
...THREAD_ROOT,
unsigned: {
...THREAD_ROOT.unsigned,
"m.relations": {
...THREAD_ROOT.unsigned!["m.relations"],
"io.element.thread": {
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
count: 3,
latest_event: THREAD_REPLY3.event,
},
},
},
};

// @ts-ignore
client.clientOpts.threadSupport = true;
client.canSupport.set(Feature.RelationsRecursion, ServerSupport.Stable);
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);

client.fetchRoomEvent = () => Promise.resolve(THREAD_ROOT_UPDATED);

await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;

const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
room.addLiveEvents([THREAD_REPLY2]);
httpBackend
.when(
"GET",
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
"/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) +
buildRelationPaginationQuery({
dir: Direction.Backward,
limit: 3,
recurse: true,
}),
)
.respond(200, {
chunk: [THREAD_REPLY3.event, THREAD_ROOT_REACTION, THREAD_REPLY2.event, THREAD_REPLY],
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
room.addLiveEvents([THREAD_REPLY3]);
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
THREAD_ROOT.event_id,
THREAD_REPLY.event_id,
THREAD_REPLY2.getId(),
THREAD_REPLY3.getId(),
]);
});

describe("paginateEventTimeline for thread list timeline", function () {
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";

Expand Down Expand Up @@ -1847,7 +1961,10 @@ describe("MatrixClient event timelines", function () {
encodeURIComponent(THREAD_ROOT.event_id!) +
"/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) +
buildRelationPaginationQuery({ dir: Direction.Backward, from: "start_token" }),
buildRelationPaginationQuery({
dir: Direction.Backward,
from: "start_token",
}),
)
.respond(200, function () {
return {
Expand Down
1 change: 1 addition & 0 deletions spec/unit/event-timeline-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe("EventTimelineSet", () => {
beforeEach(() => {
client = utils.mock(MatrixClient, "MatrixClient");
client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
client.canSupport = new Map();
room = new Room(roomId, client, userA);
eventTimelineSet = new EventTimelineSet(room);
eventTimeline = new EventTimeline(eventTimelineSet);
Expand Down
1 change: 1 addition & 0 deletions src/@types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export interface IRelationsRequestOpts {
to?: string;
limit?: number;
dir?: Direction;
recurse?: boolean; // MSC3981 Relations Recursion https://github.com/matrix-org/matrix-spec-proposals/pull/3981
}

export interface IRelationsResponse {
Expand Down
17 changes: 12 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5736,6 +5736,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return undefined;
}

const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
if (Thread.hasServerSideSupport) {
if (Thread.hasServerSideFwdPaginationSupport) {
if (!timelineSet.thread) {
Expand All @@ -5748,14 +5749,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
thread.id,
THREAD_RELATION_TYPE.name,
null,
{ dir: Direction.Backward, from: res.start },
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
);
const resNewer: IRelationsResponse = await this.fetchRelations(
timelineSet.room.roomId,
thread.id,
THREAD_RELATION_TYPE.name,
null,
{ dir: Direction.Forward, from: res.end },
{ dir: Direction.Forward, from: res.end, recurse: recurse || undefined },
);
const events = [
// Order events from most recent to oldest (reverse-chronological).
Expand Down Expand Up @@ -5803,7 +5804,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
thread.id,
THREAD_RELATION_TYPE.name,
null,
{ dir: Direction.Backward, from: res.start },
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
);
const eventsNewer: IEvent[] = [];
let nextBatch: Optional<string> = res.end;
Expand All @@ -5813,7 +5814,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
thread.id,
THREAD_RELATION_TYPE.name,
null,
{ dir: Direction.Forward, from: nextBatch },
{ dir: Direction.Forward, from: nextBatch, recurse: recurse || undefined },
);
nextBatch = resNewer.next_batch ?? null;
eventsNewer.push(...resNewer.chunk);
Expand Down Expand Up @@ -5884,12 +5885,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
);
event = res.chunk?.[0];
} else if (timelineSet.thread && Thread.hasServerSideSupport) {
const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
const res = await this.fetchRelations(
timelineSet.room.roomId,
timelineSet.thread.id,
THREAD_RELATION_TYPE.name,
null,
{ dir: Direction.Backward, limit: 1 },
{ dir: Direction.Backward, limit: 1, recurse: recurse || undefined },
);
event = res.chunk?.[0];
} else {
Expand Down Expand Up @@ -6164,10 +6166,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error("Unknown room " + eventTimeline.getRoomId());
}

const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
promise = this.fetchRelations(eventTimeline.getRoomId() ?? "", thread.id, THREAD_RELATION_TYPE.name, null, {
dir,
limit: opts.limit,
from: token ?? undefined,
recurse: recurse || undefined,
})
.then(async (res) => {
const mapper = this.getEventMapper();
Expand Down Expand Up @@ -7956,6 +7960,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
params = replaceParam("dir", "org.matrix.msc3715.dir", params);
}
if (this.canSupport.get(Feature.RelationsRecursion) === ServerSupport.Unstable) {
params = replaceParam("recurse", "org.matrix.msc3981.recurse", params);
}
const queryString = utils.encodeParams(params);

let templatedUrl = "/rooms/$roomId/relations/$eventId";
Expand Down
4 changes: 4 additions & 0 deletions src/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum Feature {
LoginTokenRequest = "LoginTokenRequest",
RelationBasedRedactions = "RelationBasedRedactions",
AccountDataDeletion = "AccountDataDeletion",
RelationsRecursion = "RelationsRecursion",
}

type FeatureSupportCondition = {
Expand All @@ -56,6 +57,9 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
[Feature.AccountDataDeletion]: {
unstablePrefixes: ["org.matrix.msc3391"],
},
[Feature.RelationsRecursion]: {
unstablePrefixes: ["org.matrix.msc3981"],
},
};

export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
Expand Down
42 changes: 23 additions & 19 deletions src/models/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ServerControlledNamespacedValue } from "../NamespacedValue";
import { logger } from "../logger";
import { ReadReceipt } from "./read-receipt";
import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";
import { Feature, ServerSupport } from "../feature";

export enum ThreadEvent {
New = "Thread.new",
Expand Down Expand Up @@ -458,25 +459,28 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {

// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
return Promise.all(
events
.filter((e) => e.isEncrypted())
.map((event: MatrixEvent) => {
if (event.isRelation()) return; // skip - relations don't get edits
return this.client
.relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), {
limit: 1,
})
.then((relations) => {
if (relations.events.length) {
event.makeReplaced(relations.events[0]);
}
})
.catch((e) => {
logger.error("Failed to load edits for encrypted thread event", e);
});
}),
);
const recursionSupport = this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported;
if (recursionSupport !== ServerSupport.Unsupported) {
return Promise.all(
events
.filter((e) => e.isEncrypted())
.map((event: MatrixEvent) => {
if (event.isRelation()) return; // skip - relations don't get edits
return this.client
.relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), {
limit: 1,
})
.then((relations) => {
if (relations.events.length) {
event.makeReplaced(relations.events[0]);
}
})
.catch((e) => {
logger.error("Failed to load edits for encrypted thread event", e);
});
}),
);
}
}

public setEventMetadata(event: Optional<MatrixEvent>): void {
Expand Down

0 comments on commit e10db6f

Please sign in to comment.