You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Implement MSC 3981 (#3248)
* 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 f7401e05
* Fix broken tests
This commit is contained in:
committed by
GitHub
parent
1e041a2957
commit
e10db6f7e9
@ -21,11 +21,13 @@ import {
|
||||
EventStatus,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
@ -33,6 +35,7 @@ import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/util
|
||||
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";
|
||||
@ -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";
|
||||
|
||||
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
@ -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).
|
||||
@ -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;
|
||||
@ -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);
|
||||
@ -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 {
|
||||
@ -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();
|
||||
@ -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";
|
||||
|
@ -31,6 +31,7 @@ export enum Feature {
|
||||
LoginTokenRequest = "LoginTokenRequest",
|
||||
RelationBasedRedactions = "RelationBasedRedactions",
|
||||
AccountDataDeletion = "AccountDataDeletion",
|
||||
RelationsRecursion = "RelationsRecursion",
|
||||
}
|
||||
|
||||
type FeatureSupportCondition = {
|
||||
@ -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>> {
|
||||
|
@ -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",
|
||||
@ -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 {
|
||||
|
Reference in New Issue
Block a user