import {
  Box,
  Button,
  Dialog,
  Header,
  Icon,
  IconButton,
  Icons,
  Input,
  Line,
  Menu,
  MenuItem,
  Modal,
  Overlay,
  OverlayBackdrop,
  OverlayCenter,
  PopOut,
  RectCords,
  Spinner,
  Text,
  as,
  color,
  config,
} from 'folds';
import React, {
  ComponentProps,
  FormEventHandler,
  memo,
  MouseEventHandler,
  ReactNode,
  useCallback,
  useMemo,
  useState,
} from 'react';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { useHover, useFocusWithin } from 'react-aria';
import { MatrixEvent, MsgType, Room } from 'matrix-js-sdk';
import type { StateEvents } from 'matrix-js-sdk';
import { Relations } from 'matrix-js-sdk/lib/models/relations';
import classNames from 'classnames';
import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
import {
  CHANNEL_MESSAGE_SPACING,
  ChannelLayout,
  ChannelMessageAvatar,
  MessageBase,
  STREAM_MESSAGE_SPACING,
  StreamLayout,
  Time,
  Username,
  UsernameBold,
} from '../../../components/message';
import { StreamMediaContext } from '../../../components/RenderMessageContent';
import { logMedia } from '../../../components/message/attachment/streamMediaDebug';
import { canEditEvent, getEventEdits, getMemberDisplayName } from '../../../utils/room';
import { getMxIdLocalPart } from '../../../utils/matrix';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
import { useDotColor } from '../../../hooks/useDotColor';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import * as css from './styles.css';
import { EventReaders } from '../../../components/event-readers';
import { TextViewer } from '../../../components/text-viewer';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { EmojiBoard } from '../../../components/emoji-board';
import { ReactionViewer } from '../reaction-viewer';
import { MessageEditor } from './MessageEditor';
import { copyToClipboard } from '../../../utils/dom';
import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoomEvent } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers';
import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents';
import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room';
import colorMXID from '../../../../util/colorMXID';

export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;

type MessageQuickReactionsProps = {
  onReaction: ReactionHandler;
};
export const MessageQuickReactions = as<'div', MessageQuickReactionsProps>(
  ({ onReaction, ...props }, ref) => {
    const mx = useMatrixClient();
    const recentEmojis = useRecentEmoji(mx, 4);

    if (recentEmojis.length === 0) return <span />;
    return (
      <>
        <Box
          style={{ padding: config.space.S200 }}
          alignItems="Center"
          justifyContent="Center"
          gap="200"
          {...props}
          ref={ref}
        >
          {recentEmojis.map((emoji) => (
            <IconButton
              key={emoji.unicode}
              className={css.MessageQuickReaction}
              size="300"
              variant="SurfaceVariant"
              radii="Pill"
              title={emoji.shortcode}
              aria-label={emoji.shortcode}
              onClick={() => onReaction(emoji.unicode, emoji.shortcode)}
            >
              <Text size="T500">{emoji.unicode}</Text>
            </IconButton>
          ))}
        </Box>
        <Line size="300" />
      </>
    );
  }
);

// Default reactions shown in the hover rail before a user has built up any
// recent-emoji history, so the "быстрый рельс" always offers one-tap reactions.
const RAIL_DEFAULT_REACTIONS: { unicode: string; shortcode: string }[] = [
  { unicode: '👍', shortcode: 'thumbsup' },
  { unicode: '❤️', shortcode: 'heart' },
  { unicode: '😂', shortcode: 'joy' },
];

type RailQuickReactionsProps = {
  onReaction: (key: string, shortcode?: string) => void;
};
// Inline quick-reactions surfaced directly in the hover action rail — react in
// one tap without opening the emoji board. Mounts ONLY inside the
// conditionally-rendered rail (one row at a time), so there is no per-row cost.
function RailQuickReactions({ onReaction }: RailQuickReactionsProps) {
  const mx = useMatrixClient();
  const recent = useRecentEmoji(mx, 3);
  const items =
    recent.length > 0
      ? recent.slice(0, 3).map((e) => ({ unicode: e.unicode, shortcode: e.shortcode }))
      : RAIL_DEFAULT_REACTIONS;
  return (
    <>
      {items.map((emoji) => (
        <IconButton
          key={emoji.unicode}
          size="300"
          variant="SurfaceVariant"
          radii="300"
          title={emoji.shortcode}
          aria-label={emoji.shortcode}
          onClick={() => onReaction(emoji.unicode, emoji.shortcode)}
        >
          <Text size="T400">{emoji.unicode}</Text>
        </IconButton>
      ))}
    </>
  );
}

export const MessageAllReactionItem = as<
  'button',
  {
    room: Room;
    relations: Relations;
    onClose?: () => void;
  }
>(({ room, relations, onClose, ...props }, ref) => {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);

  const handleClose = () => {
    setOpen(false);
    onClose?.();
  };

  return (
    <>
      <Overlay
        onContextMenu={(evt: React.MouseEvent<HTMLDivElement>) => {
          evt.stopPropagation();
        }}
        open={open}
        backdrop={<OverlayBackdrop />}
      >
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              returnFocusOnDeactivate: false,
              onDeactivate: () => handleClose(),
              clickOutsideDeactivates: true,
              escapeDeactivates: stopPropagation,
            }}
          >
            <Modal variant="Surface" size="300">
              <ReactionViewer
                room={room}
                relations={relations}
                requestClose={() => setOpen(false)}
              />
            </Modal>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
      <MenuItem
        size="300"
        after={<Icon size="100" src={Icons.Smile} />}
        radii="300"
        onClick={() => setOpen(true)}
        {...props}
        ref={ref}
        aria-pressed={open}
      >
        <Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
          {t('Room.view_reactions')}
        </Text>
      </MenuItem>
    </>
  );
});

export const MessageReadReceiptItem = as<
  'button',
  {
    room: Room;
    eventId: string;
    onClose?: () => void;
  }
