From 6c9ec4330be2f17d0cb446cef42801959b2c7f76 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 29 Sep 2022 03:00:42 +0200 Subject: [PATCH 01/17] Fix bug with read receipts --- src/components/structures/MessagePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 223eb0a6dbf..121948faa49 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -831,7 +831,7 @@ export default class MessagePanel extends React.Component { } const receiptDestination: ReadReceipt> = this.context.threadId - ? room.getThread(this.context.threadId) + ? room.getThread(this.context.threadId) ?? room : room; const receipts: IReadReceiptProps[] = []; From 3696f9b65b750cb7a681ea259dea9e7a38d950ba Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 29 Sep 2022 03:01:15 +0200 Subject: [PATCH 02/17] Fix bug with message context menu --- .../views/context_menus/MessageContextMenu.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index eca720412d2..f6961b627ea 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -382,7 +382,13 @@ export default class MessageContextMenu extends React.Component public render(): JSX.Element { const cli = MatrixClientPeg.get(); const me = cli.getUserId(); - const { mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain } = this.props; + const { + mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain, + ...other + } = this.props; + delete other.getRelationsForEvent; + delete other.permalinkCreator; + const eventStatus = mxEvent.status; const unsentReactionsCount = this.getUnsentReactions().length; const contentActionable = isContentActionable(mxEvent); @@ -747,7 +753,7 @@ export default class MessageContextMenu extends React.Component return ( Date: Thu, 6 Oct 2022 01:56:09 +0200 Subject: [PATCH 03/17] fix bug where ThreadSummary failed if no last reply is available --- src/components/views/rooms/ThreadSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx index c14a4cc9e14..c9e7b300181 100644 --- a/src/components/views/rooms/ThreadSummary.tsx +++ b/src/components/views/rooms/ThreadSummary.tsx @@ -94,7 +94,7 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi await cli.decryptEventIfNeeded(lastReply); return MessagePreviewStore.instance.generatePreviewForEvent(lastReply); }, [lastReply, content]); - if (!preview) return null; + if (!preview || !lastReply) return null; return <> Date: Thu, 6 Oct 2022 01:56:54 +0200 Subject: [PATCH 04/17] disable promise short-circuiting for now --- src/components/structures/TimelinePanel.tsx | 32 ++++++++------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 7ddeca11bc5..01911c2768d 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1409,24 +1409,16 @@ class TimelinePanel extends React.Component { // quite slow. So we detect that situation and shortcut straight to // calling _reloadEvents and updating the state. - const timeline = this.props.timelineSet.getTimelineForEvent(eventId); - if (timeline) { - // This is a hot-path optimization by skipping a promise tick - // by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline - this.timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time - onLoaded(); - } else { - const prom = this.timelineWindow.load(eventId, INITIAL_SIZE); - this.buildLegacyCallEventGroupers(); - this.setState({ - events: [], - liveEvents: [], - canBackPaginate: false, - canForwardPaginate: false, - timelineLoading: true, - }); - prom.then(onLoaded, onError); - } + const prom = this.timelineWindow.load(eventId, INITIAL_SIZE); + this.buildLegacyCallEventGroupers(); + this.setState({ + events: [], + liveEvents: [], + canBackPaginate: false, + canForwardPaginate: false, + timelineLoading: true, + }); + prom.then(onLoaded, onError); } // handle the completion of a timeline load or localEchoUpdate, by @@ -1443,8 +1435,8 @@ class TimelinePanel extends React.Component { } // Force refresh the timeline before threads support pending events - public refreshTimeline(): void { - this.loadTimeline(); + public refreshTimeline(eventId?: string): void { + this.loadTimeline(eventId, undefined, undefined, false); this.reloadEvents(); } From ea13433dcaccee7c855ab712bf5969fd52842f96 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 13 Oct 2022 01:22:53 +0200 Subject: [PATCH 05/17] Fix relations direction API --- src/stores/widgets/StopGapWidgetDriver.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index e1fb1d6729a..5be5dd6a27d 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -451,12 +451,8 @@ export class StopGapWidgetDriver extends WidgetDriver { eventId, relationType ?? null, eventType ?? null, - { - from, - to, - limit, - dir, - }); + { from, to, limit, dir }, + ); return { chunk: events.map(e => e.getEffectiveEvent()), From 89446491256303dd980d8d493d6f7f555fb4a5a7 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 13 Oct 2022 01:23:42 +0200 Subject: [PATCH 06/17] Use same API for threads as for any other timeline --- src/components/structures/ThreadView.tsx | 33 ++---------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 8350b5e734a..94a72b11baa 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -20,7 +20,6 @@ import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room'; import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; -import { IRelationsRequestOpts } from 'matrix-js-sdk/src/@types/requests'; import { logger } from 'matrix-js-sdk/src/logger'; import classNames from 'classnames'; @@ -226,10 +225,8 @@ export default class ThreadView extends React.Component { private async postThreadUpdate(thread: Thread): Promise { thread.emit(ThreadEvent.ViewThread); - await thread.fetchInitialEvents(); this.updateThreadRelation(); - this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward); - this.timelinePanel.current?.refreshTimeline(); + this.timelinePanel.current?.refreshTimeline(this.props.initialEvent?.getId()); } private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void { @@ -283,38 +280,12 @@ export default class ThreadView extends React.Component { } }; - private nextBatch: string | undefined | null = null; - private onPaginationRequest = async ( timelineWindow: TimelineWindow | null, direction = Direction.Backward, limit = 20, ): Promise => { - if (!Thread.hasServerSideSupport && timelineWindow) { - timelineWindow.extend(direction, limit); - return true; - } - - const opts: IRelationsRequestOpts = { - limit, - }; - - if (this.nextBatch) { - opts.from = this.nextBatch; - } - - let nextBatch: string | null | undefined = null; - if (this.state.thread) { - const response = await this.state.thread.fetchEvents(opts); - nextBatch = response.nextBatch; - this.nextBatch = nextBatch; - } - - // Advances the marker on the TimelineWindow to define the correct - // window of events to display on screen - timelineWindow?.extend(direction, limit); - - return !!nextBatch; + return timelineWindow.paginate(direction, limit); }; private onFileDrop = (dataTransfer: DataTransfer) => { From 80d86594691d46f2b06ace8cc09895ddc6ace9f3 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Thu, 13 Oct 2022 01:24:27 +0200 Subject: [PATCH 07/17] Determine if event belongs to thread on jumping to event This allows us to jump to any event in any thread, even if neither the thread nor the event are part of any timeline yet --- src/utils/EventUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index d8cf66e5573..69e322ac6dc 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -238,8 +238,11 @@ export async function fetchInitialEvent( ) { const threadId = initialEvent.threadRootId; const room = client.getRoom(roomId); + const mapper = client.getEventMapper(); + const rootEvent = room.findEventById(threadId) + ?? mapper(await client.fetchRoomEvent(roomId, threadId)); try { - room.createThread(threadId, room.findEventById(threadId), [initialEvent], true); + room.createThread(threadId, rootEvent, [initialEvent], true); } catch (e) { logger.warn("Could not find root event: " + threadId); } From 615a4aa6b765bc954d7aecc23c991df0ae5f9384 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Fri, 14 Oct 2022 13:00:27 +0200 Subject: [PATCH 08/17] re-enable promise short-circuiting --- src/components/structures/TimelinePanel.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 01911c2768d..fe7ecc82484 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1409,6 +1409,18 @@ class TimelinePanel extends React.Component { // quite slow. So we detect that situation and shortcut straight to // calling _reloadEvents and updating the state. + // This is a hot-path optimization by skipping a promise tick + // by repeating a no-op sync branch in + // TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline + if (this.props.timelineSet.getTimelineForEvent(eventId)) { + // if we've got an eventId, and the timeline exists, we can skip + // the promise tick. + this.timelineWindow.load(eventId, INITIAL_SIZE); + // in this branch this method will happen in sync time + onLoaded(); + return; + } + const prom = this.timelineWindow.load(eventId, INITIAL_SIZE); this.buildLegacyCallEventGroupers(); this.setState({ From 468135bd241718b17c3e93258a0a502c84a1dcc3 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 17 Oct 2022 12:26:22 +0200 Subject: [PATCH 09/17] properly listen to thread deletion --- src/components/views/rooms/EventTile.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 670a291a422..2603d526d77 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -485,6 +485,7 @@ export class UnwrappedEventTile extends React.Component const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); room?.off(ThreadEvent.New, this.onNewThread); + room?.off(ThreadEvent.Delete, this.onThreadDeleted); if (this.threadState) { this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate); } @@ -503,6 +504,16 @@ export class UnwrappedEventTile extends React.Component this.updateThread(thread); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); room.off(ThreadEvent.New, this.onNewThread); + room.on(ThreadEvent.Delete, this.onThreadDeleted); + } + }; + + private onThreadDeleted = (thread: Thread) => { + if (thread.id === this.props.mxEvent.getId()) { + this.updateThread(thread); + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + room.on(ThreadEvent.New, this.onNewThread); + room.off(ThreadEvent.Delete, this.onThreadDeleted); } }; From 4988bbd94531bc6620347ccce7b2dfc4a287bc3e Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 24 Oct 2022 12:09:59 +0200 Subject: [PATCH 10/17] adapt test to changed thread API --- test/test-utils/threads.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index 2259527178a..f67a8845b5c 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -115,10 +115,12 @@ export const mkThread = ({ ts, currentUserId: client.getUserId(), }); + expect(rootEvent).toBeTruthy(); 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 }; }; From 13a0f0a7b97009c8f036b9b16c376fdeca60cf41 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Tue, 25 Oct 2022 14:11:47 +0200 Subject: [PATCH 11/17] ci: empty commit to force CI to run again From e438d9826da0fee15f7967ca3254a50c6e795620 Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 25 Oct 2022 21:48:16 +0100 Subject: [PATCH 12/17] Add thread redaction tests --- src/components/views/rooms/EventTile.tsx | 6 ++- src/components/views/rooms/ThreadSummary.tsx | 7 ++- .../components/views/rooms/EventTile-test.tsx | 53 ++++++++++++++++--- test/test-utils/test-utils.ts | 2 + test/test-utils/threads.ts | 10 +++- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 2603d526d77..c4a7027eca7 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -551,7 +551,11 @@ export class UnwrappedEventTile extends React.Component private renderThreadInfo(): React.ReactNode { if (this.state.thread?.id === this.props.mxEvent.getId()) { - return ; + return ; } if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) { diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx index c9e7b300181..83dbf0d45a8 100644 --- a/src/components/views/rooms/ThreadSummary.tsx +++ b/src/components/views/rooms/ThreadSummary.tsx @@ -37,7 +37,7 @@ interface IProps { thread: Thread; } -const ThreadSummary = ({ mxEvent, thread }: IProps) => { +const ThreadSummary = ({ mxEvent, thread, ...props }: IProps) => { const roomContext = useContext(RoomContext); const cardContext = useContext(CardContext); const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length); @@ -50,6 +50,7 @@ const ThreadSummary = ({ mxEvent, thread }: IProps) => { return ( { defaultDispatcher.dispatch({ @@ -94,7 +95,9 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi await cli.decryptEventIfNeeded(lastReply); return MessagePreviewStore.instance.generatePreviewForEvent(lastReply); }, [lastReply, content]); - if (!preview || !lastReply) return null; + if (!preview || !lastReply) { + return null; + } return <> { @@ -52,9 +55,11 @@ describe("EventTile", () => { timelineRenderingType: renderingType, }); return render( - - - , + + + + , + , ); } @@ -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({ diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 97a17e3b70a..3f588462514 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -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; diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index f67a8845b5c..7e29b5786f1 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -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"; @@ -117,10 +117,16 @@ export const mkThread = ({ }); 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); + room.processThreadedEvents(events, true); return { thread, rootEvent, events }; }; From 7cd72c30952793f95f32ebde224663551fefb615 Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 25 Oct 2022 22:21:00 +0100 Subject: [PATCH 13/17] fix mkThread --- test/test-utils/threads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index 7e29b5786f1..3b07c45051b 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -126,7 +126,7 @@ export const mkThread = ({ const thread = room.createThread(rootEvent.getId(), rootEvent, events, true); // So that we do not have to mock the thread loading thread.initialEventsFetched = true; - room.processThreadedEvents(events, true); + thread.addEvents(events, true); return { thread, rootEvent, events }; }; From fedc5d82ab00198e0adc2849256003bc55b45b42 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 26 Oct 2022 09:56:03 +0100 Subject: [PATCH 14/17] Add fetchInitialEvent tests --- test/utils/EventUtils-test.ts | 96 ++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts index 120d47aa1d4..644f274c19c 100644 --- a/test/utils/EventUtils-test.ts +++ b/test/utils/EventUtils-test.ts @@ -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> = { + [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); + }); + }); }); From 498f6a1890fd321ee0766684e2016cd32429c709 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 26 Oct 2022 12:31:10 +0100 Subject: [PATCH 15/17] Paginate using default TimelinePanel behaviour --- src/components/structures/ThreadView.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 37a99ac0ede..3463bcd3049 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -18,8 +18,6 @@ import React, { createRef, KeyboardEvent } from 'react'; import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room'; import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; -import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; import { logger } from 'matrix-js-sdk/src/logger'; import classNames from 'classnames'; @@ -290,14 +288,6 @@ export default class ThreadView extends React.Component { } }; - private onPaginationRequest = async ( - timelineWindow: TimelineWindow | null, - direction = Direction.Backward, - limit = 20, - ): Promise => { - return timelineWindow.paginate(direction, limit); - }; - private onFileDrop = (dataTransfer: DataTransfer) => { const roomId = this.props.mxEvent.getRoomId(); if (roomId) { @@ -380,7 +370,6 @@ export default class ThreadView extends React.Component { highlightedEventId={highlightedEventId} eventScrollIntoView={this.props.initialEventScrollIntoView} onEventScrolledIntoView={this.resetJumpToEvent} - onPaginationRequest={this.onPaginationRequest} /> ; } else { From 1aef92acfc7c6766f1c0ded81afaea40278c0416 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 26 Oct 2022 13:59:42 +0100 Subject: [PATCH 16/17] Revert "Fix bug with read receipts" This reverts commit 6c9ec4330be2f17d0cb446cef42801959b2c7f76. --- src/components/structures/MessagePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 121948faa49..223eb0a6dbf 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -831,7 +831,7 @@ export default class MessagePanel extends React.Component { } const receiptDestination: ReadReceipt> = this.context.threadId - ? room.getThread(this.context.threadId) ?? room + ? room.getThread(this.context.threadId) : room; const receipts: IReadReceiptProps[] = []; From 215c2f384bf677a33c2b504d083bd6cd713dcd12 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 26 Oct 2022 14:11:26 +0100 Subject: [PATCH 17/17] Remove unused threads deleted code --- src/components/views/rooms/EventTile.tsx | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index c4a7027eca7..f6b249b2913 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -485,7 +485,6 @@ export class UnwrappedEventTile extends React.Component const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); room?.off(ThreadEvent.New, this.onNewThread); - room?.off(ThreadEvent.Delete, this.onThreadDeleted); if (this.threadState) { this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate); } @@ -504,16 +503,6 @@ export class UnwrappedEventTile extends React.Component this.updateThread(thread); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); room.off(ThreadEvent.New, this.onNewThread); - room.on(ThreadEvent.Delete, this.onThreadDeleted); - } - }; - - private onThreadDeleted = (thread: Thread) => { - if (thread.id === this.props.mxEvent.getId()) { - this.updateThread(thread); - const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - room.on(ThreadEvent.New, this.onNewThread); - room.off(ThreadEvent.Delete, this.onThreadDeleted); } }; @@ -1543,9 +1532,11 @@ export class UnwrappedEventTile extends React.Component // Wrap all event tiles with the tile error boundary so that any throws even during construction are captured const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject) => { - return - - ; + return <> + + + + ; }); export default SafeEventTile;