Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
734 changes: 50 additions & 684 deletions apps/web/src/components/views/rooms/EventTile.tsx

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions apps/web/src/components/views/rooms/EventTile/ActionBarAdapter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
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, { useCallback, useContext, useEffect, useState, type JSX } from "react";
import { type MatrixEvent, type Relations } from "matrix-js-sdk/src/matrix";
import { ActionBarView } from "@element-hq/web-shared-components";

import type ReplyChain from "../../elements/ReplyChain";
import ReactionPicker from "../../emojipicker/ReactionPicker";
import MessageContextMenu from "../../context_menus/MessageContextMenu";
import ContextMenu, { aboveLeftOf } from "../../../structures/ContextMenu";
import RoomContext from "../../../../contexts/RoomContext";
import { CardContext } from "../../right_panel/context";
import { type RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks";
import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
import { type GetRelationsForEvent } from "../../../../viewmodels/room/timeline/event-tile/reactions/EventTileReactionState";

interface ActionBarEventTileOps {
isWidgetHidden(): boolean;
unhideWidget(): void;
}

interface ActionBarEventTile {
getEventTileOps?(): ActionBarEventTileOps;
}

interface ActionBarAdapterProps {
eventTileViewModel: EventTileViewModel;
mxEvent: MatrixEvent;
reactions?: Relations | null;
permalinkCreator?: RoomPermalinkCreator;
getTile: () => ActionBarEventTile | null;
getReplyChain: () => ReplyChain | null;
onFocusChange?: (focused: boolean) => void;
isQuoteExpanded?: boolean;
toggleThreadExpanded: () => void;
getRelationsForEvent?: GetRelationsForEvent;
}

export function ActionBarAdapter({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this get some doc?

eventTileViewModel,
mxEvent,
reactions,
permalinkCreator,
getTile,
getReplyChain,
onFocusChange,
isQuoteExpanded,
toggleThreadExpanded,
getRelationsForEvent,
}: Readonly<ActionBarAdapterProps>): JSX.Element {
const roomContext = useContext(RoomContext);
const { isCard } = useContext(CardContext);
const [optionsMenuAnchorRect, setOptionsMenuAnchorRect] = useState<DOMRect | null>(null);
const [reactionsMenuAnchorRect, setReactionsMenuAnchorRect] = useState<DOMRect | null>(null);
const isSearch = Boolean(roomContext.search);
const handleOptionsClick = useCallback((anchor: HTMLElement | null): void => {
setOptionsMenuAnchorRect(anchor?.getBoundingClientRect() ?? null);
}, []);
const handleReactionsClick = useCallback((anchor: HTMLElement | null): void => {
setReactionsMenuAnchorRect(anchor?.getBoundingClientRect() ?? null);
}, []);
const vm = eventTileViewModel.getActionBarViewModel({
mxEvent,
timelineRenderingType: roomContext.timelineRenderingType,
canSendMessages: roomContext.canSendMessages,
canReact: roomContext.canReact,
isSearch,
isCard,
isQuoteExpanded,
onToggleThreadExpanded: toggleThreadExpanded,
onOptionsClick: handleOptionsClick,
onReactionsClick: handleReactionsClick,
getRelationsForEvent,
});

useEffect(() => {
// This child VM owns Matrix and settings listeners, so release it when the view using it leaves the tree.
return () => eventTileViewModel.releaseActionBarViewModel();
}, [eventTileViewModel]);

useEffect(() => {
vm.setProps({
mxEvent,
timelineRenderingType: roomContext.timelineRenderingType,
canSendMessages: roomContext.canSendMessages,
canReact: roomContext.canReact,
isSearch,
isCard,
isQuoteExpanded,
getRelationsForEvent,
onToggleThreadExpanded: toggleThreadExpanded,
onOptionsClick: handleOptionsClick,
onReactionsClick: handleReactionsClick,
});
}, [
vm,
mxEvent,
roomContext.timelineRenderingType,
roomContext.canSendMessages,
roomContext.canReact,
isSearch,
isCard,
isQuoteExpanded,
getRelationsForEvent,
handleOptionsClick,
handleReactionsClick,
toggleThreadExpanded,
]);

useEffect(() => {
onFocusChange?.(Boolean(optionsMenuAnchorRect || reactionsMenuAnchorRect));
}, [onFocusChange, optionsMenuAnchorRect, reactionsMenuAnchorRect]);

useEffect(() => {
setOptionsMenuAnchorRect(null);
setReactionsMenuAnchorRect(null);
}, [mxEvent]);

const closeOptionsMenu = useCallback((): void => {
setOptionsMenuAnchorRect(null);
}, []);

const closeReactionsMenu = useCallback((): void => {
setReactionsMenuAnchorRect(null);
}, []);

const tile = getTile();
const replyChain = getReplyChain();
const eventTileOps = tile?.getEventTileOps ? tile.getEventTileOps() : undefined;
const collapseReplyChain = replyChain?.canCollapse() ? replyChain.collapse : undefined;

return (
<>
<ActionBarView vm={vm} className="mx_MessageActionBar" />
{optionsMenuAnchorRect ? (
<MessageContextMenu
{...aboveLeftOf(optionsMenuAnchorRect)}
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={eventTileOps}
collapseReplyChain={collapseReplyChain}
onFinished={closeOptionsMenu}
getRelationsForEvent={getRelationsForEvent}
/>
) : null}
{reactionsMenuAnchorRect ? (
<ContextMenu
{...aboveLeftOf(reactionsMenuAnchorRect)}
onFinished={closeReactionsMenu}
managed={false}
focusLock
>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeReactionsMenu} />
</ContextMenu>
) : null}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
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, { useEffect, type JSX } from "react";
import { E2eMessageSharedIconView } from "@element-hq/web-shared-components";

import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";

interface E2eMessageSharedIconAdapterProps {
eventTileViewModel: EventTileViewModel;
/**
* The ID of the room containing the event whose keys were shared.
*/
roomId: string;
/**
* The ID of the user who shared the keys.
*/
keyForwardingUserId: string;
}

export function E2eMessageSharedIconAdapter({
eventTileViewModel,
roomId,
keyForwardingUserId,
}: Readonly<E2eMessageSharedIconAdapterProps>): JSX.Element {
const client = useMatrixClientContext();
const vm = eventTileViewModel.getE2eMessageSharedIconViewModel({
client,
roomId,
keyForwardingUserId,
});

useEffect(() => {
// This child VM owns Matrix listeners, so release it when the view using it leaves the tree.
return () => eventTileViewModel.releaseE2eMessageSharedIconViewModel();
}, [eventTileViewModel]);

useEffect(() => {
vm.setClient(client);
}, [client, vm]);

useEffect(() => {
vm.setRoomId(roomId);
}, [roomId, vm]);

useEffect(() => {
vm.setKeyForwardingUserId(keyForwardingUserId);
}, [keyForwardingUserId, vm]);

return (
<E2eMessageSharedIconView
vm={vm}
className={
// Timeline PCSS uses this app class as a layout hook for positioning and layout variants.
"mx_EventTile_e2eIcon"
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
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, { useEffect, type JSX } from "react";
import { MessageTimestampView } from "@element-hq/web-shared-components";

import { type EventTileViewModel } from "../../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
import { type MessageTimestampViewModelProps } from "../../../../viewmodels/room/timeline/event-tile/timestamp/MessageTimestampViewModel.ts";
import { Icon as LateIcon } from "../../../../../res/img/sensor.svg";

interface MessageTimestampAdapterProps {
eventTileViewModel: EventTileViewModel;
kind: "plain" | "linked";
timestampProps: MessageTimestampViewModelProps;
}

export function MessageTimestampAdapter({
eventTileViewModel,
kind,
timestampProps,
}: Readonly<MessageTimestampAdapterProps>): JSX.Element {
const vm =
kind === "linked"
? eventTileViewModel.getLinkedMessageTimestampViewModel(timestampProps)
: eventTileViewModel.getMessageTimestampViewModel(timestampProps);

useEffect(() => {
vm.setProps(timestampProps);
}, [vm, timestampProps]);

return (
<>
{timestampProps.receivedTs ? (
<LateIcon className="mx_MessageTimestamp_lateIcon" width="16" height="16" />
) : undefined}
<MessageTimestampView vm={vm} className="mx_MessageTimestamp" />
</>
);
}
Loading
Loading