You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
Remove m.thread filter from relations API call (#3959)
* Remove m.thread filter from relations API call We used MSC3981 to pass the recurse param to the /relations endpoint so that we could get relations to events in a thread, but we kept the rel_type filter on (as m.thread) so no second-order relations would ever have been returned (a nested thread isn't a thing). This removes the filter and does some filtering on the client side to remove any events that shouldn't live in the threaded timeline (ie. non-thread relations to the thread root event). This should help fix stuck unreads because it will avoid the event that the receipt refers to going missing (but only on HSes that support MSC3981). For https://github.com/vector-im/element-web/issues/26718 * Fix import cycle * Remove params from expected calls in tests to match * Unused import
This commit is contained in:
@@ -33,7 +33,7 @@ import {
|
|||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/utils";
|
import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/utils";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
|
import { FeatureSupport, Thread, 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";
|
import { Feature, ServerSupport } from "../../src/feature";
|
||||||
|
|
||||||
@@ -623,9 +623,7 @@ describe("MatrixClient event timelines", function () {
|
|||||||
"GET",
|
"GET",
|
||||||
"/rooms/!foo%3Abar/relations/" +
|
"/rooms/!foo%3Abar/relations/" +
|
||||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||||
"/" +
|
buildRelationPaginationQuery({ dir: Direction.Backward }),
|
||||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
|
||||||
buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }),
|
|
||||||
)
|
)
|
||||||
.respond(200, function () {
|
.respond(200, function () {
|
||||||
return {
|
return {
|
||||||
@@ -1154,10 +1152,7 @@ describe("MatrixClient event timelines", function () {
|
|||||||
httpBackend
|
httpBackend
|
||||||
.when(
|
.when(
|
||||||
"GET",
|
"GET",
|
||||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT_UPDATED.event_id!),
|
||||||
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
|
|
||||||
"/" +
|
|
||||||
encodeURIComponent(THREAD_RELATION_TYPE.name),
|
|
||||||
)
|
)
|
||||||
.respond(200, {
|
.respond(200, {
|
||||||
chunk: [THREAD_REPLY3.event, THREAD_REPLY2.event, THREAD_REPLY],
|
chunk: [THREAD_REPLY3.event, THREAD_REPLY2.event, THREAD_REPLY],
|
||||||
@@ -1262,11 +1257,8 @@ describe("MatrixClient event timelines", function () {
|
|||||||
"GET",
|
"GET",
|
||||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||||
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
|
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
|
||||||
"/" +
|
|
||||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
|
||||||
buildRelationPaginationQuery({
|
buildRelationPaginationQuery({
|
||||||
dir: Direction.Backward,
|
dir: Direction.Backward,
|
||||||
limit: 3,
|
|
||||||
recurse: true,
|
recurse: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -1321,11 +1313,7 @@ describe("MatrixClient event timelines", function () {
|
|||||||
function respondToThread(root: Partial<IEvent>, replies: Partial<IEvent>[]): ExpectedHttpRequest {
|
function respondToThread(root: Partial<IEvent>, replies: Partial<IEvent>[]): ExpectedHttpRequest {
|
||||||
const request = httpBackend.when(
|
const request = httpBackend.when(
|
||||||
"GET",
|
"GET",
|
||||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(root.event_id!) + "?dir=b",
|
||||||
encodeURIComponent(root.event_id!) +
|
|
||||||
"/" +
|
|
||||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
|
||||||
"?dir=b&limit=1",
|
|
||||||
);
|
);
|
||||||
request.respond(200, function () {
|
request.respond(200, function () {
|
||||||
return {
|
return {
|
||||||
@@ -2034,9 +2022,7 @@ describe("MatrixClient event timelines", function () {
|
|||||||
"GET",
|
"GET",
|
||||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||||
"/" +
|
buildRelationPaginationQuery({ dir: Direction.Backward }),
|
||||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
|
||||||
buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }),
|
|
||||||
)
|
)
|
||||||
.respond(200, function () {
|
.respond(200, function () {
|
||||||
return {
|
return {
|
||||||
|
51
spec/unit/thread-utils.spec.ts
Normal file
51
spec/unit/thread-utils.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IEvent } from "../../src";
|
||||||
|
import { randomString } from "../../src/randomstring";
|
||||||
|
import { getRelationsThreadFilter } from "../../src/thread-utils";
|
||||||
|
|
||||||
|
function makeEvent(relatesToEvent: string, relType: string): Partial<IEvent> {
|
||||||
|
return {
|
||||||
|
event_id: randomString(10),
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "foo",
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: relType,
|
||||||
|
event_id: relatesToEvent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getRelationsThreadFilter", () => {
|
||||||
|
it("should filter out relations directly to the thread root event", () => {
|
||||||
|
const threadId = "thisIsMyThreadRoot";
|
||||||
|
|
||||||
|
const reactionToRoot = makeEvent(threadId, "m.annotation");
|
||||||
|
const editToRoot = makeEvent(threadId, "m.replace");
|
||||||
|
const firstThreadedReply = makeEvent(threadId, "m.thread");
|
||||||
|
const reactionToThreadedEvent = makeEvent(firstThreadedReply.event_id!, "m.annotation");
|
||||||
|
|
||||||
|
const filteredEvents = [reactionToRoot, editToRoot, firstThreadedReply, reactionToThreadedEvent].filter(
|
||||||
|
getRelationsThreadFilter(threadId),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filteredEvents).toEqual([firstThreadedReply, reactionToThreadedEvent]);
|
||||||
|
});
|
||||||
|
});
|
@@ -221,6 +221,7 @@ import {
|
|||||||
} from "./secret-storage";
|
} from "./secret-storage";
|
||||||
import { RegisterRequest, RegisterResponse } from "./@types/registration";
|
import { RegisterRequest, RegisterResponse } from "./@types/registration";
|
||||||
import { MatrixRTCSessionManager } from "./matrixrtc/MatrixRTCSessionManager";
|
import { MatrixRTCSessionManager } from "./matrixrtc/MatrixRTCSessionManager";
|
||||||
|
import { getRelationsThreadFilter } from "./thread-utils";
|
||||||
|
|
||||||
export type Store = IStore;
|
export type Store = IStore;
|
||||||
|
|
||||||
@@ -5968,14 +5969,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
const resOlder: IRelationsResponse = await this.fetchRelations(
|
const resOlder: IRelationsResponse = await this.fetchRelations(
|
||||||
timelineSet.room.roomId,
|
timelineSet.room.roomId,
|
||||||
thread.id,
|
thread.id,
|
||||||
THREAD_RELATION_TYPE.name,
|
null,
|
||||||
null,
|
null,
|
||||||
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
|
{ 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,
|
null,
|
||||||
null,
|
null,
|
||||||
{ dir: Direction.Forward, from: res.end, recurse: recurse || undefined },
|
{ dir: Direction.Forward, from: res.end, recurse: recurse || undefined },
|
||||||
);
|
);
|
||||||
@@ -5983,10 +5984,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// Order events from most recent to oldest (reverse-chronological).
|
// Order events from most recent to oldest (reverse-chronological).
|
||||||
// We start with the last event, since that's the point at which we have known state.
|
// We start with the last event, since that's the point at which we have known state.
|
||||||
// events_after is already backwards; events_before is forwards.
|
// events_after is already backwards; events_before is forwards.
|
||||||
...resNewer.chunk.reverse().map(mapper),
|
...resNewer.chunk.reverse().filter(getRelationsThreadFilter(thread.id)).map(mapper),
|
||||||
event,
|
event,
|
||||||
...resOlder.chunk.map(mapper),
|
...resOlder.chunk.filter(getRelationsThreadFilter(thread.id)).map(mapper),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
await timelineSet.thread?.processEvent(event);
|
await timelineSet.thread?.processEvent(event);
|
||||||
}
|
}
|
||||||
@@ -6361,6 +6363,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
const stateEvents = res.state.filter(noUnsafeEventProps).map(this.getEventMapper());
|
const stateEvents = res.state.filter(noUnsafeEventProps).map(this.getEventMapper());
|
||||||
roomState.setUnknownStateEvents(stateEvents);
|
roomState.setUnknownStateEvents(stateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = res.end;
|
const token = res.end;
|
||||||
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
|
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
|
||||||
|
|
||||||
@@ -6388,7 +6391,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
|
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, null, null, {
|
||||||
dir,
|
dir,
|
||||||
limit: opts.limit,
|
limit: opts.limit,
|
||||||
from: token ?? undefined,
|
from: token ?? undefined,
|
||||||
@@ -6396,7 +6399,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const mapper = this.getEventMapper();
|
const mapper = this.getEventMapper();
|
||||||
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(mapper);
|
const matrixEvents = res.chunk
|
||||||
|
.filter(noUnsafeEventProps)
|
||||||
|
.filter(getRelationsThreadFilter(thread.id))
|
||||||
|
.map(mapper);
|
||||||
|
|
||||||
// Process latest events first
|
// Process latest events first
|
||||||
for (const event of matrixEvents.slice().reverse()) {
|
for (const event of matrixEvents.slice().reverse()) {
|
||||||
@@ -7591,7 +7597,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
public async relations(
|
public async relations(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
eventId: string,
|
eventId: string,
|
||||||
relationType?: RelationType | string | null,
|
relationType: RelationType | string | null,
|
||||||
eventType?: EventType | string | null,
|
eventType?: EventType | string | null,
|
||||||
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@@ -8114,7 +8120,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
public fetchRelations(
|
public fetchRelations(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
eventId: string,
|
eventId: string,
|
||||||
relationType?: RelationType | string | null,
|
relationType: RelationType | string | null,
|
||||||
eventType?: EventType | string | null,
|
eventType?: EventType | string | null,
|
||||||
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||||
): Promise<IRelationsResponse> {
|
): Promise<IRelationsResponse> {
|
||||||
|
@@ -609,7 +609,6 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
|||||||
} else {
|
} else {
|
||||||
await this.client.paginateEventTimeline(this.liveTimeline, {
|
await this.client.paginateEventTimeline(this.liveTimeline, {
|
||||||
backwards: true,
|
backwards: true,
|
||||||
limit: Math.max(1, this.length),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const event of this.replayEvents!) {
|
for (const event of this.replayEvents!) {
|
||||||
|
31
src/thread-utils.ts
Normal file
31
src/thread-utils.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { THREAD_RELATION_TYPE } from "./models/thread";
|
||||||
|
import { IEvent } from "./models/event";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a filter function for the /relations endpoint to filter out relations directly
|
||||||
|
* to the thread root event that should not live in the thread timeline
|
||||||
|
*
|
||||||
|
* @param threadId - the thread ID (ie. the event ID of the root event of the thread)
|
||||||
|
* @returns the filtered list of events
|
||||||
|
*/
|
||||||
|
export function getRelationsThreadFilter(threadId: string): (e: Partial<IEvent>) => boolean {
|
||||||
|
return (e: Partial<IEvent>) =>
|
||||||
|
e.content?.["m.relates_to"]?.event_id !== threadId ||
|
||||||
|
e.content?.["m.relates_to"]?.rel_type === THREAD_RELATION_TYPE.name;
|
||||||
|
}
|
Reference in New Issue
Block a user