import React, {
  MouseEventHandler,
  forwardRef,
  useCallback,
  useMemo,
  useRef,
  useState,
} from 'react';
import { MatrixClient, Room } from 'matrix-js-sdk';
import {
  Avatar,
  Box,
  Button,
  Dialog,
  Header,
  Icon,
  IconButton,
  Icons,
  Input,
  Text,
  Menu,
  MenuItem,
  Overlay,
  OverlayBackdrop,
  OverlayCenter,
  config,
  PopOut,
  toRem,
  Line,
  RectCords,
  Badge,
  Spinner,
} from 'folds';
import { useFocusWithin, useHover } from 'react-aria';
import FocusTrap from 'focus-trap-react';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { selectAtom } from 'jotai/utils';
import dayjs from 'dayjs';
import isToday from 'dayjs/plugin/isToday';
import isYesterday from 'dayjs/plugin/isYesterday';
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { getDirectRoomAvatarUrl, getRoomAvatarUrl, getStateEvent } from '../../utils/room';
import { setAccountData } from '../../utils/accountData';
import { nameInitials } from '../../utils/common';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { markedUnreadAtom, setMarkedUnread } from '../../state/room/markedUnread';
import { roomIdToMsgDraftAtomFamily } from '../../state/room/roomInputDrafts';
import { hasMsgDraft } from '../../utils/draft';
import { DraftDot } from '../room/DraftIndicator.css';
import { getPowersLevelFromMatrixEvent, usePowerLevels } from '../../hooks/usePowerLevels';
import { markAsRead } from '../../utils/notifications';
import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { TypingIndicator } from '../../components/typing-indicator';
import { stopPropagation } from '../../utils/keyboard';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { useSpaceOptionally } from '../../hooks/useSpace';
import {
  getRoomNotificationModeIcon,
  RoomNotificationMode,
} from '../../hooks/useRoomsNotificationPreferences';
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
import { getRoomCreatorsForRoomId, useRoomCreators } from '../../hooks/useRoomCreators';
import { getRoomPermissionsAPI, useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt';
import {
  LOCAL_ROOM_NAMES_KEY,
  getLocalRoomNamesContent,
  useHasLocalRoomName,
  useLocalRoomName,
} from '../../hooks/useRoomMeta';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
import { callChatAtom } from '../../state/callEmbed';
import { createErrorToast, toastQueueAtom } from '../../state/toast';
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
import { livekitSupport } from '../../hooks/useLivekitSupport';
import { MessageEvent, StateEvent } from '../../../types/matrix/room';
import { webRTCSupported } from '../../utils/rtc';
import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
import { EmojiBoard } from '../../components/emoji-board';

dayjs.extend(isToday);
dayjs.extend(isYesterday);

const PREVIEW_MAX_CHARS = 48;

function formatDmTimestamp(ts: number): string {
  const d = dayjs(ts);
  const now = dayjs();
  const diffMinutes = now.diff(d, 'minute');
  if (diffMinutes < 60) {
    return `${diffMinutes < 1 ? 0 : diffMinutes}m`;
  }
  const diffHours = now.diff(d, 'hour');
  if (diffHours < 24) {
    return `${diffHours}h`;
  }
  if (d.isYesterday()) {
    return 'Yesterday';
  }
  return d.format('D MMM');
}

type RenameRoomDialogProps = {
  room: Room;
  onClose: () => void;
};
function RenameRoomDialog({ room, onClose }: RenameRoomDialogProps) {
  const mx = useMatrixClient();
  const inputRef = useRef<HTMLInputElement>(null);
  const [emojiAnchor, setEmojiAnchor] = useState<RectCords>();

  const handleEmojiSelect = useCallback((unicode: string) => {
    if (inputRef.current) {
      inputRef.current.value = unicode + inputRef.current.value;
      inputRef.current.focus();
    }
    setEmojiAnchor(undefined);
  }, []);

  const getCurrentLocalName = useCallback((): string => {
    const content = getLocalRoomNamesContent(mx);
    return content.rooms[room.roomId] ?? '';
  }, [mx, room.roomId]);

  const handleSave = useCallback(() => {
    const newName = inputRef.current?.value.trim() ?? '';
    if (newName.length > 255) return;
    const existing = getLocalRoomNamesContent(mx);
    if (newName === '') {
      const { [room.roomId]: _removed, ...rest } = existing.rooms;
      setAccountData(mx, LOCAL_ROOM_NAMES_KEY, { rooms: rest });
    } else {
      setAccountData(mx, LOCAL_ROOM_NAMES_KEY, {
        rooms: { ...existing.rooms, [room.roomId]: newName },
      });
    }
    onClose();
  }, [mx, room.roomId, onClose]);

  const handleClear = useCallback(() => {
    const existing = getLocalRoomNamesContent(mx);
    const { [room.roomId]: _removed, ...rest } = existing.rooms;
    setAccountData(mx, LOCAL_ROOM_NAMES_KEY, { rooms: rest });
    onClose();
  }, [mx, room.roomId, onClose]);

  const handleKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => {
    if (evt.key === 'Enter') handleSave();
    if (evt.key === 'Escape') onClose();
  };

  return (
    <Overlay open backdrop={<OverlayBackdrop />}>
      <OverlayCenter>
        <FocusTrap
          focusTrapOptions={{
            initialFocus: false,
            onDeactivate: onClose,
            clickOutsideDeactivates: true,
            escapeDeactivates: stopPropagation,
          }}
        >
          <Dialog variant="Surface" aria-labelledby="rename-room-dialog-title">
            <Header
              style={{
                padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
                borderBottomWidth: config.borderWidth.B300,
              }}
              variant="Surface"
              size="500"
            >
              <Box grow="Yes">
                <Text as="h2" size="H4" id="rename-room-dialog-title">
                  Rename room (for you only)
                </Text>
              </Box>
              <IconButton size="300" onClick={onClose} radii="300" aria-label="Cancel">
                <Icon src={Icons.Cross} />
              </IconButton>
            </Header>
            <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
              <Box direction="Column" gap="200">
                <Text size="L400">Custom name</Text>
                <Box direction="Row" gap="100" alignItems="Center">
                  <PopOut
                    anchor={emojiAnchor}
                    position="Top"
                    align="End"
                    content={
                      <EmojiBoard
                        imagePackRooms={[]}
                        returnFocusOnDeactivate={false}
                        onEmojiSelect={handleEmojiSelect}
                        requestClose={() => setEmojiAnchor(undefined)}
                      />
                    }
                  >
                    <IconButton
                      type="button"
                      size="400"
                      radii="400"
                      variant="Surface"
                      fill="Soft"
                      outlined
                      aria-label="Insert emoji"
                      aria-expanded={!!emojiAnchor}
                      aria-haspopup="dialog"
                      onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
                        const rect = evt.currentTarget.getBoundingClientRect();
                        setEmojiAnchor((prev) => (prev ? undefined : rect));
                      }}
                    >
                      <Icon src={Icons.Smile} size="400" />
                    </IconButton>
                  </PopOut>
                  <Box grow="Yes">
                    <Input
                      ref={inputRef}
                      defaultValue={getCurrentLocalName()}
                      placeholder={room.name}
                      variant="Secondary"
                      radii="300"
                      maxLength={255}
                      onKeyDown={handleKeyDown}
                      autoFocus
                      style={{ width: '100%' }}
                    />
                  </Box>
                </Box>
                <Text size="T300" priority="300">
                  Only visible to you. Leave blank to use the original name.
                </Text>
              </Box>
              <Box gap="300">
                <Box grow="Yes">
                  <Button
                    onClick={handleClear}
                    variant="Secondary"
                    fill="Soft"
                    radii="300"
                    style={{ width: '100%' }}
                  >
                    <Text size="B400">Clear</Text>
                  </Button>
                </Box>
                <Box grow="Yes">
                  <Button
                    onClick={handleSave}
                    variant="Primary"
                    radii="300"
                    style={{ width: '100%' }}
                  >
                    <Text size="B400">Save</Text>
                  </Button>
                </Box>
              </Box>
            </Box>
          </Dialog>
        </FocusTrap>
      </OverlayCenter>
    </Overlay>
  );
}

