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

Commit 48cd83b

Browse files
authored
Fix issues with thread summaries being wrong or stale (#8015)
1 parent 3d6dece commit 48cd83b

File tree

3 files changed

+104
-84
lines changed

3 files changed

+104
-84
lines changed

src/components/views/rooms/EventTile.tsx

Lines changed: 5 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNo
7575
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
7676
import { NotificationColor } from '../../../stores/notifications/NotificationColor';
7777
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
78-
import { CardContext } from '../right_panel/BaseCard';
7978
import { copyPlaintext } from '../../../utils/strings';
8079
import { DecryptionFailureTracker } from '../../../DecryptionFailureTracker';
8180
import RedactedBody from '../messages/RedactedBody';
8281
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
8382
import { shouldDisplayReply } from '../../../utils/Reply';
8483
import PosthogTrackers from "../../../PosthogTrackers";
8584
import TileErrorBoundary from '../messages/TileErrorBoundary';
85+
import ThreadSummary, { ThreadMessagePreview } from './ThreadSummary';
8686

8787
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
8888

@@ -348,9 +348,6 @@ interface IState {
348348
isQuoteExpanded?: boolean;
349349

350350
thread: Thread;
351-
threadReplyCount: number;
352-
threadLastReply: MatrixEvent;
353-
threadLastSender: RoomMember | null;
354351
threadNotification?: NotificationCountType;
355352
}
356353

@@ -397,9 +394,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
397394
hover: false,
398395

399396
thread,
400-
threadReplyCount: thread?.length,
401-
threadLastReply: thread?.replyToEvent,
402-
threadLastSender: thread?.replyToEvent?.sender,
403397
};
404398

405399
// don't do RR animations until we are mounted
@@ -514,11 +508,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
514508

515509
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
516510
room?.on(ThreadEvent.New, this.onNewThread);
517-
518-
if (this.state.threadLastReply?.isEncrypted()) {
519-
this.state.threadLastReply.once(MatrixEventEvent.Decrypted, this.onEventDecryption);
520-
MatrixClientPeg.get().decryptEventIfNeeded(this.state.threadLastReply);
521-
}
522511
}
523512

524513
private setupNotificationListener = (thread: Thread): void => {
@@ -556,12 +545,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
556545
this.setupNotificationListener(thread);
557546
}
558547

559-
this.setState({
560-
threadLastReply: thread?.replyToEvent,
561-
threadLastSender: thread?.replyToEvent?.sender,
562-
threadReplyCount: thread?.length,
563-
thread,
564-
});
548+
this.setState({ thread });
565549
};
566550

567551
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@@ -601,7 +585,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
601585
if (this.threadState) {
602586
this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
603587
}
604-
this.state.threadLastReply?.removeListener(MatrixEventEvent.Decrypted, this.onEventDecryption);
605588
}
606589

607590
componentDidUpdate(prevProps: IProps, prevState: IState, snapshot) {
@@ -610,20 +593,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
610593
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
611594
this.isListeningForReceipts = true;
612595
}
613-
614-
if (this.state.threadLastReply !== prevState.threadLastReply) {
615-
if (this.state.threadLastReply.isEncrypted()) {
616-
this.state.threadLastReply.once(MatrixEventEvent.Decrypted, this.onEventDecryption);
617-
MatrixClientPeg.get().decryptEventIfNeeded(this.state.threadLastReply);
618-
}
619-
prevState.threadLastReply?.removeListener(MatrixEventEvent.Decrypted, this.onEventDecryption);
620-
}
621596
}
622597

