Refactor EventPreview to shared MVVM (#33646)
* Refactor EventPreview to shared MVVM * Fix EventPreviewView export formatting * Snapshots images for stories * Deduplicate event preview formatting * Handle event preview update failures * Coalesce event preview updates * Wait for event preview test expectations
This commit is contained in:
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 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, { type HTMLProps, type JSX, useContext, useState } from "react";
|
||||
import { type IContent, M_POLL_START, type MatrixEvent, MatrixEventEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MessagePreviewStore } from "../../../stores/message-preview";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter.ts";
|
||||
|
||||
/**
|
||||
* The props for the {@link EventPreview} component.
|
||||
*/
|
||||
interface Props extends HTMLProps<HTMLSpanElement> {
|
||||
/**
|
||||
* The event to display the preview for
|
||||
*/
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays a preview for the given event.
|
||||
* Wraps both `useEventPreview` & `EventPreviewTile`.
|
||||
*/
|
||||
export function EventPreview({ mxEvent, className, ...props }: Props): JSX.Element | null {
|
||||
const preview = useEventPreview(mxEvent);
|
||||
if (!preview) return null;
|
||||
|
||||
return <EventPreviewTile {...props} preview={preview} className={className} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* The props for the {@link EventPreviewTile} component.
|
||||
*/
|
||||
interface EventPreviewTileProps extends HTMLProps<HTMLSpanElement> {
|
||||
/**
|
||||
* The preview to display
|
||||
*/
|
||||
preview: Preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays a preview given the output from `useEventPreview`.
|
||||
*/
|
||||
export function EventPreviewTile({
|
||||
preview: [preview, prefix],
|
||||
className,
|
||||
...props
|
||||
}: EventPreviewTileProps): JSX.Element | null {
|
||||
const classes = classNames("mx_EventPreview", className);
|
||||
if (!prefix)
|
||||
return (
|
||||
<span {...props} className={classes} title={preview}>
|
||||
{preview}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<span {...props} className={classes}>
|
||||
{_t(
|
||||
"event_preview|preview",
|
||||
{
|
||||
prefix,
|
||||
preview,
|
||||
},
|
||||
{
|
||||
bold: (sub) => <span className="mx_EventPreview_prefix">{sub}</span>,
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
type Preview = [preview: string, prefix: string | null];
|
||||
|
||||
/**
|
||||
* Hooks to generate a preview for the event.
|
||||
* @param mxEvent
|
||||
*/
|
||||
export function useEventPreview(mxEvent: MatrixEvent | undefined): Preview | null {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
// track the content as a means to regenerate the preview upon edits & decryption
|
||||
const [content, setContent] = useState<IContent | undefined>(mxEvent?.getContent());
|
||||
useTypedEventEmitter(mxEvent ?? undefined, MatrixEventEvent.Replaced, () => {
|
||||
setContent(mxEvent!.getContent());
|
||||
});
|
||||
const awaitDecryption = mxEvent?.shouldAttemptDecryption() || mxEvent?.isBeingDecrypted();
|
||||
useTypedEventEmitter(awaitDecryption ? (mxEvent ?? undefined) : undefined, MatrixEventEvent.Decrypted, () => {
|
||||
setContent(mxEvent!.getContent());
|
||||
});
|
||||
|
||||
return useAsyncMemo(
|
||||
async () => {
|
||||
if (!mxEvent || mxEvent.isRedacted() || mxEvent.isDecryptionFailure()) return null;
|
||||
await cli.decryptEventIfNeeded(mxEvent);
|
||||
return [
|
||||
MessagePreviewStore.instance.generatePreviewForEvent(mxEvent),
|
||||
getPreviewPrefix(mxEvent.getType(), content?.msgtype as MsgType),
|
||||
];
|
||||
},
|
||||
[mxEvent, content],
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prefix for the preview based on the type and the message type.
|
||||
* @param type
|
||||
* @param msgType
|
||||
*/
|
||||
function getPreviewPrefix(type: string, msgType: MsgType): string | null {
|
||||
switch (type) {
|
||||
case M_POLL_START.name:
|
||||
return _t("event_preview|prefix|poll");
|
||||
default:
|
||||
}
|
||||
|
||||
switch (msgType) {
|
||||
case MsgType.Audio:
|
||||
return _t("event_preview|prefix|audio");
|
||||
case MsgType.Image:
|
||||
return _t("event_preview|prefix|image");
|
||||
case MsgType.Video:
|
||||
return _t("event_preview|prefix|video");
|
||||
case MsgType.File:
|
||||
return _t("event_preview|prefix|file");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -62,10 +62,10 @@ import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadP
|
||||
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
||||
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
|
||||
import PinningUtils from "../../../utils/PinningUtils";
|
||||
import { EventPreview } from "./EventPreview";
|
||||
import { ActionBarAdapter } from "./EventTile/ActionBarAdapter";
|
||||
import { E2eStandardPadlockIcon } from "./EventTile/E2eStandardPadlockIcon";
|
||||
import { E2eMessageSharedIconAdapter } from "./EventTile/E2eMessageSharedIconAdapter";
|
||||
import { EventPreviewAdapter } from "./EventTile/EventPreviewAdapter";
|
||||
import { MessageTimestampAdapter } from "./EventTile/MessageTimestampAdapter";
|
||||
import { ReactionsRowAdapter } from "./EventTile/ReactionsRowAdapter";
|
||||
import { ReceiptAdapter } from "./EventTile/ReceiptAdapter";
|
||||
@@ -1259,7 +1259,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
) : this.props.mxEvent.isDecryptionFailure() ? (
|
||||
<DecryptionFailureBodyFactory mxEvent={this.props.mxEvent} />
|
||||
) : (
|
||||
<EventPreview mxEvent={this.props.mxEvent} />
|
||||
<EventPreviewAdapter
|
||||
eventTileViewModel={this.viewModel}
|
||||
mxEvent={this.props.mxEvent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{this.renderThreadPanelSummary(threadState)}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
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, { useEffect, type JSX } from "react";
|
||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { EventPreviewView } from "@element-hq/web-shared-components";
|
||||
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||
|
||||
/**
|
||||
* Props for the {@link EventPreviewAdapter} component.
|
||||
*/
|
||||
interface EventPreviewAdapterProps extends Omit<React.ComponentPropsWithoutRef<"span">, "children" | "title"> {
|
||||
/** View model backing the event tile. */
|
||||
eventTileViewModel: EventTileViewModel;
|
||||
/** Matrix event whose preview is rendered. */
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an event preview inside notification timeline tiles.
|
||||
*/
|
||||
export function EventPreviewAdapter({
|
||||
eventTileViewModel,
|
||||
mxEvent,
|
||||
...props
|
||||
}: Readonly<EventPreviewAdapterProps>): JSX.Element {
|
||||
const cli = useMatrixClientContext();
|
||||
const vm = eventTileViewModel.getEventPreviewViewModel({ cli, mxEvent });
|
||||
|
||||
useEffect(() => {
|
||||
// This child VM owns Matrix listeners, so release it when the view using it leaves the tree.
|
||||
return () => eventTileViewModel.releaseEventPreviewViewModel();
|
||||
}, [eventTileViewModel]);
|
||||
|
||||
useEffect(() => {
|
||||
vm.setClient(cli);
|
||||
}, [cli, vm]);
|
||||
|
||||
useEffect(() => {
|
||||
vm.setEvent(mxEvent);
|
||||
}, [mxEvent, vm]);
|
||||
|
||||
return <EventPreviewView {...props} vm={vm} />;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-soli
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
import { EventPreviewView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
|
||||
|
||||
import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents";
|
||||
import { _t } from "../../../languageHandler";
|
||||
@@ -24,8 +25,9 @@ import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPaylo
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import MessageEvent from "../messages/MessageEvent";
|
||||
import PosthogTrackers from "../../../PosthogTrackers.ts";
|
||||
import { EventPreview } from "./EventPreview.tsx";
|
||||
import { SDKContext } from "../../../contexts/SDKContext.ts";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { EventPreviewViewModel } from "../../../viewmodels/room/timeline/event-tile/EventPreviewViewModel";
|
||||
|
||||
/**
|
||||
* The props for the {@link PinnedMessageBanner} component.
|
||||
@@ -118,7 +120,7 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<EventPreview
|
||||
<EventPreviewWrapper
|
||||
mxEvent={pinnedEvent}
|
||||
className="mx_PinnedMessageBanner_message"
|
||||
data-testid="banner-message"
|
||||
@@ -141,6 +143,25 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
|
||||
);
|
||||
}
|
||||
|
||||
type EventPreviewWrapperProps = Omit<React.ComponentPropsWithoutRef<"span">, "children" | "title"> & {
|
||||
mxEvent: MatrixEvent;
|
||||
};
|
||||
|
||||
function EventPreviewWrapper({ mxEvent, ...props }: Readonly<EventPreviewWrapperProps>): JSX.Element {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const vm = useCreateAutoDisposedViewModel(() => new EventPreviewViewModel({ cli, mxEvent }));
|
||||
|
||||
useEffect(() => {
|
||||
vm.setEvent(mxEvent);
|
||||
}, [mxEvent, vm]);
|
||||
|
||||
useEffect(() => {
|
||||
vm.setClient(cli);
|
||||
}, [cli, vm]);
|
||||
|
||||
return <EventPreviewView {...props} vm={vm} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the banner is displayed or hidden, we want to notify the timeline to resize itself.
|
||||
* @param pinnedEvent
|
||||
|
||||
Reference in New Issue
Block a user