Fix issues with thread summaries being wrong or stale (#8015)
This commit is contained in:
parent
3d6dece344
commit
48cd83b9d5
3 changed files with 104 additions and 84 deletions
|
@ -75,7 +75,6 @@ import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNo
|
|||
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
|
||||
import { NotificationColor } from '../../../stores/notifications/NotificationColor';
|
||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import { CardContext } from '../right_panel/BaseCard';
|
||||
import { copyPlaintext } from '../../../utils/strings';
|
||||
import { DecryptionFailureTracker } from '../../../DecryptionFailureTracker';
|
||||
import RedactedBody from '../messages/RedactedBody';
|
||||
|
@ -83,6 +82,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|||
import { shouldDisplayReply } from '../../../utils/Reply';
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import TileErrorBoundary from '../messages/TileErrorBoundary';
|
||||
import ThreadSummary, { ThreadMessagePreview } from './ThreadSummary';
|
||||
|
||||
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
||||
|
||||
|
@ -348,9 +348,6 @@ interface IState {
|
|||
isQuoteExpanded?: boolean;
|
||||
|
||||
thread: Thread;
|
||||
threadReplyCount: number;
|
||||
threadLastReply: MatrixEvent;
|
||||
threadLastSender: RoomMember | null;
|
||||
threadNotification?: NotificationCountType;
|
||||
}
|
||||
|
||||
|
@ -397,9 +394,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
hover: false,
|
||||
|
||||
thread,
|
||||
threadReplyCount: thread?.length,
|
||||
threadLastReply: thread?.replyToEvent,
|
||||
threadLastSender: thread?.replyToEvent?.sender,
|
||||
};
|
||||
|
||||
// don't do RR animations until we are mounted
|
||||
|
@ -514,11 +508,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
room?.on(ThreadEvent.New, this.onNewThread);
|
||||
|
||||
if (this.state.threadLastReply?.isEncrypted()) {
|
||||
this.state.threadLastReply.once(MatrixEventEvent.Decrypted, this.onEventDecryption);
|
||||
MatrixClientPeg.get().decryptEventIfNeeded(this.state.threadLastReply);
|
||||
}
|
||||
}
|
||||
|
||||
private setupNotificationListener = (thread: Thread): void => {
|
||||
|
@ -556,12 +545,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
this.setupNotificationListener(thread);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
threadLastReply: thread?.replyToEvent,
|
||||
threadLastSender: thread?.replyToEvent?.sender,
|
||||
threadReplyCount: thread?.length,
|
||||
thread,
|
||||
});
|
||||
this.setState({ thread });
|
||||
};
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
|
@ -601,7 +585,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
if (this.threadState) {
|
||||
this.threadState.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
||||
}
|
||||
this.state.threadLastReply?.removeListener(MatrixEventEvent.Decrypted, this.onEventDecryption);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IProps, prevState: IState, snapshot) {
|
||||
|
@ -610,20 +593,8 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
|
||||
this.isListeningForReceipts = true;
|
||||
}
|
||||
|
||||
if (this.state.threadLastReply !== prevState.threadLastReply) {
|
||||
if (this.state.threadLastReply.isEncrypted()) {
|
||||
this.state.threadLastReply.once(MatrixEventEvent.Decrypted, this.onEventDecryption);
|
||||
MatrixClientPeg.get().decryptEventIfNeeded(this.state.threadLastReply);
|
||||
}
|
||||
prevState.threadLastReply?.removeListener(MatrixEventEvent.Decrypted, this.onEventDecryption);
|
||||
}
|
||||
}
|
||||
|
||||
private onEventDecryption = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onNewThread = (thread: Thread) => {
|
||||
if (thread.id === this.props.mxEvent.getId()) {
|
||||
this.updateThread(thread);
|
||||
|
@ -658,64 +629,17 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
<span className="mx_ThreadPanel_repliesSummary">
|
||||
{ this.state.thread.length }
|
||||
</span>
|
||||
{ this.renderThreadLastMessagePreview() }
|
||||
<ThreadMessagePreview thread={this.state.thread} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
private renderThreadLastMessagePreview(): JSX.Element | null {
|
||||
const { threadLastReply } = this.state;
|
||||
const threadMessagePreview = MessagePreviewStore.instance.generatePreviewForEvent(threadLastReply);
|
||||
|
||||
const sender = this.state.thread?.roomState.getSentinelMember(threadLastReply.getSender());
|
||||
return <>
|
||||
<MemberAvatar
|
||||
member={sender}
|
||||
fallbackUserId={threadLastReply.getSender()}
|
||||
width={24}
|
||||
height={24}
|
||||
className="mx_ThreadInfo_avatar"
|
||||
/>
|
||||
{ threadMessagePreview && (
|
||||
<div className="mx_ThreadInfo_content">
|
||||
<span className="mx_ThreadInfo_message-preview">
|
||||
{ threadMessagePreview }
|
||||
</span>
|
||||
</div>
|
||||
) }
|
||||
</>;
|
||||
}
|
||||
|
||||
private renderThreadInfo(): React.ReactNode {
|
||||
if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) {
|
||||
return (
|
||||
<p className="mx_ThreadSummaryIcon">{ _t("From a thread") }</p>
|
||||
);
|
||||
} else if (this.state.threadReplyCount && this.state.thread.id === this.props.mxEvent.getId()) {
|
||||
let count: string | number = this.state.threadReplyCount;
|
||||
if (!this.context.narrow) {
|
||||
count = _t("%(count)s reply", {
|
||||
count: this.state.threadReplyCount,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<CardContext.Consumer>
|
||||
{ context =>
|
||||
<AccessibleButton
|
||||
className="mx_ThreadInfo"
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
showThread({ rootEvent: this.props.mxEvent, push: context.isCard });
|
||||
PosthogTrackers.trackInteraction("WebRoomTimelineThreadSummaryButton", ev);
|
||||
}}
|
||||
aria-label={_t("Open thread")}
|
||||
>
|
||||
<span className="mx_ThreadInfo_threads-amount">
|
||||
{ count }
|
||||
</span>
|
||||
{ this.renderThreadLastMessagePreview() }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</CardContext.Consumer>
|
||||
);
|
||||
} else if (this.state.thread?.id === this.props.mxEvent.getId()) {
|
||||
return <ThreadSummary mxEvent={this.props.mxEvent} thread={this.state.thread} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
96
src/components/views/rooms/ThreadSummary.tsx
Normal file
96
src/components/views/rooms/ThreadSummary.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { CardContext } from "../right_panel/BaseCard";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
thread: Thread;
|
||||
}
|
||||
|
||||
const ThreadSummary = ({ mxEvent, thread }: IProps) => {
|
||||
const roomContext = useContext(RoomContext);
|
||||
const cardContext = useContext(CardContext);
|
||||
const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length);
|
||||
if (!count) return null; // We don't want to show a thread summary if the thread doesn't have replies yet
|
||||
|
||||
let countSection: string | number = count;
|
||||
if (!roomContext.narrow) {
|
||||
countSection = _t("%(count)s reply", { count });
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_ThreadInfo"
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
showThread({
|
||||
rootEvent: mxEvent,
|
||||
push: cardContext.isCard,
|
||||
});
|
||||
PosthogTrackers.trackInteraction("WebRoomTimelineThreadSummaryButton", ev);
|
||||
}}
|
||||
aria-label={_t("Open thread")}
|
||||
>
|
||||
<span className="mx_ThreadInfo_threads-amount">
|
||||
{ countSection }
|
||||
</span>
|
||||
<ThreadMessagePreview thread={thread} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreadMessagePreview = ({ thread }: Pick<IProps, "thread">) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const lastReply = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.lastReply());
|
||||
const preview = useAsyncMemo(async () => {
|
||||
await cli.decryptEventIfNeeded(lastReply);
|
||||
return MessagePreviewStore.instance.generatePreviewForEvent(lastReply);
|
||||
}, [lastReply]);
|
||||
|
||||
const sender = thread.roomState.getSentinelMember(lastReply.getSender());
|
||||
return <>
|
||||
<MemberAvatar
|
||||
member={sender}
|
||||
fallbackUserId={lastReply.getSender()}
|
||||
width={24}
|
||||
height={24}
|
||||
className="mx_ThreadInfo_avatar"
|
||||
/>
|
||||
{ preview && (
|
||||
<div className="mx_ThreadInfo_content">
|
||||
<span className="mx_ThreadInfo_message-preview">
|
||||
{ preview }
|
||||
</span>
|
||||
</div>
|
||||
) }
|
||||
</>;
|
||||
};
|
||||
|
||||
export default ThreadSummary;
|
Loading…
Add table
Add a link
Reference in a new issue