* Make EventTileViewModel usage coherent * Extract render helpers * Extracted small render views * Reduce Render Branch Duplication * Clean Up Dead or Legacy Inline Code * Add/Update documentation * Updated snapshot * Fix Sonar issue * Updated snapshots after merge error * Fix prettier issue
This commit is contained in:
@@ -18,7 +18,6 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
type EventStatus,
|
type EventStatus,
|
||||||
EventType,
|
|
||||||
type MatrixEvent,
|
type MatrixEvent,
|
||||||
MatrixEventEvent,
|
MatrixEventEvent,
|
||||||
type Relations,
|
type Relations,
|
||||||
@@ -29,10 +28,8 @@ import {
|
|||||||
ThreadEvent,
|
ThreadEvent,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
|
|
||||||
import { uniqueId } from "lodash";
|
import { uniqueId } from "lodash";
|
||||||
import { ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
import { useCreateAutoDisposedViewModel, TileErrorView } from "@element-hq/web-shared-components";
|
||||||
import { useCreateAutoDisposedViewModel, PinnedMessageBadge, TileErrorView } from "@element-hq/web-shared-components";
|
|
||||||
|
|
||||||
import ReplyChain from "../elements/ReplyChain";
|
import ReplyChain from "../elements/ReplyChain";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
@@ -57,7 +54,12 @@ import { copyPlaintext } from "../../../utils/strings";
|
|||||||
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
||||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../events/EventTileFactory";
|
import {
|
||||||
|
haveRendererForEvent,
|
||||||
|
isMessageEvent,
|
||||||
|
renderTile,
|
||||||
|
type EventTileTypeProps,
|
||||||
|
} from "../../../events/EventTileFactory";
|
||||||
import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||||
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
||||||
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
|
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
|
||||||
@@ -65,14 +67,13 @@ import PinningUtils from "../../../utils/PinningUtils";
|
|||||||
import { ActionBarAdapter } from "./EventTile/ActionBarAdapter";
|
import { ActionBarAdapter } from "./EventTile/ActionBarAdapter";
|
||||||
import { E2eStandardPadlockIcon } from "./EventTile/E2eStandardPadlockIcon";
|
import { E2eStandardPadlockIcon } from "./EventTile/E2eStandardPadlockIcon";
|
||||||
import { E2eMessageSharedIconAdapter } from "./EventTile/E2eMessageSharedIconAdapter";
|
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";
|
import { ReceiptAdapter } from "./EventTile/ReceiptAdapter";
|
||||||
import { EventTileAvatarAdapter, EventTileSenderAdapter } from "./EventTile/SenderIdentityAdapter";
|
import { EventTileAvatarAdapter, EventTileSenderAdapter } from "./EventTile/SenderIdentityAdapter";
|
||||||
import { ThreadListActionBarAdapter } from "./EventTile/ThreadListActionBarAdapter";
|
import { ThreadListActionBarAdapter } from "./EventTile/ThreadListActionBarAdapter";
|
||||||
import { ThreadMessagePreviewAdapter } from "./EventTile/ThreadMessagePreviewAdapter";
|
import { EventTileFooter } from "./EventTile/EventTileFooter";
|
||||||
import { ThreadSummaryAdapter } from "./EventTile/ThreadSummaryAdapter";
|
import { EventTilePreviewBody } from "./EventTile/EventTilePreviewBody";
|
||||||
|
import { EventTileThreadInfo, EventTileThreadPanelSummary } from "./EventTile/EventTileThreadInfo";
|
||||||
|
import { EventTileTimestampSlot } from "./EventTile/EventTileTimestampSlot";
|
||||||
import {
|
import {
|
||||||
EventTileViewModel,
|
EventTileViewModel,
|
||||||
type EventTileViewModelProps,
|
type EventTileViewModelProps,
|
||||||
@@ -107,8 +108,9 @@ import {
|
|||||||
} from "../../../viewmodels/room/timeline/event-tile/reactions/EventTileReactionState";
|
} from "../../../viewmodels/room/timeline/event-tile/reactions/EventTileReactionState";
|
||||||
import { TileErrorViewModel } from "../../../viewmodels/message-body/TileErrorViewModel";
|
import { TileErrorViewModel } from "../../../viewmodels/message-body/TileErrorViewModel";
|
||||||
import { useSettingValue } from "../../../hooks/useSettings";
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
import { DecryptionFailureBodyFactory, RedactedBodyFactory } from "../messages/MBodyFactory";
|
|
||||||
import { EventTileE2eViewModel } from "../../../viewmodels/room/timeline/event-tile/EventTileE2eViewModel";
|
import { EventTileE2eViewModel } from "../../../viewmodels/room/timeline/event-tile/EventTileE2eViewModel";
|
||||||
|
import { shouldHighlightEventTile } from "../../../viewmodels/room/timeline/event-tile/EventTileHighlightState";
|
||||||
|
import { shouldHideEventTile } from "../../../viewmodels/room/timeline/event-tile/EventTileVisibilityState";
|
||||||
|
|
||||||
/** Relation lookup type retained for EventTile consumers. */
|
/** Relation lookup type retained for EventTile consumers. */
|
||||||
export type { GetRelationsForEvent } from "../../../viewmodels/room/timeline/event-tile/reactions/EventTileReactionState";
|
export type { GetRelationsForEvent } from "../../../viewmodels/room/timeline/event-tile/reactions/EventTileReactionState";
|
||||||
@@ -124,134 +126,136 @@ export type { GetRelationsForEvent } from "../../../viewmodels/room/timeline/eve
|
|||||||
// | '--------------------------------------' |
|
// | '--------------------------------------' |
|
||||||
// '----------------------------------------------------------'
|
// '----------------------------------------------------------'
|
||||||
|
|
||||||
|
/** Read receipt entry rendered beside an EventTile. */
|
||||||
export interface IReadReceiptProps {
|
export interface IReadReceiptProps {
|
||||||
|
/** User ID that owns the receipt. */
|
||||||
userId: string;
|
userId: string;
|
||||||
|
/** Room member profile for the receipt user, if available. */
|
||||||
roomMember: RoomMember | null;
|
roomMember: RoomMember | null;
|
||||||
|
/** Receipt timestamp in milliseconds. */
|
||||||
ts: number;
|
ts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Operations exposed by specialized event tile body renderers. */
|
||||||
export interface IEventTileOps {
|
export interface IEventTileOps {
|
||||||
|
/** Whether the body renderer is currently hiding a widget. */
|
||||||
isWidgetHidden(): boolean;
|
isWidgetHidden(): boolean;
|
||||||
|
/** Reveal a hidden widget in the body renderer. */
|
||||||
unhideWidget(): void;
|
unhideWidget(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ref surface exposed by concrete event tile body components. */
|
||||||
export interface IEventTileType extends React.Component<HTMLDivElement> {
|
export interface IEventTileType extends React.Component<HTMLDivElement> {
|
||||||
|
/** Optional body-specific operations for context menu actions. */
|
||||||
getEventTileOps?(): IEventTileOps;
|
getEventTileOps?(): IEventTileOps;
|
||||||
|
/** Media helper owned by the body renderer, when the event has media. */
|
||||||
getMediaHelper(): MediaEventHelper | undefined;
|
getMediaHelper(): MediaEventHelper | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Props used to render an event in a room timeline surface. */
|
||||||
export interface EventTileProps {
|
export interface EventTileProps {
|
||||||
// the MatrixEvent to show
|
/** Matrix event to render. */
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
|
||||||
// true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
|
/**
|
||||||
// might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
|
* Whether the event is redacted.
|
||||||
// references the same this.props.mxEvent.
|
* This mirrors `mxEvent.isRedacted()` because `mxEvent` may retain object identity across updates.
|
||||||
|
*/
|
||||||
isRedacted?: boolean;
|
isRedacted?: boolean;
|
||||||
|
|
||||||
// true if this is a continuation of the previous event (which has the
|
/** Whether this event continues the previous sender block. */
|
||||||
// effect of not showing another avatar/displayname
|
|
||||||
continuation?: boolean;
|
continuation?: boolean;
|
||||||
|
|
||||||
// true if this is the last event in the timeline (which has the effect
|
/** Whether this is the last event in the timeline. */
|
||||||
// of always showing the timestamp)
|
|
||||||
last?: boolean;
|
last?: boolean;
|
||||||
|
|
||||||
// true if the event is the last event in a section (adds a css class for
|
/** Whether this is the last event in a timeline section. */
|
||||||
// targeting)
|
|
||||||
lastInSection?: boolean;
|
lastInSection?: boolean;
|
||||||
|
|
||||||
// True if the event is the last successful (sent) event.
|
/** Whether this is the most recent successfully sent event. */
|
||||||
lastSuccessful?: boolean;
|
lastSuccessful?: boolean;
|
||||||
|
|
||||||
// true if this is search context (which has the effect of greying out
|
/** Whether this tile is rendered with search-context styling. */
|
||||||
// the text
|
|
||||||
contextual?: boolean;
|
contextual?: boolean;
|
||||||
|
|
||||||
// a list of words to highlight, ordered by longest first
|
/** Words to highlight in the event body, ordered longest first. */
|
||||||
highlights?: string[];
|
highlights?: string[];
|
||||||
|
|
||||||
// link URL for the highlights
|
/** Link URL used for highlighted terms. */
|
||||||
highlightLink?: string;
|
highlightLink?: string;
|
||||||
|
|
||||||
// should show URL previews for this event
|
/** Whether URL previews should be shown for this event. */
|
||||||
showUrlPreview?: boolean;
|
showUrlPreview?: boolean;
|
||||||
|
|
||||||
// is this the focused event
|
/** Whether this is the currently selected event. */
|
||||||
isSelectedEvent?: boolean;
|
isSelectedEvent?: boolean;
|
||||||
|
|
||||||
|
/** Resize observer used by the parent timeline, if any. */
|
||||||
resizeObserver?: ResizeObserver;
|
resizeObserver?: ResizeObserver;
|
||||||
|
|
||||||
// a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'.
|
/** Read receipts to show for this event. */
|
||||||
readReceipts?: IReadReceiptProps[];
|
readReceipts?: IReadReceiptProps[];
|
||||||
|
|
||||||
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
/** Opaque read receipt animation state keyed by user ID. */
|
||||||
// to manage its animations. Should be an empty object when the room
|
|
||||||
// first loads
|
|
||||||
readReceiptMap?: { [userId: string]: IReadReceiptPosition };
|
readReceiptMap?: { [userId: string]: IReadReceiptPosition };
|
||||||
|
|
||||||
// A function which is used to check if the parent panel is being
|
/** Returns whether the parent panel is unmounting and tile work should be skipped. */
|
||||||
// unmounted, to avoid unnecessary work. Should return true if we
|
|
||||||
// are being unmounted.
|
|
||||||
checkUnmounting?: () => boolean;
|
checkUnmounting?: () => boolean;
|
||||||
|
|
||||||
// the status of this event - ie, mxEvent.status. Denormalised to here so
|
/** Denormalized `mxEvent.status` so status changes can trigger tile updates. */
|
||||||
// that we can tell when it changes.
|
|
||||||
eventSendStatus?: EventStatus;
|
eventSendStatus?: EventStatus;
|
||||||
|
|
||||||
|
/** Whether this tile is being rendered for export. */
|
||||||
forExport?: boolean;
|
forExport?: boolean;
|
||||||
|
|
||||||
// show twelve hour timestamps
|
/** Whether timestamps should use twelve-hour formatting. */
|
||||||
isTwelveHour?: boolean;
|
isTwelveHour?: boolean;
|
||||||
|
|
||||||
// helper function to access relations for this event
|
/** Helper used to access relations for this event. */
|
||||||
getRelationsForEvent?: GetRelationsForEvent;
|
getRelationsForEvent?: GetRelationsForEvent;
|
||||||
|
|
||||||
// whether to show reactions for this event
|
/** Whether reactions should be shown for this event. */
|
||||||
showReactions?: boolean;
|
showReactions?: boolean;
|
||||||
|
|
||||||
// which layout to use
|
/** Timeline layout used by this tile. */
|
||||||
layout?: Layout;
|
layout?: Layout;
|
||||||
|
|
||||||
// whether or not to show read receipts
|
/** Whether read receipts should be shown for this event. */
|
||||||
showReadReceipts?: boolean;
|
showReadReceipts?: boolean;
|
||||||
|
|
||||||
// Used while editing, to pass the event, and to preserve editor state
|
/** Editor state used while editing and across remote echo remounts. */
|
||||||
// from one editor instance to another when remounting the editor
|
|
||||||
// upon receiving the remote echo for an unsent event.
|
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
|
|
||||||
// Event ID of the event replacing the content of this event, if any
|
/** Event ID replacing this event's content, if any. */
|
||||||
replacingEventId?: string;
|
replacingEventId?: string;
|
||||||
|
|
||||||
// Helper to build permalinks for the room
|
/** Helper used to build room and event permalinks. */
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
|
||||||
// LegacyCallEventGrouper for this event
|
/** Legacy call event grouping state for this event. */
|
||||||
callEventGrouper?: LegacyCallEventGrouper;
|
callEventGrouper?: LegacyCallEventGrouper;
|
||||||
|
|
||||||
// Symbol of the root node
|
/** Element type used for the root node. */
|
||||||
as?: string;
|
as?: string;
|
||||||
|
|
||||||
// whether or not to always show timestamps
|
/** Whether timestamps should always be visible. */
|
||||||
alwaysShowTimestamps?: boolean;
|
alwaysShowTimestamps?: boolean;
|
||||||
|
|
||||||
// whether or not to display the sender
|
/** Whether sender identity should be hidden. */
|
||||||
hideSender?: boolean;
|
hideSender?: boolean;
|
||||||
|
|
||||||
// whether or not to display thread info
|
/** Whether thread information should be shown. */
|
||||||
showThreadInfo?: boolean;
|
showThreadInfo?: boolean;
|
||||||
|
|
||||||
// if specified and `true`, the message is being
|
/** Whether the current user can see a message hidden from other users for moderation. */
|
||||||
// hidden for moderation from other users but is
|
|
||||||
// displayed to the current user either because they're
|
|
||||||
// the author or they are a moderator
|
|
||||||
isSeeingThroughMessageHiddenForModeration?: boolean;
|
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||||
|
|
||||||
// The following properties are used by EventTilePreview to disable tab indexes within the event tile
|
/** Whether the timestamp should be hidden for preview rendering. */
|
||||||
hideTimestamp?: boolean;
|
hideTimestamp?: boolean;
|
||||||
|
/** Whether interactive controls inside the tile should be inhibited. */
|
||||||
inhibitInteraction?: boolean;
|
inhibitInteraction?: boolean;
|
||||||
|
|
||||||
|
/** Ref for imperative access to the unwrapped tile instance. */
|
||||||
ref?: Ref<UnwrappedEventTile>;
|
ref?: Ref<UnwrappedEventTile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +278,13 @@ interface EventTileRenderInputs {
|
|||||||
isOwnEvent: boolean;
|
isOwnEvent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MUST be rendered within a RoomContext with a set timelineRenderingType
|
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> {
|
export class UnwrappedEventTile extends React.Component<EventTileProps, IState> {
|
||||||
private suppressReadReceiptAnimation: boolean;
|
private suppressReadReceiptAnimation: boolean;
|
||||||
private isListeningForReceipts: boolean;
|
private isListeningForReceipts: boolean;
|
||||||
@@ -467,51 +477,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderThreadPanelSummary(threadState: EventTileThreadState): JSX.Element | null {
|
|
||||||
if (!threadState.shouldShowThreadPanelSummary || !threadState.thread) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_ThreadPanel_replies">
|
|
||||||
<ThreadsIcon />
|
|
||||||
<span className="mx_ThreadPanel_replies_amount">{threadState.thread.length}</span>
|
|
||||||
<ThreadMessagePreviewAdapter eventTileViewModel={this.viewModel} thread={threadState.thread} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderThreadInfo(threadState: EventTileThreadState): React.ReactNode {
|
|
||||||
if (threadState.shouldShowThreadSummary && threadState.thread) {
|
|
||||||
return (
|
|
||||||
<ThreadSummaryAdapter
|
|
||||||
eventTileViewModel={this.viewModel}
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
thread={threadState.thread}
|
|
||||||
data-testid="thread-summary"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threadState.searchThreadInfo.kind === "link") {
|
|
||||||
return (
|
|
||||||
<a className="mx_ThreadSummary_icon" href={threadState.searchThreadInfo.href}>
|
|
||||||
<ThreadsIcon />
|
|
||||||
{_t("timeline|thread_info_basic")}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threadState.searchThreadInfo.kind === "text") {
|
|
||||||
return (
|
|
||||||
<p className="mx_ThreadSummary_icon">
|
|
||||||
<ThreadsIcon />
|
|
||||||
{_t("timeline|thread_info_basic")}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly onViewInRoomClick = (_anchor: HTMLElement | null): void => {
|
private readonly onViewInRoomClick = (_anchor: HTMLElement | null): void => {
|
||||||
dis.dispatch<ViewRoomPayload>({
|
dis.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
@@ -610,42 +575,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine whether an event should be highlighted
|
|
||||||
* For edited events, if a previous version of the event was highlighted
|
|
||||||
* the event should remain highlighted as the user may have been notified
|
|
||||||
* (Clearer explanation of why an event is highlighted is planned -
|
|
||||||
* https://github.com/vector-im/element-web/issues/24927)
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
|
||||||
private shouldHighlight(): boolean {
|
|
||||||
if (this.props.forExport) return false;
|
|
||||||
if (this.context.timelineRenderingType === TimelineRenderingType.Notification) return false;
|
|
||||||
if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) return false;
|
|
||||||
|
|
||||||
if (this.props.isRedacted) return false;
|
|
||||||
|
|
||||||
// This event is a room mention but we don't want the call tile to have a highlight.
|
|
||||||
if (this.props.mxEvent.getType() === EventType.RTCNotification) return false;
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.safeGet();
|
|
||||||
const actions = cli.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
|
||||||
// get the actions for the previous version of the event too if it is an edit
|
|
||||||
const previousActions = this.props.mxEvent.replacingEvent()
|
|
||||||
? cli.getPushActionsForEvent(this.props.mxEvent)
|
|
||||||
: undefined;
|
|
||||||
if (!actions?.tweaks && !previousActions?.tweaks) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't show self-highlights from another of our clients
|
|
||||||
if (this.props.mxEvent.getSender() === cli.credentials.userId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!(actions?.tweaks.highlight || previousActions?.tweaks.highlight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly onSenderProfileClick = (): void => {
|
private readonly onSenderProfileClick = (): void => {
|
||||||
dis.dispatch<ComposerInsertPayload>({
|
dis.dispatch<ComposerInsertPayload>({
|
||||||
action: Action.ComposerInsert,
|
action: Action.ComposerInsert,
|
||||||
@@ -821,18 +750,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* In some cases we can't use shouldHideEvent() since whether or not we hide
|
|
||||||
* an event depends on other things that the event itself
|
|
||||||
* @returns {boolean} true if event should be hidden
|
|
||||||
*/
|
|
||||||
private shouldHideEvent(): boolean {
|
|
||||||
// If the call was replaced we don't render anything since we render the other call
|
|
||||||
if (this.props.callEventGrouper?.hangupReason === CallErrorCode.Replaced) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createMessageTimestampProps(ts: number): MessageTimestampViewModelProps {
|
private createMessageTimestampProps(ts: number): MessageTimestampViewModelProps {
|
||||||
return {
|
return {
|
||||||
showRelative: this.context.timelineRenderingType === TimelineRenderingType.ThreadsList,
|
showRelative: this.context.timelineRenderingType === TimelineRenderingType.ThreadsList,
|
||||||
@@ -861,12 +778,57 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
return "#";
|
return "#";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createRootAttributes({
|
||||||
|
tileClasses,
|
||||||
|
tileAriaLive,
|
||||||
|
scrollToken,
|
||||||
|
}: EventTileRootRenderState): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
"className": tileClasses,
|
||||||
|
"aria-live": tileAriaLive,
|
||||||
|
"aria-atomic": true,
|
||||||
|
"data-scroll-tokens": scrollToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createInteractiveRootAttributes(rootRenderState: EventTileRootRenderState): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
...this.createRootAttributes(rootRenderState),
|
||||||
|
ref: this.ref,
|
||||||
|
onMouseEnter: this.onMouseEnter,
|
||||||
|
onMouseLeave: this.onMouseLeave,
|
||||||
|
onFocus: this.onFocusWithin,
|
||||||
|
onBlur: this.onBlurWithin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRenderTileProps({
|
||||||
|
replacingEventId,
|
||||||
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
|
permalinkCreator = this.props.permalinkCreator,
|
||||||
|
}: {
|
||||||
|
replacingEventId?: string;
|
||||||
|
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||||
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
}): EventTileTypeProps {
|
||||||
|
return {
|
||||||
|
...this.props,
|
||||||
|
ref: this.tile,
|
||||||
|
...(replacingEventId === undefined ? {} : { replacingEventId }),
|
||||||
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
|
highlights: this.props.highlights,
|
||||||
|
highlightLink: this.props.highlightLink,
|
||||||
|
permalinkCreator,
|
||||||
|
showHiddenEvents: this.context.showHiddenEvents,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private createRenderInputs(
|
private createRenderInputs(
|
||||||
displayInfo = getEventDisplayInfo(
|
displayInfo = getEventDisplayInfo(
|
||||||
MatrixClientPeg.safeGet(),
|
MatrixClientPeg.safeGet(),
|
||||||
this.props.mxEvent,
|
this.props.mxEvent,
|
||||||
this.context.showHiddenEvents,
|
this.context.showHiddenEvents,
|
||||||
this.shouldHideEvent(),
|
shouldHideEventTile({ callEventGrouper: this.props.callEventGrouper }),
|
||||||
),
|
),
|
||||||
): EventTileRenderInputs {
|
): EventTileRenderInputs {
|
||||||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||||
@@ -910,7 +872,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
isInfoMessage: displayInfo.isInfoMessage,
|
isInfoMessage: displayInfo.isInfoMessage,
|
||||||
noBubbleEvent: displayInfo.noBubbleEvent,
|
noBubbleEvent: displayInfo.noBubbleEvent,
|
||||||
isTwelveHour: this.props.isTwelveHour,
|
isTwelveHour: this.props.isTwelveHour,
|
||||||
isHighlighted: this.shouldHighlight(),
|
isHighlighted: shouldHighlightEventTile({
|
||||||
|
cli: MatrixClientPeg.safeGet(),
|
||||||
|
mxEvent: this.props.mxEvent,
|
||||||
|
timelineRenderingType: this.context.timelineRenderingType,
|
||||||
|
forExport: this.props.forExport,
|
||||||
|
isRedacted: this.props.isRedacted,
|
||||||
|
}),
|
||||||
isSelected: this.props.isSelectedEvent || !!this.state.interaction.contextMenu,
|
isSelected: this.props.isSelectedEvent || !!this.state.interaction.contextMenu,
|
||||||
isLast: this.props.last,
|
isLast: this.props.last,
|
||||||
isLastInSection: this.props.lastInSection,
|
isLastInSection: this.props.lastInSection,
|
||||||
@@ -973,7 +941,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
MatrixClientPeg.safeGet(),
|
MatrixClientPeg.safeGet(),
|
||||||
this.props.mxEvent,
|
this.props.mxEvent,
|
||||||
this.context.showHiddenEvents,
|
this.context.showHiddenEvents,
|
||||||
this.shouldHideEvent(),
|
shouldHideEventTile({ callEventGrouper: this.props.callEventGrouper }),
|
||||||
);
|
);
|
||||||
const { hasRenderer, isSeeingThroughMessageHiddenForModeration } = displayInfo;
|
const { hasRenderer, isSeeingThroughMessageHiddenForModeration } = displayInfo;
|
||||||
const { isQuoteExpanded } = this.state;
|
const { isQuoteExpanded } = this.state;
|
||||||
@@ -992,7 +960,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
const renderInputs = this.createRenderInputs(displayInfo);
|
const renderInputs = this.createRenderInputs(displayInfo);
|
||||||
const { hasPinnedMessageBadge, hasReactionsRow, threadState, isOwnEvent } = renderInputs;
|
const { hasPinnedMessageBadge, hasReactionsRow, threadState, isOwnEvent } = renderInputs;
|
||||||
|
|
||||||
const eventTileRenderState = EventTileViewModel.createRenderState(this.createViewModelProps(renderInputs));
|
this.viewModel.setProps(this.createViewModelProps(renderInputs));
|
||||||
|
const eventTileRenderState = this.viewModel.getSnapshot();
|
||||||
const eventTileSnapshot = eventTileRenderState.snapshot;
|
const eventTileSnapshot = eventTileRenderState.snapshot;
|
||||||
|
|
||||||
const lineClasses = eventTileRenderState.line.className;
|
const lineClasses = eventTileRenderState.line.className;
|
||||||
@@ -1002,9 +971,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
|
|
||||||
const permalink = this.getPermalink();
|
const permalink = this.getPermalink();
|
||||||
|
|
||||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
|
||||||
// Local echos have a send "status".
|
|
||||||
const scrollToken = eventTileRenderState.root.scrollToken;
|
const scrollToken = eventTileRenderState.root.scrollToken;
|
||||||
|
const rootRenderState = { tileClasses, tileAriaLive, scrollToken };
|
||||||
|
|
||||||
const avatar = (
|
const avatar = (
|
||||||
<EventTileAvatarAdapter mxEvent={this.props.mxEvent} senderSnapshot={eventTileSnapshot.sender} />
|
<EventTileAvatarAdapter mxEvent={this.props.mxEvent} senderSnapshot={eventTileSnapshot.sender} />
|
||||||
@@ -1032,52 +1000,30 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
/>
|
/>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
// Thread panel shows the timestamp of the last reply in that thread
|
|
||||||
const ts = eventTileRenderState.timestamp.value;
|
const ts = eventTileRenderState.timestamp.value;
|
||||||
|
|
||||||
const messageTimestampProps = this.createMessageTimestampProps(ts);
|
const messageTimestampProps = this.createMessageTimestampProps(ts);
|
||||||
const linkedMessageTimestampProps = this.createLinkedMessageTimestampProps(messageTimestampProps);
|
const linkedMessageTimestampProps = this.createLinkedMessageTimestampProps(messageTimestampProps);
|
||||||
|
|
||||||
// Used to simplify the UI layout where necessary by not conditionally rendering an element at the start
|
const timestamp = (
|
||||||
const dummyTimestamp = eventTileRenderState.timestamp.showDummy ? (
|
<EventTileTimestampSlot
|
||||||
<span className="mx_MessageTimestamp" />
|
|
||||||
) : null;
|
|
||||||
const timestamp = eventTileRenderState.timestamp.displayState.showRealTimestamp ? (
|
|
||||||
<MessageTimestampAdapter
|
|
||||||
eventTileViewModel={this.viewModel}
|
eventTileViewModel={this.viewModel}
|
||||||
kind="plain"
|
kind="plain"
|
||||||
timestampProps={messageTimestampProps}
|
timestampProps={messageTimestampProps}
|
||||||
|
showTimestamp={eventTileRenderState.timestamp.displayState.showRealTimestamp}
|
||||||
|
showDummy={eventTileRenderState.timestamp.showDummy}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
dummyTimestamp
|
|
||||||
);
|
);
|
||||||
const linkedTimestamp = eventTileRenderState.timestamp.displayState.showLinkedTimestamp ? (
|
const linkedTimestamp = (
|
||||||
<MessageTimestampAdapter
|
<EventTileTimestampSlot
|
||||||
eventTileViewModel={this.viewModel}
|
eventTileViewModel={this.viewModel}
|
||||||
kind="linked"
|
kind="linked"
|
||||||
timestampProps={linkedMessageTimestampProps}
|
timestampProps={linkedMessageTimestampProps}
|
||||||
|
showTimestamp={eventTileRenderState.timestamp.displayState.showLinkedTimestamp}
|
||||||
|
showDummy={eventTileRenderState.timestamp.showDummy}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
dummyTimestamp
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let pinnedMessageBadge: JSX.Element | undefined;
|
|
||||||
if (hasPinnedMessageBadge) {
|
|
||||||
pinnedMessageBadge = <PinnedMessageBadge aria-describedby={this.id} tabIndex={0} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let reactionsRow: JSX.Element | undefined;
|
|
||||||
if (hasReactionsRow) {
|
|
||||||
reactionsRow = (
|
|
||||||
<ReactionsRowAdapter
|
|
||||||
eventTileViewModel={this.viewModel}
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
reactions={this.state.reactions}
|
|
||||||
key="mx_EventTile_reactionsRow"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupTimestamp = eventTileRenderState.timestamp.showInGroupLine ? linkedTimestamp : null;
|
const groupTimestamp = eventTileRenderState.timestamp.showInGroupLine ? linkedTimestamp : null;
|
||||||
const ircTimestamp = eventTileRenderState.timestamp.showInIrcLine ? linkedTimestamp : null;
|
const ircTimestamp = eventTileRenderState.timestamp.showInIrcLine ? linkedTimestamp : null;
|
||||||
const groupPadlock = eventTileRenderState.e2ePadlock.showInGroupLine && this.renderE2EPadlock();
|
const groupPadlock = eventTileRenderState.e2ePadlock.showInGroupLine && this.renderE2EPadlock();
|
||||||
@@ -1128,19 +1074,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
return React.createElement(
|
return React.createElement(
|
||||||
this.props.as || "li",
|
this.props.as || "li",
|
||||||
{
|
{
|
||||||
"ref": this.ref,
|
...this.createInteractiveRootAttributes(rootRenderState),
|
||||||
"className": tileClasses,
|
|
||||||
"aria-live": tileAriaLive,
|
|
||||||
"aria-atomic": true,
|
|
||||||
"data-scroll-tokens": scrollToken,
|
|
||||||
"data-has-reply": !!replyChain,
|
"data-has-reply": !!replyChain,
|
||||||
"data-layout": this.props.layout,
|
"data-layout": this.props.layout,
|
||||||
"data-self": isOwnEvent,
|
"data-self": isOwnEvent,
|
||||||
"data-event-id": this.props.mxEvent.getId(),
|
"data-event-id": this.props.mxEvent.getId(),
|
||||||
"onMouseEnter": this.onMouseEnter,
|
|
||||||
"onMouseLeave": this.onMouseLeave,
|
|
||||||
"onFocus": this.onFocusWithin,
|
|
||||||
"onBlur": this.onBlurWithin,
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||||
@@ -1155,55 +1093,46 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
>
|
>
|
||||||
{this.renderContextMenu()}
|
{this.renderContextMenu()}
|
||||||
{replyChain}
|
{replyChain}
|
||||||
{renderTile(TimelineRenderingType.Thread, {
|
{renderTile(
|
||||||
...this.props,
|
TimelineRenderingType.Thread,
|
||||||
|
this.createRenderTileProps({
|
||||||
// overrides
|
replacingEventId,
|
||||||
ref: this.tile,
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
replacingEventId,
|
permalinkCreator: this.props.permalinkCreator!,
|
||||||
isSeeingThroughMessageHiddenForModeration,
|
}),
|
||||||
|
)}
|
||||||
// appease TS
|
|
||||||
highlights: this.props.highlights,
|
|
||||||
highlightLink: this.props.highlightLink,
|
|
||||||
permalinkCreator: this.props.permalinkCreator!,
|
|
||||||
showHiddenEvents: this.context.showHiddenEvents,
|
|
||||||
})}
|
|
||||||
{actionBar}
|
{actionBar}
|
||||||
{linkedTimestamp}
|
{linkedTimestamp}
|
||||||
{msgOption}
|
{msgOption}
|
||||||
</div>,
|
</div>,
|
||||||
hasFooter && (
|
<EventTileFooter
|
||||||
<div className="mx_EventTile_footer" key="mx_EventTile_footer">
|
key="mx_EventTile_footer"
|
||||||
{showMainPinnedMessageBadge && pinnedMessageBadge}
|
eventTileViewModel={this.viewModel}
|
||||||
{reactionsRow}
|
mxEvent={this.props.mxEvent}
|
||||||
{showBubblePinnedMessageBadge && pinnedMessageBadge}
|
reactions={this.state.reactions}
|
||||||
</div>
|
hasFooter={hasFooter}
|
||||||
),
|
hasPinnedMessageBadge={hasPinnedMessageBadge}
|
||||||
|
hasReactionsRow={hasReactionsRow}
|
||||||
|
pinnedMessageBadgeAriaDescribedBy={this.id}
|
||||||
|
placement="default"
|
||||||
|
showMainPinnedMessageBadge={showMainPinnedMessageBadge}
|
||||||
|
showBubblePinnedMessageBadge={showBubblePinnedMessageBadge}
|
||||||
|
/>,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case TimelineRenderingType.Notification:
|
case TimelineRenderingType.Notification:
|
||||||
case TimelineRenderingType.ThreadsList: {
|
case TimelineRenderingType.ThreadsList: {
|
||||||
const room = MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId());
|
const room = MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId());
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
this.props.as || "li",
|
this.props.as || "li",
|
||||||
{
|
{
|
||||||
"ref": this.ref,
|
...this.createInteractiveRootAttributes(rootRenderState),
|
||||||
"className": tileClasses,
|
|
||||||
"tabIndex": -1,
|
"tabIndex": -1,
|
||||||
"aria-live": tileAriaLive,
|
|
||||||
"aria-atomic": "true",
|
|
||||||
"data-scroll-tokens": scrollToken,
|
|
||||||
"data-layout": this.props.layout,
|
"data-layout": this.props.layout,
|
||||||
"data-shape": this.context.timelineRenderingType,
|
"data-shape": this.context.timelineRenderingType,
|
||||||
"data-self": isOwnEvent,
|
"data-self": isOwnEvent,
|
||||||
"data-has-reply": !!replyChain,
|
"data-has-reply": !!replyChain,
|
||||||
"onMouseEnter": this.onMouseEnter,
|
|
||||||
"onMouseLeave": this.onMouseLeave,
|
|
||||||
"onFocus": this.onFocusWithin,
|
|
||||||
"onBlur": this.onBlurWithin,
|
|
||||||
"onClick": (ev: MouseEvent) => {
|
"onClick": (ev: MouseEvent) => {
|
||||||
const target = ev.currentTarget as HTMLElement;
|
const target = ev.currentTarget as HTMLElement;
|
||||||
let index = -1;
|
let index = -1;
|
||||||
@@ -1253,19 +1182,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
avatar
|
avatar
|
||||||
)}
|
)}
|
||||||
<div className={lineClasses} key="mx_EventTile_line">
|
<div className={lineClasses} key="mx_EventTile_line">
|
||||||
<div className="mx_EventTile_body">
|
<EventTilePreviewBody eventTileViewModel={this.viewModel} mxEvent={this.props.mxEvent} />
|
||||||
{this.props.mxEvent.isRedacted() ? (
|
<EventTileThreadPanelSummary
|
||||||
<RedactedBodyFactory mxEvent={this.props.mxEvent} />
|
eventTileViewModel={this.viewModel}
|
||||||
) : this.props.mxEvent.isDecryptionFailure() ? (
|
threadState={threadState}
|
||||||
<DecryptionFailureBodyFactory mxEvent={this.props.mxEvent} />
|
/>
|
||||||
) : (
|
|
||||||
<EventPreviewAdapter
|
|
||||||
eventTileViewModel={this.viewModel}
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{this.renderThreadPanelSummary(threadState)}
|
|
||||||
</div>
|
</div>
|
||||||
{this.context.timelineRenderingType === TimelineRenderingType.ThreadsList && (
|
{this.context.timelineRenderingType === TimelineRenderingType.ThreadsList && (
|
||||||
<ThreadListActionBarAdapter
|
<ThreadListActionBarAdapter
|
||||||
@@ -1281,67 +1202,42 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case TimelineRenderingType.File: {
|
case TimelineRenderingType.File: {
|
||||||
return React.createElement(
|
return React.createElement(this.props.as || "li", this.createRootAttributes(rootRenderState), [
|
||||||
this.props.as || "li",
|
<a
|
||||||
{
|
className="mx_EventTile_senderDetailsLink"
|
||||||
"className": tileClasses,
|
key="mx_EventTile_senderDetailsLink"
|
||||||
"aria-live": tileAriaLive,
|
href={permalink}
|
||||||
"aria-atomic": true,
|
onClick={this.onPermalinkClicked}
|
||||||
"data-scroll-tokens": scrollToken,
|
>
|
||||||
},
|
<div className="mx_EventTile_senderDetails" onContextMenu={this.onTimestampContextMenu}>
|
||||||
[
|
{avatar}
|
||||||
<a
|
{sender}
|
||||||
className="mx_EventTile_senderDetailsLink"
|
{timestamp}
|
||||||
key="mx_EventTile_senderDetailsLink"
|
</div>
|
||||||
href={permalink}
|
</a>,
|
||||||
onClick={this.onPermalinkClicked}
|
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
>
|
{this.renderContextMenu()}
|
||||||
<div className="mx_EventTile_senderDetails" onContextMenu={this.onTimestampContextMenu}>
|
{renderTile(
|
||||||
{avatar}
|
TimelineRenderingType.File,
|
||||||
{sender}
|
this.createRenderTileProps({
|
||||||
{timestamp}
|
|
||||||
</div>
|
|
||||||
</a>,
|
|
||||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
|
||||||
{this.renderContextMenu()}
|
|
||||||
{renderTile(TimelineRenderingType.File, {
|
|
||||||
...this.props,
|
|
||||||
|
|
||||||
// overrides
|
|
||||||
ref: this.tile,
|
|
||||||
isSeeingThroughMessageHiddenForModeration,
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
|
}),
|
||||||
// appease TS
|
)}
|
||||||
highlights: this.props.highlights,
|
</div>,
|
||||||
highlightLink: this.props.highlightLink,
|
]);
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
|
||||||
showHiddenEvents: this.context.showHiddenEvents,
|
|
||||||
})}
|
|
||||||
</div>,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
// Pinned, Room, Search
|
// Pinned, Room, Search
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
this.props.as || "li",
|
this.props.as || "li",
|
||||||
{
|
{
|
||||||
"ref": this.ref,
|
...this.createInteractiveRootAttributes(rootRenderState),
|
||||||
"className": tileClasses,
|
|
||||||
"tabIndex": -1,
|
"tabIndex": -1,
|
||||||
"aria-live": tileAriaLive,
|
|
||||||
"aria-atomic": "true",
|
|
||||||
"data-scroll-tokens": scrollToken,
|
|
||||||
"data-layout": this.props.layout,
|
"data-layout": this.props.layout,
|
||||||
"data-self": isOwnEvent,
|
"data-self": isOwnEvent,
|
||||||
"data-event-id": this.props.mxEvent.getId(),
|
"data-event-id": this.props.mxEvent.getId(),
|
||||||
"data-has-reply": !!replyChain,
|
"data-has-reply": !!replyChain,
|
||||||
"onMouseEnter": this.onMouseEnter,
|
|
||||||
"onMouseLeave": this.onMouseLeave,
|
|
||||||
"onFocus": this.onFocusWithin,
|
|
||||||
"onBlur": this.onBlurWithin,
|
|
||||||
},
|
},
|
||||||
<>
|
<>
|
||||||
{ircTimestamp}
|
{ircTimestamp}
|
||||||
@@ -1358,42 +1254,52 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
{groupTimestamp}
|
{groupTimestamp}
|
||||||
{groupPadlock}
|
{groupPadlock}
|
||||||
{replyChain}
|
{replyChain}
|
||||||
{renderTile(this.context.timelineRenderingType, {
|
{renderTile(
|
||||||
...this.props,
|
this.context.timelineRenderingType,
|
||||||
|
this.createRenderTileProps({
|
||||||
// overrides
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
ref: this.tile,
|
}),
|
||||||
isSeeingThroughMessageHiddenForModeration,
|
)}
|
||||||
|
|
||||||
// appease TS
|
|
||||||
highlights: this.props.highlights,
|
|
||||||
highlightLink: this.props.highlightLink,
|
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
|
||||||
showHiddenEvents: this.context.showHiddenEvents,
|
|
||||||
})}
|
|
||||||
{actionBar}
|
{actionBar}
|
||||||
{eventTileRenderState.footer.showInIrcLayout && (
|
{eventTileRenderState.footer.showInIrcLayout && (
|
||||||
<>
|
<>
|
||||||
{hasFooter && (
|
<EventTileFooter
|
||||||
<div className="mx_EventTile_footer">
|
eventTileViewModel={this.viewModel}
|
||||||
{pinnedMessageBadge}
|
mxEvent={this.props.mxEvent}
|
||||||
{reactionsRow}
|
reactions={this.state.reactions}
|
||||||
</div>
|
hasFooter={hasFooter}
|
||||||
)}
|
hasPinnedMessageBadge={hasPinnedMessageBadge}
|
||||||
{this.renderThreadInfo(threadState)}
|
hasReactionsRow={hasReactionsRow}
|
||||||
|
pinnedMessageBadgeAriaDescribedBy={this.id}
|
||||||
|
placement="irc"
|
||||||
|
/>
|
||||||
|
<EventTileThreadInfo
|
||||||
|
eventTileViewModel={this.viewModel}
|
||||||
|
mxEvent={this.props.mxEvent}
|
||||||
|
threadState={threadState}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{eventTileRenderState.footer.showInDefaultLayout && (
|
{eventTileRenderState.footer.showInDefaultLayout && (
|
||||||
<>
|
<>
|
||||||
{hasFooter && (
|
<EventTileFooter
|
||||||
<div className="mx_EventTile_footer">
|
eventTileViewModel={this.viewModel}
|
||||||
{showMainPinnedMessageBadge && pinnedMessageBadge}
|
mxEvent={this.props.mxEvent}
|
||||||
{reactionsRow}
|
reactions={this.state.reactions}
|
||||||
{showBubblePinnedMessageBadge && pinnedMessageBadge}
|
hasFooter={hasFooter}
|
||||||
</div>
|
hasPinnedMessageBadge={hasPinnedMessageBadge}
|
||||||
)}
|
hasReactionsRow={hasReactionsRow}
|
||||||
{this.renderThreadInfo(threadState)}
|
pinnedMessageBadgeAriaDescribedBy={this.id}
|
||||||
|
placement="default"
|
||||||
|
showMainPinnedMessageBadge={showMainPinnedMessageBadge}
|
||||||
|
showBubblePinnedMessageBadge={showBubblePinnedMessageBadge}
|
||||||
|
/>
|
||||||
|
<EventTileThreadInfo
|
||||||
|
eventTileViewModel={this.viewModel}
|
||||||
|
mxEvent={this.props.mxEvent}
|
||||||
|
threadState={threadState}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{msgOption}
|
{msgOption}
|
||||||
@@ -1455,7 +1361,7 @@ class EventTileErrorBoundary extends React.Component<EventTileErrorBoundaryProps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
|
/** EventTile wrapped in an error boundary so render and construction failures show a tile fallback. */
|
||||||
const SafeEventTile = (props: EventTileProps): JSX.Element => {
|
const SafeEventTile = (props: EventTileProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<EventTileErrorBoundary mxEvent={props.mxEvent}>
|
<EventTileErrorBoundary mxEvent={props.mxEvent}>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
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, { type JSX } from "react";
|
||||||
|
import { type MatrixEvent, type Relations } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { PinnedMessageBadge } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
|
import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||||
|
import { ReactionsRowAdapter } from "./ReactionsRowAdapter";
|
||||||
|
|
||||||
|
interface EventTileFooterProps {
|
||||||
|
eventTileViewModel: EventTileViewModel;
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
reactions?: Relations | null;
|
||||||
|
hasFooter: boolean;
|
||||||
|
hasPinnedMessageBadge: boolean;
|
||||||
|
hasReactionsRow: boolean;
|
||||||
|
pinnedMessageBadgeAriaDescribedBy: string;
|
||||||
|
placement: "default" | "irc";
|
||||||
|
showMainPinnedMessageBadge?: boolean;
|
||||||
|
showBubblePinnedMessageBadge?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders the pinned-message badge and reactions row for an EventTile. */
|
||||||
|
export function EventTileFooter({
|
||||||
|
eventTileViewModel,
|
||||||
|
mxEvent,
|
||||||
|
reactions,
|
||||||
|
hasFooter,
|
||||||
|
hasPinnedMessageBadge,
|
||||||
|
hasReactionsRow,
|
||||||
|
pinnedMessageBadgeAriaDescribedBy,
|
||||||
|
placement,
|
||||||
|
showMainPinnedMessageBadge,
|
||||||
|
showBubblePinnedMessageBadge,
|
||||||
|
}: Readonly<EventTileFooterProps>): JSX.Element | null {
|
||||||
|
if (!hasFooter) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinnedMessageBadge = hasPinnedMessageBadge ? (
|
||||||
|
<PinnedMessageBadge aria-describedby={pinnedMessageBadgeAriaDescribedBy} tabIndex={0} />
|
||||||
|
) : undefined;
|
||||||
|
const reactionsRow = hasReactionsRow ? (
|
||||||
|
<ReactionsRowAdapter
|
||||||
|
eventTileViewModel={eventTileViewModel}
|
||||||
|
mxEvent={mxEvent}
|
||||||
|
reactions={reactions}
|
||||||
|
key="mx_EventTile_reactionsRow"
|
||||||
|
/>
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
if (placement === "irc") {
|
||||||
|
return (
|
||||||
|
<div className="mx_EventTile_footer">
|
||||||
|
{pinnedMessageBadge}
|
||||||
|
{reactionsRow}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_EventTile_footer">
|
||||||
|
{showMainPinnedMessageBadge && pinnedMessageBadge}
|
||||||
|
{reactionsRow}
|
||||||
|
{showBubblePinnedMessageBadge && pinnedMessageBadge}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
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, { type JSX } from "react";
|
||||||
|
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { DecryptionFailureBodyFactory, RedactedBodyFactory } from "../../messages/MBodyFactory";
|
||||||
|
import { EventPreviewAdapter } from "./EventPreviewAdapter";
|
||||||
|
import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||||
|
|
||||||
|
interface EventTilePreviewBodyProps {
|
||||||
|
eventTileViewModel: EventTileViewModel;
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders the compact body preview used by notification and thread-list tiles. */
|
||||||
|
export function EventTilePreviewBody({
|
||||||
|
eventTileViewModel,
|
||||||
|
mxEvent,
|
||||||
|
}: Readonly<EventTilePreviewBodyProps>): JSX.Element {
|
||||||
|
let body: JSX.Element;
|
||||||
|
|
||||||
|
if (mxEvent.isRedacted()) {
|
||||||
|
body = <RedactedBodyFactory mxEvent={mxEvent} />;
|
||||||
|
} else if (mxEvent.isDecryptionFailure()) {
|
||||||
|
body = <DecryptionFailureBodyFactory mxEvent={mxEvent} />;
|
||||||
|
} else {
|
||||||
|
body = <EventPreviewAdapter eventTileViewModel={eventTileViewModel} mxEvent={mxEvent} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_EventTile_body">{body}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
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, { type JSX, type ReactNode } from "react";
|
||||||
|
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||||
|
import { type EventTileThreadState } from "../../../../viewmodels/room/timeline/event-tile/EventTileThreadState";
|
||||||
|
import { ThreadMessagePreviewAdapter } from "./ThreadMessagePreviewAdapter";
|
||||||
|
import { ThreadSummaryAdapter } from "./ThreadSummaryAdapter";
|
||||||
|
|
||||||
|
interface EventTileThreadInfoProps {
|
||||||
|
eventTileViewModel: EventTileViewModel;
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
threadState: EventTileThreadState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders the thread-panel reply summary shown under a preview tile. */
|
||||||
|
export function EventTileThreadPanelSummary({
|
||||||
|
eventTileViewModel,
|
||||||
|
threadState,
|
||||||
|
}: Readonly<Omit<EventTileThreadInfoProps, "mxEvent">>): JSX.Element | null {
|
||||||
|
if (!threadState.shouldShowThreadPanelSummary || !threadState.thread) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_ThreadPanel_replies">
|
||||||
|
<ThreadsIcon />
|
||||||
|
<span className="mx_ThreadPanel_replies_amount">{threadState.thread.length}</span>
|
||||||
|
<ThreadMessagePreviewAdapter eventTileViewModel={eventTileViewModel} thread={threadState.thread} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders thread summary or search thread affordances for an EventTile. */
|
||||||
|
export function EventTileThreadInfo({
|
||||||
|
eventTileViewModel,
|
||||||
|
mxEvent,
|
||||||
|
threadState,
|
||||||
|
}: Readonly<EventTileThreadInfoProps>): ReactNode {
|
||||||
|
if (threadState.shouldShowThreadSummary && threadState.thread) {
|
||||||
|
return (
|
||||||
|
<ThreadSummaryAdapter
|
||||||
|
eventTileViewModel={eventTileViewModel}
|
||||||
|
mxEvent={mxEvent}
|
||||||
|
thread={threadState.thread}
|
||||||
|
data-testid="thread-summary"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadState.searchThreadInfo.kind === "link") {
|
||||||
|
return (
|
||||||
|
<a className="mx_ThreadSummary_icon" href={threadState.searchThreadInfo.href}>
|
||||||
|
<ThreadsIcon />
|
||||||
|
{_t("timeline|thread_info_basic")}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadState.searchThreadInfo.kind === "text") {
|
||||||
|
return (
|
||||||
|
<p className="mx_ThreadSummary_icon">
|
||||||
|
<ThreadsIcon />
|
||||||
|
{_t("timeline|thread_info_basic")}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
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, { type JSX } from "react";
|
||||||
|
|
||||||
|
import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||||
|
import { type MessageTimestampViewModelProps } from "../../../../viewmodels/room/timeline/event-tile/timestamp/MessageTimestampViewModel";
|
||||||
|
import { MessageTimestampAdapter } from "./MessageTimestampAdapter";
|
||||||
|
|
||||||
|
interface EventTileTimestampSlotProps {
|
||||||
|
eventTileViewModel: EventTileViewModel;
|
||||||
|
kind: "plain" | "linked";
|
||||||
|
timestampProps: MessageTimestampViewModelProps;
|
||||||
|
showTimestamp: boolean;
|
||||||
|
showDummy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders a real or placeholder timestamp slot for an EventTile. */
|
||||||
|
export function EventTileTimestampSlot({
|
||||||
|
eventTileViewModel,
|
||||||
|
kind,
|
||||||
|
timestampProps,
|
||||||
|
showTimestamp,
|
||||||
|
showDummy,
|
||||||
|
}: Readonly<EventTileTimestampSlotProps>): JSX.Element | null {
|
||||||
|
if (showTimestamp) {
|
||||||
|
return (
|
||||||
|
<MessageTimestampAdapter
|
||||||
|
eventTileViewModel={eventTileViewModel}
|
||||||
|
kind={kind}
|
||||||
|
timestampProps={timestampProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDummy) {
|
||||||
|
return <span className="mx_MessageTimestamp" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
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 { EventType, type MatrixClient, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { TimelineRenderingType } from "../../../../contexts/RoomContext";
|
||||||
|
|
||||||
|
/** Inputs for deriving EventTile highlight display state. */
|
||||||
|
export interface EventTileHighlightStateInput {
|
||||||
|
/** Matrix client used to resolve push actions and the current user. */
|
||||||
|
cli: MatrixClient;
|
||||||
|
/** Matrix event rendered by the tile. */
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
/** Current timeline rendering mode. */
|
||||||
|
timelineRenderingType: TimelineRenderingType;
|
||||||
|
/** Whether the tile is rendering for export. */
|
||||||
|
forExport?: boolean;
|
||||||
|
/** Whether the event is considered redacted by the tile. */
|
||||||
|
isRedacted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether an event should be highlighted.
|
||||||
|
* For edited events, if a previous version of the event was highlighted, the event should remain highlighted as the
|
||||||
|
* user may have been notified.
|
||||||
|
*/
|
||||||
|
export function shouldHighlightEventTile({
|
||||||
|
cli,
|
||||||
|
mxEvent,
|
||||||
|
timelineRenderingType,
|
||||||
|
forExport,
|
||||||
|
isRedacted,
|
||||||
|
}: EventTileHighlightStateInput): boolean {
|
||||||
|
if (forExport) return false;
|
||||||
|
if (timelineRenderingType === TimelineRenderingType.Notification) return false;
|
||||||
|
if (timelineRenderingType === TimelineRenderingType.ThreadsList) return false;
|
||||||
|
if (isRedacted) return false;
|
||||||
|
|
||||||
|
// This event is a room mention but we don't want the call tile to have a highlight.
|
||||||
|
if (mxEvent.getType() === EventType.RTCNotification) return false;
|
||||||
|
|
||||||
|
const actions = cli.getPushActionsForEvent(mxEvent.replacingEvent() || mxEvent);
|
||||||
|
const previousActions = mxEvent.replacingEvent() ? cli.getPushActionsForEvent(mxEvent) : undefined;
|
||||||
|
if (!actions?.tweaks && !previousActions?.tweaks) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show self-highlights from another of our clients.
|
||||||
|
if (mxEvent.getSender() === cli.credentials.userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!(actions?.tweaks.highlight || previousActions?.tweaks.highlight);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
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 { CallErrorCode } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
|
/** Call event state used to decide whether EventTile should suppress rendering. */
|
||||||
|
export interface EventTileCallEventState {
|
||||||
|
/** Legacy call hangup reason, when available. */
|
||||||
|
hangupReason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inputs for deriving EventTile hidden display state. */
|
||||||
|
export interface EventTileVisibilityStateInput {
|
||||||
|
/** Legacy call event grouping state for this tile. */
|
||||||
|
callEventGrouper?: EventTileCallEventState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In some cases EventTile cannot rely on the event's own visibility because replacement call events are rendered by
|
||||||
|
* the replacing call tile instead.
|
||||||
|
*/
|
||||||
|
export function shouldHideEventTile({ callEventGrouper }: EventTileVisibilityStateInput): boolean {
|
||||||
|
return callEventGrouper?.hangupReason === CallErrorCode.Replaced;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user