623-
private onEventDecryption = () => {
624-
this.forceUpdate();
625-
};
626-
627598
private onNewThread = (thread: Thread) => {
628599
if (thread.id === this.props.mxEvent.getId()) {
629600
this.updateThread(thread);
@@ -658,64 +629,17 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
658629
<span className="mx_ThreadPanel_repliesSummary">
659630
{ this.state.thread.length }
660631
</span>
661-
{ this.renderThreadLastMessagePreview() }
632+
<ThreadMessagePreview thread={this.state.thread} />
662633
</div>;
663634
}
664635

665-
private renderThreadLastMessagePreview(): JSX.Element | null {
666-
const { threadLastReply } = this.state;
667-
const threadMessagePreview = MessagePreviewStore.instance.generatePreviewForEvent(threadLastReply);
668-
669-
const sender = this.state.thread?.roomState.getSentinelMember(threadLastReply.getSender());
670-
return <>
671-
<MemberAvatar
672-
member={sender}
673-
fallbackUserId={threadLastReply.getSender()}
674-
width={24}
675-
height={24}
676-
className="mx_ThreadInfo_avatar"
677-
/>
678-
{ threadMessagePreview && (
679-
<div className="mx_ThreadInfo_content">
680-
<span className="mx_ThreadInfo_message-preview">
681-
{ threadMessagePreview }
682-
</span>
683-
</div>
684-
) }
685-
</>;
686-
}
687-
688636
private renderThreadInfo(): React.ReactNode {
689637
if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) {
690638
return (
691639
<p className="mx_ThreadSummaryIcon">{ _t("From a thread") }</p>
692640
);
693-
} else if (this.state.threadReplyCount && this.state.thread.id === this.props.mxEvent.getId()) {
694-
let count: string | number = this.state.threadReplyCount;
695-
if (!this.context.narrow) {
696-
count = _t("%(count)s reply", {
697-
count: this.state.threadReplyCount,
698-
});
699-
}
700-
return (
701-
<CardContext.Consumer>
702-
{ context =>
703-
<AccessibleButton
704-
className="mx_ThreadInfo"
705-
onClick={(ev: ButtonEvent) => {
706-
showThread({ rootEvent: this.props.mxEvent, push: context.isCard });
707-
PosthogTrackers.trackInteraction("WebRoomTimelineThreadSummaryButton", ev);
708-
}}
709-
aria-label={_t("Open thread")}
710-
>
711-
<span className="mx_ThreadInfo_threads-amount">
712-
{ count }
713-
</span>
714-
{ this.renderThreadLastMessagePreview() }
715-
</AccessibleButton>
716-
}
717-
</CardContext.Consumer>
718-
);
641+
} else if (this.state.thread?.id === this.props.mxEvent.getId()) {
642+
return <ThreadSummary mxEvent={this.props.mxEvent} thread={this.state.thread} />;
719643
}
720644
}
721645

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { useContext } from "react";
18+
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
19+
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
20+
21+
import { _t } from "../../../languageHandler";
22+
import { CardContext } from "../right_panel/BaseCard";
23+
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
24+
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
25+
import PosthogTrackers from "../../../PosthogTrackers";
26+
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
27+
import RoomContext from "../../../contexts/RoomContext";
28+
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
29+
import MemberAvatar from "../avatars/MemberAvatar";
30+
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
31+
import MatrixClientContext from "../../../contexts/MatrixClientContext";
32+
33+
interface IProps {
34+
mxEvent: MatrixEvent;
35+
thread: Thread;
36+
}
37+
38+
const ThreadSummary = ({ mxEvent, thread }: IProps) => {
39+
const roomContext = useContext(RoomContext);
40+
const cardContext = useContext(CardContext);
41+
const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length);
42+
if (!count) return null; // We don't want to show a thread summary if the thread doesn't have replies yet
43+
44+
let countSection: string | number = count;
45+
if (!roomContext.narrow) {
46+
countSection = _t("%(count)s reply", { count });
47+
}
48+
49+
return (
50+
<AccessibleButton
51+
className="mx_ThreadInfo"
52+
onClick={(ev: ButtonEvent) => {
53+
showThread({
54+
rootEvent: mxEvent,
55+
push: cardContext.isCard,
56+
});
57+
PosthogTrackers.trackInteraction("WebRoomTimelineThreadSummaryButton", ev);
58+
}}
59+
aria-label={_t("Open thread")}
60+
>
61+
<span className="mx_ThreadInfo_threads-amount">
62+
{ countSection }
63+
</span>
64+
<ThreadMessagePreview thread={thread} />
65+
</AccessibleButton>
66+
);
67+
};
68+
69+
export const ThreadMessagePreview = ({ thread }: Pick<IProps, "thread">) => {
70+
const cli = useContext(MatrixClientContext);
71+
const lastReply = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.lastReply());
72+
const preview = useAsyncMemo(async () => {
73+
await cli.decryptEventIfNeeded(lastReply);
74+
return MessagePreviewStore.instance.generatePreviewForEvent(lastReply);
75+
}, [lastReply]);
76+
77+
const sender = thread.roomState.getSentinelMember(lastReply.getSender());
78+
return <>
79+
<MemberAvatar
80+
member={sender}
81+
fallbackUserId={lastReply.getSender()}
82+
width={24}
83+
height={24}
84+
className="mx_ThreadInfo_avatar"
85+
/>
86+
{ preview && (
87+
<div className="mx_ThreadInfo_content">
88+
<span className="mx_ThreadInfo_message-preview">
89+
{ preview }
90+
</span>
91+
</div>
92+
) }
93+
</>;
94+
};
95+
96+
export default ThreadSummary;

src/i18n/strings/en_EN.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,9 +1661,6 @@
16611661
"Edit message": "Edit message",
16621662
"Mod": "Mod",
16631663
"From a thread": "From a thread",
1664-
"%(count)s reply|other": "%(count)s replies",
1665-
"%(count)s reply|one": "%(count)s reply",
1666-
"Open thread": "Open thread",
16671664
"This event could not be displayed": "This event could not be displayed",
16681665
"Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.",
16691666
"Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.",
@@ -1884,6 +1881,9 @@
18841881
"Admin Tools": "Admin Tools",
18851882
"Revoke invite": "Revoke invite",
18861883
"Invited by %(sender)s": "Invited by %(sender)s",
1884+
"%(count)s reply|other": "%(count)s replies",
1885+
"%(count)s reply|one": "%(count)s reply",
1886+
"Open thread": "Open thread",
18871887
"Jump to first unread message.": "Jump to first unread message.",
18881888
"Mark all as read": "Mark all as read",
18891889
"Unable to access your microphone": "Unable to access your microphone",

0 commit comments

Comments
 (0)