Fix race conditions around threads (#8448)

pull/28217/head
Michael Telatynski 2022-05-03 14:25:08 +01:00 committed by GitHub
parent 1aaaad2f32
commit f29ef04751
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 37 additions and 67 deletions

View File

@ -1398,7 +1398,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name); .getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
if (!bundledRelationship || event.getThread()) continue; if (!bundledRelationship || event.getThread()) continue;
const room = this.context.getRoom(event.getRoomId()); const room = this.context.getRoom(event.getRoomId());
event.setThread(room.findThreadForEvent(event) ?? room.createThread(event, [], true)); const thread = room.findThreadForEvent(event);
if (thread) {
event.setThread(thread);
} else {
room.createThread(event.getId(), event, [], true);
}
} }
} }
} }

View File

@ -191,7 +191,6 @@ const ThreadPanel: React.FC<IProps> = ({
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All); const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
const [room, setRoom] = useState<Room | null>(null); const [room, setRoom] = useState<Room | null>(null);
const [threadCount, setThreadCount] = useState<number>(0);
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null); const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
const [narrow, setNarrow] = useState<boolean>(false); const [narrow, setNarrow] = useState<boolean>(false);
@ -206,23 +205,13 @@ const ThreadPanel: React.FC<IProps> = ({
}, [mxClient, roomId]); }, [mxClient, roomId]);
useEffect(() => { useEffect(() => {
function onNewThread(): void {
setThreadCount(room.threads.size);
}
function refreshTimeline() { function refreshTimeline() {
if (timelineSet) timelinePanel.current.refreshTimeline(); timelinePanel?.current.refreshTimeline();
} }
if (room) { room?.on(ThreadEvent.Update, refreshTimeline);
setThreadCount(room.threads.size);
room.on(ThreadEvent.New, onNewThread);
room.on(ThreadEvent.Update, refreshTimeline);
}
return () => { return () => {
room?.removeListener(ThreadEvent.New, onNewThread);
room?.removeListener(ThreadEvent.Update, refreshTimeline); room?.removeListener(ThreadEvent.Update, refreshTimeline);
}; };
}, [room, mxClient, timelineSet]); }, [room, mxClient, timelineSet]);
@ -260,7 +249,7 @@ const ThreadPanel: React.FC<IProps> = ({
header={<ThreadPanelHeader header={<ThreadPanelHeader
filterOption={filterOption} filterOption={filterOption}
setFilterOption={setFilterOption} setFilterOption={setFilterOption}
empty={threadCount === 0} empty={!timelineSet?.getLiveTimeline()?.getEvents().length}
/>} />}
footer={<> footer={<>
<BetaPill <BetaPill

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React, { createRef, KeyboardEvent } from 'react'; import React, { createRef, KeyboardEvent } from 'react';
import { Thread, ThreadEvent, THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread'; import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
@ -66,7 +66,6 @@ interface IProps {
interface IState { interface IState {
thread?: Thread; thread?: Thread;
lastThreadReply?: MatrixEvent;
layout: Layout; layout: Layout;
editState?: EditorStateTransfer; editState?: EditorStateTransfer;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
@ -104,7 +103,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
this.teardownThread();
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
const roomId = this.props.mxEvent.getRoomId(); const roomId = this.props.mxEvent.getRoomId();
const room = MatrixClientPeg.get().getRoom(roomId); const room = MatrixClientPeg.get().getRoom(roomId);
@ -123,7 +121,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
public componentDidUpdate(prevProps) { public componentDidUpdate(prevProps) {
if (prevProps.mxEvent !== this.props.mxEvent) { if (prevProps.mxEvent !== this.props.mxEvent) {
this.teardownThread();
this.setupThread(this.props.mxEvent); this.setupThread(this.props.mxEvent);
} }
@ -134,7 +131,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
private onAction = (payload: ActionPayload): void => { private onAction = (payload: ActionPayload): void => {
if (payload.phase == RightPanelPhases.ThreadView && payload.event) { if (payload.phase == RightPanelPhases.ThreadView && payload.event) {
this.teardownThread();
this.setupThread(payload.event); this.setupThread(payload.event);
} }
switch (payload.action) { switch (payload.action) {
@ -164,23 +160,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
}; };
private setupThread = (mxEv: MatrixEvent) => { private setupThread = (mxEv: MatrixEvent) => {
let thread = this.props.room.threads?.get(mxEv.getId()); let thread = this.props.room.getThread(mxEv.getId());
if (!thread) { if (!thread) {
thread = this.props.room.createThread(mxEv, [mxEv], true); thread = this.props.room.createThread(mxEv.getId(), mxEv, [mxEv], true);
} }
thread.on(ThreadEvent.Update, this.updateLastThreadReply);
this.updateThread(thread); this.updateThread(thread);
}; };
private teardownThread = () => {
if (this.state.thread) {
this.state.thread.removeListener(ThreadEvent.Update, this.updateLastThreadReply);
}
};
private onNewThread = (thread: Thread) => { private onNewThread = (thread: Thread) => {
if (thread.id === this.props.mxEvent.getId()) { if (thread.id === this.props.mxEvent.getId()) {
this.teardownThread();
this.setupThread(this.props.mxEvent); this.setupThread(this.props.mxEvent);
} }
}; };
@ -189,33 +177,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
if (thread && this.state.thread !== thread) { if (thread && this.state.thread !== thread) {
this.setState({ this.setState({
thread, thread,
lastThreadReply: thread.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
}),
}, async () => { }, async () => {
thread.emit(ThreadEvent.ViewThread); thread.emit(ThreadEvent.ViewThread);
if (!thread.initialEventsFetched) { await thread.fetchInitialEvents();
const response = await thread.fetchInitialEvents(); this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
if (response?.nextBatch) {
this.nextBatch = response.nextBatch;
}
}
this.timelinePanel.current?.refreshTimeline(); this.timelinePanel.current?.refreshTimeline();
}); });
} }
}; };
private updateLastThreadReply = () => {
if (this.state.thread) {
this.setState({
lastThreadReply: this.state.thread.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
}),
});
}
};
private resetJumpToEvent = (event?: string): void => { private resetJumpToEvent = (event?: string): void => {
if (this.props.initialEvent && this.props.initialEventScrollIntoView && if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
event === this.props.initialEvent?.getId()) { event === this.props.initialEvent?.getId()) {
@ -298,12 +268,16 @@ export default class ThreadView extends React.Component<IProps, IState> {
}; };
private get threadRelation(): IEventRelation { private get threadRelation(): IEventRelation {
const lastThreadReply = this.state.thread?.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
});
return { return {
"rel_type": THREAD_RELATION_TYPE.name, "rel_type": THREAD_RELATION_TYPE.name,
"event_id": this.state.thread?.id, "event_id": this.state.thread?.id,
"is_falling_back": true, "is_falling_back": true,
"m.in_reply_to": { "m.in_reply_to": {
"event_id": this.state.lastThreadReply?.getId() ?? this.state.thread?.id, "event_id": lastThreadReply?.getId() ?? this.state.thread?.id,
}, },
}; };
} }
@ -356,6 +330,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
{ this.state.thread && <div className="mx_ThreadView_timelinePanelWrapper"> { this.state.thread && <div className="mx_ThreadView_timelinePanelWrapper">
<FileDropTarget parent={this.card.current} onFileDrop={this.onFileDrop} /> <FileDropTarget parent={this.card.current} onFileDrop={this.onFileDrop} />
<TimelinePanel <TimelinePanel
key={this.state?.thread?.id}
ref={this.timelinePanel} ref={this.timelinePanel}
showReadReceipts={false} // Hide the read receipts showReadReceipts={false} // Hide the read receipts
// until homeservers speak threads language // until homeservers speak threads language

View File

@ -499,16 +499,18 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
return null; return null;
} }
let thread = this.props.mxEvent.getThread();
/** /**
* Accessing the threads value through the room due to a race condition * Accessing the threads value through the room due to a race condition
* that will be solved when there are proper backend support for threads * that will be solved when there are proper backend support for threads
* We currently have no reliable way to discover than an event is a thread * We currently have no reliable way to discover than an event is a thread
* when we are at the sync stage * when we are at the sync stage
*/ */
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); if (!thread) {
const thread = room?.threads?.get(this.props.mxEvent.getId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
thread = room?.findThreadForEvent(this.props.mxEvent);
return thread || null; }
return thread ?? null;
} }
private renderThreadPanelSummary(): JSX.Element | null { private renderThreadPanelSummary(): JSX.Element | null {

View File

@ -31,10 +31,8 @@ export class ThreadsRoomNotificationState extends NotificationState implements I
constructor(public readonly room: Room) { constructor(public readonly room: Room) {
super(); super();
if (this.room?.threads) { for (const thread of this.room.getThreads()) {
for (const [, thread] of this.room.threads) { this.onNewThread(thread);
this.onNewThread(thread);
}
} }
this.room.on(ThreadEvent.New, this.onNewThread); this.room.on(ThreadEvent.New, this.onNewThread);
} }

View File

@ -217,7 +217,8 @@ export function isVoiceMessage(mxEvent: MatrixEvent): boolean {
export async function fetchInitialEvent( export async function fetchInitialEvent(
client: MatrixClient, client: MatrixClient,
roomId: string, roomId: string,
eventId: string): Promise<MatrixEvent | null> { eventId: string,
): Promise<MatrixEvent | null> {
let initialEvent: MatrixEvent; let initialEvent: MatrixEvent;
try { try {
@ -228,14 +229,13 @@ export async function fetchInitialEvent(
initialEvent = null; initialEvent = null;
} }
if (initialEvent?.isThreadRelation && client.supportsExperimentalThreads()) { if (initialEvent?.isThreadRelation && client.supportsExperimentalThreads() && !initialEvent.getThread()) {
const threadId = initialEvent.threadRootId;
const room = client.getRoom(roomId);
try { try {
const rootEventData = await client.fetchRoomEvent(roomId, initialEvent.threadRootId); room.createThread(threadId, room.findEventById(threadId), [initialEvent], true);
const rootEvent = new MatrixEvent(rootEventData);
const room = client.getRoom(roomId);
room.createThread(rootEvent, [rootEvent], true);
} catch (e) { } catch (e) {
logger.warn("Could not find root event: " + initialEvent.threadRootId); logger.warn("Could not find root event: " + threadId);
} }
} }

View File

@ -381,6 +381,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl
client, client,
myUserId: client?.getUserId(), myUserId: client?.getUserId(),
canInvite: jest.fn(), canInvite: jest.fn(),
getThreads: jest.fn().mockReturnValue([]),
} as unknown as Room; } as unknown as Room;
} }

View File

@ -92,5 +92,5 @@ export const makeThreadEvents = ({
export const makeThread = (client: MatrixClient, room: Room, props: MakeThreadEventsProps): Thread => { export const makeThread = (client: MatrixClient, room: Room, props: MakeThreadEventsProps): Thread => {
const { rootEvent, events } = makeThreadEvents(props); const { rootEvent, events } = makeThreadEvents(props);
return new Thread(rootEvent, { initialEvents: events, room, client }); return new Thread(rootEvent.getId(), rootEvent, { initialEvents: events, room, client });
}; };