1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-08-07 21:23:00 +03:00

Loading threads with server-side assistance (#9356)

* Fix bug with message context menu
* fix bug where ThreadSummary failed if no last reply is available
* Fix relations direction API
* Use same API for threads as for any other timeline
* Determine if event belongs to thread on jumping to event
* properly listen to thread deletion
* Add thread redaction tests
* Add fetchInitialEvent tests
* Paginate using default TimelinePanel behaviour
* Remove unused threads deleted code

Co-authored-by: Germain <germain@souquet.com>
Co-authored-by: Germain <germains@element.io>
This commit is contained in:
Janne Mareike Koschinski
2022-10-28 13:48:15 +02:00
committed by GitHub
parent 750ca78e98
commit d92fdc1f5b
11 changed files with 205 additions and 82 deletions

View File

@@ -14,16 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { act, render } from "@testing-library/react";
import React from "react";
import { act, render, screen, waitFor } from "@testing-library/react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import React from "react";
import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { getRoomContext, mkMessage, stubClient } from "../../../test-utils";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { getRoomContext, mkEvent, mkMessage, stubClient } from "../../../test-utils";
import { mkThread } from "../../../test-utils/threads";
describe("EventTile", () => {
@@ -52,9 +55,11 @@ describe("EventTile", () => {
timelineRenderingType: renderingType,
});
return render(
<RoomContext.Provider value={context}>
<TestEventTile {...overrides} />
</RoomContext.Provider>,
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={context}>
<TestEventTile {...overrides} />
</RoomContext.Provider>,
</MatrixClientContext.Provider>,
);
}
@@ -69,6 +74,8 @@ describe("EventTile", () => {
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
jest.spyOn(client, "decryptEventIfNeeded").mockResolvedValue();
jest.spyOn(SettingsStore, "getValue").mockImplementation(name => name === "feature_thread");
mxEvent = mkMessage({
room: room.roomId,
@@ -78,6 +85,40 @@ describe("EventTile", () => {
});
});
describe("EventTile thread summary", () => {
beforeEach(() => {
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
});
it("removes the thread summary when thread is deleted", async () => {
const { rootEvent, events: [, reply] } = mkThread({
room,
client,
authorId: "@alice:example.org",
participantUserIds: ["@alice:example.org"],
length: 2, // root + 1 answer
});
getComponent({
mxEvent: rootEvent,
}, TimelineRenderingType.Room);
await waitFor(() => expect(screen.queryByTestId("thread-summary")).not.toBeNull());
const redaction = mkEvent({
event: true,
type: EventType.RoomRedaction,
user: "@alice:example.org",
room: room.roomId,
redacts: reply.getId(),
content: {},
});
act(() => room.processThreadedEvents([redaction], false));
await waitFor(() => expect(screen.queryByTestId("thread-summary")).toBeNull());
});
});
describe("EventTile renderingType: ThreadsList", () => {
beforeEach(() => {
const { rootEvent } = mkThread({

View File

@@ -212,6 +212,7 @@ type MakeEventPassThruProps = {
};
type MakeEventProps = MakeEventPassThruProps & {
type: string;
redacts?: string;
content: IContent;
room?: Room["roomId"]; // to-device messages are roomless
// eslint-disable-next-line camelcase
@@ -245,6 +246,7 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent {
event_id: "$" + Math.random() + "-" + Math.random(),
origin_server_ts: opts.ts ?? 0,
unsigned: opts.unsigned,
redacts: opts.redacts,
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, MatrixEvent, RelationType, Room } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent, MatrixEventEvent, RelationType, Room } from "matrix-js-sdk/src/matrix";
import { Thread } from "matrix-js-sdk/src/models/thread";
import { mkMessage, MessageEventProps } from "./test-utils";
@@ -115,10 +115,18 @@ export const mkThread = ({
ts,
currentUserId: client.getUserId(),
});
expect(rootEvent).toBeTruthy();
for (const evt of events) {
room?.reEmitter.reEmit(evt, [
MatrixEventEvent.BeforeRedaction,
]);
}
const thread = room.createThread(rootEvent.getId(), rootEvent, events, true);
// So that we do not have to mock the thread loading
thread.initialEventsFetched = true;
thread.addEvents(events, true);
return { thread, rootEvent, events };
};

View File

@@ -18,21 +18,27 @@ import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import {
EventStatus,
EventType,
IEvent,
MatrixClient,
MatrixEvent,
MsgType,
PendingEventOrdering,
RelationType,
Room,
} from "matrix-js-sdk/src/matrix";
import { Thread } from "matrix-js-sdk/src/models/thread";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import {
canCancel,
canEditContent,
canEditOwnEvent,
fetchInitialEvent,
isContentActionable,
isLocationEvent,
isVoiceMessage,
} from "../../src/utils/EventUtils";
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent } from "../test-utils";
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../test-utils";
describe('EventUtils', () => {
const userId = '@user:server';
@@ -336,4 +342,92 @@ describe('EventUtils', () => {
expect(canCancel(status)).toBe(false);
});
});
describe("fetchInitialEvent", () => {
const ROOM_ID = "!roomId:example.org";
let room: Room;
let client: MatrixClient;
const NORMAL_EVENT = "$normalEvent";
const THREAD_ROOT = "$threadRoot";
const THREAD_REPLY = "$threadReply";
const events: Record<string, Partial<IEvent>> = {
[NORMAL_EVENT]: {
event_id: NORMAL_EVENT,
type: EventType.RoomMessage,
content: {
"body": "Classic event",
"msgtype": MsgType.Text,
},
},
[THREAD_ROOT]: {
event_id: THREAD_ROOT,
type: EventType.RoomMessage,
content: {
"body": "Thread root",
"msgtype": "m.text",
},
unsigned: {
"m.relations": {
[RelationType.Thread]: {
latest_event: {
event_id: THREAD_REPLY,
type: EventType.RoomMessage,
content: {
"body": "Thread reply",
"msgtype": MsgType.Text,
"m.relates_to": {
event_id: "$threadRoot",
rel_type: RelationType.Thread,
},
},
},
count: 1,
current_user_participated: false,
},
},
},
},
[THREAD_REPLY]: {
event_id: THREAD_REPLY,
type: EventType.RoomMessage,
content: {
"body": "Thread reply",
"msgtype": MsgType.Text,
"m.relates_to": {
event_id: THREAD_ROOT,
rel_type: RelationType.Thread,
},
},
},
};
beforeEach(() => {
jest.clearAllMocks();
stubClient();
client = MatrixClientPeg.get();
room = new Room(ROOM_ID, client, client.getUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
jest.spyOn(client, "getRoom").mockReturnValue(room);
jest.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => {
return events[eventId] ?? Promise.reject();
});
});
it("returns null for unknown events", async () => {
expect(await fetchInitialEvent(client, room.roomId, "$UNKNOWN")).toBeNull();
expect(await fetchInitialEvent(client, room.roomId, NORMAL_EVENT)).toBeInstanceOf(MatrixEvent);
});
it("creates a thread when needed", async () => {
await fetchInitialEvent(client, room.roomId, THREAD_REPLY);
expect(room.getThread(THREAD_ROOT)).toBeInstanceOf(Thread);
});
});
});