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:
parent
750ca78e98
commit
d92fdc1f5b
11 changed files with 205 additions and 82 deletions
|
@ -18,9 +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 { IRelationsRequestOpts } from 'matrix-js-sdk/src/@types/requests';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -236,10 +233,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
thread_id: thread.id,
|
||||
});
|
||||
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 {
|
||||
|
@ -293,40 +288,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private nextBatch: string | undefined | null = null;
|
||||
|
||||
private onPaginationRequest = async (
|
||||
timelineWindow: TimelineWindow | null,
|
||||
direction = Direction.Backward,
|
||||
limit = 20,
|
||||
): Promise<boolean> => {
|
||||
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;
|
||||
};
|
||||
|
||||
private onFileDrop = (dataTransfer: DataTransfer) => {
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
if (roomId) {
|
||||
|
@ -409,7 +370,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
highlightedEventId={highlightedEventId}
|
||||
eventScrollIntoView={this.props.initialEventScrollIntoView}
|
||||
onEventScrolledIntoView={this.resetJumpToEvent}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
/>
|
||||
</>;
|
||||
} else {
|
||||
|
|
|
@ -1409,24 +1409,28 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
// 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
|
||||
// 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();
|
||||
} 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);
|
||||
return;
|
||||
}
|
||||
|
||||
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 +1447,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -382,7 +382,13 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
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<IProps, IState>
|
|||
return (
|
||||
<React.Fragment>
|
||||
<IconizedContextMenu
|
||||
{...this.props}
|
||||
{...other}
|
||||
className="mx_MessageContextMenu"
|
||||
compact={true}
|
||||
data-testid="mx_MessageContextMenu"
|
||||
|
|
|
@ -540,7 +540,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
|
||||
private renderThreadInfo(): React.ReactNode {
|
||||
if (this.state.thread?.id === this.props.mxEvent.getId()) {
|
||||
return <ThreadSummary mxEvent={this.props.mxEvent} thread={this.state.thread} />;
|
||||
return <ThreadSummary
|
||||
mxEvent={this.props.mxEvent}
|
||||
thread={this.state.thread}
|
||||
data-testid="thread-summary"
|
||||
/>;
|
||||
}
|
||||
|
||||
if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) {
|
||||
|
@ -1528,9 +1532,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
|
||||
// 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<UnwrappedEventTile>) => {
|
||||
return <TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}>
|
||||
<UnwrappedEventTile ref={ref} {...props} />
|
||||
</TileErrorBoundary>;
|
||||
return <>
|
||||
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}>
|
||||
<UnwrappedEventTile ref={ref} {...props} />
|
||||
</TileErrorBoundary>
|
||||
</>;
|
||||
});
|
||||
export default SafeEventTile;
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
className="mx_ThreadSummary"
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
defaultDispatcher.dispatch<ShowThreadPayload>({
|
||||
|
@ -94,7 +95,9 @@ 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 <>
|
||||
<MemberAvatar
|
||||
|
|
|
@ -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() as IRoomEvent),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue