You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +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,
|
EventStatus,
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventTimelineSet,
|
EventTimelineSet,
|
||||||
|
EventType,
|
||||||
Filter,
|
Filter,
|
||||||
IEvent,
|
IEvent,
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
PendingEventOrdering,
|
PendingEventOrdering,
|
||||||
|
RelationType,
|
||||||
Room,
|
Room,
|
||||||
} from "../../src/matrix";
|
} from "../../src/matrix";
|
||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
@@ -33,6 +35,7 @@ import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/util
|
|||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
|
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
|
||||||
import { emitPromise } from "../test-utils/test-utils";
|
import { emitPromise } from "../test-utils/test-utils";
|
||||||
|
import { Feature, ServerSupport } from "../../src/feature";
|
||||||
|
|
||||||
const userId = "@alice:localhost";
|
const userId = "@alice:localhost";
|
||||||
const userName = "Alice";
|
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 () {
|
describe("paginateEventTimeline for thread list timeline", function () {
|
||||||
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";
|
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";
|
||||||
|
|
||||||
@@ -1847,7 +1961,10 @@ describe("MatrixClient event timelines", function () {
|
|||||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||||
"/" +
|
"/" +
|
||||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||||
buildRelationPaginationQuery({ dir: Direction.Backward, from: "start_token" }),
|
buildRelationPaginationQuery({
|
||||||
|
dir: Direction.Backward,
|
||||||
|
from: "start_token",
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.respond(200, function () {
|
.respond(200, function () {
|
||||||
return {
|
return {
|
||||||
|
@@ -82,6 +82,7 @@ describe("EventTimelineSet", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = utils.mock(MatrixClient, "MatrixClient");
|
client = utils.mock(MatrixClient, "MatrixClient");
|
||||||
client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
|
client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
|
||||||
|
client.canSupport = new Map();
|
||||||
room = new Room(roomId, client, userA);
|
room = new Room(roomId, client, userA);
|
||||||
eventTimelineSet = new EventTimelineSet(room);
|
eventTimelineSet = new EventTimelineSet(room);
|
||||||
eventTimeline = new EventTimeline(eventTimelineSet);
|
eventTimeline = new EventTimeline(eventTimelineSet);
|
||||||
|
@@ -186,6 +186,7 @@ export interface IRelationsRequestOpts {
|
|||||||
to?: string;
|
to?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
dir?: Direction;
|
dir?: Direction;
|
||||||
|
recurse?: boolean; // MSC3981 Relations Recursion https://github.com/matrix-org/matrix-spec-proposals/pull/3981
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRelationsResponse {
|
export interface IRelationsResponse {
|
||||||
|
@@ -5736,6 +5736,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
|
||||||
if (Thread.hasServerSideSupport) {
|
if (Thread.hasServerSideSupport) {
|
||||||
if (Thread.hasServerSideFwdPaginationSupport) {
|
if (Thread.hasServerSideFwdPaginationSupport) {
|
||||||
if (!timelineSet.thread) {
|
if (!timelineSet.thread) {
|
||||||
@@ -5748,14 +5749,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
thread.id,
|
thread.id,
|
||||||
THREAD_RELATION_TYPE.name,
|
THREAD_RELATION_TYPE.name,
|
||||||
null,
|
null,
|
||||||
{ dir: Direction.Backward, from: res.start },
|
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
|
||||||
);
|
);
|
||||||
const resNewer: IRelationsResponse = await this.fetchRelations(
|
const resNewer: IRelationsResponse = await this.fetchRelations(
|
||||||
timelineSet.room.roomId,
|
timelineSet.room.roomId,
|
||||||
thread.id,
|
thread.id,
|
||||||
THREAD_RELATION_TYPE.name,
|
THREAD_RELATION_TYPE.name,
|
||||||
null,
|
null,
|
||||||
{ dir: Direction.Forward, from: res.end },
|
{ dir: Direction.Forward, from: res.end, recurse: recurse || undefined },
|
||||||
);
|
);
|
||||||
const events = [
|
const events = [
|
||||||
// Order events from most recent to oldest (reverse-chronological).
|
// Order events from most recent to oldest (reverse-chronological).
|
||||||
@@ -5803,7 +5804,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
thread.id,
|
thread.id,
|
||||||
THREAD_RELATION_TYPE.name,
|
THREAD_RELATION_TYPE.name,
|
||||||
null,
|
null,
|
||||||
{ dir: Direction.Backward, from: res.start },
|
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
|
||||||
);
|
);
|
||||||
const eventsNewer: IEvent[] = [];
|
const eventsNewer: IEvent[] = [];
|
||||||
let nextBatch: Optional<string> = res.end;
|
let nextBatch: Optional<string> = res.end;
|
||||||
@@ -5813,7 +5814,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
thread.id,
|
thread.id,
|
||||||
THREAD_RELATION_TYPE.name,
|
THREAD_RELATION_TYPE.name,
|
||||||
null,
|
null,
|
||||||
{ dir: Direction.Forward, from: nextBatch },
|
{ dir: Direction.Forward, from: nextBatch, recurse: recurse || undefined },
|
||||||
);
|
);
|
||||||
nextBatch = resNewer.next_batch ?? null;
|
nextBatch = resNewer.next_batch ?? null;
|
||||||
eventsNewer.push(...resNewer.chunk);
|
eventsNewer.push(...resNewer.chunk);
|
||||||
@@ -5884,12 +5885,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
);
|
);
|
||||||
event = res.chunk?.[0];
|
event = res.chunk?.[0];
|
||||||
} else if (timelineSet.thread && Thread.hasServerSideSupport) {
|
} else if (timelineSet.thread && Thread.hasServerSideSupport) {
|
||||||
|
const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
|
||||||
const res = await this.fetchRelations(
|
const res = await this.fetchRelations(
|
||||||
timelineSet.room.roomId,
|
timelineSet.room.roomId,
|
||||||
timelineSet.thread.id,
|
timelineSet.thread.id,
|
||||||
THREAD_RELATION_TYPE.name,
|
THREAD_RELATION_TYPE.name,
|
||||||
null,
|
null,
|
||||||
{ dir: Direction.Backward, limit: 1 },
|
{ dir: Direction.Backward, limit: 1, recurse: recurse || undefined },
|
||||||
);
|
);
|
||||||
event = res.chunk?.[0];
|
event = res.chunk?.[0];
|
||||||
} else {
|
} else {
|
||||||
@@ -6164,10 +6166,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
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, {
|
promise = this.fetchRelations(eventTimeline.getRoomId() ?? "", thread.id, THREAD_RELATION_TYPE.name, null, {
|
||||||
dir,
|
dir,
|
||||||
limit: opts.limit,
|
limit: opts.limit,
|
||||||
from: token ?? undefined,
|
from: token ?? undefined,
|
||||||
|
recurse: recurse || undefined,
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const mapper = this.getEventMapper();
|
const mapper = this.getEventMapper();
|
||||||
@@ -7956,6 +7960,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
|
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
|
||||||
params = replaceParam("dir", "org.matrix.msc3715.dir", params);
|
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);
|
const queryString = utils.encodeParams(params);
|
||||||
|
|
||||||
let templatedUrl = "/rooms/$roomId/relations/$eventId";
|
let templatedUrl = "/rooms/$roomId/relations/$eventId";
|
||||||
|
@@ -31,6 +31,7 @@ export enum Feature {
|
|||||||
LoginTokenRequest = "LoginTokenRequest",
|
LoginTokenRequest = "LoginTokenRequest",
|
||||||
RelationBasedRedactions = "RelationBasedRedactions",
|
RelationBasedRedactions = "RelationBasedRedactions",
|
||||||
AccountDataDeletion = "AccountDataDeletion",
|
AccountDataDeletion = "AccountDataDeletion",
|
||||||
|
RelationsRecursion = "RelationsRecursion",
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeatureSupportCondition = {
|
type FeatureSupportCondition = {
|
||||||
@@ -56,6 +57,9 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
|||||||
[Feature.AccountDataDeletion]: {
|
[Feature.AccountDataDeletion]: {
|
||||||
unstablePrefixes: ["org.matrix.msc3391"],
|
unstablePrefixes: ["org.matrix.msc3391"],
|
||||||
},
|
},
|
||||||
|
[Feature.RelationsRecursion]: {
|
||||||
|
unstablePrefixes: ["org.matrix.msc3981"],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
||||||
|
@@ -28,6 +28,7 @@ import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
|||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { ReadReceipt } from "./read-receipt";
|
import { ReadReceipt } from "./read-receipt";
|
||||||
import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";
|
import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";
|
||||||
|
import { Feature, ServerSupport } from "../feature";
|
||||||
|
|
||||||
export enum ThreadEvent {
|
export enum ThreadEvent {
|
||||||
New = "Thread.new",
|
New = "Thread.new",
|
||||||
@@ -458,6 +459,8 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
|
|
||||||
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
|
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
|
||||||
private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
|
private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
|
||||||
|
const recursionSupport = this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported;
|
||||||
|
if (recursionSupport !== ServerSupport.Unsupported) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
events
|
events
|
||||||
.filter((e) => e.isEncrypted())
|
.filter((e) => e.isEncrypted())
|
||||||
@@ -478,6 +481,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public setEventMetadata(event: Optional<MatrixEvent>): void {
|
public setEventMetadata(event: Optional<MatrixEvent>): void {
|
||||||
if (event) {
|
if (event) {
|
||||||
|
Reference in New Issue
Block a user