// localStorage key for timed mute timers
export const MUTE_TIMERS_KEY = 'io.lotus.mute_timers';

// setTimeout's delay is a signed 32-bit int; larger values overflow and fire
// immediately. Clamp long delays to this max (~24.8 days).
export const MAX_MUTE_TIMEOUT_MS = 2_147_483_647;

export type MuteTimerEntry = { roomId: string; unmuteAt: number };

export function loadMuteTimers(): MuteTimerEntry[] {
  try {
    const parsed = JSON.parse(localStorage.getItem(MUTE_TIMERS_KEY) ?? '[]');
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}

export function saveMuteTimers(timers: MuteTimerEntry[]): void {
  localStorage.setItem(MUTE_TIMERS_KEY, JSON.stringify(timers));
}

// Reverse a timed mute: restore the room's notification mode to Unset and drop
// its persisted timer. Shared by the in-session timer and the boot-time restore.
export async function unmuteRoom(mx: MatrixClient, roomId: string): Promise<void> {
  const { setRoomNotificationPreference } =
    await import('../../hooks/useRoomsNotificationPreferences');
  await setRoomNotificationPreference(
    mx,
    roomId,
    RoomNotificationMode.Unset,
    RoomNotificationMode.Mute,
  ).catch(() => {});
  saveMuteTimers(loadMuteTimers().filter((e) => e.roomId !== roomId));
}

function scheduleMuteTimer(roomId: string, durationMs: number, onUnmute: () => void): void {
  const unmuteAt = Date.now() + durationMs;
  const existing = loadMuteTimers().filter((e) => e.roomId !== roomId);
  saveMuteTimers([...existing, { roomId, unmuteAt }]);
  setTimeout(onUnmute, Math.min(durationMs, MAX_MUTE_TIMEOUT_MS));
}

type RoomNavItemMenuProps = {
  room: Room;
  requestClose: () => void;
  onRenameClick: () => void;
  notificationMode?: RoomNotificationMode;
};
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
  ({ room, requestClose, onRenameClick, notificationMode }, ref) => {
    const mx = useMatrixClient();
    const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
    const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
    const powerLevels = usePowerLevels(room);
    const creators = useRoomCreators(room);

    const permissions = useRoomPermissions(creators, powerLevels);
    const canInvite = permissions.action('invite', mx.getSafeUserId());
    const openRoomSettings = useOpenRoomSettings();
    const space = useSpaceOptionally();
    const setToast = useSetAtom(toastQueueAtom);

    const [invitePrompt, setInvitePrompt] = useState(false);
    const [muteMenuAnchor, setMuteMenuAnchor] = useState<RectCords>();
    const isServerNotice = room.getType() === 'm.server_notice';

    const isFavorite = !!room.tags?.['m.favourite'];
    const isLowPriority = !!room.tags?.['m.lowpriority'];

    // Surface a room-tag write failure instead of letting the toggle fail silently.
    const notifyTagFailure = (err: unknown) => {
      console.error('Failed to update room tag:', err);
      setToast(
        createErrorToast('Could not update this room. Please try again.', Icons.Warning, 'Failed'),
      );
    };

    const handleToggleFavorite = () => {
      if (isFavorite) {
        mx.deleteRoomTag(room.roomId, 'm.favourite').catch(notifyTagFailure);
      } else {
        // Favourite and low-priority are mutually exclusive. Batch both writes so a
        // failure surfaces a single toast, not one per operation.
        const ops: Promise<unknown>[] = [mx.setRoomTag(room.roomId, 'm.favourite', { order: 0.5 })];
        if (isLowPriority) ops.push(mx.deleteRoomTag(room.roomId, 'm.lowpriority'));
        Promise.all(ops).catch(notifyTagFailure);
      }
      requestClose();
    };

    const handleToggleLowPriority = () => {
      if (isLowPriority) {
        mx.deleteRoomTag(room.roomId, 'm.lowpriority').catch(notifyTagFailure);
      } else {
        const ops: Promise<unknown>[] = [
          mx.setRoomTag(room.roomId, 'm.lowpriority', { order: 0.5 }),
        ];
        if (isFavorite) ops.push(mx.deleteRoomTag(room.roomId, 'm.favourite'));
        Promise.all(ops).catch(notifyTagFailure);
      }
      requestClose();
    };

    const markedUnread = useAtomValue(markedUnreadAtom).has(room.roomId);

    const handleMarkAsRead = () => {
      markAsRead(mx, room.roomId, hideActivity);
      if (markedUnread) setMarkedUnread(mx, room.roomId, false).catch(() => undefined);
      requestClose();
    };

    const handleMarkAsUnread = () => {
      setMarkedUnread(mx, room.roomId, true).catch(() => undefined);
      requestClose();
    };

    const handleMuteFor = useCallback(
      async (durationMs: number | null) => {
        const { setRoomNotificationPreference } =
          await import('../../hooks/useRoomsNotificationPreferences');
        const prevMode = notificationMode ?? RoomNotificationMode.Unset;
        await setRoomNotificationPreference(
          mx,
          room.roomId,
          RoomNotificationMode.Mute,
          prevMode,
        ).catch(() => {});
        if (durationMs !== null) {
          scheduleMuteTimer(room.roomId, durationMs, () => {
            unmuteRoom(mx, room.roomId);
          });
        }
        requestClose();
      },
      [mx, room.roomId, notificationMode, requestClose],
    );

    const handleInvite = () => {
      setInvitePrompt(true);
    };

    const handleRoomSettings = () => {
      openRoomSettings(room.roomId, space?.roomId);
      requestClose();
    };

    const isMuted = notificationMode === RoomNotificationMode.Mute;

    return (
      <Menu ref={ref} style={{ maxWidth: toRem(180), width: '100vw' }}>
        {invitePrompt && room && (
          <InviteUserPrompt
            room={room}
            requestClose={() => {
              setInvitePrompt(false);
              requestClose();
            }}
          />
        )}
        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
          <MenuItem
            onClick={handleMarkAsRead}
            size="300"
            after={<Icon size="100" src={Icons.CheckTwice} />}
            radii="300"
            disabled={!unread && !markedUnread}
          >
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
              Mark as Read
            </Text>
          </MenuItem>
          <MenuItem
            onClick={handleMarkAsUnread}
            size="300"
            after={<Icon size="100" src={Icons.MessageUnread} />}
            radii="300"
            disabled={!!unread || markedUnread}
          >
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
              Mark as Unread
            </Text>
          </MenuItem>
          <RoomNotificationModeSwitcher roomId={room.roomId} value={notificationMode}>
            {(handleOpen, opened, changing) => (
              <MenuItem
                size="300"
                after={
                  changing ? (
                    <Spinner size="100" variant="Secondary" />
                  ) : (
                    <Icon size="100" src={getRoomNotificationModeIcon(notificationMode)} />
                  )
                }
                radii="300"
                aria-pressed={opened}
                onClick={handleOpen}
              >
                <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
                  Notifications
                </Text>
              </MenuItem>
            )}
          </RoomNotificationModeSwitcher>
        </Box>
        {!isMuted && (
          <>
            <Line variant="Surface" size="300" />
            <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
              <PopOut
                anchor={muteMenuAnchor}
                position="Right"
                align="Start"
                offset={4}
                content={
                  <FocusTrap
                    focusTrapOptions={{
                      initialFocus: false,
                      onDeactivate: () => setMuteMenuAnchor(undefined),
                      clickOutsideDeactivates: true,
                      escapeDeactivates: stopPropagation,
                    }}
                  >
                    <Menu style={{ maxWidth: toRem(160), width: '100vw' }}>
                      <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
                        {[
                          { label: '15 minutes', ms: 15 * 60 * 1000 },
                          { label: '1 hour', ms: 60 * 60 * 1000 },
                          { label: '8 hours', ms: 8 * 60 * 60 * 1000 },
                          { label: '24 hours', ms: 24 * 60 * 60 * 1000 },
                          { label: 'Indefinitely', ms: null },
                        ].map(({ label, ms }) => (
                          <MenuItem
                            key={label}
                            size="300"
                            radii="300"
                            onClick={() => handleMuteFor(ms)}
                          >
                            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
                              {label}
                            </Text>
                          </MenuItem>
                        ))}
                      </Box>
                    </Menu>
                  </FocusTrap>
                }
              >
                <MenuItem
                  size="300"
                  before={<Icon size="100" src={Icons.BellMute} />}
                  after={<Icon size="100" src={Icons.ChevronRight} />}
                  radii="300"
                  aria-pressed={!!muteMenuAnchor}
                  onClick={(e) => setMuteMenuAnchor(e.currentTarget.getBoundingClientRect())}
                >
                  <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
                    Mute
                  </Text>
                </MenuItem>
              </PopOut>
            </Box>
          </>
        )}
        <Line variant="Surface" size="300" />
        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
          <MenuItem
            onClick={handleToggleFavorite}
            size="300"
            after={<Icon size="100" src={Icons.Star} filled={isFavorite} />}
            radii="300"
            aria-pressed={isFavorite}
          >
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
              {isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}
            </Text>
          </MenuItem>
          <MenuItem
            onClick={handleToggleLowPriority}
            size="300"
            after={<Icon size="100" src={Icons.ChevronBottom} />}
            radii="300"
            aria-pressed={isLowPriority}
          >
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
              {isLowPriority ? 'Remove from Low Priority' : 'Add to Low Priority'}
            </Text>
          </MenuItem>
          <MenuItem
            onClick={handleInvite}
            variant="Primary"
            fill="None"
            size="300"
            after={<Icon size="100" src={Icons.UserPlus} />}
            radii="300"
            aria-pressed={invitePrompt}
            disabled={!canInvite}
          >
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
              Invite
            </Text>
          </MenuItem>
          <MenuItem
            onClick={() => {
              requestClose();
              onRenameClick();
            }}
            size="300"
            after={<Icon size="100" src={Icons.Pencil} />}
            radii="300"
          >
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
              Rename for me…
            </Text>
          </MenuItem>
          {!isServerNotice && (
            <MenuItem
              onClick={handleRoomSettings}
              size="300"
              after={<Icon size="100" src={Icons.Setting} />}
              radii="300"
            >
              <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
                Room Settings
              </Text>
            </MenuItem>
          )}
        </Box>
        <Line variant="Surface" size="300" />
        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
          <UseStateProvider initial={false}>
            {(promptLeave, setPromptLeave) => (
              <>
                <MenuItem
                  onClick={() => setPromptLeave(true)}
                  variant="Critical"
                  fill="None"
                  size="300"
                  after={<Icon size="100" src={Icons.ArrowGoLeft} />}
                  radii="300"
                  aria-pressed={promptLeave}
                >
                  <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
                    Leave Room
                  </Text>
                </MenuItem>
                {promptLeave && (
                  <LeaveRoomPrompt
                    roomId={room.roomId}
                    onDone={requestClose}
                    onCancel={() => setPromptLeave(false)}
                  />
                )}
              </>
            )}
          </UseStateProvider>
        </Box>
      </Menu>
    );
  },
);

