Skip to content

Commit df7c20d

Browse files
committed
Extraction of leaf components to improve readability
1 parent f75134f commit df7c20d

7 files changed

Lines changed: 1293 additions & 1167 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright 2026 Element Creations Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React, { useCallback, useContext, useEffect, useMemo, useState, type JSX } from "react";
9+
import { ActionBarView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
10+
11+
import type { MatrixEvent, Relations } from "matrix-js-sdk/src/matrix";
12+
import RoomContext from "../../../../contexts/RoomContext";
13+
import { EventTileActionBarViewModel } from "../../../../viewmodels/room/timeline/event-tile/actions/EventTileActionBarViewModel";
14+
import type { EventTileActionBarViewModelProps } from "../../../../viewmodels/room/timeline/event-tile/actions/EventTileActionBarViewModel";
15+
import type ReplyChain from "../../elements/ReplyChain";
16+
import type { RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks";
17+
import MessageContextMenu from "../../context_menus/MessageContextMenu";
18+
import ContextMenu, { aboveLeftOf } from "../../../structures/ContextMenu";
19+
import ReactionPicker from "../../emojipicker/ReactionPicker";
20+
import { CardContext } from "../../right_panel/context";
21+
import type { EventTileOps, GetRelationsForEvent } from "./types";
22+
23+
type ActionBarProps = {
24+
mxEvent: MatrixEvent;
25+
reactions: Relations | null;
26+
permalinkCreator?: RoomPermalinkCreator;
27+
getRelationsForEvent?: GetRelationsForEvent;
28+
isQuoteExpanded?: boolean;
29+
tileRef: React.RefObject<EventTileOps | null>;
30+
replyChainRef: React.RefObject<ReplyChain | null>;
31+
onFocusChange: (focused: boolean) => void;
32+
toggleThreadExpanded: () => void;
33+
};
34+
35+
function buildEventTileActionBarViewModelProps(
36+
props: Pick<ActionBarProps, "mxEvent" | "isQuoteExpanded" | "getRelationsForEvent" | "toggleThreadExpanded">,
37+
roomContext: Pick<React.ContextType<typeof RoomContext>, "timelineRenderingType" | "canSendMessages" | "canReact">,
38+
isSearch: boolean,
39+
isCard: boolean,
40+
handleOptionsClick: NonNullable<EventTileActionBarViewModelProps["onOptionsClick"]>,
41+
handleReactionsClick: NonNullable<EventTileActionBarViewModelProps["onReactionsClick"]>,
42+
): EventTileActionBarViewModelProps {
43+
return {
44+
mxEvent: props.mxEvent,
45+
timelineRenderingType: roomContext.timelineRenderingType,
46+
canSendMessages: roomContext.canSendMessages,
47+
canReact: roomContext.canReact,
48+
isSearch,
49+
isCard,
50+
isQuoteExpanded: props.isQuoteExpanded,
51+
onToggleThreadExpanded: props.toggleThreadExpanded,
52+
onOptionsClick: handleOptionsClick,
53+
onReactionsClick: handleReactionsClick,
54+
getRelationsForEvent: props.getRelationsForEvent,
55+
};
56+
}
57+
58+
export function ActionBar({
59+
mxEvent,
60+
reactions,
61+
permalinkCreator,
62+
getRelationsForEvent,
63+
isQuoteExpanded,
64+
tileRef,
65+
replyChainRef,
66+
onFocusChange,
67+
toggleThreadExpanded,
68+
}: Readonly<ActionBarProps>): JSX.Element {
69+
const roomContext = useContext(RoomContext);
70+
const { isCard } = useContext(CardContext);
71+
const [optionsMenuAnchorRect, setOptionsMenuAnchorRect] = useState<DOMRect | null>(null);
72+
const [reactionsMenuAnchorRect, setReactionsMenuAnchorRect] = useState<DOMRect | null>(null);
73+
const isSearch = Boolean(roomContext.search);
74+
const handleOptionsClick = useCallback((anchor: HTMLElement | null): void => {
75+
setOptionsMenuAnchorRect(anchor?.getBoundingClientRect() ?? null);
76+
}, []);
77+
const handleReactionsClick = useCallback((anchor: HTMLElement | null): void => {
78+
setReactionsMenuAnchorRect(anchor?.getBoundingClientRect() ?? null);
79+
}, []);
80+
const actionBarViewModelProps = useMemo(
81+
() =>
82+
buildEventTileActionBarViewModelProps(
83+
{ mxEvent, isQuoteExpanded, getRelationsForEvent, toggleThreadExpanded },
84+
roomContext,
85+
isSearch,
86+
isCard,
87+
handleOptionsClick,
88+
handleReactionsClick,
89+
),
90+
[
91+
mxEvent,
92+
isQuoteExpanded,
93+
getRelationsForEvent,
94+
toggleThreadExpanded,
95+
roomContext,
96+
isSearch,
97+
isCard,
98+
handleOptionsClick,
99+
handleReactionsClick,
100+
],
101+
);
102+
const vm = useCreateAutoDisposedViewModel(() => new EventTileActionBarViewModel(actionBarViewModelProps));
103+
104+
useEffect(() => {
105+
vm.setProps(actionBarViewModelProps);
106+
}, [vm, actionBarViewModelProps]);
107+
useEffect(() => {
108+
onFocusChange(Boolean(optionsMenuAnchorRect || reactionsMenuAnchorRect));
109+
}, [onFocusChange, optionsMenuAnchorRect, reactionsMenuAnchorRect]);
110+
useEffect(() => {
111+
setOptionsMenuAnchorRect(null);
112+
setReactionsMenuAnchorRect(null);
113+
}, [mxEvent]);
114+
115+
const closeOptionsMenu = useCallback((): void => {
116+
setOptionsMenuAnchorRect(null);
117+
}, []);
118+
const closeReactionsMenu = useCallback((): void => {
119+
setReactionsMenuAnchorRect(null);
120+
}, []);
121+
const collapseReplyChain = replyChainRef.current?.canCollapse() ? replyChainRef.current.collapse : undefined;
122+
123+
return (
124+
<>
125+
<ActionBarView vm={vm} className="mx_MessageActionBar" />
126+
{optionsMenuAnchorRect ? (
127+
<MessageContextMenu
128+
{...aboveLeftOf(optionsMenuAnchorRect)}
129+
mxEvent={mxEvent}
130+
permalinkCreator={permalinkCreator}
131+
eventTileOps={tileRef.current ?? undefined}
132+
collapseReplyChain={collapseReplyChain}
133+
onFinished={closeOptionsMenu}
134+
getRelationsForEvent={getRelationsForEvent}
135+
/>
136+
) : null}
137+
{reactionsMenuAnchorRect ? (
138+
<ContextMenu
139+
{...aboveLeftOf(reactionsMenuAnchorRect)}
140+
onFinished={closeReactionsMenu}
141+
managed={false}
142+
focusLock
143+
>
144+
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeReactionsMenu} />
145+
</ContextMenu>
146+
) : null}
147+
</>
148+
);
149+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
Copyright 2026 Element Creations Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React, { useCallback, type JSX } from "react";
9+
10+
import type { MatrixEvent, Relations } from "matrix-js-sdk/src/matrix";
11+
import type {
12+
EventTileViewModel,
13+
EventTileViewSnapshot,
14+
} from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
15+
import type ReplyChain from "../../elements/ReplyChain";
16+
import type { RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks";
17+
import MessageContextMenu from "../../context_menus/MessageContextMenu";
18+
import { aboveRightOf } from "../../../structures/ContextMenu";
19+
import type { EventTileContextMenuState, EventTileOps, GetRelationsForEvent } from "./types";
20+
21+
type UseContextMenuNodeArgs = {
22+
props: {
23+
mxEvent: MatrixEvent;
24+
permalinkCreator?: RoomPermalinkCreator;
25+
getRelationsForEvent?: GetRelationsForEvent;
26+
};
27+
snapshot: Pick<EventTileViewSnapshot, "contextMenuState" | "isContextMenuOpen" | "reactions">;
28+
tileRef: React.RefObject<EventTileOps | null>;
29+
replyChainRef: React.RefObject<ReplyChain | null>;
30+
vm: EventTileViewModel;
31+
};
32+
33+
type ContextMenuProps = {
34+
contextMenu: EventTileContextMenuState;
35+
mxEvent: MatrixEvent;
36+
reactions: Relations | null;
37+
permalinkCreator?: RoomPermalinkCreator;
38+
getRelationsForEvent?: GetRelationsForEvent;
39+
tileRef: React.RefObject<EventTileOps | null>;
40+
replyChainRef: React.RefObject<ReplyChain | null>;
41+
onFinished: () => void;
42+
};
43+
44+
export function useContextMenuNode({
45+
props,
46+
snapshot,
47+
tileRef,
48+
replyChainRef,
49+
vm,
50+
}: UseContextMenuNodeArgs): JSX.Element | undefined {
51+
const closeContextMenu = useCallback((): void => {
52+
vm.closeContextMenu();
53+
}, [vm]);
54+
55+
return snapshot.contextMenuState && snapshot.isContextMenuOpen ? (
56+
<ContextMenu
57+
mxEvent={props.mxEvent}
58+
permalinkCreator={props.permalinkCreator}
59+
getRelationsForEvent={props.getRelationsForEvent}
60+
reactions={snapshot.reactions}
61+
contextMenu={snapshot.contextMenuState}
62+
tileRef={tileRef}
63+
replyChainRef={replyChainRef}
64+
onFinished={closeContextMenu}
65+
/>
66+
) : undefined;
67+
}
68+
69+
export function ContextMenu({
70+
contextMenu,
71+
mxEvent,
72+
reactions,
73+
permalinkCreator,
74+
getRelationsForEvent,
75+
tileRef,
76+
replyChainRef,
77+
onFinished,
78+
}: Readonly<ContextMenuProps>): JSX.Element {
79+
return (
80+
<MessageContextMenu
81+
{...aboveRightOf(contextMenu.position)}
82+
mxEvent={mxEvent}
83+
permalinkCreator={permalinkCreator}
84+
eventTileOps={tileRef.current ?? undefined}
85+
collapseReplyChain={replyChainRef.current?.canCollapse() ? replyChainRef.current.collapse : undefined}
86+
onFinished={onFinished}
87+
rightClick={true}
88+
reactions={reactions}
89+
link={contextMenu.link}
90+
getRelationsForEvent={getRelationsForEvent}
91+
/>
92+
);
93+
}

0 commit comments

Comments
 (0)