Move EventTile to shared components - #4a (#34469)
* Add EventTile root properties to view model * Move render-related event reads into the vm * Remove duplicate root derivation in UnwrappedEventTile * Move data-has-reply derivation into EventTileViewModel * Move getEventDisplayInfo() and dependencies into EventTileViewModel * Add tests for improved coverage * Cleanup and comments * Fix: Replacement events have a fallback tile but must not show their own reply chain * Combine dependency and prop updates into one atomic VM update * Remove property which was never read and fix oxfmt issue
This commit is contained in:
@@ -18,8 +18,6 @@ import React, {
|
||||
} from "react";
|
||||
import {
|
||||
EventStatus,
|
||||
EventType,
|
||||
MsgType,
|
||||
type MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
type Relations,
|
||||
@@ -50,19 +48,13 @@ import { type ComposerInsertPayload } from "../../../dispatcher/payloads/Compose
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import { type IReadReceiptPosition } from "./ReadReceiptMarker";
|
||||
import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import {
|
||||
haveRendererForEvent,
|
||||
isMessageEvent,
|
||||
renderTile,
|
||||
type EventTileTypeProps,
|
||||
} from "../../../events/EventTileFactory";
|
||||
import { isMessageEvent, renderTile, type EventTileTypeProps } from "../../../events/EventTileFactory";
|
||||
import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
||||
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
|
||||
@@ -80,7 +72,9 @@ import { EventTileThreadInfo, EventTileThreadPanelSummary } from "./EventTile/Ev
|
||||
import { EventTileTimestampSlot } from "./EventTile/EventTileTimestampSlot";
|
||||
import {
|
||||
EventTileViewModel,
|
||||
type EventTileRenderState,
|
||||
type EventTileViewModelProps,
|
||||
type EventTileViewModelDependencies,
|
||||
} from "../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||
import {
|
||||
getEventTileReceiptState,
|
||||
@@ -91,7 +85,6 @@ import {
|
||||
getEventTileThreadState,
|
||||
type EventTileThreadState,
|
||||
} from "../../../viewmodels/room/timeline/event-tile/EventTileThreadState";
|
||||
import { getEventTileReplyChainState } from "../../../viewmodels/room/timeline/event-tile/EventTileReplyChainState";
|
||||
import {
|
||||
eventTileActionBarFocusChange,
|
||||
eventTileBlurWithin,
|
||||
@@ -279,19 +272,12 @@ interface IState {
|
||||
}
|
||||
|
||||
interface EventTileRenderInputs {
|
||||
displayInfo: ReturnType<typeof getEventDisplayInfo>;
|
||||
hasPinnedMessageBadge: boolean;
|
||||
hasReactionsRow: boolean;
|
||||
threadState: EventTileThreadState;
|
||||
isOwnEvent: boolean;
|
||||
}
|
||||
|
||||
interface EventTileRootRenderState {
|
||||
tileClasses: string;
|
||||
tileAriaLive?: "off";
|
||||
scrollToken?: string;
|
||||
}
|
||||
|
||||
/** EventTile implementation rendered inside a RoomContext with `timelineRenderingType` set. */
|
||||
export class UnwrappedEventTile extends React.Component<EventTileProps, IState> {
|
||||
private suppressReadReceiptAnimation: boolean;
|
||||
@@ -332,7 +318,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
thread,
|
||||
};
|
||||
|
||||
this.viewModel = new EventTileViewModel(this.createViewModelProps());
|
||||
this.viewModel = new EventTileViewModel(this.createViewModelDependencies(), this.createViewModelProps());
|
||||
|
||||
this.e2eViewModel = new EventTileE2eViewModel({
|
||||
cli: MatrixClientPeg.safeGet(),
|
||||
@@ -795,19 +781,19 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
}
|
||||
|
||||
private createRootAttributes({
|
||||
tileClasses,
|
||||
tileAriaLive,
|
||||
className,
|
||||
ariaLive,
|
||||
scrollToken,
|
||||
}: EventTileRootRenderState): Record<string, unknown> {
|
||||
}: EventTileRenderState["root"]): Record<string, unknown> {
|
||||
return {
|
||||
"className": tileClasses,
|
||||
"aria-live": tileAriaLive,
|
||||
"className": className,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
};
|
||||
}
|
||||
|
||||
private createInteractiveRootAttributes(rootRenderState: EventTileRootRenderState): Record<string, unknown> {
|
||||
private createInteractiveRootAttributes(rootRenderState: EventTileRenderState["root"]): Record<string, unknown> {
|
||||
return {
|
||||
...this.createRootAttributes(rootRenderState),
|
||||
ref: this.ref,
|
||||
@@ -839,14 +825,16 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
};
|
||||
}
|
||||
|
||||
private createRenderInputs(
|
||||
displayInfo = getEventDisplayInfo(
|
||||
MatrixClientPeg.safeGet(),
|
||||
this.props.mxEvent,
|
||||
this.context.showHiddenEvents,
|
||||
shouldHideEventTile({ callEventGrouper: this.props.callEventGrouper }),
|
||||
),
|
||||
): EventTileRenderInputs {
|
||||
private createViewModelDependencies(): EventTileViewModelDependencies {
|
||||
return {
|
||||
mxEvent: this.props.mxEvent,
|
||||
matrixClient: MatrixClientPeg.safeGet(),
|
||||
showHiddenEvents: this.context.showHiddenEvents,
|
||||
hideEvent: shouldHideEventTile({ callEventGrouper: this.props.callEventGrouper }),
|
||||
};
|
||||
}
|
||||
|
||||
private createRenderInputs(): EventTileRenderInputs {
|
||||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||
const hasPinnedMessageBadge = PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent);
|
||||
const hasReactionsRow = !isRedacted;
|
||||
@@ -855,7 +843,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.safeGet().getUserId();
|
||||
|
||||
return {
|
||||
displayInfo,
|
||||
hasPinnedMessageBadge,
|
||||
hasReactionsRow,
|
||||
threadState,
|
||||
@@ -864,12 +851,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
}
|
||||
|
||||
private createViewModelProps(inputs: EventTileRenderInputs = this.createRenderInputs()): EventTileViewModelProps {
|
||||
const { displayInfo, hasPinnedMessageBadge, hasReactionsRow, threadState, isOwnEvent } = inputs;
|
||||
const { hasPinnedMessageBadge, hasReactionsRow, threadState, isOwnEvent } = inputs;
|
||||
const isProbablyMedia = MediaEventHelper.isEligible(this.props.mxEvent);
|
||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||
const isEditing = !!this.props.editState;
|
||||
const eventType = this.props.mxEvent.getType();
|
||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||
const isSending =
|
||||
this.props.eventSendStatus === EventStatus.SENDING ||
|
||||
this.props.eventSendStatus === EventStatus.QUEUED ||
|
||||
@@ -877,18 +861,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
|
||||
return {
|
||||
event: {
|
||||
eventType,
|
||||
msgtype,
|
||||
eventTs: this.props.mxEvent.getTs(),
|
||||
eventId: this.props.mxEvent.getId() ?? undefined,
|
||||
isLocalEcho: !!this.props.mxEvent.status,
|
||||
isSending,
|
||||
ariaLive: this.props.eventSendStatus === null ? undefined : "off",
|
||||
isRoomCreate: eventType === EventType.RoomCreate,
|
||||
isCallInvite: eventType === EventType.CallInvite,
|
||||
isRtcNotification: eventType === EventType.RTCNotification,
|
||||
isEditing,
|
||||
isEncryptionFailure,
|
||||
forExport: this.props.forExport,
|
||||
},
|
||||
display: {
|
||||
@@ -896,11 +871,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
layout: this.props.layout,
|
||||
continuation: this.props.continuation,
|
||||
isProbablyMedia,
|
||||
isBubbleMessage: displayInfo.isBubbleMessage,
|
||||
isLeftAlignedBubbleMessage: displayInfo.isLeftAlignedBubbleMessage,
|
||||
isAlignedBetweenBubbles: displayInfo.isAlignedBetweenBubbles,
|
||||
isInfoMessage: displayInfo.isInfoMessage,
|
||||
noBubbleEvent: displayInfo.noBubbleEvent,
|
||||
isTwelveHour: this.props.isTwelveHour,
|
||||
isHighlighted: shouldHighlightEventTile({
|
||||
cli: MatrixClientPeg.safeGet(),
|
||||
@@ -923,7 +893,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
inhibitInteraction: this.props.inhibitInteraction,
|
||||
},
|
||||
sender: {
|
||||
senderId: this.props.mxEvent.getSender() ?? undefined,
|
||||
member: roomMemberToMemberInfo(
|
||||
this.props.useEventSenderSnapshot
|
||||
? this.getAvatarMember()
|
||||
@@ -936,7 +905,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
}),
|
||||
),
|
||||
hideSender: this.props.hideSender,
|
||||
isEmote: this.props.mxEvent.getContent().msgtype === MsgType.Emote,
|
||||
},
|
||||
timestamp: {
|
||||
alwaysShowTimestamps: this.props.alwaysShowTimestamps,
|
||||
@@ -977,22 +945,20 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
const eventType = this.props.mxEvent.getType();
|
||||
const replacingEventId = this.props.mxEvent.replacingEventId();
|
||||
|
||||
const displayInfo = getEventDisplayInfo(
|
||||
MatrixClientPeg.safeGet(),
|
||||
this.props.mxEvent,
|
||||
this.context.showHiddenEvents,
|
||||
shouldHideEventTile({ callEventGrouper: this.props.callEventGrouper }),
|
||||
);
|
||||
const { hasRenderer, isSeeingThroughMessageHiddenForModeration } = displayInfo;
|
||||
const { isQuoteExpanded } = this.state;
|
||||
const renderInputs = this.createRenderInputs();
|
||||
const { hasPinnedMessageBadge, hasReactionsRow, threadState } = renderInputs;
|
||||
|
||||
this.viewModel.setInputs(this.createViewModelDependencies(), this.createViewModelProps(renderInputs));
|
||||
const eventTileRenderState = this.viewModel.getSnapshot();
|
||||
const eventTileSnapshot = eventTileRenderState.snapshot;
|
||||
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
// before trying to instantiate us
|
||||
if (!hasRenderer) {
|
||||
const { mxEvent } = this.props;
|
||||
logger.warn(`Event type not supported: type:${eventType} isState:${mxEvent.isState()}`);
|
||||
if (!eventTileSnapshot.event.hasRenderer) {
|
||||
logger.warn(
|
||||
`Event type not supported: type:${eventTileSnapshot.event.eventType} isState:${eventTileSnapshot.event.isState}`,
|
||||
);
|
||||
return (
|
||||
<div className="mx_EventTile mx_EventTile_info mx_MNoticeBody">
|
||||
<div className="mx_EventTile_line">{_t("timeline|error_no_renderer")}</div>
|
||||
@@ -1000,22 +966,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
);
|
||||
}
|
||||
|
||||
const renderInputs = this.createRenderInputs(displayInfo);
|
||||
const { hasPinnedMessageBadge, hasReactionsRow, threadState, isOwnEvent } = renderInputs;
|
||||
|
||||
this.viewModel.setProps(this.createViewModelProps(renderInputs));
|
||||
const eventTileRenderState = this.viewModel.getSnapshot();
|
||||
const eventTileSnapshot = eventTileRenderState.snapshot;
|
||||
|
||||
const lineClasses = eventTileRenderState.line.className;
|
||||
const tileClasses = eventTileRenderState.root.className;
|
||||
const tileAriaLive = eventTileRenderState.root.ariaLive;
|
||||
const isRenderingNotification = eventTileRenderState.root.isRenderingNotification;
|
||||
const isRenderingNotification = eventTileSnapshot.event.isRenderingNotification;
|
||||
const isSeeingThroughMessageHiddenForModeration =
|
||||
eventTileSnapshot.event.isSeeingThroughMessageHiddenForModeration;
|
||||
|
||||
const permalink = this.getPermalink();
|
||||
|
||||
const scrollToken = eventTileRenderState.root.scrollToken;
|
||||
const rootRenderState = { tileClasses, tileAriaLive, scrollToken };
|
||||
const rootRenderState = eventTileRenderState.root;
|
||||
|
||||
const avatarMember = this.getAvatarMember();
|
||||
const avatar = <EventTileAvatarAdapter avatarMember={avatarMember} senderSnapshot={eventTileSnapshot.sender} />;
|
||||
@@ -1083,16 +1040,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
/>
|
||||
);
|
||||
|
||||
const replyChainState = getEventTileReplyChainState({
|
||||
mxEvent: this.props.mxEvent,
|
||||
hasRenderer: haveRendererForEvent(
|
||||
this.props.mxEvent,
|
||||
MatrixClientPeg.safeGet(),
|
||||
this.context.showHiddenEvents,
|
||||
),
|
||||
});
|
||||
let replyChain: JSX.Element | undefined;
|
||||
if (replyChainState.shouldShowReplyChain) {
|
||||
if (eventTileSnapshot.root.data.hasReply) {
|
||||
replyChain = (
|
||||
<ReplyChain
|
||||
parentEv={this.props.mxEvent}
|
||||
@@ -1115,10 +1064,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
this.props.as || "li",
|
||||
{
|
||||
...this.createInteractiveRootAttributes(rootRenderState),
|
||||
"data-has-reply": !!replyChain,
|
||||
"data-layout": this.props.layout,
|
||||
"data-self": isOwnEvent,
|
||||
"data-event-id": this.props.mxEvent.getId(),
|
||||
"data-has-reply": eventTileSnapshot.root.data.hasReply,
|
||||
"data-layout": eventTileSnapshot.root.data.layout,
|
||||
"data-self": eventTileSnapshot.root.data.isOwnEvent,
|
||||
"data-event-id": eventTileSnapshot.root.data.eventId,
|
||||
},
|
||||
[
|
||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||
@@ -1136,7 +1085,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
{renderTile(
|
||||
TimelineRenderingType.Thread,
|
||||
this.createRenderTileProps({
|
||||
replacingEventId,
|
||||
replacingEventId: eventTileSnapshot.event.replacingEventId,
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
permalinkCreator: this.props.permalinkCreator!,
|
||||
}),
|
||||
@@ -1169,10 +1118,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
{
|
||||
...this.createInteractiveRootAttributes(rootRenderState),
|
||||
"tabIndex": -1,
|
||||
"data-layout": this.props.layout,
|
||||
"data-shape": this.context.timelineRenderingType,
|
||||
"data-self": isOwnEvent,
|
||||
"data-has-reply": !!replyChain,
|
||||
"data-layout": eventTileSnapshot.root.data.layout,
|
||||
"data-shape": eventTileSnapshot.root.data.shape,
|
||||
"data-self": eventTileSnapshot.root.data.isOwnEvent,
|
||||
"data-has-reply": eventTileSnapshot.root.data.hasReply,
|
||||
"onClick": (ev: MouseEvent) => {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
let index = -1;
|
||||
@@ -1210,7 +1159,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
{timestamp}
|
||||
<UnreadNotificationBadge
|
||||
room={room || undefined}
|
||||
threadId={this.props.mxEvent.getId()}
|
||||
threadId={eventTileSnapshot.root.data.eventId}
|
||||
forceDot={true}
|
||||
/>
|
||||
</div>
|
||||
@@ -1274,10 +1223,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
{
|
||||
...this.createInteractiveRootAttributes(rootRenderState),
|
||||
"tabIndex": -1,
|
||||
"data-layout": this.props.layout,
|
||||
"data-self": isOwnEvent,
|
||||
"data-event-id": this.props.mxEvent.getId(),
|
||||
"data-has-reply": !!replyChain,
|
||||
"data-layout": eventTileSnapshot.root.data.layout,
|
||||
"data-self": eventTileSnapshot.root.data.isOwnEvent,
|
||||
"data-event-id": eventTileSnapshot.root.data.eventId,
|
||||
"data-has-reply": eventTileSnapshot.root.data.hasReply,
|
||||
},
|
||||
<>
|
||||
{ircTimestamp}
|
||||
@@ -1295,7 +1244,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
{groupPadlock}
|
||||
{replyChain}
|
||||
{renderTile(
|
||||
this.context.timelineRenderingType,
|
||||
eventTileSnapshot.root.data.shape,
|
||||
this.createRenderTileProps({
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import classNames from "classnames";
|
||||
import { BaseViewModel } from "@element-hq/web-shared-components";
|
||||
import { EventType, MsgType, type MatrixClient, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
type EventTileSenderProfileState,
|
||||
@@ -42,6 +43,9 @@ import {
|
||||
type E2eMessageSharedIconViewModelProps,
|
||||
} from "./E2eMessageSharedIconViewModel";
|
||||
import { EventPreviewViewModel, type EventPreviewViewModelProps } from "./EventPreviewViewModel";
|
||||
import { getEventTileReplyChainState } from "./EventTileReplyChainState";
|
||||
import { getEventDisplayInfo } from "../../../../utils/EventRenderingUtils";
|
||||
import { haveRendererForEvent } from "../../../../events/EventTileFactory";
|
||||
import {
|
||||
ThreadListActionBarViewModel,
|
||||
type ThreadListActionBarViewModelProps,
|
||||
@@ -51,6 +55,18 @@ import { ReactionsRowViewModel, type ReactionsRowViewModelProps } from "./reacti
|
||||
|
||||
/** Event-level inputs for deriving the EventTile snapshot. */
|
||||
export interface EventTileEventInput {
|
||||
/** Whether the event is in a pending send state. */
|
||||
isSending: boolean;
|
||||
/** Whether EventTile should announce updates in an aria-live region. */
|
||||
ariaLive?: "off";
|
||||
/** Whether the event is currently being edited. */
|
||||
isEditing: boolean;
|
||||
/** Whether the tile is rendering for export. */
|
||||
forExport?: boolean;
|
||||
}
|
||||
|
||||
/** Event-level inputs after SDK data has been converted to pure values. */
|
||||
export interface EventTileDerivedEventInput extends EventTileEventInput {
|
||||
/** The event type rendered by the tile. */
|
||||
eventType: string;
|
||||
/** The Matrix message type rendered by the tile. */
|
||||
@@ -59,24 +75,26 @@ export interface EventTileEventInput {
|
||||
eventTs: number;
|
||||
/** The stable event identifier, when available. */
|
||||
eventId?: string;
|
||||
/** The event identifier replaced by this event, when available. */
|
||||
replacingEventId?: string;
|
||||
/** Whether the event is a state event. */
|
||||
isState: boolean;
|
||||
/** Whether the event is a local echo. */
|
||||
isLocalEcho: boolean;
|
||||
/** Whether the event is in a pending send state. */
|
||||
isSending: boolean;
|
||||
/** Whether EventTile should announce updates in an aria-live region. */
|
||||
ariaLive?: "off";
|
||||
/** Whether the event is a room create event. */
|
||||
isRoomCreate: boolean;
|
||||
/** Whether the event is a call invite. */
|
||||
isCallInvite: boolean;
|
||||
/** Whether the event is an RTC notification. */
|
||||
isRtcNotification: boolean;
|
||||
/** Whether the event is currently being edited. */
|
||||
isEditing: boolean;
|
||||
/** Whether the event failed decryption. */
|
||||
isEncryptionFailure: boolean;
|
||||
/** Whether the tile is rendering for export. */
|
||||
forExport?: boolean;
|
||||
/** Whether a renderer is available for the event. */
|
||||
hasRenderer: boolean;
|
||||
/** Whether the event should be rendered through the moderation fallback. */
|
||||
isSeeingThroughMessageHiddenForModeration: boolean;
|
||||
/** Whether EventTile should render the reply chain. */
|
||||
hasReplyChain: boolean;
|
||||
}
|
||||
|
||||
/** Display inputs for deriving the EventTile snapshot. */
|
||||
@@ -90,15 +108,15 @@ export interface EventTileDisplayInput {
|
||||
/** Whether the event body is likely to render media content. */
|
||||
isProbablyMedia: boolean;
|
||||
/** Whether the tile should use bubble container styling. */
|
||||
isBubbleMessage: boolean;
|
||||
isBubbleMessage?: boolean;
|
||||
/** Whether the bubble tile is left-aligned. */
|
||||
isLeftAlignedBubbleMessage: boolean;
|
||||
isLeftAlignedBubbleMessage?: boolean;
|
||||
/** Whether the event is aligned between bubble columns. */
|
||||
isAlignedBetweenBubbles: boolean;
|
||||
isAlignedBetweenBubbles?: boolean;
|
||||
/** Whether the event renders as an informational timeline item. */
|
||||
isInfoMessage: boolean;
|
||||
isInfoMessage?: boolean;
|
||||
/** Whether bubble styling should be suppressed for this event. */
|
||||
noBubbleEvent: boolean;
|
||||
noBubbleEvent?: boolean;
|
||||
/** Whether timestamps use twelve-hour formatting. */
|
||||
isTwelveHour?: boolean;
|
||||
/** Whether the event should be highlighted. */
|
||||
@@ -113,6 +131,15 @@ export interface EventTileDisplayInput {
|
||||
isContextual?: boolean;
|
||||
}
|
||||
|
||||
/** Event display inputs after renderer and event display information has been normalized. */
|
||||
export interface EventTileDerivedDisplayInput extends EventTileDisplayInput {
|
||||
isBubbleMessage: boolean;
|
||||
isLeftAlignedBubbleMessage: boolean;
|
||||
isAlignedBetweenBubbles: boolean;
|
||||
isInfoMessage: boolean;
|
||||
noBubbleEvent: boolean;
|
||||
}
|
||||
|
||||
/** Interaction inputs for deriving the EventTile snapshot. */
|
||||
export interface EventTileInteractionInput {
|
||||
/** Whether the tile is currently hovered. */
|
||||
@@ -138,7 +165,7 @@ export interface EventTileSenderInput {
|
||||
/** Whether sender details should be hidden. */
|
||||
hideSender?: boolean;
|
||||
/** Whether the event body renders as an emote. */
|
||||
isEmote: boolean;
|
||||
isEmote?: boolean;
|
||||
}
|
||||
|
||||
/** Timestamp inputs for deriving the EventTile snapshot. */
|
||||
@@ -179,12 +206,44 @@ export interface EventTileViewModelProps {
|
||||
footer: EventTileFooterInput;
|
||||
}
|
||||
|
||||
/** Pure EventTile inputs after event and sender data has been normalized. */
|
||||
export interface NormalizedEventTileViewModelProps {
|
||||
event: EventTileDerivedEventInput;
|
||||
display: EventTileDerivedDisplayInput;
|
||||
interaction: EventTileInteractionInput;
|
||||
sender: EventTileSenderInput;
|
||||
timestamp: EventTileTimestampInput;
|
||||
footer: EventTileFooterInput;
|
||||
}
|
||||
|
||||
/** Application dependencies used by EventTileViewModel to derive render data. */
|
||||
export interface EventTileViewModelDependencies {
|
||||
/** The Matrix event being rendered. */
|
||||
mxEvent: MatrixEvent;
|
||||
/** Matrix client used to select the event renderer. */
|
||||
matrixClient: MatrixClient;
|
||||
/** Whether hidden events should use their fallback renderer. */
|
||||
showHiddenEvents: boolean;
|
||||
/** Whether the event is hidden by the current tile context. */
|
||||
hideEvent?: boolean;
|
||||
}
|
||||
|
||||
/** Event-level state derived for the EventTile snapshot. */
|
||||
export interface EventTileEventSnapshot {
|
||||
/** The Matrix event type. */
|
||||
eventType: string;
|
||||
/** The Matrix message type. */
|
||||
msgtype?: string;
|
||||
/** The stable event identifier, when available. */
|
||||
eventId?: string;
|
||||
/** The event identifier replaced by this event, when available. */
|
||||
replacingEventId?: string;
|
||||
/** Whether the event is a state event. */
|
||||
isState: boolean;
|
||||
/** The event origin timestamp. */
|
||||
eventTs: number;
|
||||
/** Whether the event is a local echo. */
|
||||
isLocalEcho: boolean;
|
||||
/** Whether the event is in a pending send state. */
|
||||
isSending: boolean;
|
||||
/** Whether the event is currently being edited. */
|
||||
@@ -193,6 +252,26 @@ export interface EventTileEventSnapshot {
|
||||
isContinuation?: boolean;
|
||||
/** Whether the tile is rendering as a notification. */
|
||||
isRenderingNotification: boolean;
|
||||
/** Whether the event failed decryption. */
|
||||
isEncryptionFailure: boolean;
|
||||
/** Whether a renderer is available for the event. */
|
||||
hasRenderer: boolean;
|
||||
/** Whether the event should be rendered through the moderation fallback. */
|
||||
isSeeingThroughMessageHiddenForModeration: boolean;
|
||||
}
|
||||
|
||||
/** Plain data attributes rendered on the EventTile root element. */
|
||||
export interface EventTileRootData {
|
||||
/** The event identifier exposed through `data-event-id`. */
|
||||
eventId?: string;
|
||||
/** The configured tile layout exposed through `data-layout`. */
|
||||
layout?: Layout;
|
||||
/** The timeline rendering mode exposed through `data-shape`. */
|
||||
shape: TimelineRenderingType;
|
||||
/** Whether the event belongs to the current user, exposed through `data-self`. */
|
||||
isOwnEvent: boolean;
|
||||
/** Whether EventTile renders a reply chain, exposed through `data-has-reply`. */
|
||||
hasReply: boolean;
|
||||
}
|
||||
|
||||
/** Root state derived for the EventTile snapshot. */
|
||||
@@ -201,6 +280,8 @@ export interface EventTileRootSnapshot {
|
||||
ariaLive?: "off";
|
||||
/** The stable scroll token for the event. */
|
||||
scrollToken?: string;
|
||||
/** Plain data attributes used by the EventTile root element. */
|
||||
data: EventTileRootData;
|
||||
/** EventTile root CSS class flags. */
|
||||
classState: ReturnType<typeof getEventTileClassState>;
|
||||
}
|
||||
@@ -288,6 +369,8 @@ export interface EventTileRenderState {
|
||||
scrollToken?: string;
|
||||
/** Whether the tile is rendering as a notification. */
|
||||
isRenderingNotification: boolean;
|
||||
/** Plain data attributes used by the EventTile root element. */
|
||||
data: EventTileRootData;
|
||||
};
|
||||
/** EventTile line render state. */
|
||||
line: {
|
||||
@@ -319,7 +402,11 @@ export interface EventTileRenderState {
|
||||
};
|
||||
}
|
||||
|
||||
/** Derives the current EventTile snapshot from component-owned inputs. */
|
||||
/**
|
||||
* Aggregate application-side render-state boundary for EventTile.
|
||||
*
|
||||
* SDK objects are converted to plain render data here before the existing render tree consumes it.
|
||||
*/
|
||||
export class EventTileViewModel extends BaseViewModel<EventTileRenderState, EventTileViewModelProps> {
|
||||
private messageTimestampViewModel?: MessageTimestampViewModel;
|
||||
private linkedMessageTimestampViewModel?: MessageTimestampViewModel;
|
||||
@@ -331,16 +418,18 @@ export class EventTileViewModel extends BaseViewModel<EventTileRenderState, Even
|
||||
private actionBarViewModel?: EventTileActionBarViewModel;
|
||||
private reactionsRowViewModel?: ReactionsRowViewModel;
|
||||
|
||||
public constructor(props: EventTileViewModelProps) {
|
||||
const initialRenderState = EventTileViewModel.createRenderState(props);
|
||||
public constructor(dependencies: EventTileViewModelDependencies, props: EventTileViewModelProps) {
|
||||
const normalizedProps = EventTileViewModel.normalizeDependencies(dependencies, props);
|
||||
const initialRenderState = EventTileViewModel.createRenderState(normalizedProps);
|
||||
|
||||
super(props, initialRenderState);
|
||||
super(normalizedProps, initialRenderState);
|
||||
}
|
||||
|
||||
/** Updates root EventTile inputs and refreshes the derived render state. */
|
||||
public setProps(props: EventTileViewModelProps): void {
|
||||
this.props = props;
|
||||
this.snapshot.set(EventTileViewModel.createRenderState(props));
|
||||
/** Updates dependencies and root inputs together, emitting one consistent render state. */
|
||||
public setInputs(dependencies: EventTileViewModelDependencies, props: EventTileViewModelProps): void {
|
||||
const normalizedProps = EventTileViewModel.normalizeDependencies(dependencies, props);
|
||||
this.props = normalizedProps;
|
||||
this.snapshot.set(EventTileViewModel.createRenderState(normalizedProps));
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
@@ -447,7 +536,7 @@ export class EventTileViewModel extends BaseViewModel<EventTileRenderState, Even
|
||||
}
|
||||
|
||||
/** Derives render-ready EventTile state from component-owned inputs. */
|
||||
public static createRenderState(props: EventTileViewModelProps): EventTileRenderState {
|
||||
public static createRenderState(props: NormalizedEventTileViewModelProps): EventTileRenderState {
|
||||
const snapshot = EventTileViewModel.createSnapshot(props);
|
||||
const useIRCLayout = snapshot.timestamp.displayState.useIRCLayout;
|
||||
const showPadlock = !props.display.isBubbleMessage;
|
||||
@@ -459,6 +548,7 @@ export class EventTileViewModel extends BaseViewModel<EventTileRenderState, Even
|
||||
ariaLive: snapshot.root.ariaLive,
|
||||
scrollToken: snapshot.root.scrollToken,
|
||||
isRenderingNotification: snapshot.event.isRenderingNotification,
|
||||
data: snapshot.root.data,
|
||||
},
|
||||
line: {
|
||||
className: classNames("mx_EventTile_line", snapshot.line.classState),
|
||||
@@ -481,18 +571,78 @@ export class EventTileViewModel extends BaseViewModel<EventTileRenderState, Even
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives pure inputs from application dependencies while keeping the VM's public props SDK-free.
|
||||
*/
|
||||
private static normalizeDependencies(
|
||||
dependencies: EventTileViewModelDependencies,
|
||||
props: EventTileViewModelProps,
|
||||
): NormalizedEventTileViewModelProps {
|
||||
const { mxEvent } = dependencies;
|
||||
const eventType = mxEvent.getType();
|
||||
const displayInfo = getEventDisplayInfo(
|
||||
dependencies.matrixClient,
|
||||
mxEvent,
|
||||
dependencies.showHiddenEvents,
|
||||
dependencies.hideEvent,
|
||||
);
|
||||
const replyChainState = getEventTileReplyChainState({
|
||||
mxEvent,
|
||||
// Replacement events have a fallback tile but must not show their own reply chain
|
||||
hasRenderer: haveRendererForEvent(mxEvent, dependencies.matrixClient, dependencies.showHiddenEvents),
|
||||
});
|
||||
|
||||
return {
|
||||
...props,
|
||||
event: {
|
||||
...props.event,
|
||||
eventType,
|
||||
msgtype: mxEvent.getContent().msgtype,
|
||||
eventTs: mxEvent.getTs(),
|
||||
eventId: mxEvent.getId() ?? undefined,
|
||||
replacingEventId: mxEvent.replacingEventId() ?? undefined,
|
||||
isState: mxEvent.isState(),
|
||||
isLocalEcho: !!mxEvent.status,
|
||||
isRoomCreate: eventType === EventType.RoomCreate,
|
||||
isCallInvite: eventType === EventType.CallInvite,
|
||||
isRtcNotification: eventType === EventType.RTCNotification,
|
||||
isEncryptionFailure: mxEvent.isDecryptionFailure(),
|
||||
hasRenderer: displayInfo.hasRenderer,
|
||||
isSeeingThroughMessageHiddenForModeration: displayInfo.isSeeingThroughMessageHiddenForModeration,
|
||||
hasReplyChain: replyChainState.shouldShowReplyChain,
|
||||
},
|
||||
display: {
|
||||
...props.display,
|
||||
...displayInfo,
|
||||
},
|
||||
sender: {
|
||||
...props.sender,
|
||||
senderId: mxEvent.getSender() ?? undefined,
|
||||
isEmote: mxEvent.getContent().msgtype === MsgType.Emote,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Creates an EventTile view model snapshot. */
|
||||
public static createSnapshot(props: EventTileViewModelProps): EventTileViewModelSnapshot {
|
||||
public static createSnapshot(props: NormalizedEventTileViewModelProps): EventTileViewModelSnapshot {
|
||||
const { event, display, interaction, sender, timestamp, footer } = props;
|
||||
const isContinuation = getIsContinuation(display.continuation, display.timelineRenderingType, display.layout);
|
||||
const isRenderingNotification = display.timelineRenderingType === TimelineRenderingType.Notification;
|
||||
const eventSnapshot: EventTileEventSnapshot = {
|
||||
eventType: event.eventType,
|
||||
msgtype: event.msgtype,
|
||||
eventId: event.eventId,
|
||||
replacingEventId: event.replacingEventId,
|
||||
isState: event.isState,
|
||||
eventTs: event.eventTs,
|
||||
isLocalEcho: event.isLocalEcho,
|
||||
isSending: event.isSending,
|
||||
isEditing: event.isEditing,
|
||||
isContinuation,
|
||||
isRenderingNotification,
|
||||
isEncryptionFailure: event.isEncryptionFailure,
|
||||
hasRenderer: event.hasRenderer,
|
||||
isSeeingThroughMessageHiddenForModeration: event.isSeeingThroughMessageHiddenForModeration,
|
||||
};
|
||||
const senderProfileState = getEventTileSenderProfileState({
|
||||
isRenderingNotification,
|
||||
@@ -531,6 +681,13 @@ export class EventTileViewModel extends BaseViewModel<EventTileRenderState, Even
|
||||
eventId: event.eventId,
|
||||
isLocalEcho: event.isLocalEcho,
|
||||
}),
|
||||
data: {
|
||||
eventId: event.eventId,
|
||||
layout: display.layout,
|
||||
shape: display.timelineRenderingType,
|
||||
isOwnEvent: footer.isOwnEvent,
|
||||
hasReply: event.hasReplyChain,
|
||||
},
|
||||
classState: EventTileViewModel.getClassState({
|
||||
event,
|
||||
display,
|
||||
@@ -565,7 +722,7 @@ export class EventTileViewModel extends BaseViewModel<EventTileRenderState, Even
|
||||
timelineRenderingType: display.timelineRenderingType,
|
||||
}),
|
||||
forceHistoricalAvatar: event.eventType === "m.room.member",
|
||||
isEmote: sender.isEmote,
|
||||
isEmote: sender.isEmote ?? false,
|
||||
},
|
||||
actionBar: {
|
||||
show: getShouldShowMessageActionBar({
|
||||
@@ -616,8 +773,8 @@ export class EventTileViewModel extends BaseViewModel<EventTileRenderState, Even
|
||||
isContinuation,
|
||||
isRenderingNotification,
|
||||
}: {
|
||||
event: EventTileEventInput;
|
||||
display: EventTileDisplayInput;
|
||||
event: EventTileDerivedEventInput;
|
||||
display: EventTileDerivedDisplayInput;
|
||||
interaction: EventTileInteractionInput;
|
||||
sender: EventTileSenderInput;
|
||||
eventType: string;
|
||||
|
||||
@@ -292,6 +292,15 @@ describe("EventTile", () => {
|
||||
expect(getTile(container)).toContainElement(getLine(container));
|
||||
});
|
||||
|
||||
it("preserves the existing root and line markup", () => {
|
||||
const { container } = getComponent();
|
||||
const tile = getTile(container);
|
||||
|
||||
expect(tile.tagName).toBe("LI");
|
||||
expect(tile).toContainElement(getLine(container));
|
||||
expect(getLine(container)).toHaveClass("mx_EventTile_line");
|
||||
});
|
||||
|
||||
it("does not expose a scroll token for local echo events", () => {
|
||||
const localEcho = makeOwnMessage();
|
||||
localEcho.setStatus(EventStatus.SENDING);
|
||||
|
||||
@@ -7,14 +7,36 @@
|
||||
|
||||
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { mkEvent, stubClient } from "../../test-utils";
|
||||
import {
|
||||
EventTileViewModel,
|
||||
type EventTileViewModelDependencies,
|
||||
type NormalizedEventTileViewModelProps,
|
||||
type EventTileViewModelProps,
|
||||
} from "../../../src/viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||
|
||||
describe("EventTileViewModel", () => {
|
||||
const matrixClient = stubClient();
|
||||
|
||||
const makeEvent = () =>
|
||||
mkEvent({
|
||||
event: true,
|
||||
id: "$event",
|
||||
room: "!room:example.org",
|
||||
ts: 123,
|
||||
type: "m.room.message",
|
||||
user: "@alice:example.org",
|
||||
content: { msgtype: "m.text" },
|
||||
});
|
||||
|
||||
const makeDependencies = (mxEvent = makeEvent()): EventTileViewModelDependencies => ({
|
||||
mxEvent,
|
||||
matrixClient,
|
||||
showHiddenEvents: false,
|
||||
});
|
||||
|
||||
type EventTileViewModelPropsOverrides = {
|
||||
event?: Partial<EventTileViewModelProps["event"]>;
|
||||
event?: Partial<NormalizedEventTileViewModelProps["event"]>;
|
||||
display?: Partial<EventTileViewModelProps["display"]>;
|
||||
interaction?: Partial<EventTileViewModelProps["interaction"]>;
|
||||
sender?: Partial<EventTileViewModelProps["sender"]>;
|
||||
@@ -22,13 +44,15 @@ describe("EventTileViewModel", () => {
|
||||
footer?: Partial<EventTileViewModelProps["footer"]>;
|
||||
};
|
||||
|
||||
function makeProps(overrides: EventTileViewModelPropsOverrides = {}): EventTileViewModelProps {
|
||||
function makeProps(overrides: EventTileViewModelPropsOverrides = {}): NormalizedEventTileViewModelProps {
|
||||
return {
|
||||
event: {
|
||||
eventType: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
eventTs: 123,
|
||||
eventId: "$event",
|
||||
isState: false,
|
||||
hasReplyChain: false,
|
||||
isLocalEcho: false,
|
||||
isSending: false,
|
||||
ariaLive: "off",
|
||||
@@ -37,6 +61,8 @@ describe("EventTileViewModel", () => {
|
||||
isRtcNotification: false,
|
||||
isEditing: false,
|
||||
isEncryptionFailure: false,
|
||||
hasRenderer: true,
|
||||
isSeeingThroughMessageHiddenForModeration: false,
|
||||
forExport: false,
|
||||
...overrides.event,
|
||||
},
|
||||
@@ -91,8 +117,21 @@ describe("EventTileViewModel", () => {
|
||||
);
|
||||
|
||||
expect(snapshot.event.isSending).toBe(true);
|
||||
expect(snapshot.event).toMatchObject({
|
||||
eventId: "$event",
|
||||
eventTs: 123,
|
||||
isLocalEcho: true,
|
||||
isEncryptionFailure: false,
|
||||
});
|
||||
expect(snapshot.root.ariaLive).toBe("off");
|
||||
expect(snapshot.root.scrollToken).toBeUndefined();
|
||||
expect(snapshot.root.data).toEqual({
|
||||
eventId: "$event",
|
||||
layout: Layout.Group,
|
||||
shape: TimelineRenderingType.Room,
|
||||
isOwnEvent: false,
|
||||
hasReply: false,
|
||||
});
|
||||
expect(snapshot.root.classState.mx_EventTile_sending).toBe(true);
|
||||
});
|
||||
|
||||
@@ -468,13 +507,13 @@ describe("EventTileViewModel", () => {
|
||||
});
|
||||
|
||||
it("updates an instance snapshot when inputs change", () => {
|
||||
const vm = new EventTileViewModel(makeProps());
|
||||
const vm = new EventTileViewModel(makeDependencies(), makeProps());
|
||||
const listener = jest.fn();
|
||||
const unsubscribe = vm.subscribe(listener);
|
||||
|
||||
expect(vm.getSnapshot().snapshot.timestamp.show).toBe(false);
|
||||
|
||||
vm.setProps(makeProps({ interaction: { hover: true } }));
|
||||
vm.setInputs(makeDependencies(), makeProps({ interaction: { hover: true } }));
|
||||
|
||||
expect(vm.getSnapshot().snapshot.timestamp.show).toBe(true);
|
||||
expect(listener).toHaveBeenCalled();
|
||||
@@ -483,8 +522,162 @@ describe("EventTileViewModel", () => {
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("emits once when dependencies and inputs are updated together", () => {
|
||||
const vm = new EventTileViewModel(makeDependencies(), makeProps());
|
||||
const listener = jest.fn();
|
||||
const unsubscribe = vm.subscribe(listener);
|
||||
|
||||
vm.setInputs(makeDependencies(), makeProps({ interaction: { hover: true } }));
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
|
||||
unsubscribe();
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("normalizes event and sender state from its SDK dependency", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
id: "$member-event",
|
||||
room: "!room:example.org",
|
||||
ts: 456,
|
||||
type: "m.room.member",
|
||||
user: "@bob:example.org",
|
||||
content: { membership: "join" },
|
||||
});
|
||||
const vm = new EventTileViewModel(
|
||||
makeDependencies(event),
|
||||
makeProps({
|
||||
event: {
|
||||
eventType: "m.room.message",
|
||||
eventId: "$wrong-event",
|
||||
eventTs: 123,
|
||||
},
|
||||
sender: {
|
||||
senderId: "@wrong:example.org",
|
||||
isEmote: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(vm.getSnapshot().snapshot.event).toMatchObject({
|
||||
eventType: "m.room.member",
|
||||
eventId: "$member-event",
|
||||
eventTs: 456,
|
||||
isState: true,
|
||||
});
|
||||
expect(vm.getSnapshot().snapshot.sender).toMatchObject({
|
||||
senderId: "@bob:example.org",
|
||||
forceHistoricalAvatar: true,
|
||||
isEmote: false,
|
||||
});
|
||||
|
||||
vm.setInputs(
|
||||
makeDependencies(
|
||||
mkEvent({
|
||||
event: true,
|
||||
id: "$updated-event",
|
||||
room: "!room:example.org",
|
||||
ts: 789,
|
||||
type: "m.call.invite",
|
||||
user: "@carol:example.org",
|
||||
content: { msgtype: "m.call.invite" },
|
||||
}),
|
||||
),
|
||||
makeProps(),
|
||||
);
|
||||
|
||||
expect(vm.getSnapshot().snapshot.event).toMatchObject({
|
||||
eventType: "m.call.invite",
|
||||
eventId: "$updated-event",
|
||||
eventTs: 789,
|
||||
});
|
||||
expect(vm.getSnapshot().snapshot.sender.senderId).toBe("@carol:example.org");
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("normalizes event identity, replacement, renderer, and decryption state", () => {
|
||||
const event = makeEvent();
|
||||
jest.spyOn(event, "replacingEventId").mockReturnValue("$replaced-event");
|
||||
jest.spyOn(event, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
const vm = new EventTileViewModel(makeDependencies(event), makeProps());
|
||||
const snapshot = vm.getSnapshot().snapshot;
|
||||
|
||||
expect(snapshot.event).toMatchObject({
|
||||
eventType: "m.room.message",
|
||||
eventId: "$event",
|
||||
replacingEventId: "$replaced-event",
|
||||
isEncryptionFailure: true,
|
||||
hasRenderer: true,
|
||||
});
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("does not show a reply chain for replacement events", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
id: "$replacement-event",
|
||||
room: "!room:example.org",
|
||||
ts: 123,
|
||||
type: "m.room.message",
|
||||
user: "@alice:example.org",
|
||||
content: {
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.replace",
|
||||
"event_id": "$original-event",
|
||||
"m.in_reply_to": {
|
||||
event_id: "$parent-event",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const vm = new EventTileViewModel(makeDependencies(event), makeProps());
|
||||
|
||||
expect(vm.getSnapshot().snapshot.root.data.hasReply).toBe(false);
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("derives an unavailable renderer for unsupported events", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
room: "!room:example.org",
|
||||
type: "org.example.unsupported",
|
||||
user: "@alice:example.org",
|
||||
content: {},
|
||||
});
|
||||
const vm = new EventTileViewModel(makeDependencies(event), makeProps());
|
||||
|
||||
expect(vm.getSnapshot().snapshot.event.hasRenderer).toBe(false);
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("recalculates renderer state when dependencies change", () => {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
room: "!room:example.org",
|
||||
type: "org.example.unsupported",
|
||||
user: "@alice:example.org",
|
||||
content: {},
|
||||
});
|
||||
const vm = new EventTileViewModel(makeDependencies(event), makeProps());
|
||||
|
||||
expect(vm.getSnapshot().snapshot.event.hasRenderer).toBe(false);
|
||||
|
||||
vm.setInputs(makeDependencies(makeEvent()), makeProps());
|
||||
|
||||
expect(vm.getSnapshot().snapshot.event.hasRenderer).toBe(true);
|
||||
|
||||
vm.dispose();
|
||||
});
|
||||
|
||||
it("lazily owns timestamp child view models", () => {
|
||||
const vm = new EventTileViewModel(makeProps());
|
||||
const vm = new EventTileViewModel(makeDependencies(), makeProps());
|
||||
const messageTimestampViewModel = vm.getMessageTimestampViewModel({ ts: 123 });
|
||||
const linkedMessageTimestampViewModel = vm.getLinkedMessageTimestampViewModel({ ts: 456 });
|
||||
|
||||
@@ -496,6 +689,7 @@ describe("EventTileViewModel", () => {
|
||||
|
||||
it("does not initialize timestamp child view models for events without an origin timestamp", () => {
|
||||
const vm = new EventTileViewModel(
|
||||
makeDependencies(),
|
||||
makeProps({
|
||||
event: {
|
||||
eventTs: 0,
|
||||
@@ -512,7 +706,7 @@ describe("EventTileViewModel", () => {
|
||||
});
|
||||
|
||||
it("owns and updates the thread-list action bar child view model", () => {
|
||||
const vm = new EventTileViewModel(makeProps());
|
||||
const vm = new EventTileViewModel(makeDependencies(), makeProps());
|
||||
const onViewInRoomClick = jest.fn();
|
||||
const onCopyLinkClick = jest.fn();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user