Refactor EventTile using the MVVM pattern - #5a (#33587)

* Extract EventTile receipt state

* Extract EventTile thread state

* Extract EventTile reaction relation state

* Extract EventTile reply-chain state

* Keep reply chain collapse wiring in EventTile

* Reuse message event eligibility for receipts
This commit is contained in:
rbondesson
2026-05-22 18:25:32 +00:00
committed by GitHub
parent 6574863fcd
commit 03360eb260
10 changed files with 824 additions and 129 deletions
@@ -0,0 +1,80 @@
/*
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 } from "matrix-js-sdk/src/matrix";
import { TimelineRenderingType } from "../../../../contexts/RoomContext";
import { isMessageEvent } from "../../../../events/EventTileFactory";
interface ReadReceiptLike {
userId: string;
}
/** Inputs for deriving EventTile receipt display state. */
export interface EventTileReceiptStateInput {
/** The Matrix event rendered by the tile. */
mxEvent: MatrixEvent;
/** Read receipts supplied for the tile. */
readReceipts?: ReadReceiptLike[];
/** Whether the event room is known locally. */
hasRoom: boolean;
/** Current user's safe user ID, used for sender eligibility. */
ownUserId: string;
/** Whether this is the last successful event sent by the current user. */
lastSuccessful?: boolean;
/** Current event send status. */
eventSendStatus?: EventStatus | null;
/** The current timeline rendering mode. */
timelineRenderingType: TimelineRenderingType;
}
/** EventTile receipt display state. */
export interface EventTileReceiptState {
/** Whether the event is eligible for a sent/sending receipt. */
isEligibleForSpecialReceipt: boolean;
/** Whether EventTile should render the sent receipt. */
shouldShowSentReceipt: boolean;
/** Whether EventTile should render the sending receipt. */
shouldShowSendingReceipt: boolean;
/** Whether EventTile should listen for receipt updates. */
shouldListenForReceipts: boolean;
}
/**
* Whether the event type qualifies for a sent/sending receipt.
* This excludes state events and other events that are not sent by the composer.
*/
export function isEligibleForSpecialReceipt(mxEvent: MatrixEvent): boolean {
return isMessageEvent(mxEvent) || mxEvent.getType() === EventType.RoomMessageEncrypted;
}
/** Derives receipt display state for EventTile. */
export function getEventTileReceiptState({
mxEvent,
readReceipts,
hasRoom,
ownUserId,
lastSuccessful,
eventSendStatus,
timelineRenderingType,
}: EventTileReceiptStateInput): EventTileReceiptState {
const isEligible =
!readReceipts?.length && hasRoom && mxEvent.getSender() === ownUserId && isEligibleForSpecialReceipt(mxEvent);
const shouldShowSentReceipt =
isEligible &&
!!lastSuccessful &&
timelineRenderingType !== TimelineRenderingType.ThreadsList &&
(!eventSendStatus || eventSendStatus === EventStatus.SENT);
const shouldShowSendingReceipt = isEligible && !!eventSendStatus && eventSendStatus !== EventStatus.SENT;
return {
isEligibleForSpecialReceipt: isEligible,
shouldShowSentReceipt,
shouldShowSendingReceipt,
shouldListenForReceipts: shouldShowSentReceipt || shouldShowSendingReceipt,
};
}
@@ -0,0 +1,34 @@
/*
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 MatrixEvent } from "matrix-js-sdk/src/matrix";
import { shouldDisplayReply } from "../../../../utils/Reply";
/** Inputs for deriving EventTile reply-chain display state. */
export interface EventTileReplyChainStateInput {
/** Matrix event rendered by the tile. */
mxEvent: MatrixEvent;
/** Whether the event has a renderer in the current timeline context. */
hasRenderer: boolean;
}
/** EventTile reply-chain display state. */
export interface EventTileReplyChainState {
/** Whether EventTile should render ReplyChain. */
shouldShowReplyChain: boolean;
}
/** Derives reply-chain display state for EventTile. */
export function getEventTileReplyChainState({
mxEvent,
hasRenderer,
}: EventTileReplyChainStateInput): EventTileReplyChainState {
return {
shouldShowReplyChain: hasRenderer && shouldDisplayReply(mxEvent),
};
}
@@ -0,0 +1,96 @@
/*
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 MatrixEvent, type Thread } from "matrix-js-sdk/src/matrix";
import { TimelineRenderingType } from "../../../../contexts/RoomContext";
/** Minimal room surface used to look up a thread for an event. */
export interface EventTileThreadLookup {
/** Finds the thread associated with an event. */
findThreadForEvent(mxEvent: MatrixEvent): Thread | null | undefined;
}
/** Search thread-info rendering kind. */
export type SearchThreadInfoKind = "none" | "text" | "link";
/** Search timeline thread-info display state. */
export interface EventTileSearchThreadInfo {
/** Kind of search thread info to render. */
kind: SearchThreadInfoKind;
/** Link target when rendering linked thread info. */
href?: string;
}
/** Inputs for deriving EventTile thread display state. */
export interface EventTileThreadStateInput {
/** Matrix event rendered by the tile. */
mxEvent: MatrixEvent;
/** Thread associated with the event, when available. */
thread: Thread | null;
/** Current timeline rendering mode. */
timelineRenderingType: TimelineRenderingType;
/** Optional search-result link for thread info. */
highlightLink?: string;
}
/** EventTile thread display state. */
export interface EventTileThreadState {
/** Thread associated with the event, when available. */
thread: Thread | null;
/** Whether EventTile should render the main thread summary. */
shouldShowThreadSummary: boolean;
/** Whether EventTile should render the thread panel reply summary. */
shouldShowThreadPanelSummary: boolean;
/** Timestamp of the latest thread reply, when available. */
threadReplyEventTs?: number;
/** Search timeline thread-info display state. */
searchThreadInfo: EventTileSearchThreadInfo;
}
/**
* Finds the thread associated with an event.
*
* Accessing the thread through the room covers a race where the event has not
* discovered its thread yet during sync.
*/
export function getEventTileThread(mxEvent: MatrixEvent, room?: EventTileThreadLookup | null): Thread | null {
return mxEvent.getThread() ?? room?.findThreadForEvent(mxEvent) ?? null;
}
function getSearchThreadInfo(shouldShowSearchThreadInfo: boolean, highlightLink?: string): EventTileSearchThreadInfo {
if (!shouldShowSearchThreadInfo) {
return { kind: "none" };
}
if (highlightLink) {
return { kind: "link", href: highlightLink };
}
return { kind: "text" };
}
/** Derives thread display state for EventTile. */
export function getEventTileThreadState({
mxEvent,
thread,
timelineRenderingType,
highlightLink,
}: EventTileThreadStateInput): EventTileThreadState {
const shouldShowThreadSummary = !!thread && thread.id === mxEvent.getId();
const shouldShowSearchThreadInfo =
!shouldShowThreadSummary && timelineRenderingType === TimelineRenderingType.Search && !!mxEvent.threadRootId;
const searchThreadInfo = getSearchThreadInfo(shouldShowSearchThreadInfo, highlightLink);
return {
thread,
shouldShowThreadSummary,
shouldShowThreadPanelSummary: !!thread,
threadReplyEventTs: thread?.replyToEvent?.getTs(),
searchThreadInfo,
};
}
@@ -0,0 +1,52 @@
/*
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 MatrixEvent, type Relations, type RelationType } from "matrix-js-sdk/src/matrix";
/** Looks up relations for an event by relation and event type. */
export type GetRelationsForEvent = (
eventId: string,
relationType: RelationType | string,
eventType: EventType | string,
) => Relations | null | undefined;
/** Inputs for fetching EventTile reaction relations. */
export interface EventTileReactionRelationsInput {
/** Matrix event rendered by the tile. */
mxEvent: MatrixEvent;
/** Whether reactions are enabled for the tile. */
showReactions?: boolean;
/** Relation lookup function supplied by the timeline. */
getRelationsForEvent?: GetRelationsForEvent;
}
/** Relation type used for EventTile reaction annotations. */
export const EVENT_TILE_REACTION_RELATION_TYPE = "m.annotation";
/** Event type used for EventTile reaction annotations. */
export const EVENT_TILE_REACTION_EVENT_TYPE = EventType.Reaction;
/** Whether a created relation event should refresh EventTile reactions. */
export function isEventTileReactionRelation(relationType: string, eventType: string): boolean {
return relationType === EVENT_TILE_REACTION_RELATION_TYPE && eventType === EVENT_TILE_REACTION_EVENT_TYPE;
}
/** Fetches reaction relations for EventTile when reactions are enabled. */
export function getEventTileReactionRelations({
mxEvent,
showReactions,
getRelationsForEvent,
}: EventTileReactionRelationsInput): Relations | null {
if (!showReactions || !getRelationsForEvent) {
return null;
}
return (
getRelationsForEvent(mxEvent.getId()!, EVENT_TILE_REACTION_RELATION_TYPE, EVENT_TILE_REACTION_EVENT_TYPE) ??
null
);
}