* Introduce thin EventTile view model snapshot * Fix errors after merge conflict resolution
This commit is contained in:
@@ -101,26 +101,9 @@ import PinningUtils from "../../../utils/PinningUtils";
|
|||||||
import { EventPreview } from "./EventPreview";
|
import { EventPreview } from "./EventPreview";
|
||||||
import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx";
|
import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx";
|
||||||
import { E2ePadlock, E2ePadlockIcon } from "./EventTile/E2ePadlock.tsx";
|
import { E2ePadlock, E2ePadlockIcon } from "./EventTile/E2ePadlock.tsx";
|
||||||
import {
|
|
||||||
getAriaLive,
|
|
||||||
getEventTileAvatarMember,
|
|
||||||
getEventTileClassState,
|
|
||||||
getEventTileLineClassState,
|
|
||||||
getEventTileSenderProfileState,
|
|
||||||
getEventTileTimestamp,
|
|
||||||
getFooterDisplayState,
|
|
||||||
getIsContinuation,
|
|
||||||
getReplyChainAlwaysShowTimestamps,
|
|
||||||
getScrollToken,
|
|
||||||
getSenderProfileMode,
|
|
||||||
getShouldShowMessageActionBar,
|
|
||||||
getShouldShowTimestamp,
|
|
||||||
getShouldViewUserOnClick,
|
|
||||||
getTimestampDisplayState,
|
|
||||||
isSendingStatus,
|
|
||||||
} from "./EventTile/eventTileDerivedState";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CardContext } from "../right_panel/context";
|
import { CardContext } from "../right_panel/context";
|
||||||
|
import { EventTileViewModel } from "../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||||
import {
|
import {
|
||||||
MessageTimestampViewModel,
|
MessageTimestampViewModel,
|
||||||
type MessageTimestampViewModelProps,
|
type MessageTimestampViewModelProps,
|
||||||
@@ -1060,7 +1043,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
|
||||||
const eventType = this.props.mxEvent.getType();
|
const eventType = this.props.mxEvent.getType();
|
||||||
const replacingEventId = this.props.mxEvent.replacingEventId();
|
const replacingEventId = this.props.mxEvent.replacingEventId();
|
||||||
|
|
||||||
@@ -1093,56 +1075,67 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
|
|
||||||
const isProbablyMedia = MediaEventHelper.isEligible(this.props.mxEvent);
|
const isProbablyMedia = MediaEventHelper.isEligible(this.props.mxEvent);
|
||||||
|
|
||||||
const lineClasses = classNames(
|
|
||||||
"mx_EventTile_line",
|
|
||||||
getEventTileLineClassState({
|
|
||||||
isProbablyMedia,
|
|
||||||
eventType,
|
|
||||||
msgtype,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSending = isSendingStatus(this.props.eventSendStatus);
|
|
||||||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||||
|
|
||||||
const isContinuation = getIsContinuation(
|
|
||||||
this.props.continuation,
|
|
||||||
this.context.timelineRenderingType,
|
|
||||||
this.props.layout,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isRenderingNotification = this.context.timelineRenderingType === TimelineRenderingType.Notification;
|
|
||||||
|
|
||||||
const isEditing = !!this.props.editState;
|
const isEditing = !!this.props.editState;
|
||||||
const classes = classNames(
|
const hasPinnedMessageBadge = PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent);
|
||||||
getEventTileClassState({
|
const hasReactionsRow = !isRedacted;
|
||||||
|
// Use `getSender()` because searched events might not have a proper `sender`.
|
||||||
|
const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.safeGet().getUserId();
|
||||||
|
|
||||||
|
const eventTileSnapshot = EventTileViewModel.createSnapshot({
|
||||||
|
event: {
|
||||||
|
mxEvent: this.props.mxEvent,
|
||||||
|
eventSendStatus: this.props.eventSendStatus,
|
||||||
|
isEditing,
|
||||||
|
isEncryptionFailure,
|
||||||
|
forExport: this.props.forExport,
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
layout: this.props.layout,
|
||||||
|
continuation: this.props.continuation,
|
||||||
|
isProbablyMedia,
|
||||||
isBubbleMessage,
|
isBubbleMessage,
|
||||||
isLeftAlignedBubbleMessage,
|
isLeftAlignedBubbleMessage,
|
||||||
isAlignedBetweenBubbles,
|
isAlignedBetweenBubbles,
|
||||||
isEditing,
|
|
||||||
isInfoMessage,
|
isInfoMessage,
|
||||||
|
noBubbleEvent,
|
||||||
isTwelveHour: this.props.isTwelveHour,
|
isTwelveHour: this.props.isTwelveHour,
|
||||||
isSending,
|
|
||||||
isHighlighted: this.shouldHighlight(),
|
isHighlighted: this.shouldHighlight(),
|
||||||
isSelected: this.props.isSelectedEvent || !!this.state.contextMenu,
|
isSelected: this.props.isSelectedEvent || !!this.state.contextMenu,
|
||||||
isContinuation,
|
|
||||||
eventType,
|
|
||||||
isLast: this.props.last,
|
isLast: this.props.last,
|
||||||
isLastInSection: this.props.lastInSection,
|
isLastInSection: this.props.lastInSection,
|
||||||
isContextual: this.props.contextual,
|
isContextual: this.props.contextual,
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
hover: this.state.hover,
|
||||||
|
showActionBarFromFocus: this.state.showActionBarFromFocus,
|
||||||
|
focusWithin: this.state.focusWithin,
|
||||||
isActionBarFocused: this.state.actionBarFocused,
|
isActionBarFocused: this.state.actionBarFocused,
|
||||||
isEncryptionFailure,
|
hasContextMenu: !!this.state.contextMenu,
|
||||||
msgtype,
|
inhibitInteraction: this.props.inhibitInteraction,
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
hideSender: this.props.hideSender,
|
hideSender: this.props.hideSender,
|
||||||
timelineRenderingType: this.context.timelineRenderingType,
|
},
|
||||||
isRenderingNotification,
|
timestamp: {
|
||||||
noBubbleEvent,
|
alwaysShowTimestamps: this.props.alwaysShowTimestamps,
|
||||||
}),
|
hideTimestamp: this.props.hideTimestamp,
|
||||||
);
|
threadReplyEventTs: this.state.thread?.replyToEvent?.getTs(),
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
isOwnEvent,
|
||||||
|
hasReactionsRow,
|
||||||
|
hasReactions: !!this.state.reactions,
|
||||||
|
hasPinnedMessageBadge,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// If the tile is in the Sending state, don't speak the message.
|
const lineClasses = classNames("mx_EventTile_line", eventTileSnapshot.line.classState);
|
||||||
const ariaLive = getAriaLive(this.props.eventSendStatus);
|
const tileClasses = classNames(eventTileSnapshot.root.classState);
|
||||||
|
const tileAriaLive = eventTileSnapshot.root.ariaLive;
|
||||||
|
const isRenderingNotification = eventTileSnapshot.event.isRenderingNotification;
|
||||||
|
|
||||||
let permalink = "#";
|
let permalink = "#";
|
||||||
if (this.props.permalinkCreator) {
|
if (this.props.permalinkCreator) {
|
||||||
@@ -1151,43 +1144,26 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
|
|
||||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||||
// Local echos have a send "status".
|
// Local echos have a send "status".
|
||||||
const scrollToken = getScrollToken(this.props.mxEvent);
|
const scrollToken = eventTileSnapshot.root.scrollToken;
|
||||||
|
|
||||||
let avatar: JSX.Element | null = null;
|
let avatar: JSX.Element | null = null;
|
||||||
let sender: JSX.Element | null = null;
|
let sender: JSX.Element | null = null;
|
||||||
const { avatarSize, needsSenderProfile } = getEventTileSenderProfileState({
|
const { avatarSize } = eventTileSnapshot.sender.profileState;
|
||||||
isRenderingNotification,
|
|
||||||
isInfoMessage,
|
|
||||||
timelineRenderingType: this.context.timelineRenderingType,
|
|
||||||
continuation: this.props.continuation,
|
|
||||||
eventType,
|
|
||||||
isBubbleMessage,
|
|
||||||
layout: this.props.layout,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.props.mxEvent.sender && avatarSize !== null) {
|
if (this.props.mxEvent.sender && avatarSize !== null) {
|
||||||
const member = getEventTileAvatarMember(this.props.mxEvent);
|
|
||||||
const viewUserOnClick = getShouldViewUserOnClick(
|
|
||||||
this.props.inhibitInteraction,
|
|
||||||
this.context.timelineRenderingType,
|
|
||||||
);
|
|
||||||
avatar = (
|
avatar = (
|
||||||
<div className="mx_EventTile_avatar">
|
<div className="mx_EventTile_avatar">
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
member={member}
|
member={eventTileSnapshot.sender.avatarMember}
|
||||||
size={avatarSize}
|
size={avatarSize}
|
||||||
viewUserOnClick={viewUserOnClick}
|
viewUserOnClick={eventTileSnapshot.sender.viewUserOnClick}
|
||||||
forceHistorical={this.props.mxEvent.getType() === EventType.RoomMember}
|
forceHistorical={this.props.mxEvent.getType() === EventType.RoomMember}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const senderProfileMode = getSenderProfileMode({
|
const senderProfileMode = eventTileSnapshot.sender.profileMode;
|
||||||
needsSenderProfile,
|
|
||||||
hideSender: this.props.hideSender,
|
|
||||||
timelineRenderingType: this.context.timelineRenderingType,
|
|
||||||
});
|
|
||||||
if (senderProfileMode === "clickable") {
|
if (senderProfileMode === "clickable") {
|
||||||
sender = <SenderProfile onClick={this.onSenderProfileClick} mxEvent={this.props.mxEvent} />;
|
sender = <SenderProfile onClick={this.onSenderProfileClick} mxEvent={this.props.mxEvent} />;
|
||||||
} else if (senderProfileMode === "tooltip") {
|
} else if (senderProfileMode === "tooltip") {
|
||||||
@@ -1196,15 +1172,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
|
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showMessageActionBar = getShouldShowMessageActionBar({
|
const actionBar = eventTileSnapshot.actionBar.show ? (
|
||||||
isEditing,
|
|
||||||
forExport: this.props.forExport,
|
|
||||||
hover: this.state.hover,
|
|
||||||
showActionBarFromFocus: this.state.showActionBarFromFocus,
|
|
||||||
actionBarFocused: this.state.actionBarFocused,
|
|
||||||
hasContextMenu: !!this.state.contextMenu,
|
|
||||||
});
|
|
||||||
const actionBar = showMessageActionBar ? (
|
|
||||||
<ActionBarWrapper
|
<ActionBarWrapper
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
reactions={this.state.reactions}
|
reactions={this.state.reactions}
|
||||||
@@ -1218,24 +1186,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
/>
|
/>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
const showTimestamp = getShouldShowTimestamp({
|
|
||||||
eventTs: this.props.mxEvent.getTs(),
|
|
||||||
eventType,
|
|
||||||
hideTimestamp: this.props.hideTimestamp,
|
|
||||||
alwaysShowTimestamps: this.props.alwaysShowTimestamps,
|
|
||||||
last: this.props.last,
|
|
||||||
hover: this.state.hover,
|
|
||||||
focusWithin: this.state.focusWithin,
|
|
||||||
actionBarFocused: this.state.actionBarFocused,
|
|
||||||
hasContextMenu: Boolean(this.state.contextMenu),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Thread panel shows the timestamp of the last reply in that thread
|
// Thread panel shows the timestamp of the last reply in that thread
|
||||||
const ts = getEventTileTimestamp({
|
const ts = eventTileSnapshot.timestamp.value;
|
||||||
timelineRenderingType: this.context.timelineRenderingType,
|
|
||||||
eventTs: this.props.mxEvent.getTs(),
|
|
||||||
threadReplyEventTs: this.state.thread?.replyToEvent?.getTs(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageTimestampProps: MessageTimestampViewModelProps = {
|
const messageTimestampProps: MessageTimestampViewModelProps = {
|
||||||
showRelative: this.context.timelineRenderingType === TimelineRenderingType.ThreadsList,
|
showRelative: this.context.timelineRenderingType === TimelineRenderingType.ThreadsList,
|
||||||
@@ -1253,24 +1205,19 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const { useIRCLayout, showRealTimestamp, showLinkedTimestamp } = getTimestampDisplayState({
|
const { useIRCLayout, showRealTimestamp, showLinkedTimestamp } = eventTileSnapshot.timestamp.displayState;
|
||||||
layout: this.props.layout,
|
|
||||||
showTimestamp,
|
|
||||||
timestamp: ts,
|
|
||||||
hideTimestamp: this.props.hideTimestamp,
|
|
||||||
});
|
|
||||||
// Used to simplify the UI layout where necessary by not conditionally rendering an element at the start
|
// Used to simplify the UI layout where necessary by not conditionally rendering an element at the start
|
||||||
const dummyTimestamp = useIRCLayout ? <span className="mx_MessageTimestamp" /> : null;
|
const dummyTimestamp = useIRCLayout ? <span className="mx_MessageTimestamp" /> : null;
|
||||||
const timestamp = showRealTimestamp ? messageTimestamp : dummyTimestamp;
|
const timestamp = showRealTimestamp ? messageTimestamp : dummyTimestamp;
|
||||||
const linkedTimestamp = showLinkedTimestamp ? linkedMessageTimestamp : dummyTimestamp;
|
const linkedTimestamp = showLinkedTimestamp ? linkedMessageTimestamp : dummyTimestamp;
|
||||||
|
|
||||||
let pinnedMessageBadge: JSX.Element | undefined;
|
let pinnedMessageBadge: JSX.Element | undefined;
|
||||||
if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
|
if (hasPinnedMessageBadge) {
|
||||||
pinnedMessageBadge = <PinnedMessageBadge aria-describedby={this.id} tabIndex={0} />;
|
pinnedMessageBadge = <PinnedMessageBadge aria-describedby={this.id} tabIndex={0} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let reactionsRow: JSX.Element | undefined;
|
let reactionsRow: JSX.Element | undefined;
|
||||||
if (!isRedacted) {
|
if (hasReactionsRow) {
|
||||||
reactionsRow = (
|
reactionsRow = (
|
||||||
<ReactionsRowWrapper
|
<ReactionsRowWrapper
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
@@ -1312,11 +1259,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
forExport={this.props.forExport}
|
forExport={this.props.forExport}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
alwaysShowTimestamps={getReplyChainAlwaysShowTimestamps({
|
alwaysShowTimestamps={eventTileSnapshot.replyChain.alwaysShowTimestamps}
|
||||||
alwaysShowTimestamps: this.props.alwaysShowTimestamps,
|
|
||||||
hover: this.state.hover,
|
|
||||||
focusWithin: this.state.focusWithin,
|
|
||||||
})}
|
|
||||||
isQuoteExpanded={isQuoteExpanded}
|
isQuoteExpanded={isQuoteExpanded}
|
||||||
setQuoteExpanded={this.setQuoteExpanded}
|
setQuoteExpanded={this.setQuoteExpanded}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
@@ -1324,16 +1267,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `getSender()` because searched events might not have a proper `sender`.
|
const { hasFooter, showMainPinnedMessageBadge, showBubblePinnedMessageBadge } = eventTileSnapshot.footer;
|
||||||
const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.safeGet().getUserId();
|
|
||||||
|
|
||||||
const { hasFooter, showMainPinnedMessageBadge, showBubblePinnedMessageBadge } = getFooterDisplayState({
|
|
||||||
hasReactionsRow: !!reactionsRow,
|
|
||||||
hasReactions: !!this.state.reactions,
|
|
||||||
hasPinnedMessageBadge: !!pinnedMessageBadge,
|
|
||||||
layout: this.props.layout,
|
|
||||||
isOwnEvent,
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (this.context.timelineRenderingType) {
|
switch (this.context.timelineRenderingType) {
|
||||||
case TimelineRenderingType.Thread: {
|
case TimelineRenderingType.Thread: {
|
||||||
@@ -1341,8 +1275,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
this.props.as || "li",
|
this.props.as || "li",
|
||||||
{
|
{
|
||||||
"ref": this.ref,
|
"ref": this.ref,
|
||||||
"className": classes,
|
"className": tileClasses,
|
||||||
"aria-live": ariaLive,
|
"aria-live": tileAriaLive,
|
||||||
"aria-atomic": true,
|
"aria-atomic": true,
|
||||||
"data-scroll-tokens": scrollToken,
|
"data-scroll-tokens": scrollToken,
|
||||||
"data-has-reply": !!replyChain,
|
"data-has-reply": !!replyChain,
|
||||||
@@ -1403,9 +1337,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
this.props.as || "li",
|
this.props.as || "li",
|
||||||
{
|
{
|
||||||
"ref": this.ref,
|
"ref": this.ref,
|
||||||
"className": classes,
|
"className": tileClasses,
|
||||||
"tabIndex": -1,
|
"tabIndex": -1,
|
||||||
"aria-live": ariaLive,
|
"aria-live": tileAriaLive,
|
||||||
"aria-atomic": "true",
|
"aria-atomic": "true",
|
||||||
"data-scroll-tokens": scrollToken,
|
"data-scroll-tokens": scrollToken,
|
||||||
"data-layout": this.props.layout,
|
"data-layout": this.props.layout,
|
||||||
@@ -1491,8 +1425,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
return React.createElement(
|
return React.createElement(
|
||||||
this.props.as || "li",
|
this.props.as || "li",
|
||||||
{
|
{
|
||||||
"className": classes,
|
"className": tileClasses,
|
||||||
"aria-live": ariaLive,
|
"aria-live": tileAriaLive,
|
||||||
"aria-atomic": true,
|
"aria-atomic": true,
|
||||||
"data-scroll-tokens": scrollToken,
|
"data-scroll-tokens": scrollToken,
|
||||||
},
|
},
|
||||||
@@ -1536,9 +1470,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
this.props.as || "li",
|
this.props.as || "li",
|
||||||
{
|
{
|
||||||
"ref": this.ref,
|
"ref": this.ref,
|
||||||
"className": classes,
|
"className": tileClasses,
|
||||||
"tabIndex": -1,
|
"tabIndex": -1,
|
||||||
"aria-live": ariaLive,
|
"aria-live": tileAriaLive,
|
||||||
"aria-atomic": "true",
|
"aria-atomic": "true",
|
||||||
"data-scroll-tokens": scrollToken,
|
"data-scroll-tokens": scrollToken,
|
||||||
"data-layout": this.props.layout,
|
"data-layout": this.props.layout,
|
||||||
|
|||||||
@@ -0,0 +1,397 @@
|
|||||||
|
/*
|
||||||
|
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 { type EventStatus, type MatrixEvent, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type EventTileSenderProfileState,
|
||||||
|
type FooterDisplayState,
|
||||||
|
getAriaLive,
|
||||||
|
getEventTileAvatarMember,
|
||||||
|
getEventTileClassState,
|
||||||
|
getEventTileLineClassState,
|
||||||
|
getEventTileSenderProfileState,
|
||||||
|
getEventTileTimestamp,
|
||||||
|
getFooterDisplayState,
|
||||||
|
getIsContinuation,
|
||||||
|
getReplyChainAlwaysShowTimestamps,
|
||||||
|
getScrollToken,
|
||||||
|
getSenderProfileMode,
|
||||||
|
getShouldShowMessageActionBar,
|
||||||
|
getShouldShowTimestamp,
|
||||||
|
getShouldViewUserOnClick,
|
||||||
|
getTimestampDisplayState,
|
||||||
|
isSendingStatus,
|
||||||
|
type SenderProfileMode,
|
||||||
|
type TimestampDisplayState,
|
||||||
|
} from "./EventTileDerivedState";
|
||||||
|
import { TimelineRenderingType } from "../../../../contexts/RoomContext";
|
||||||
|
import { type Layout } from "../../../../settings/enums/Layout";
|
||||||
|
|
||||||
|
/** Event-level inputs for deriving the EventTile snapshot. */
|
||||||
|
export interface EventTileEventInput {
|
||||||
|
/** The Matrix event rendered by the tile. */
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
/** The event send status supplied by EventTile. */
|
||||||
|
eventSendStatus?: EventStatus | null;
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Display inputs for deriving the EventTile snapshot. */
|
||||||
|
export interface EventTileDisplayInput {
|
||||||
|
/** The current timeline rendering mode. */
|
||||||
|
timelineRenderingType: TimelineRenderingType;
|
||||||
|
/** The current timeline layout. */
|
||||||
|
layout?: Layout;
|
||||||
|
/** Whether the tile is a continuation of the previous event. */
|
||||||
|
continuation?: boolean;
|
||||||
|
/** Whether the event body is likely to render media content. */
|
||||||
|
isProbablyMedia: boolean;
|
||||||
|
/** Whether the tile should use bubble container styling. */
|
||||||
|
isBubbleMessage: boolean;
|
||||||
|
/** Whether the bubble tile is left-aligned. */
|
||||||
|
isLeftAlignedBubbleMessage: boolean;
|
||||||
|
/** Whether the event is aligned between bubble columns. */
|
||||||
|
isAlignedBetweenBubbles: boolean;
|
||||||
|
/** Whether the event renders as an informational timeline item. */
|
||||||
|
isInfoMessage: boolean;
|
||||||
|
/** Whether bubble styling should be suppressed for this event. */
|
||||||
|
noBubbleEvent: boolean;
|
||||||
|
/** Whether timestamps use twelve-hour formatting. */
|
||||||
|
isTwelveHour?: boolean;
|
||||||
|
/** Whether the event should be highlighted. */
|
||||||
|
isHighlighted: boolean;
|
||||||
|
/** Whether the tile is selected or has an open context menu. */
|
||||||
|
isSelected: boolean;
|
||||||
|
/** Whether the tile is the last event in the timeline. */
|
||||||
|
isLast?: boolean;
|
||||||
|
/** Whether the tile is the last event in its section. */
|
||||||
|
isLastInSection?: boolean;
|
||||||
|
/** Whether the tile is being rendered in contextual mode. */
|
||||||
|
isContextual?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interaction inputs for deriving the EventTile snapshot. */
|
||||||
|
export interface EventTileInteractionInput {
|
||||||
|
/** Whether the tile is currently hovered. */
|
||||||
|
hover: boolean;
|
||||||
|
/** Whether focus should force the action bar visible. */
|
||||||
|
showActionBarFromFocus: boolean;
|
||||||
|
/** Whether focus is currently inside the tile. */
|
||||||
|
focusWithin: boolean;
|
||||||
|
/** Whether the action bar currently has focus. */
|
||||||
|
isActionBarFocused: boolean;
|
||||||
|
/** Whether an EventTile context menu is currently open. */
|
||||||
|
hasContextMenu: boolean;
|
||||||
|
/** Whether interaction should be inhibited inside the tile. */
|
||||||
|
inhibitInteraction?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sender inputs for deriving the EventTile snapshot. */
|
||||||
|
export interface EventTileSenderInput {
|
||||||
|
/** Whether sender details should be hidden. */
|
||||||
|
hideSender?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Timestamp inputs for deriving the EventTile snapshot. */
|
||||||
|
export interface EventTileTimestampInput {
|
||||||
|
/** Whether timestamps should always show. */
|
||||||
|
alwaysShowTimestamps?: boolean;
|
||||||
|
/** Whether timestamp rendering is disabled. */
|
||||||
|
hideTimestamp?: boolean;
|
||||||
|
/** The latest thread reply timestamp, when available. */
|
||||||
|
threadReplyEventTs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Footer inputs for deriving the EventTile snapshot. */
|
||||||
|
export interface EventTileFooterInput {
|
||||||
|
/** Whether the event was sent by the current user. */
|
||||||
|
isOwnEvent: boolean;
|
||||||
|
/** Whether a reactions row element will render. */
|
||||||
|
hasReactionsRow: boolean;
|
||||||
|
/** Whether reactions data is available. */
|
||||||
|
hasReactions: boolean;
|
||||||
|
/** Whether a pinned message badge element will render. */
|
||||||
|
hasPinnedMessageBadge: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inputs for deriving the EventTile view model snapshot. */
|
||||||
|
export interface EventTileViewModelProps {
|
||||||
|
/** Event-level inputs. */
|
||||||
|
event: EventTileEventInput;
|
||||||
|
/** Display inputs. */
|
||||||
|
display: EventTileDisplayInput;
|
||||||
|
/** Interaction inputs. */
|
||||||
|
interaction: EventTileInteractionInput;
|
||||||
|
/** Sender inputs. */
|
||||||
|
sender: EventTileSenderInput;
|
||||||
|
/** Timestamp inputs. */
|
||||||
|
timestamp: EventTileTimestampInput;
|
||||||
|
/** Footer inputs. */
|
||||||
|
footer: EventTileFooterInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Event-level state derived for the EventTile snapshot. */
|
||||||
|
export interface EventTileEventSnapshot {
|
||||||
|
/** The Matrix event type. */
|
||||||
|
eventType: string;
|
||||||
|
/** The Matrix message type. */
|
||||||
|
msgtype?: string;
|
||||||
|
/** Whether the event is in a pending send state. */
|
||||||
|
isSending: boolean;
|
||||||
|
/** Whether the event is currently being edited. */
|
||||||
|
isEditing: boolean;
|
||||||
|
/** Whether EventTile should render as a continuation. */
|
||||||
|
isContinuation?: boolean;
|
||||||
|
/** Whether the tile is rendering as a notification. */
|
||||||
|
isRenderingNotification: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Root state derived for the EventTile snapshot. */
|
||||||
|
export interface EventTileRootSnapshot {
|
||||||
|
/** The aria-live setting used by EventTile. */
|
||||||
|
ariaLive?: "off";
|
||||||
|
/** The stable scroll token for the event. */
|
||||||
|
scrollToken?: string;
|
||||||
|
/** EventTile root CSS class flags. */
|
||||||
|
classState: ReturnType<typeof getEventTileClassState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Line state derived for the EventTile snapshot. */
|
||||||
|
export interface EventTileLineSnapshot {
|
||||||
|
/** EventTile line CSS class flags. */
|
||||||
|
classState: ReturnType<typeof getEventTileLineClassState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sender state derived for the EventTile snapshot. */
|
||||||
|
export interface EventTileSenderSnapshot {
|
||||||
|
/** EventTile avatar and sender profile display state. */
|
||||||
|
profileState: EventTileSenderProfileState;
|
||||||
|
/** The room member whose avatar should render. */
|
||||||
|
avatarMember: RoomMember | null;
|
||||||
|
/** Whether clicking the avatar should open the user profile. */
|
||||||
|
viewUserOnClick: boolean;
|
||||||
|
/** SenderProfile rendering mode. */
|
||||||
|
profileMode: SenderProfileMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Action bar state derived for the EventTile snapshot. */
|
||||||
|
export interface EventTileActionBarSnapshot {
|
||||||
|
/** Whether EventTile should render the message action bar. */
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Timestamp state derived for the EventTile snapshot. */
|
||||||
|
export interface EventTileTimestampSnapshot {
|
||||||
|
/** Whether EventTile should render the message timestamp. */
|
||||||
|
show: boolean;
|
||||||
|
/** The timestamp EventTile should display. */
|
||||||
|
value: number;
|
||||||
|
/** EventTile timestamp display state. */
|
||||||
|
displayState: TimestampDisplayState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reply chain state derived for the EventTile snapshot. */
|
||||||
|
export interface EventTileReplyChainSnapshot {
|
||||||
|
/** Whether ReplyChain should always show timestamps. */
|
||||||
|
alwaysShowTimestamps: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Footer state derived for the EventTile snapshot. */
|
||||||
|
export type EventTileFooterSnapshot = FooterDisplayState;
|
||||||
|
|
||||||
|
/** Derived EventTile view state consumed by the existing component. */
|
||||||
|
export interface EventTileViewModelSnapshot {
|
||||||
|
/** Event-level derived state. */
|
||||||
|
event: EventTileEventSnapshot;
|
||||||
|
/** Root derived state. */
|
||||||
|
root: EventTileRootSnapshot;
|
||||||
|
/** Line derived state. */
|
||||||
|
line: EventTileLineSnapshot;
|
||||||
|
/** Sender derived state. */
|
||||||
|
sender: EventTileSenderSnapshot;
|
||||||
|
/** Action bar derived state. */
|
||||||
|
actionBar: EventTileActionBarSnapshot;
|
||||||
|
/** Timestamp derived state. */
|
||||||
|
timestamp: EventTileTimestampSnapshot;
|
||||||
|
/** Reply chain derived state. */
|
||||||
|
replyChain: EventTileReplyChainSnapshot;
|
||||||
|
/** Footer derived state. */
|
||||||
|
footer: EventTileFooterSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Derives the current EventTile snapshot from component-owned inputs. */
|
||||||
|
export class EventTileViewModel {
|
||||||
|
/** Creates an EventTile view model snapshot. */
|
||||||
|
public static createSnapshot(props: EventTileViewModelProps): EventTileViewModelSnapshot {
|
||||||
|
const { event, display, interaction, sender, timestamp, footer } = props;
|
||||||
|
const eventType = event.mxEvent.getType();
|
||||||
|
const msgtype = event.mxEvent.getContent().msgtype;
|
||||||
|
const isSending = isSendingStatus(event.eventSendStatus ?? undefined);
|
||||||
|
const isContinuation = getIsContinuation(display.continuation, display.timelineRenderingType, display.layout);
|
||||||
|
const isRenderingNotification = display.timelineRenderingType === TimelineRenderingType.Notification;
|
||||||
|
const eventSnapshot: EventTileEventSnapshot = {
|
||||||
|
eventType,
|
||||||
|
msgtype,
|
||||||
|
isSending,
|
||||||
|
isEditing: event.isEditing,
|
||||||
|
isContinuation,
|
||||||
|
isRenderingNotification,
|
||||||
|
};
|
||||||
|
const senderProfileState = getEventTileSenderProfileState({
|
||||||
|
isRenderingNotification,
|
||||||
|
isInfoMessage: display.isInfoMessage,
|
||||||
|
timelineRenderingType: display.timelineRenderingType,
|
||||||
|
continuation: display.continuation,
|
||||||
|
eventType,
|
||||||
|
isBubbleMessage: display.isBubbleMessage,
|
||||||
|
layout: display.layout,
|
||||||
|
});
|
||||||
|
const showTimestamp = getShouldShowTimestamp({
|
||||||
|
eventTs: event.mxEvent.getTs(),
|
||||||
|
eventType,
|
||||||
|
hideTimestamp: timestamp.hideTimestamp,
|
||||||
|
alwaysShowTimestamps: timestamp.alwaysShowTimestamps,
|
||||||
|
last: display.isLast,
|
||||||
|
hover: interaction.hover,
|
||||||
|
focusWithin: interaction.focusWithin,
|
||||||
|
actionBarFocused: interaction.isActionBarFocused,
|
||||||
|
hasContextMenu: interaction.hasContextMenu,
|
||||||
|
});
|
||||||
|
const timestampValue = getEventTileTimestamp({
|
||||||
|
timelineRenderingType: display.timelineRenderingType,
|
||||||
|
eventTs: event.mxEvent.getTs(),
|
||||||
|
threadReplyEventTs: timestamp.threadReplyEventTs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
event: eventSnapshot,
|
||||||
|
root: {
|
||||||
|
ariaLive: getAriaLive(event.eventSendStatus),
|
||||||
|
scrollToken: getScrollToken(event.mxEvent),
|
||||||
|
classState: EventTileViewModel.getClassState({
|
||||||
|
event,
|
||||||
|
display,
|
||||||
|
interaction,
|
||||||
|
sender,
|
||||||
|
eventType,
|
||||||
|
msgtype,
|
||||||
|
isSending,
|
||||||
|
isContinuation,
|
||||||
|
isRenderingNotification,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
classState: getEventTileLineClassState({
|
||||||
|
isProbablyMedia: display.isProbablyMedia,
|
||||||
|
eventType,
|
||||||
|
msgtype,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
profileState: senderProfileState,
|
||||||
|
avatarMember: getEventTileAvatarMember(event.mxEvent),
|
||||||
|
viewUserOnClick: getShouldViewUserOnClick(
|
||||||
|
interaction.inhibitInteraction,
|
||||||
|
display.timelineRenderingType,
|
||||||
|
),
|
||||||
|
profileMode: getSenderProfileMode({
|
||||||
|
needsSenderProfile: senderProfileState.needsSenderProfile,
|
||||||
|
hideSender: sender.hideSender,
|
||||||
|
timelineRenderingType: display.timelineRenderingType,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
actionBar: {
|
||||||
|
show: getShouldShowMessageActionBar({
|
||||||
|
isEditing: event.isEditing,
|
||||||
|
forExport: event.forExport,
|
||||||
|
hover: interaction.hover,
|
||||||
|
showActionBarFromFocus: interaction.showActionBarFromFocus,
|
||||||
|
actionBarFocused: interaction.isActionBarFocused,
|
||||||
|
hasContextMenu: interaction.hasContextMenu,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
show: showTimestamp,
|
||||||
|
value: timestampValue,
|
||||||
|
displayState: getTimestampDisplayState({
|
||||||
|
layout: display.layout,
|
||||||
|
showTimestamp,
|
||||||
|
timestamp: timestampValue,
|
||||||
|
hideTimestamp: timestamp.hideTimestamp,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
replyChain: {
|
||||||
|
alwaysShowTimestamps: getReplyChainAlwaysShowTimestamps({
|
||||||
|
alwaysShowTimestamps: timestamp.alwaysShowTimestamps,
|
||||||
|
hover: interaction.hover,
|
||||||
|
focusWithin: interaction.focusWithin,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
footer: getFooterDisplayState({
|
||||||
|
hasReactionsRow: footer.hasReactionsRow,
|
||||||
|
hasReactions: footer.hasReactions,
|
||||||
|
hasPinnedMessageBadge: footer.hasPinnedMessageBadge,
|
||||||
|
layout: display.layout,
|
||||||
|
isOwnEvent: footer.isOwnEvent,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getClassState({
|
||||||
|
event,
|
||||||
|
display,
|
||||||
|
interaction,
|
||||||
|
sender,
|
||||||
|
eventType,
|
||||||
|
msgtype,
|
||||||
|
isSending,
|
||||||
|
isContinuation,
|
||||||
|
isRenderingNotification,
|
||||||
|
}: {
|
||||||
|
event: EventTileEventInput;
|
||||||
|
display: EventTileDisplayInput;
|
||||||
|
interaction: EventTileInteractionInput;
|
||||||
|
sender: EventTileSenderInput;
|
||||||
|
eventType: string;
|
||||||
|
msgtype?: string;
|
||||||
|
isSending: boolean;
|
||||||
|
isContinuation?: boolean;
|
||||||
|
isRenderingNotification: boolean;
|
||||||
|
}): ReturnType<typeof getEventTileClassState> {
|
||||||
|
return getEventTileClassState({
|
||||||
|
isBubbleMessage: display.isBubbleMessage,
|
||||||
|
isLeftAlignedBubbleMessage: display.isLeftAlignedBubbleMessage,
|
||||||
|
isAlignedBetweenBubbles: display.isAlignedBetweenBubbles,
|
||||||
|
isEditing: event.isEditing,
|
||||||
|
isInfoMessage: display.isInfoMessage,
|
||||||
|
isTwelveHour: display.isTwelveHour,
|
||||||
|
isSending,
|
||||||
|
isHighlighted: display.isHighlighted,
|
||||||
|
isSelected: display.isSelected,
|
||||||
|
isContinuation,
|
||||||
|
eventType,
|
||||||
|
isLast: display.isLast,
|
||||||
|
isLastInSection: display.isLastInSection,
|
||||||
|
isContextual: display.isContextual,
|
||||||
|
isActionBarFocused: interaction.isActionBarFocused,
|
||||||
|
isEncryptionFailure: event.isEncryptionFailure,
|
||||||
|
msgtype,
|
||||||
|
hideSender: sender.hideSender,
|
||||||
|
timelineRenderingType: display.timelineRenderingType,
|
||||||
|
isRenderingNotification,
|
||||||
|
noBubbleEvent: display.noBubbleEvent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
/*
|
||||||
|
* 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 { EventStatus, EventType, type MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { mkEvent } from "../../test-utils";
|
||||||
|
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
|
||||||
|
import { Layout } from "../../../src/settings/enums/Layout";
|
||||||
|
import {
|
||||||
|
EventTileViewModel,
|
||||||
|
type EventTileViewModelProps,
|
||||||
|
} from "../../../src/viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||||
|
|
||||||
|
describe("EventTileViewModel", () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const userId = "@alice:example.org";
|
||||||
|
type EventTileViewModelPropsOverrides = {
|
||||||
|
event?: Partial<EventTileViewModelProps["event"]>;
|
||||||
|
display?: Partial<EventTileViewModelProps["display"]>;
|
||||||
|
interaction?: Partial<EventTileViewModelProps["interaction"]>;
|
||||||
|
sender?: Partial<EventTileViewModelProps["sender"]>;
|
||||||
|
timestamp?: Partial<EventTileViewModelProps["timestamp"]>;
|
||||||
|
footer?: Partial<EventTileViewModelProps["footer"]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeMessageEvent({
|
||||||
|
type = EventType.RoomMessage,
|
||||||
|
content = { msgtype: MsgType.Text, body: "Hello" },
|
||||||
|
ts = 123,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
type?: string;
|
||||||
|
content?: Record<string, unknown>;
|
||||||
|
ts?: number;
|
||||||
|
status?: EventStatus;
|
||||||
|
} = {}): MatrixEvent {
|
||||||
|
return mkEvent({
|
||||||
|
event: true,
|
||||||
|
type,
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content,
|
||||||
|
ts,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeProps(overrides: EventTileViewModelPropsOverrides = {}): EventTileViewModelProps {
|
||||||
|
return {
|
||||||
|
event: {
|
||||||
|
mxEvent: makeMessageEvent(),
|
||||||
|
isEditing: false,
|
||||||
|
isEncryptionFailure: false,
|
||||||
|
...overrides.event,
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
timelineRenderingType: TimelineRenderingType.Room,
|
||||||
|
layout: Layout.Group,
|
||||||
|
isProbablyMedia: false,
|
||||||
|
isBubbleMessage: false,
|
||||||
|
isLeftAlignedBubbleMessage: false,
|
||||||
|
isAlignedBetweenBubbles: false,
|
||||||
|
isInfoMessage: false,
|
||||||
|
noBubbleEvent: false,
|
||||||
|
isHighlighted: false,
|
||||||
|
isSelected: false,
|
||||||
|
...overrides.display,
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
hover: false,
|
||||||
|
showActionBarFromFocus: false,
|
||||||
|
focusWithin: false,
|
||||||
|
isActionBarFocused: false,
|
||||||
|
hasContextMenu: false,
|
||||||
|
...overrides.interaction,
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
...overrides.sender,
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
...overrides.timestamp,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
isOwnEvent: false,
|
||||||
|
hasReactionsRow: false,
|
||||||
|
hasReactions: false,
|
||||||
|
hasPinnedMessageBadge: false,
|
||||||
|
...overrides.footer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("derives sending, aria-live, and local echo scroll state", () => {
|
||||||
|
const mxEvent = makeMessageEvent({ status: EventStatus.SENDING });
|
||||||
|
|
||||||
|
const snapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
event: {
|
||||||
|
mxEvent,
|
||||||
|
eventSendStatus: EventStatus.SENDING,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.event.isSending).toBe(true);
|
||||||
|
expect(snapshot.root.ariaLive).toBe("off");
|
||||||
|
expect(snapshot.root.scrollToken).toBeUndefined();
|
||||||
|
expect(snapshot.root.classState.mx_EventTile_sending).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes continuation by rendering mode and bubble layout", () => {
|
||||||
|
const fileSnapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
display: {
|
||||||
|
continuation: true,
|
||||||
|
timelineRenderingType: TimelineRenderingType.File,
|
||||||
|
layout: Layout.Group,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const bubbleSnapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
display: {
|
||||||
|
continuation: true,
|
||||||
|
timelineRenderingType: TimelineRenderingType.File,
|
||||||
|
layout: Layout.Bubble,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fileSnapshot.event.isContinuation).toBe(false);
|
||||||
|
expect(bubbleSnapshot.event.isContinuation).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives line classes from event type, message type, and media eligibility", () => {
|
||||||
|
const snapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
event: {
|
||||||
|
mxEvent: makeMessageEvent({
|
||||||
|
content: { msgtype: MsgType.Image, body: "image" },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
isProbablyMedia: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.line.classState).toMatchObject({
|
||||||
|
mx_EventTile_mediaLine: true,
|
||||||
|
mx_EventTile_image: true,
|
||||||
|
mx_EventTile_sticker: false,
|
||||||
|
mx_EventTile_emote: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives aligned-between-bubbles root class state", () => {
|
||||||
|
const snapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
display: {
|
||||||
|
isAlignedBetweenBubbles: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.root.classState.mx_EventTile_alignedBetweenBubbles).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives avatar and sender profile state for thread timelines", () => {
|
||||||
|
const snapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
display: {
|
||||||
|
timelineRenderingType: TimelineRenderingType.Thread,
|
||||||
|
continuation: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.sender.profileState).toEqual({
|
||||||
|
avatarSize: "32px",
|
||||||
|
needsSenderProfile: true,
|
||||||
|
});
|
||||||
|
expect(snapshot.sender.profileMode).toBe("clickable");
|
||||||
|
expect(snapshot.sender.viewUserOnClick).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives action bar visibility from interaction state", () => {
|
||||||
|
const hoverSnapshot = EventTileViewModel.createSnapshot(makeProps({ interaction: { hover: true } }));
|
||||||
|
const contextMenuSnapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
interaction: {
|
||||||
|
isActionBarFocused: true,
|
||||||
|
hasContextMenu: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(hoverSnapshot.actionBar.show).toBe(true);
|
||||||
|
expect(contextMenuSnapshot.actionBar.show).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives timestamp state for thread list events", () => {
|
||||||
|
const snapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
display: {
|
||||||
|
timelineRenderingType: TimelineRenderingType.ThreadsList,
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
hover: true,
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
threadReplyEventTs: 456,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.timestamp.show).toBe(true);
|
||||||
|
expect(snapshot.timestamp.value).toBe(456);
|
||||||
|
expect(snapshot.timestamp.displayState.showRealTimestamp).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses RTC notification timestamps", () => {
|
||||||
|
const snapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
event: {
|
||||||
|
mxEvent: makeMessageEvent({
|
||||||
|
type: EventType.RTCNotification,
|
||||||
|
content: {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
hover: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapshot.timestamp.show).toBe(false);
|
||||||
|
expect(snapshot.timestamp.displayState.showRealTimestamp).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives footer placement state", () => {
|
||||||
|
const groupSnapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
display: {
|
||||||
|
layout: Layout.Group,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
isOwnEvent: true,
|
||||||
|
hasReactionsRow: true,
|
||||||
|
hasReactions: true,
|
||||||
|
hasPinnedMessageBadge: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const bubbleSnapshot = EventTileViewModel.createSnapshot(
|
||||||
|
makeProps({
|
||||||
|
display: {
|
||||||
|
layout: Layout.Bubble,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
isOwnEvent: true,
|
||||||
|
hasPinnedMessageBadge: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(groupSnapshot.footer).toMatchObject({
|
||||||
|
hasFooter: true,
|
||||||
|
showMainPinnedMessageBadge: true,
|
||||||
|
showBubblePinnedMessageBadge: false,
|
||||||
|
});
|
||||||
|
expect(bubbleSnapshot.footer).toMatchObject({
|
||||||
|
hasFooter: true,
|
||||||
|
showMainPinnedMessageBadge: false,
|
||||||
|
showBubblePinnedMessageBadge: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user