Refactor ThreadSummary to MVVM (#33603)
* Refactor ThreadSummary to MVVM * Stories Snapshot images * Add ThreadSummary ViewModel coverage * Fix ThreadSummary preview avatar rendering * Remove ThreadSummary classnames helper * Fix Prettier * Match ThreadMessagePreview typography * Move folder to correct path and fix storybook path * Catch ThreadSummary preview refresh errors * Update snapshot images + fix prettier * Fix ThreadSummary classNames import * Update Images * Remove wrong path
This commit is contained in:
@@ -20,6 +20,7 @@ import React, {
|
||||
type MouseEvent,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
type EventStatus,
|
||||
EventType,
|
||||
@@ -46,6 +47,8 @@ import {
|
||||
PinnedMessageBadge,
|
||||
ReactionsRowButtonView,
|
||||
ReactionsRowView,
|
||||
ThreadMessagePreviewView,
|
||||
ThreadSummaryView,
|
||||
TileErrorView,
|
||||
useViewModel,
|
||||
} from "@element-hq/web-shared-components";
|
||||
@@ -80,7 +83,6 @@ import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../events/EventTileFactory";
|
||||
import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary";
|
||||
import { ReadReceiptGroup } from "./ReadReceiptGroup";
|
||||
import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
||||
@@ -91,6 +93,7 @@ import { EventPreview } from "./EventPreview";
|
||||
import { E2eStandardPadlockIcon } from "./EventTile/E2eStandardPadlockIcon";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { CardContext } from "../right_panel/context";
|
||||
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
|
||||
import { EventTileViewModel } from "../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||
import { E2eMessageSharedIconViewModel } from "../../../viewmodels/room/timeline/event-tile/E2eMessageSharedIconViewModel";
|
||||
import {
|
||||
@@ -119,6 +122,10 @@ import {
|
||||
MessageTimestampViewModel,
|
||||
type MessageTimestampViewModelProps,
|
||||
} from "../../../viewmodels/room/timeline/event-tile/timestamp/MessageTimestampViewModel.ts";
|
||||
import {
|
||||
ThreadMessagePreviewViewModel,
|
||||
ThreadSummaryViewModel,
|
||||
} from "../../../viewmodels/room/timeline/event-tile/ThreadSummaryViewModel.tsx";
|
||||
import { ReactionsRowButtonViewModel } from "../../../viewmodels/room/timeline/event-tile/reactions/ReactionsRowButtonViewModel";
|
||||
import {
|
||||
MAX_ITEMS_WHEN_LIMITED,
|
||||
@@ -535,7 +542,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
<div className="mx_ThreadPanel_replies">
|
||||
<ThreadsIcon />
|
||||
<span className="mx_ThreadPanel_replies_amount">{threadState.thread.length}</span>
|
||||
<ThreadMessagePreview thread={threadState.thread} />
|
||||
<ThreadMessagePreviewWrapper thread={threadState.thread} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -543,7 +550,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
private renderThreadInfo(threadState: EventTileThreadState): React.ReactNode {
|
||||
if (threadState.shouldShowThreadSummary && threadState.thread) {
|
||||
return (
|
||||
<ThreadSummary mxEvent={this.props.mxEvent} thread={threadState.thread} data-testid="thread-summary" />
|
||||
<ThreadSummaryWrapper
|
||||
mxEvent={this.props.mxEvent}
|
||||
thread={threadState.thread}
|
||||
data-testid="thread-summary"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1552,6 +1563,100 @@ function MessageTimestampWrapper(props: MessageTimestampViewModelProps): JSX.Ele
|
||||
);
|
||||
}
|
||||
|
||||
interface ThreadMessagePreviewWrapperProps {
|
||||
thread: Thread;
|
||||
showDisplayName?: boolean;
|
||||
}
|
||||
|
||||
function ThreadMessagePreviewWrapper({
|
||||
thread,
|
||||
showDisplayName = false,
|
||||
}: Readonly<ThreadMessagePreviewWrapperProps>): JSX.Element {
|
||||
const cli = useMatrixClientContext();
|
||||
const { room, timelineRenderingType, lowBandwidth } = useScopedRoomContext(
|
||||
"room",
|
||||
"timelineRenderingType",
|
||||
"lowBandwidth",
|
||||
);
|
||||
const useOnlyCurrentProfiles = useSettingValue("useOnlyCurrentProfiles");
|
||||
const vm = useCreateAutoDisposedViewModel(
|
||||
() =>
|
||||
new ThreadMessagePreviewViewModel({
|
||||
cli,
|
||||
thread,
|
||||
room,
|
||||
timelineRenderingType,
|
||||
lowBandwidth,
|
||||
useOnlyCurrentProfiles,
|
||||
showDisplayName,
|
||||
avatarClassName: "mx_BaseAvatar",
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
vm.setClient(cli);
|
||||
vm.setThread(thread);
|
||||
vm.setRoom(room);
|
||||
vm.setTimelineRenderingType(timelineRenderingType);
|
||||
vm.setLowBandwidth(lowBandwidth);
|
||||
vm.setUseOnlyCurrentProfiles(useOnlyCurrentProfiles);
|
||||
vm.setShowDisplayName(showDisplayName);
|
||||
}, [vm, cli, thread, room, timelineRenderingType, lowBandwidth, useOnlyCurrentProfiles, showDisplayName]);
|
||||
|
||||
return <ThreadMessagePreviewView vm={vm} />;
|
||||
}
|
||||
|
||||
interface ThreadSummaryWrapperProps extends Omit<React.ComponentPropsWithoutRef<"button">, "aria-label" | "onClick"> {
|
||||
mxEvent: MatrixEvent;
|
||||
thread: Thread;
|
||||
}
|
||||
|
||||
function ThreadSummaryWrapper({
|
||||
mxEvent,
|
||||
thread,
|
||||
className,
|
||||
...props
|
||||
}: Readonly<ThreadSummaryWrapperProps>): JSX.Element {
|
||||
const cli = useMatrixClientContext();
|
||||
const { isCard } = useContext(CardContext);
|
||||
const { narrow, room, timelineRenderingType, lowBandwidth } = useScopedRoomContext(
|
||||
"narrow",
|
||||
"room",
|
||||
"timelineRenderingType",
|
||||
"lowBandwidth",
|
||||
);
|
||||
const useOnlyCurrentProfiles = useSettingValue("useOnlyCurrentProfiles");
|
||||
const vm = useCreateAutoDisposedViewModel(
|
||||
() =>
|
||||
new ThreadSummaryViewModel({
|
||||
cli,
|
||||
mxEvent,
|
||||
thread,
|
||||
narrow,
|
||||
isCard,
|
||||
room,
|
||||
timelineRenderingType,
|
||||
lowBandwidth,
|
||||
useOnlyCurrentProfiles,
|
||||
avatarClassName: "mx_BaseAvatar",
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
vm.setClient(cli);
|
||||
vm.setRootEvent(mxEvent);
|
||||
vm.setThread(thread);
|
||||
vm.setNarrow(narrow);
|
||||
vm.setIsCard(isCard);
|
||||
vm.setRoom(room);
|
||||
vm.setTimelineRenderingType(timelineRenderingType);
|
||||
vm.setLowBandwidth(lowBandwidth);
|
||||
vm.setUseOnlyCurrentProfiles(useOnlyCurrentProfiles);
|
||||
}, [vm, cli, mxEvent, thread, narrow, isCard, room, timelineRenderingType, lowBandwidth, useOnlyCurrentProfiles]);
|
||||
|
||||
return <ThreadSummaryView {...props} vm={vm} className={classNames("mx_ThreadSummary", className)} />;
|
||||
}
|
||||
|
||||
interface ReactionsRowButtonItemProps {
|
||||
mxEvent: MatrixEvent;
|
||||
content: string;
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import { type Thread, ThreadEvent, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { IndicatorIcon } from "@vector-im/compound-web";
|
||||
import { ThreadsSolidIcon, ChevronRightIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { CardContext } from "../right_panel/context";
|
||||
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications";
|
||||
import { notificationLevelToIndicator } from "../../../utils/notifications";
|
||||
import { EventPreviewTile, useEventPreview } from "./EventPreview.tsx";
|
||||
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
thread: Thread;
|
||||
}
|
||||
|
||||
const ThreadSummary: React.FC<IProps> = ({ mxEvent, thread, ...props }) => {
|
||||
const roomContext = useScopedRoomContext("narrow");
|
||||
const cardContext = useContext(CardContext);
|
||||
const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length);
|
||||
const { level } = useUnreadNotifications(thread.room, thread.id);
|
||||
|
||||
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("threads|count_of_reply", { count });
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
className="mx_ThreadSummary"
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
defaultDispatcher.dispatch<ShowThreadPayload>({
|
||||
action: Action.ShowThread,
|
||||
rootEvent: mxEvent,
|
||||
push: cardContext.isCard,
|
||||
});
|
||||
PosthogTrackers.trackInteraction("WebRoomTimelineThreadSummaryButton", ev);
|
||||
}}
|
||||
aria-label={_t("threads|open_thread")}
|
||||
>
|
||||
<IndicatorIcon size="24px" indicator={notificationLevelToIndicator(level)}>
|
||||
<ThreadsSolidIcon />
|
||||
</IndicatorIcon>
|
||||
<span className="mx_ThreadSummary_replies_amount">{countSection}</span>
|
||||
<ThreadMessagePreview thread={thread} showDisplayname={!roomContext.narrow} />
|
||||
<div className="mx_ThreadSummary_chevron">
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface IPreviewProps {
|
||||
thread: Thread;
|
||||
showDisplayname?: boolean;
|
||||
}
|
||||
|
||||
export const ThreadMessagePreview: React.FC<IPreviewProps> = ({ thread, showDisplayname = false }) => {
|
||||
const lastReply = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.replyToEvent) ?? undefined;
|
||||
const preview = useEventPreview(lastReply);
|
||||
|
||||
if (!preview || !lastReply) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MemberAvatar
|
||||
member={lastReply.sender}
|
||||
fallbackUserId={lastReply.getSender()}
|
||||
size="24px"
|
||||
className="mx_ThreadSummary_avatar"
|
||||
/>
|
||||
{showDisplayname && (
|
||||
<div className="mx_ThreadSummary_sender">{lastReply.sender?.name ?? lastReply.getSender()}</div>
|
||||
)}
|
||||
|
||||
{lastReply.isDecryptionFailure() ? (
|
||||
<div
|
||||
className="mx_ThreadSummary_content mx_DecryptionFailureBody"
|
||||
title={_t("timeline|decryption_failure|unable_to_decrypt")}
|
||||
>
|
||||
{_t("timeline|decryption_failure|unable_to_decrypt")}
|
||||
</div>
|
||||
) : (
|
||||
<EventPreviewTile preview={preview} className="mx_ThreadSummary_content" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThreadSummary;
|
||||
Reference in New Issue
Block a user