function CallChatToggle() {
  const [chat, setChat] = useAtom(callChatAtom);

  return (
    <IconButton
      onClick={() => setChat(!chat)}
      aria-pressed={chat}
      aria-label="Toggle Chat"
      variant="Background"
      fill="None"
      size="300"
      radii="300"
    >
      <Icon size="50" src={Icons.Message} filled={chat} />
    </IconButton>
  );
}

type RoomNavItemProps = {
  room: Room;
  selected: boolean;
  linkPath: string;
  notificationMode?: RoomNotificationMode;
  showAvatar?: boolean;
  direct?: boolean;
};
function RoomNavItem_({
  room,
  selected,
  showAvatar,
  direct,
  notificationMode,
  linkPath,
}: RoomNavItemProps) {
  const isFavorite = !!room.tags?.['m.favourite'];
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();
  const [hover, setHover] = useState(false);
  const { hoverProps } = useHover({ onHoverChange: setHover });
  const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
  const [menuAnchor, setMenuAnchor] = useState<RectCords>();
  const [renameDialog, setRenameDialog] = useState(false);
  const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
  // MSC2867: an explicit "mark as unread" lights the row even with no unread
  // count. `hasUnread` drives the bold name / icon emphasis below.
  const markedUnread = useAtomValue(markedUnreadAtom).has(room.roomId);
  const hasUnread = !!unread || markedUnread;
  const typingMember = useRoomTypingMember(room.roomId).filter(
    (receipt) => receipt.userId !== mx.getUserId(),
  );

  const roomName = useLocalRoomName(room);
  const hasLocalName = useHasLocalRoomName(room.roomId);

  // Whether this room has an unsent message draft. selectAtom maps to a boolean
  // so the row only re-renders when that flips (the draft atom itself is written
  // on room-leave, not per keystroke).
  const hasDraftAtom = useMemo(
    () => selectAtom(roomIdToMsgDraftAtomFamily(room.roomId), hasMsgDraft),
    [room.roomId],
  );
  const hasDraft = useAtomValue(hasDraftAtom);

  // Only DM rows render this preview — pass `direct` so non-DM nav items don't
  // register the global decryption listener (PERF-5).
  const latestEvent = useRoomLatestRenderedEvent(room, !!direct);
  const dmPreview = (() => {
    if (!direct || !latestEvent) return null;
    const type = latestEvent.getType();
    const ts = latestEvent.getTs();
    if (!ts) return null;
    // Skip pure membership events
    if (type === StateEvent.RoomMember) return null;
    let body: string;
    if (latestEvent.isDecryptionFailure()) {
      body = 'Encrypted message';
    } else if (type === MessageEvent.Sticker) {
      body = 'Sticker';
    } else {
      const rawBody: unknown = latestEvent.getContent()?.body;
      body = typeof rawBody === 'string' ? rawBody.replace(/\s+/g, ' ').trim() : '';
    }
    if (!body) return null;
    const preview = body.length > PREVIEW_MAX_CHARS ? `${body.slice(0, PREVIEW_MAX_CHARS)}…` : body;
    return { preview, time: formatDmTimestamp(ts) };
  })();

  const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
    evt.preventDefault();
    setMenuAnchor({
      x: evt.clientX,
      y: evt.clientY,
      width: 0,
      height: 0,
    });
  };

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

  const optionsVisible = hover || !!menuAnchor;
  const callSession = useCallSession(room);
  const callMembers = useCallMembers(callSession);
  const startCall = useCallStart(direct);
  const callEmbed = useCallEmbed();
  const callPref = useAtomValue(useCallPreferencesAtom());
  const autoDiscoveryInfo = useAutoDiscoveryInfo();

  const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
    const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
    const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
    const creators = getRoomCreatorsForRoomId(mx, room.roomId);
    const permissions = getRoomPermissionsAPI(creators, powerLevels);

    const hasCallPermission = permissions.stateEvent(
      StateEvent.GroupCallMemberPrefix,
      mx.getSafeUserId(),
    );

    // Do not join if missing permissions or no livekit support or no webRTC support
    if (!hasCallPermission || !livekitSupport(autoDiscoveryInfo) || !webRTCSupported()) {
      return;
    }

    // Do not join if already in call
    if (callEmbed) {
      return;
    }
    // Start call in second click
    if (selected) {
      evt.preventDefault();
      startCall(room, callPref);
    }
  };

  return (
    <NavItem
      variant="Background"
      radii="400"
      highlight={hasUnread}
      aria-selected={selected}
      data-hover={!!menuAnchor}
      onContextMenu={handleContextMenu}
      {...hoverProps}
      {...focusWithinProps}
    >
      <NavLink to={linkPath} onClick={room.isCallRoom() ? handleStartCall : undefined}>
        <NavItemContent>
          <Box as="span" grow="Yes" alignItems="Center" gap="200">
            <Avatar size="200" radii="400">
              {showAvatar ? (
                <RoomAvatar
                  roomId={room.roomId}
                  src={
                    direct
                      ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
                      : getRoomAvatarUrl(mx, room, 96, useAuthentication)
                  }
                  alt={roomName}
                  renderFallback={() => (
                    <Text as="span" size="H6">
                      {nameInitials(roomName)}
                    </Text>
                  )}
                />
              ) : (
                <RoomIcon
                  style={{
                    opacity: hasUnread ? config.opacity.P500 : config.opacity.P300,
                  }}
                  filled={selected}
                  size="100"
                  joinRule={room.getJoinRule()}
                  roomType={room.getType()}
                />
              )}
            </Avatar>
            <Box as="span" direction="Column" grow="Yes" style={{ minWidth: 0 }}>
              <Box as="span" grow="Yes" alignItems="Center" gap="100">
                <Text priority={hasUnread ? '500' : '300'} as="span" size="Inherit" truncate>
                  {roomName}
                </Text>
                {hasLocalName && (
                  <Icon
                    size="50"
                    src={Icons.Pencil}
                    aria-label="Custom local name"
                    style={{ opacity: config.opacity.P300, flexShrink: 0 }}
                  />
                )}
                {isFavorite && (
                  <Icon
                    size="50"
                    src={Icons.Star}
                    filled
                    aria-label="Favorited"
                    style={{ opacity: config.opacity.P300, flexShrink: 0 }}
                  />
                )}
                {hasDraft && !selected && (
                  <span className={DraftDot} role="img" aria-label="Unsent draft" />
                )}
              </Box>
              {dmPreview && (
                <Box as="span" alignItems="Center" gap="100" style={{ minWidth: 0 }}>
                  <Text
                    as="span"
                    size="T200"
                    truncate
                    style={{ opacity: config.opacity.P300, flexGrow: 1 }}
                  >
                    {dmPreview.preview}
                  </Text>
                  <Text
                    as="span"
                    size="T200"
                    style={{ opacity: config.opacity.P300, flexShrink: 0, whiteSpace: 'nowrap' }}
                  >
                    {dmPreview.time}
                  </Text>
                </Box>
              )}
            </Box>
            {!optionsVisible && !hasUnread && !selected && typingMember.length > 0 && (
              <Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
                <TypingIndicator size="300" disableAnimation />
              </Badge>
            )}
            {!optionsVisible && unread && (
              <UnreadBadgeCenter>
                <UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
              </UnreadBadgeCenter>
            )}
            {!optionsVisible && !unread && markedUnread && (
              <UnreadBadgeCenter>
                <UnreadBadge highlight={false} count={0} />
              </UnreadBadgeCenter>
            )}
            {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && (
              <Icon
                size="50"
                src={getRoomNotificationModeIcon(notificationMode)}
                aria-label={notificationMode}
              />
            )}
            {callMembers.length > 0 && (
              <Badge variant="Critical" fill="Solid" size="400">
                <Text as="span" size="L400" truncate>
                  {callMembers.length} Live
                </Text>
              </Badge>
            )}
          </Box>
        </NavItemContent>
      </NavLink>
      {optionsVisible && (
        <NavItemOptions>
          {selected && (callEmbed?.roomId === room.roomId || room.isCallRoom()) && (
            <CallChatToggle />
          )}
          <PopOut
            id={`menu-${room.roomId}`}
            aria-expanded={!!menuAnchor}
            anchor={menuAnchor}
            offset={menuAnchor?.width === 0 ? 0 : undefined}
            alignOffset={menuAnchor?.width === 0 ? 0 : -5}
            position="Bottom"
            align={menuAnchor?.width === 0 ? 'Start' : 'End'}
            content={
              <FocusTrap
                focusTrapOptions={{
                  initialFocus: false,
                  returnFocusOnDeactivate: false,
                  onDeactivate: () => setMenuAnchor(undefined),
                  clickOutsideDeactivates: true,
                  isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
                  isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
                  escapeDeactivates: stopPropagation,
                }}
              >
                <RoomNavItemMenu
                  room={room}
                  requestClose={() => setMenuAnchor(undefined)}
                  onRenameClick={() => setRenameDialog(true)}
                  notificationMode={notificationMode}
                />
              </FocusTrap>
            }
          >
            <IconButton
              onClick={handleOpenMenu}
              aria-pressed={!!menuAnchor}
              aria-controls={`menu-${room.roomId}`}
              aria-label="More Options"
              variant="Background"
              fill="None"
              size="300"
              radii="300"
            >
              <Icon size="50" src={Icons.VerticalDots} />
            </IconButton>
          </PopOut>
        </NavItemOptions>
      )}
      {renameDialog && <RenameRoomDialog room={room} onClose={() => setRenameDialog(false)} />}
    </NavItem>
  );
}
export const RoomNavItem = React.memo(RoomNavItem_);