>(({ room, eventId, onClose, ...props }, ref) => {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);

  const handleClose = () => {
    setOpen(false);
    onClose?.();
  };

  return (
    <>
      <Overlay open={open} backdrop={<OverlayBackdrop />}>
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              onDeactivate: handleClose,
              clickOutsideDeactivates: true,
              escapeDeactivates: stopPropagation,
            }}
          >
            <Modal variant="Surface" size="300">
              <EventReaders room={room} eventId={eventId} requestClose={handleClose} />
            </Modal>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
      <MenuItem
        size="300"
        after={<Icon size="100" src={Icons.CheckTwice} />}
        radii="300"
        onClick={() => setOpen(true)}
        {...props}
        ref={ref}
        aria-pressed={open}
      >
        <Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
          {t('Room.read_receipts')}
        </Text>
      </MenuItem>
    </>
  );
});

export const MessageSourceCodeItem = as<
  'button',
  {
    room: Room;
    mEvent: MatrixEvent;
    onClose?: () => void;
  }
>(({ room, mEvent, onClose, ...props }, ref) => {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);

  const getContent = (evt: MatrixEvent) =>
    evt.isEncrypted()
      ? {
          [`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(),
          [`<== ORIGINAL_EVENT ==>`]: evt.event,
        }
      : evt.event;

  const getText = (): string => {
    const evtId = mEvent.getId();
    if (!evtId) return '';
    const evtTimeline = room.getTimelineForEvent(evtId);
    const edits =
      evtTimeline &&
      getEventEdits(evtTimeline.getTimelineSet(), evtId, mEvent.getType())?.getRelations();

    if (!edits) return JSON.stringify(getContent(mEvent), null, 2);

    const content: Record<string, unknown> = {
      '<== MAIN_EVENT ==>': getContent(mEvent),
    };

    edits.forEach((editEvt, index) => {
      content[`<== REPLACEMENT_EVENT_${index + 1} ==>`] = getContent(editEvt);
    });

    return JSON.stringify(content, null, 2);
  };

  const handleClose = () => {
    setOpen(false);
    onClose?.();
  };

  return (
    <>
      <Overlay open={open} backdrop={<OverlayBackdrop />}>
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              onDeactivate: handleClose,
              clickOutsideDeactivates: true,
              escapeDeactivates: stopPropagation,
            }}
          >
            <Modal variant="Surface" size="500">
              <TextViewer
                name={t('Room.source_code')}
                langName="json"
                text={getText()}
                requestClose={handleClose}
              />
            </Modal>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
      <MenuItem
        size="300"
        after={<Icon size="100" src={Icons.BlockCode} />}
        radii="300"
        onClick={() => setOpen(true)}
        {...props}
        ref={ref}
        aria-pressed={open}
      >
        <Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
          {t('Room.view_source')}
        </Text>
      </MenuItem>
    </>
  );
});

export const MessageCopyLinkItem = as<
  'button',
  {
    room: Room;
    mEvent: MatrixEvent;
    onClose?: () => void;
  }
>(({ room, mEvent, onClose, ...props }, ref) => {
  const { t } = useTranslation();

  const handleCopy = () => {
    const eventId = mEvent.getId();
    if (!eventId) return;
    copyToClipboard(getMatrixToRoomEvent(room.roomId, eventId, getViaServers(room)));
    onClose?.();
  };

  return (
    <MenuItem
      size="300"
      after={<Icon size="100" src={Icons.Link} />}
      radii="300"
      onClick={handleCopy}
      {...props}
      ref={ref}
    >
      <Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
        {t('Room.copy_link')}
      </Text>
    </MenuItem>
  );
});

export const MessagePinItem = as<
  'button',
  {
    room: Room;
    mEvent: MatrixEvent;
    onClose?: () => void;
  }
>(({ room, mEvent, onClose, ...props }, ref) => {
  const { t } = useTranslation();
  const mx = useMatrixClient();
  const pinnedEvents = useRoomPinnedEvents(room);
  const isPinned = pinnedEvents.includes(mEvent.getId() ?? '');

  const handlePin = () => {
    const eventId = mEvent.getId();
    const pinContent: RoomPinnedEventsEventContent = {
      pinned: Array.from(pinnedEvents).filter((id) => id !== eventId),
    };
    if (!isPinned && eventId) {
      pinContent.pinned.push(eventId);
    }
    mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as keyof StateEvents, pinContent);
    onClose?.();
  };

  return (
    <MenuItem
      size="300"
      after={<Icon size="100" src={Icons.Pin} />}
      radii="300"
      onClick={handlePin}
      {...props}
      ref={ref}
    >
      <Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
        {isPinned ? t('Room.unpin_message') : t('Room.pin_message')}
      </Text>
    </MenuItem>
  );
});

export const MessageDeleteItem = as<
  'button',
  {
    room: Room;
    mEvent: MatrixEvent;
    onClose?: () => void;
  }
>(({ room, mEvent, onClose, ...props }, ref) => {
  const { t } = useTranslation();
  const mx = useMatrixClient();
  const [open, setOpen] = useState(false);

  const [deleteState, deleteMessage] = useAsyncCallback(
    useCallback(
      (eventId: string, reason?: string) =>
        mx.redactEvent(room.roomId, eventId, undefined, reason ? { reason } : undefined),
      [mx, room]
    )
  );

  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
    evt.preventDefault();
    const eventId = mEvent.getId();
    if (
      !eventId ||
      deleteState.status === AsyncStatus.Loading ||
      deleteState.status === AsyncStatus.Success
    )
      return;
    const target = evt.target as HTMLFormElement | undefined;
    const reasonInput = target?.reasonInput as HTMLInputElement | undefined;
    const reason = reasonInput && reasonInput.value.trim();
    deleteMessage(eventId, reason);
  };

  const handleClose = () => {
    setOpen(false);
    onClose?.();
  };

  return (
    <>
      <Overlay open={open} backdrop={<OverlayBackdrop />}>
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              onDeactivate: handleClose,
              clickOutsideDeactivates: true,
              escapeDeactivates: stopPropagation,
            }}
          >
            <Dialog variant="Surface">
              <Header
                style={{
                  padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
                  borderBottomWidth: config.borderWidth.B300,
                }}
                variant="Surface"
                size="500"
              >
                <Box grow="Yes">
                  <Text size="H4">{t('Room.delete_message')}</Text>
                </Box>
                <IconButton size="300" onClick={handleClose} radii="300">
                  <Icon src={Icons.Cross} />
                </IconButton>
              </Header>
              <Box
                as="form"
                onSubmit={handleSubmit}
                style={{ padding: config.space.S400 }}
                direction="Column"
                gap="400"
              >
                <Text priority="400">{t('Room.delete_confirm')}</Text>
                <Box direction="Column" gap="100">
                  <Text size="L400">
                    {t('Room.reason')}{' '}
                    <Text as="span" size="T200">
                      ({t('Room.optional')})
                    </Text>
                  </Text>
                  <Input name="reasonInput" variant="Background" />
                  {deleteState.status === AsyncStatus.Error && (
                    <Text style={{ color: color.Critical.Main }} size="T300">
                      {t('Room.delete_error')}
                    </Text>
                  )}
                </Box>
                <Button
                  type="submit"
                  variant="Critical"
                  before={
                    deleteState.status === AsyncStatus.Loading ? (
                      <Spinner fill="Solid" variant="Critical" size="200" />
                    ) : undefined
                  }
                  aria-disabled={deleteState.status === AsyncStatus.Loading}
                >
                  <Text size="B400">
                    {deleteState.status === AsyncStatus.Loading
                      ? t('Room.deleting')
                      : t('Room.delete')}
                  </Text>
                </Button>
              </Box>
            </Dialog>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
      <Button
        variant="Critical"
        fill="None"
        size="300"
        after={<Icon size="100" src={Icons.Delete} />}
        radii="300"
        onClick={() => setOpen(true)}
        aria-pressed={open}
        {...props}
        ref={ref}
      >
        <Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
          {t('Room.delete')}
        </Text>
      </Button>
    </>
  );
});

export const MessageReportItem = as<
  'button',
  {
    room: Room;
    mEvent: MatrixEvent;
    onClose?: () => void;
  }
>(({ room, mEvent, onClose, ...props }, ref) => {
  const { t } = useTranslation();
  const mx = useMatrixClient();
  const [open, setOpen] = useState(false);

  const [reportState, reportMessage] = useAsyncCallback(
    useCallback(
      (eventId: string, score: number, reason: string) =>
        mx.reportEvent(room.roomId, eventId, score, reason),
      [mx, room]
    )
  );

  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
    evt.preventDefault();
    const eventId = mEvent.getId();
    if (
      !eventId ||
      reportState.status === AsyncStatus.Loading ||
      reportState.status === AsyncStatus.Success
    )
      return;
    const target = evt.target as HTMLFormElement | undefined;
    const reasonInput = target?.reasonInput as HTMLInputElement | undefined;
    const reason = reasonInput && reasonInput.value.trim();
    if (reasonInput) reasonInput.value = '';
    reportMessage(eventId, reason ? -100 : -50, reason || t('Room.no_reason'));
  };

  const handleClose = () => {
    setOpen(false);
    onClose?.();
  };

  return (
    <>
      <Overlay open={open} backdrop={<OverlayBackdrop />}>
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              onDeactivate: handleClose,
              clickOutsideDeactivates: true,
              escapeDeactivates: stopPropagation,
            }}
          >
            <Dialog variant="Surface">
              <Header
                style={{
                  padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
                  borderBottomWidth: config.borderWidth.B300,
                }}
                variant="Surface"
                size="500"
              >
                <Box grow="Yes">
                  <Text size="H4">{t('Room.report_message')}</Text>
                </Box>
                <IconButton size="300" onClick={handleClose} radii="300">
                  <Icon src={Icons.Cross} />
                </IconButton>
              </Header>
              <Box
                as="form"
                onSubmit={handleSubmit}
                style={{ padding: config.space.S400 }}
                direction="Column"
                gap="400"
              >
                <Text priority="400">{t('Room.report_desc')}</Text>
                <Box direction="Column" gap="100">
                  <Text size="L400">{t('Room.report_reason')}</Text>
                  <Input name="reasonInput" variant="Background" required />
                  {reportState.status === AsyncStatus.Error && (
                    <Text style={{ color: color.Critical.Main }} size="T300">
                      {t('Room.report_error')}
                    </Text>
                  )}
                  {reportState.status === AsyncStatus.Success && (
                    <Text style={{ color: color.Success.Main }} size="T300">
                      {t('Room.report_success')}
                    </Text>
                  )}
                </Box>
                <Button
                  type="submit"
                  variant="Critical"
                  before={
                    reportState.status === AsyncStatus.Loading ? (
                      <Spinner fill="Solid" variant="Critical" size="200" />
                    ) : undefined
                  }
                  aria-disabled={
                    reportState.status === AsyncStatus.Loading ||
                    reportState.status === AsyncStatus.Success
                  }
                >
                  <Text size="B400">
                    {reportState.status === AsyncStatus.Loading
                      ? t('Room.reporting')
                      : t('Room.report')}
                  </Text>
                </Button>
              </Box>
            </Dialog>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
      <Button
        variant="Critical"
        fill="None"
        size="300"
        after={<Icon size="100" src={Icons.Warning} />}
        radii="300"
        onClick={() => setOpen(true)}
        aria-pressed={open}
        {...props}
        ref={ref}
      >
        <Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
          {t('Room.report')}
        </Text>
      </Button>
    </>
  );
});

export type MessageProps = {
  room: Room;
  mEvent: MatrixEvent;
  collapse: boolean;
  // Stream layout only: suppress the rail segment on this row — set for a
  // trailing continuation that sits after the last dot, so the rail stops at
  // the last dot instead of bleeding down the dot-less tail of a run. Ignored
  // by the channel layout.
  railHidden?: boolean;
  highlight: boolean;
  edit?: boolean;
  canDelete?: boolean;
  canSendReaction?: boolean;
  canPinEvent?: boolean;
  imagePackRooms?: Room[];
  relations?: Relations;
  onUserClick: MouseEventHandler<HTMLButtonElement>;
  onUsernameClick: MouseEventHandler<HTMLButtonElement>;
  onReplyClick: (
    ev: Parameters<MouseEventHandler<HTMLButtonElement>>[0],
    startThread?: boolean
  ) => void;
  onEditId?: (eventId?: string) => void;
  onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
  reply?: ReactNode;
  reactions?: ReactNode;
  hideReadReceipts?: boolean;
  showDeveloperTools?: boolean;
  memberPowerTag?: MemberPowerTag;
  accessibleTagColors?: Map<string, string>;
  legacyUsernameColor?: boolean;
  streamRailStart?: boolean;
  streamRailEnd?: boolean;
  // M2: hide the «Reply in Thread» menu/quick action. Set by
  // RoomTimeline outside channels-mode (where threads aren't surfaced
  // anywhere) and inside bridged channels (where the bridge has no
  // thread semantic). When true, the ThreadPlus button + the
  // `reply_in_thread` menu item are skipped; the regular «Reply»
  // affordance stays untouched.
  hideThreadReplyAffordance?: boolean;
  // M2: hide the regular «Reply» (m.in_reply_to) menu/quick action.
  // Set by RoomTimeline when the thread drawer is open — the channel
  // composer is unmounted in that state, so the reply chip would
  // write to an unread atom and the focus call would no-op silently.
  // ThreadPlus button stays usable for navigating to a different
  // thread root.
  hideMainReplyAffordance?: boolean;
  // Snapshot of `mEvent.getContent().msgtype` from the caller. Threaded as
  // a prop (not derived locally) so encrypted-then-decrypted events flip
  // mediaMode reliably when EncryptedContent re-renders post-decrypt —
  // local useState would race against the commit↔effect gap.
  msgType?: string;
  // M3a: thread summary pill (count + last reply preview) rendered
  // between the bubble and reactions. Caller (`RoomTimeline`) decides
  // whether to mount the card based on `channelsMode && !isBridged` —
  // outside channels-mode this stays `undefined` and the slot collapses.
  threadSummary?: React.ReactNode;
  // Choose timeline visual. `'stream'` = rail + dot + bubble — used for
  // 1:1 DMs and 1:1 bot control rooms. `'channel'` = avatar + bubble with
  // an in-bubble username/time header — used for every non-1:1 room
  // (group DMs, group rooms under /space/, channels). Default `'stream'`
  // so card-preview callers (pin menu, message search, inbox) get the
  // legacy DM look.
  layout?: 'stream' | 'channel';
  // Forwarded to `ChannelLayout.headerInBubble`. Every current timeline
  // caller passes `true` alongside `layout='channel'`; kept as a prop so
  // un-bubbled `ChannelLayout` consumers stay possible.
  channelHeaderInBubble?: boolean;
  // Parent-computed render snapshot (see getMessageRenderSig). Drives the
  // React.memo comparator so in-place MatrixEvent mutations (edit/redact/
  // decrypt/echo) and rename/urlPreview changes repaint the row.
  renderSig?: string;
};
const MessageInner = as<'div', MessageProps>(
  (
    {
      className,
      room,
      mEvent,
      collapse,
      railHidden,
      highlight,
      edit,
      canDelete,
      canSendReaction,
      canPinEvent,
      imagePackRooms,
      relations,
      onUserClick,
      onUsernameClick,
      onReplyClick,
      onReactionToggle,
      onEditId,
      reply,
      reactions,
      hideReadReceipts,
      showDeveloperTools,
      memberPowerTag,
      accessibleTagColors,
      legacyUsernameColor,
      streamRailStart,
      streamRailEnd,
      hideThreadReplyAffordance,
      hideMainReplyAffordance,
      msgType,
      threadSummary,
      layout = 'stream',
      channelHeaderInBubble,
      children,
      ...props
    },
    ref
  ) => {
    const { t } = useTranslation();
    const mx = useMatrixClient();
    const senderId = mEvent.getSender() ?? '';

    const [hover, setHover] = useState(false);
    const { hoverProps } = useHover({ onHoverChange: setHover });
    const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
    const [menuAnchor, setMenuAnchor] = useState<RectCords>();
    const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>();

    const isOwnMessage = !!mx.getUserId() && senderId === mx.getUserId();
    const senderDisplayName =
      getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;

    const tagColor = memberPowerTag?.color
      ? accessibleTagColors?.get(memberPowerTag.color)
      : undefined;

    const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor;

    // Only the Stream layout renders the rail dot — skip the push-action eval
    // and Receipt subscription on channel-layout rows where it is discarded.
    const dot = useDotColor(room, mEvent, layout !== 'channel', hideReadReceipts);
    const screenSize = useScreenSizeContext();
    const isMobile = screenSize === ScreenSize.Mobile;

    // msgType comes from the parent — RoomTimeline reads
    // `mEvent.getContent().msgtype` synchronously and re-evaluates inside
    // EncryptedContent's render-prop, which re-runs on Decrypted. Avoiding
    // a local useState here sidesteps the commit↔effect race where
    // decryption fires between render and listener attach.
    const isMediaMessage = msgType === MsgType.Image || msgType === MsgType.Video;
    const mediaMode = isMediaMessage && !edit;

    if (msgType === MsgType.Image || msgType === MsgType.Video || msgType === MsgType.File) {
      logMedia('Message', {
        eventId: mEvent.getId(),
        msgType,
        isMediaMessage,
        edit,
        mediaMode,
        isMobile,
        screenSize,
      });
    }

    const streamMediaCtx = useMemo(
      () =>
        // Image / video chrome (the StreamMediaImage/Video shell) — used in
        // BOTH the 1:1 Stream layout and the Discord-style channel layout, so
        // group-chat media renders identically to 1:1 (rounded shell, capped
        // size) instead of the legacy boxed attachment. Only `own` is needed
        // (the bubble's notch corner); the nick is rendered above by the header.
        mediaMode ? { own: isOwnMessage } : null,
      [mediaMode, isOwnMessage]
    );

    const msgContentJSX = (
      <Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}>
        {reply}
        {edit && onEditId ? (
          <MessageEditor
            style={{
              maxWidth: '100%',
              width: '100vw',
            }}
            roomId={room.roomId}
            room={room}
            mEvent={mEvent}
            imagePackRooms={imagePackRooms}
            onCancel={() => onEditId()}
          />
        ) : (
          children
        )}
      </Box>
    );

    const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
      if (evt.altKey || !window.getSelection()?.isCollapsed || edit) return;
      const tag = (evt.target as Element).tagName;
      if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
      evt.preventDefault();
      setMenuAnchor({
        x: evt.clientX,
        y: evt.clientY,
        width: 0,
        height: 0,
      });
    };

    const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
      const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget;
      setMenuAnchor(target.getBoundingClientRect());
    };

    const closeMenu = () => {
      setMenuAnchor(undefined);
    };

    const handleOpenEmojiBoard: MouseEventHandler<HTMLButtonElement> = (evt) => {
      const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget;
      setEmojiBoardAnchor(target.getBoundingClientRect());
    };
    const handleAddReactions: MouseEventHandler<HTMLButtonElement> = () => {
      const rect = menuAnchor;
      closeMenu();
      // open it with timeout because closeMenu
      // FocusTrap will return focus from emojiBoard

      setTimeout(() => {
        setEmojiBoardAnchor(rect);
      }, 100);
    };

    const isThreadedMessage = mEvent.threadRootId !== undefined;
    // After a Thread is created on a root event, SDK sets
    // `mEvent.threadRootId === mEvent.getId()` and `isThreadRoot ===
    // true`. We want the «Reply in Thread» button to keep working on
    // the root (re-open drawer) — only hide when the message is itself
    // a thread reply (no thread can be started on a reply, and in
    // channels-mode replies aren't visible in main timeline anyway).
    const isThreadReply = isThreadedMessage && !mEvent.isThreadRoot;

    return (
      <MessageBase
        className={classNames(css.MessageBase, className)}
        tabIndex={0}
        space={layout === 'channel' ? CHANNEL_MESSAGE_SPACING : STREAM_MESSAGE_SPACING}
        // Stream rows always render with collapsed marginTop (=0) so the
        // bubble-to-bubble gap is uniform regardless of same-sender grouping.
        // Total gap = 4×S100 (StreamRoot + MessageBase vertical padding on
        // both sides) = 16px. The rail-bridge in layout.css.ts is still S400
        // each side — it overshoots slightly across the smaller gap, but the
        // overlap lands inside DotColumn behind the dot halo and stays
        // invisible. `collapse` still drives avatar/header visibility inside
        // ChannelLayout — channel mode keeps the original behaviour.
        collapse={layout === 'channel' ? collapse : true}
        highlight={highlight}
        selected={!!menuAnchor || !!emojiBoardAnchor}
        {...props}
        {...hoverProps}
        {...focusWithinProps}
        ref={ref}
      >
        {!edit && (hover || !!menuAnchor || !!emojiBoardAnchor) && (
          <div className={css.MessageOptionsBase}>
            <Menu className={css.MessageOptionsBar} variant="SurfaceVariant">
              <Box gap="100">
                {canSendReaction && (
                  <RailQuickReactions
                    onReaction={(key, shortcode) => {
                      const evtId = mEvent.getId();
                      if (evtId) onReactionToggle(evtId, key, shortcode);
                    }}
                  />
                )}
                {canSendReaction && (
                  <PopOut
                    position="Bottom"
                    align={emojiBoardAnchor?.width === 0 ? 'Start' : 'End'}
                    offset={emojiBoardAnchor?.width === 0 ? 0 : undefined}
                    anchor={emojiBoardAnchor}
                    content={
                      <EmojiBoard
                        imagePackRooms={imagePackRooms ?? []}
                        returnFocusOnDeactivate={false}
                        allowTextCustomEmoji
                        onEmojiSelect={(key) => {
                          const evtId = mEvent.getId();
                          if (evtId) onReactionToggle(evtId, key);
                          setEmojiBoardAnchor(undefined);
                        }}
                        onCustomEmojiSelect={(mxc, shortcode) => {
                          const evtId = mEvent.getId();
                          if (evtId) onReactionToggle(evtId, mxc, shortcode);
                          setEmojiBoardAnchor(undefined);
                        }}
                        requestClose={() => {
                          setEmojiBoardAnchor(undefined);
                        }}
                      />
                    }
                  >
                    <IconButton
                      onClick={handleOpenEmojiBoard}
                      variant="SurfaceVariant"
                      size="300"
                      radii="300"
                      aria-pressed={!!emojiBoardAnchor}
                    >
                      <Icon src={Icons.SmilePlus} size="100" />
                    </IconButton>
                  </PopOut>
                )}
                {!hideMainReplyAffordance && (
                  <IconButton
                    onClick={onReplyClick}
                    data-event-id={mEvent.getId()}
                    variant="SurfaceVariant"
                    size="300"
                    radii="300"
                  >
                    <Icon src={Icons.ReplyArrow} size="100" />
                  </IconButton>
                )}
                {!isThreadReply && !hideThreadReplyAffordance && (
                  <IconButton
                    onClick={(ev: Parameters<typeof onReplyClick>[0]) => onReplyClick(ev, true)}
                    data-event-id={mEvent.getId()}
                    variant="SurfaceVariant"
                    size="300"
                    radii="300"
                  >
                    <Icon src={Icons.ThreadPlus} size="100" />
                  </IconButton>
                )}
                {canEditEvent(mx, mEvent) && onEditId && (
                  <IconButton
                    onClick={() => onEditId(mEvent.getId())}
                    variant="SurfaceVariant"
                    size="300"
                    radii="300"
                  >
                    <Icon src={Icons.Pencil} size="100" />
                  </IconButton>
                )}
                <PopOut
                  anchor={menuAnchor}
                  position="Bottom"
                  align={menuAnchor?.width === 0 ? 'Start' : 'End'}
                  offset={menuAnchor?.width === 0 ? 0 : undefined}
                  content={
                    <FocusTrap
                      focusTrapOptions={{
                        initialFocus: false,
                        onDeactivate: () => setMenuAnchor(undefined),
                        clickOutsideDeactivates: true,
                        isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
                        isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
                        escapeDeactivates: stopPropagation,
                      }}
                    >
                      <Menu>
                        {canSendReaction && (
                          <MessageQuickReactions
                            onReaction={(key, shortcode) => {
                              const evtId = mEvent.getId();
                              if (evtId) onReactionToggle(evtId, key, shortcode);
                              closeMenu();
                            }}
                          />
                        )}
                        <Box direction="Column" gap="100" className={css.MessageMenuGroup}>
                          {canSendReaction && (
                            <MenuItem
                              size="300"
                              after={<Icon size="100" src={Icons.SmilePlus} />}
                              radii="300"
                              onClick={handleAddReactions}
                            >
                              <Text
                                className={css.MessageMenuItemText}
                                as="span"
                                size="T300"
                                truncate
                              >
                                {t('Room.add_reaction')}
                              </Text>
                            </MenuItem>
                          )}
                          {relations && (
                            <MessageAllReactionItem
                              room={room}
                              relations={relations}
                              onClose={closeMenu}
                            />
                          )}
                          {!hideMainReplyAffordance && (
                            <MenuItem
                              size="300"
                              after={<Icon size="100" src={Icons.ReplyArrow} />}
                              radii="300"
                              data-event-id={mEvent.getId()}
                              onClick={(evt: Parameters<typeof onReplyClick>[0]) => {
                                onReplyClick(evt);
                                closeMenu();
                              }}
                            >
                              <Text
                                className={css.MessageMenuItemText}
                                as="span"
                                size="T300"
                                truncate
                              >
                                {t('Room.reply')}
                              </Text>
                            </MenuItem>
                          )}
                          {!isThreadReply && !hideThreadReplyAffordance && (
                            <MenuItem
                              size="300"
                              after={<Icon src={Icons.ThreadPlus} size="100" />}
                              radii="300"
                              data-event-id={mEvent.getId()}
                              onClick={(evt: Parameters<typeof onReplyClick>[0]) => {
                                onReplyClick(evt, true);
                                closeMenu();
                              }}
                            >
                              <Text
                                className={css.MessageMenuItemText}
                                as="span"
                                size="T300"
                                truncate
                              >
                                {t('Room.reply_in_thread')}
                              </Text>
                            </MenuItem>
                          )}
                          {canEditEvent(mx, mEvent) && onEditId && (
                            <MenuItem
                              size="300"
                              after={<Icon size="100" src={Icons.Pencil} />}
                              radii="300"
                              data-event-id={mEvent.getId()}
                              onClick={() => {
                                onEditId(mEvent.getId());
                                closeMenu();
                              }}
                            >
                              <Text
                                className={css.MessageMenuItemText}
                                as="span"
                                size="T300"
                                truncate
                              >
                                {t('Room.edit_message')}
                              </Text>
                            </MenuItem>
                          )}
                          {!hideReadReceipts && (
                            <MessageReadReceiptItem
                              room={room}
                              eventId={mEvent.getId() ?? ''}
                              onClose={closeMenu}
                            />
                          )}
                          {showDeveloperTools && (
                            <MessageSourceCodeItem
                              room={room}
                              mEvent={mEvent}
                              onClose={closeMenu}
                            />
                          )}
                          <MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
                          {canPinEvent && (
                            <MessagePinItem room={room} mEvent={mEvent} onClose={closeMenu} />
                          )}
                        </Box>
                        {((!mEvent.isRedacted() && canDelete) ||
                          mEvent.getSender() !== mx.getUserId()) && (
                          <>
                            <Line size="300" />
                            <Box direction="Column" gap="100" className={css.MessageMenuGroup}>
                              {!mEvent.isRedacted() && canDelete && (
                                <MessageDeleteItem
                                  room={room}
                                  mEvent={mEvent}
                                  onClose={closeMenu}
                                />
                              )}
                              {mEvent.getSender() !== mx.getUserId() && (
                                <MessageReportItem
                                  room={room}
                                  mEvent={mEvent}
                                  onClose={closeMenu}
                                />
                              )}
                            </Box>
                          </>
                        )}
                      </Menu>
                    </FocusTrap>
                  }
                >
                  <IconButton
                    variant="SurfaceVariant"
                    size="300"
                    radii="300"
                    onClick={handleOpenMenu}
                    aria-pressed={!!menuAnchor}
                  >
                    <Icon src={Icons.VerticalDots} size="100" />
                  </IconButton>
                </PopOut>
              </Box>
            </Menu>
          </div>
        )}
        {layout === 'channel' ? (
          <ChannelLayout
            isOwn={isOwnMessage}
            headerInBubble={channelHeaderInBubble}
            avatar={
              !collapse ? (
                <ChannelMessageAvatar
                  room={room}
                  senderId={senderId}
                  senderDisplayName={senderDisplayName}
                />
              ) : undefined
            }
            header={
              !collapse ? (
                <>
                  <Username
                    as="button"
                    style={{ color: usernameColor ?? color.Primary.Main }}
                    data-user-id={senderId}
                    onContextMenu={onUserClick}
                    onClick={onUsernameClick}
                  >
                    {/* In-bubble (thread) uses Stream's compact T200 size
                        so the bubble header matches the DM-chat rhythm.
                        Channels main timeline keeps T400 for the prominent
                        avatar-and-name row above an unbordered body. */}
                    <Text as="span" size={channelHeaderInBubble ? 'T200' : 'T400'} truncate>
                      {/* Own messages show the user's own nick too (not a «me»
                          label) — matches the 1:1 Stream layout. */}
                      <UsernameBold>{senderDisplayName}</UsernameBold>
                    </Text>
                  </Username>
                  <Time ts={mEvent.getTs()} compact size="T200" priority="300" />
                </>
              ) : undefined
            }
            reactions={reactions}
            threadSummary={threadSummary}
            onContextMenu={handleContextMenu}
          >
            <StreamMediaContext.Provider value={streamMediaCtx}>
              {msgContentJSX}
            </StreamMediaContext.Provider>
          </ChannelLayout>
        ) : (
          <StreamLayout
            time={<Time ts={mEvent.getTs()} compact />}
            dotColor={dot.color}
            dotOpacity={dot.opacity}
            dotProminent={dot.prominent}
            isOwn={isOwnMessage}
            compact={isMobile}
            // A same-sender continuation row (any minute): hide the dot / time
            // / nick and stack the body tight under the previous one. Only the
            // first message of a run keeps the full header. See RoomTimeline
            // `collapsed`.
            collapsed={collapse}
            railStart={streamRailStart}
            railEnd={streamRailEnd}
            railHidden={railHidden}
            mediaMode={mediaMode}
            reactions={reactions}
            threadSummary={threadSummary}
            header={
              // Author nick prints once at the head of a same-sender run; every
              // continuation row (`collapse`) drops it. Media heads show the
              // nick here too (above the media) — there's no overlay on the
              // image any more.
              collapse ? undefined : (
                // No inline colour / size — the `StreamName` wrapper supplies
                // the bold, pure white/black, larger-than-body styling, and the
                // button inherits it. css.Username already truncates.
                <Username
                  as="button"
                  data-user-id={senderId}
                  onContextMenu={onUserClick}
                  onClick={onUsernameClick}
                >
                  {/* Own messages show the user's own nick (display name), not
                      a «me» label — VS-Code-style per-author turn labels. */}
                  {senderDisplayName}
                </Username>
              )
            }
            onContextMenu={handleContextMenu}
          >
            <StreamMediaContext.Provider value={streamMediaCtx}>
              {msgContentJSX}
            </StreamMediaContext.Provider>
          </StreamLayout>
        )}
      </MessageBase>
    );
  }
);

// Parent-computed render snapshot for a timeline message row. This MUST be
// called during the PARENT's render and threaded in as the `renderSig` prop;
// it must NOT be recomputed from `mEvent` inside the memo comparator.
//
// matrix-js-sdk mutates the `MatrixEvent` IN PLACE on edit / redaction /
// decryption / local-echo (makeReplaced / makeRedacted / setClearData /
// handleRemoteEcho), and the timeline reuses one MatrixEvent instance per id,
// so `prevProps.mEvent === nextProps.mEvent`. A signature read off that live
// object inside the comparator would return the SAME (already-mutated) value
// for both prev and next and could never detect the change -- the classic
// stale-row trap element-web documents on `EventTile` (`isRedacted` is a prop
// there for exactly this reason). Capturing the snapshot here freezes it into
// the prop value, so React's retained prevProps holds the OLD snapshot and the
// comparator can diff old-vs-new. It also folds in the sender display name and
// the urlPreview setting, so a live rename / preview-toggle repaints the row.
export function getMessageRenderSig(
  room: Room,
  mEvent: MatrixEvent,
  editedEvent: MatrixEvent | undefined,
  urlPreview: boolean | undefined
): string {
  const senderId = mEvent.getSender() ?? '';
  const senderDisplayName =
    getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
  const content = mEvent.getContent();
  return [
    mEvent.getId() ?? '',
    mEvent.getType(),
    typeof content.msgtype === 'string' ? content.msgtype : '',
    mEvent.isRedacted() ? '1' : '0',
    editedEvent?.getId() ?? '',
    mEvent.status ?? '',
    senderDisplayName,
    urlPreview ? '1' : '0',
  ].join('\u0001');
}

// Custom equality for the timeline row. RoomTimeline re-renders on every live
// event / receipt / scroll-state change and rebuilds all ~80 visible rows; this
// lets React skip rows whose visible output is unchanged. SAFE because the
// dynamic children self-subscribe to their own data (Reactions→useRelations,
// Reply→useRoomEvent, ThreadSummaryCard→ThreadEvent) and the delivery dot
// self-subscribes (useDotColor→RoomEvent.Receipt) — so we only need to compare
// the row's own scalar/identity props, the PRESENCE of the dynamic node slots,
// and the parent-computed renderSig snapshot. The comparator errs toward re-rendering:
// any prop whose identity is unstable simply defeats the memo (no perf win) but
// never causes staleness. Returns true to SKIP the re-render.
//
// The mutable-event vectors (edit / redaction / decryption / local-echo)
// AND the children-only inputs (sender display name, urlPreview) are all
// folded into the parent-computed `renderSig` prop (see getMessageRenderSig),
// so they repaint correctly. `mediaAutoLoad` is mount-time (autoPlay /
// initial fetch), so it needs no signature term.
function areMessagePropsEqual(
  prev: ComponentProps<typeof MessageInner>,
  next: ComponentProps<typeof MessageInner>
): boolean {
  return (
    prev.room === next.room &&
    prev.mEvent === next.mEvent &&
    prev.relations === next.relations &&
    prev.collapse === next.collapse &&
    prev.railHidden === next.railHidden &&
    prev.highlight === next.highlight &&
    prev.edit === next.edit &&
    prev.canDelete === next.canDelete &&
    prev.canSendReaction === next.canSendReaction &&
    prev.canPinEvent === next.canPinEvent &&
    prev.imagePackRooms === next.imagePackRooms &&
    prev.hideReadReceipts === next.hideReadReceipts &&
    prev.showDeveloperTools === next.showDeveloperTools &&
    prev.memberPowerTag === next.memberPowerTag &&
    prev.accessibleTagColors === next.accessibleTagColors &&
    prev.legacyUsernameColor === next.legacyUsernameColor &&
    prev.streamRailStart === next.streamRailStart &&
    prev.streamRailEnd === next.streamRailEnd &&
    prev.hideThreadReplyAffordance === next.hideThreadReplyAffordance &&
    prev.hideMainReplyAffordance === next.hideMainReplyAffordance &&
    prev.msgType === next.msgType &&
    prev.layout === next.layout &&
    prev.channelHeaderInBubble === next.channelHeaderInBubble &&
    prev.className === next.className &&
    prev.onUserClick === next.onUserClick &&
    prev.onUsernameClick === next.onUsernameClick &&
    prev.onReplyClick === next.onReplyClick &&
    prev.onReactionToggle === next.onReactionToggle &&
    prev.onEditId === next.onEditId &&
    Boolean(prev.reply) === Boolean(next.reply) &&
    Boolean(prev.reactions) === Boolean(next.reactions) &&
    Boolean(prev.threadSummary) === Boolean(next.threadSummary) &&
    // `data-message-item` is the row's ABSOLUTE virtual-paginator index, queried
    // by scrollToItem(). It shifts for existing rows when older history
    // paginates in at the top, so a skipped render must not leave it stale —
    // live appends at the bottom don't move it, so the memo still fires there.
    (prev as Record<string, unknown>)['data-message-item'] ===
      (next as Record<string, unknown>)['data-message-item'] &&
    prev.renderSig === next.renderSig
  );
}

export const Message = memo(MessageInner, areMessagePropsEqual);

export type EventProps = {
  room: Room;
  mEvent: MatrixEvent;
  highlight: boolean;
  canDelete?: boolean;
  hideReadReceipts?: boolean;
  showDeveloperTools?: boolean;
};
export const Event = as<'div', EventProps>(
  (
    {
      className,
      room,
      mEvent,
      highlight,
      canDelete,
      hideReadReceipts,
      showDeveloperTools,
      children,
      ...props
    },
    ref
  ) => {
    const mx = useMatrixClient();
    const [hover, setHover] = useState(false);
    const { hoverProps } = useHover({ onHoverChange: setHover });
    const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
    const [menuAnchor, setMenuAnchor] = useState<RectCords>();
    const stateEvent = typeof mEvent.getStateKey() === 'string';

    const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
      if (evt.altKey || !window.getSelection()?.isCollapsed) return;
      const tag = (evt.target as Element).tagName;
      if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
      evt.preventDefault();
      setMenuAnchor({
        x: evt.clientX,
        y: evt.clientY,
        width: 0,
        height: 0,
      });
    };

    const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
      const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget;
      setMenuAnchor(target.getBoundingClientRect());
    };

    const closeMenu = () => {
      setMenuAnchor(undefined);
    };

    return (
      <MessageBase
        className={classNames(css.MessageBase, className)}
        tabIndex={0}
        space={STREAM_MESSAGE_SPACING}
        autoCollapse
        highlight={highlight}
        selected={!!menuAnchor}
        {...props}
        {...hoverProps}
        {...focusWithinProps}
        ref={ref}
      >
        {(hover || !!menuAnchor) && (
          <div className={css.MessageOptionsBase}>
            <Menu className={css.MessageOptionsBar} variant="SurfaceVariant">
              <Box gap="100">
                <PopOut
                  anchor={menuAnchor}
                  position="Bottom"
                  align={menuAnchor?.width === 0 ? 'Start' : 'End'}
                  offset={menuAnchor?.width === 0 ? 0 : undefined}
                  content={
                    <FocusTrap
                      focusTrapOptions={{
                        initialFocus: false,
                        onDeactivate: () => setMenuAnchor(undefined),
                        clickOutsideDeactivates: true,
                        isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
                        isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
                        escapeDeactivates: stopPropagation,
                      }}
                    >
                      <Menu {...props} ref={ref}>
                        <Box direction="Column" gap="100" className={css.MessageMenuGroup}>
                          {!hideReadReceipts && (
                            <MessageReadReceiptItem
                              room={room}
                              eventId={mEvent.getId() ?? ''}
                              onClose={closeMenu}
                            />
                          )}
                          {showDeveloperTools && (
                            <MessageSourceCodeItem
                              room={room}
                              mEvent={mEvent}
                              onClose={closeMenu}
                            />
                          )}
                          <MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
                        </Box>
                        {((!mEvent.isRedacted() && canDelete && !stateEvent) ||
                          (mEvent.getSender() !== mx.getUserId() && !stateEvent)) && (
                          <>
                            <Line size="300" />
                            <Box direction="Column" gap="100" className={css.MessageMenuGroup}>
                              {!mEvent.isRedacted() && canDelete && (
                                <MessageDeleteItem
                                  room={room}
                                  mEvent={mEvent}
                                  onClose={closeMenu}
                                />
                              )}
                              {mEvent.getSender() !== mx.getUserId() && (
                                <MessageReportItem
                                  room={room}
                                  mEvent={mEvent}
                                  onClose={closeMenu}
                                />
                              )}
                            </Box>
                          </>
                        )}
                      </Menu>
                    </FocusTrap>
                  }
                >
                  <IconButton
                    variant="SurfaceVariant"
                    size="300"
                    radii="300"
                    onClick={handleOpenMenu}
                    aria-pressed={!!menuAnchor}
                  >
                    <Icon src={Icons.VerticalDots} size="100" />
                  </IconButton>
                </PopOut>
              </Box>
            </Menu>
          </div>
        )}
        <div onContextMenu={handleContextMenu}>{children}</div>
      </MessageBase>
    );
  }
);
