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:
@@ -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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user