mirror of https://github.com/vector-im/riot-web
Fix race conditions around threads (#8448)
parent
1aaaad2f32
commit
f29ef04751
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue