Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit d92fdc1

Browse files
justjannegermain-ggGermain
authored
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 <[email protected]> Co-authored-by: Germain <[email protected]>
1 parent 750ca78 commit d92fdc1

File tree

11 files changed

+205
-82
lines changed

11 files changed

+205
-82
lines changed

src/components/structures/ThreadView.tsx

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ import React, { createRef, KeyboardEvent } from 'react';
1818
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
1919
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
2020
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
21-
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
22-
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
23-
import { IRelationsRequestOpts } from 'matrix-js-sdk/src/@types/requests';
2421
import { logger } from 'matrix-js-sdk/src/logger';
2522
import classNames from 'classnames';
2623

@@ -236,10 +233,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
236233
thread_id: thread.id,
237234
});
238235
thread.emit(ThreadEvent.ViewThread);
239-
await thread.fetchInitialEvents();
240236
this.updateThreadRelation();
241-
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
242-
this.timelinePanel.current?.refreshTimeline();
237+
this.timelinePanel.current?.refreshTimeline(this.props.initialEvent?.getId());
243238
}
244239

245240
private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void {
@@ -293,40 +288,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
293288
}
294289
};
295290

296-
private nextBatch: string | undefined | null = null;
297-
298-
private onPaginationRequest = async (
299-
timelineWindow: TimelineWindow | null,
300-
direction = Direction.Backward,
301-
limit = 20,
302-
): Promise<boolean> => {
303-
if (!Thread.hasServerSideSupport && timelineWindow) {
304-
timelineWindow.extend(direction, limit);
305-
return true;
306-
}
307-
308-
const opts: IRelationsRequestOpts = {
309-
limit,
310-
};
311-
312-
if (this.nextBatch) {
313-
opts.from = this.nextBatch;
314-
}
315-
316-
let nextBatch: string | null | undefined = null;
317-
if (this.state.thread) {
318-
const response = await this.state.thread.fetchEvents(opts);
319-
nextBatch = response.nextBatch;
320-
this.nextBatch = nextBatch;
321-
}
322-
323-
// Advances the marker on the TimelineWindow to define the correct
324-
// window of events to display on screen
325-
timelineWindow?.extend(direction, limit);
326-
327-
return !!nextBatch;
328-
};
329-
330291
private onFileDrop = (dataTransfer: DataTransfer) => {
331292
const roomId = this.props.mxEvent.getRoomId();
332293
if (roomId) {
@@ -409,7 +370,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
409370
highlightedEventId={highlightedEventId}
410371
eventScrollIntoView={this.props.initialEventScrollIntoView}
411372
onEventScrolledIntoView={this.resetJumpToEvent}
412-
onPaginationRequest={this.onPaginationRequest}
413373
/>
414374
</>;
415375
} else {

src/components/structures/TimelinePanel.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,24 +1409,28 @@ class TimelinePanel extends React.Component<IProps, IState> {
14091409
// quite slow. So we detect that situation and shortcut straight to
14101410
// calling _reloadEvents and updating the state.
14111411

1412-
const timeline = this.props.timelineSet.getTimelineForEvent(eventId);
1413-
if (timeline) {
1414-
// This is a hot-path optimization by skipping a promise tick
1415-
// by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
1416-
this.timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
1412+
// This is a hot-path optimization by skipping a promise tick
1413+
// by repeating a no-op sync branch in
1414+
// TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
1415+
if (this.props.timelineSet.getTimelineForEvent(eventId)) {
1416+
// if we've got an eventId, and the timeline exists, we can skip
1417+
// the promise tick.
1418+
this.timelineWindow.load(eventId, INITIAL_SIZE);
1419+
// in this branch this method will happen in sync time
14171420
onLoaded();
1418-
} else {
1419-
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
1420-
this.buildLegacyCallEventGroupers();
1421-
this.setState({
1422-
events: [],
1423-
liveEvents: [],
1424-
canBackPaginate: false,
1425-
canForwardPaginate: false,
1426-
timelineLoading: true,
1427-
});
1428-
prom.then(onLoaded, onError);
1421+
return;
14291422
}
1423+
1424+
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
1425+
this.buildLegacyCallEventGroupers();
1426+
this.setState({
1427+
events: [],
1428+
liveEvents: [],
1429+
canBackPaginate: false,
1430+
canForwardPaginate: false,
1431+
timelineLoading: true,
1432+
});
1433+
prom.then(onLoaded, onError);
14301434
}
14311435

14321436
// handle the completion of a timeline load or localEchoUpdate, by
@@ -1443,8 +1447,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
14431447
}
14441448

14451449
// Force refresh the timeline before threads support pending events
1446-
public refreshTimeline(): void {
1447-
this.loadTimeline();
1450+
public refreshTimeline(eventId?: string): void {
1451+
this.loadTimeline(eventId, undefined, undefined, false);
14481452
this.reloadEvents();
14491453
}
14501454

src/components/views/context_menus/MessageContextMenu.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,13 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
382382
public render(): JSX.Element {
383383
const cli = MatrixClientPeg.get();
384384
const me = cli.getUserId();
385-
const { mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain } = this.props;
385+
const {
386+
mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain,
387+
...other
388+
} = this.props;
389+
delete other.getRelationsForEvent;
390+
delete other.permalinkCreator;
391+
386392
const eventStatus = mxEvent.status;
387393
const unsentReactionsCount = this.getUnsentReactions().length;
388394
const contentActionable = isContentActionable(mxEvent);
@@ -747,7 +753,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
747753
return (
748754
<React.Fragment>
749755
<IconizedContextMenu
750-
{...this.props}
756+
{...other}
751757
className="mx_MessageContextMenu"
752758
compact={true}
753759
data-testid="mx_MessageContextMenu"

src/components/views/rooms/EventTile.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
540540

541541
private renderThreadInfo(): React.ReactNode {
542542
if (this.state.thread?.id === this.props.mxEvent.getId()) {
543-
return <ThreadSummary mxEvent={this.props.mxEvent} thread={this.state.thread} />;
543+
return <ThreadSummary
544+
mxEvent={this.props.mxEvent}
545+
thread={this.state.thread}
546+
data-testid="thread-summary"
547+
/>;
544548
}
545549

546550
if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) {
@@ -1528,9 +1532,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
15281532

15291533
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
15301534
const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject<UnwrappedEventTile>) => {
1531-
return <TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}>
1532-
<UnwrappedEventTile ref={ref} {...props} />
1533-
</TileErrorBoundary>;
1535+
return <>
1536+
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}>
1537+
<UnwrappedEventTile ref={ref} {...props} />
1538+
</TileErrorBoundary>
1539+
</>;
15341540
});
15351541
export default SafeEventTile;
15361542

src/components/views/rooms/ThreadSummary.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface IProps {
3737
thread: Thread;
3838
}
3939

40-
const ThreadSummary = ({ mxEvent, thread }: IProps) => {
40+
const ThreadSummary = ({ mxEvent, thread, ...props }: IProps) => {
4141
const roomContext = useContext(RoomContext);
4242
const cardContext = useContext(CardContext);
4343
const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length);
@@ -50,6 +50,7 @@ const ThreadSummary = ({ mxEvent, thread }: IProps) => {
5050

5151
return (
5252
<AccessibleButton
53+
{...props}
5354
className="mx_ThreadSummary"
5455
onClick={(ev: ButtonEvent) => {
5556
defaultDispatcher.dispatch<ShowThreadPayload>({
@@ -94,7 +95,9 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi
9495
await cli.decryptEventIfNeeded(lastReply);
9596
return MessagePreviewStore.instance.generatePreviewForEvent(lastReply);
9697
}, [lastReply, content]);
97-
if (!preview) return null;
98+
if (!preview || !lastReply) {
99+
return null;
100+
}
98101

99102
return <>
100103
<MemberAvatar

src/stores/widgets/StopGapWidgetDriver.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,12 +451,8 @@ export class StopGapWidgetDriver extends WidgetDriver {
451451
eventId,
452452
relationType ?? null,
453453
eventType ?? null,
454-
{
455-
from,
456-
to,
457-
limit,
458-
dir,
459-
});
454+
{ from, to, limit, dir },
455+
);
460456

461457
return {
462458
chunk: events.map(e => e.getEffectiveEvent() as IRoomEvent),

src/utils/EventUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,11 @@ export async function fetchInitialEvent(
238238
) {
239239
const threadId = initialEvent.threadRootId;
240240
const room = client.getRoom(roomId);
241+
const mapper = client.getEventMapper();
242+
const rootEvent = room.findEventById(threadId)
243+
?? mapper(await client.fetchRoomEvent(roomId, threadId));
241244
try {
242-
room.createThread(threadId, room.findEventById(threadId), [initialEvent], true);
245+
room.createThread(threadId, rootEvent, [initialEvent], true);
243246
} catch (e) {
244247
logger.warn("Could not find root event: " + threadId);
245248
}

test/components/views/rooms/EventTile-test.tsx

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { act, render } from "@testing-library/react";
17+
import React from "react";
18+
import { act, render, screen, waitFor } from "@testing-library/react";
19+
import { EventType } from "matrix-js-sdk/src/@types/event";
1820
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
1921
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
2022
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
21-
import React from "react";
2223

2324
import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
25+
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
2426
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
2527
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
26-
import { getRoomContext, mkMessage, stubClient } from "../../../test-utils";
28+
import SettingsStore from "../../../../src/settings/SettingsStore";
29+
import { getRoomContext, mkEvent, mkMessage, stubClient } from "../../../test-utils";
2730
import { mkThread } from "../../../test-utils/threads";
2831

2932
describe("EventTile", () => {
@@ -52,9 +55,11 @@ describe("EventTile", () => {
5255
timelineRenderingType: renderingType,
5356
});
5457
return render(
55-
<RoomContext.Provider value={context}>
56-
<TestEventTile {...overrides} />
57-
</RoomContext.Provider>,
58+
<MatrixClientContext.Provider value={client}>
59+
<RoomContext.Provider value={context}>
60+
<TestEventTile {...overrides} />
61+
</RoomContext.Provider>,
62+
</MatrixClientContext.Provider>,
5863
);
5964
}
6065

@@ -69,6 +74,8 @@ describe("EventTile", () => {
6974
});
7075

7176
jest.spyOn(client, "getRoom").mockReturnValue(room);
77+
jest.spyOn(client, "decryptEventIfNeeded").mockResolvedValue();
78+
jest.spyOn(SettingsStore, "getValue").mockImplementation(name => name === "feature_thread");
7279

7380
mxEvent = mkMessage({
7481
room: room.roomId,
@@ -78,6 +85,40 @@ describe("EventTile", () => {
7885
});
7986
});
8087

88+
describe("EventTile thread summary", () => {
89+
beforeEach(() => {
90+
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
91+
});
92+
93+
it("removes the thread summary when thread is deleted", async () => {
94+
const { rootEvent, events: [, reply] } = mkThread({
95+
room,
96+
client,
97+
authorId: "@alice:example.org",
98+
participantUserIds: ["@alice:example.org"],
99+
length: 2, // root + 1 answer
100+
});
101+
getComponent({
102+
mxEvent: rootEvent,
103+
}, TimelineRenderingType.Room);
104+
105+
await waitFor(() => expect(screen.queryByTestId("thread-summary")).not.toBeNull());
106+
107+
const redaction = mkEvent({
108+
event: true,
109+
type: EventType.RoomRedaction,
110+
user: "@alice:example.org",
111+
room: room.roomId,
112+
redacts: reply.getId(),
113+
content: {},
114+
});
115+
116+
act(() => room.processThreadedEvents([redaction], false));
117+
118+
await waitFor(() => expect(screen.queryByTestId("thread-summary")).toBeNull());
119+
});
120+
});
121+
81122
describe("EventTile renderingType: ThreadsList", () => {
82123
beforeEach(() => {
83124
const { rootEvent } = mkThread({

test/test-utils/test-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ type MakeEventPassThruProps = {
212212
};
213213
type MakeEventProps = MakeEventPassThruProps & {
214214
type: string;
215+
redacts?: string;
215216
content: IContent;
216217
room?: Room["roomId"]; // to-device messages are roomless
217218
// eslint-disable-next-line camelcase
@@ -245,6 +246,7 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent {
245246
event_id: "$" + Math.random() + "-" + Math.random(),
246247
origin_server_ts: opts.ts ?? 0,
247248
unsigned: opts.unsigned,
249+
redacts: opts.redacts,
248250
};
249251
if (opts.skey !== undefined) {
250252
event.state_key = opts.skey;

test/test-utils/threads.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { MatrixClient, MatrixEvent, RelationType, Room } from "matrix-js-sdk/src/matrix";
17+
import { MatrixClient, MatrixEvent, MatrixEventEvent, RelationType, Room } from "matrix-js-sdk/src/matrix";
1818
import { Thread } from "matrix-js-sdk/src/models/thread";
1919

2020
import { mkMessage, MessageEventProps } from "./test-utils";
@@ -115,10 +115,18 @@ export const mkThread = ({
115115
ts,
116116
currentUserId: client.getUserId(),
117117
});
118+
expect(rootEvent).toBeTruthy();
119+
120+
for (const evt of events) {
121+
room?.reEmitter.reEmit(evt, [
122+
MatrixEventEvent.BeforeRedaction,
123+
]);
124+
}
118125

119126
const thread = room.createThread(rootEvent.getId(), rootEvent, events, true);
120127
// So that we do not have to mock the thread loading
121128
thread.initialEventsFetched = true;
129+
thread.addEvents(events, true);
122130

123131
return { thread, rootEvent, events };
124132
};

0 commit comments

Comments
 (0)