import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAtomValue } from 'jotai'; import { Box, Button, Icon, Icons, Text, color, config, toRem } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useNavigate } from 'react-router-dom'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { factoryRoomIdByActivity } from '../../../utils/sort'; import { NavCategory, NavEmptyCenter, NavEmptyLayout } from '../../../components/nav'; import { getDirectCreatePath, getDirectRoomPath } from '../../pathUtils'; import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { VirtualTile } from '../../../components/virtualizer'; import { DmStreamRow } from '../../../features/room-nav'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; import { useDirectRooms } from './useDirectRooms'; import { PageNav, PageNavContent } from '../../../components/page'; import { getRoomNotificationMode, useRoomsNotificationPreferencesContext, } from '../../../hooks/useRoomsNotificationPreferences'; import { DirectStreamHeader } from './DirectStreamHeader'; import { DirectNewChatRow } from './DirectNewChatRow'; import { DirectSelfRow } from './DirectSelfRow'; const MONO_FONT = '"JetBrains Mono Variable", ui-monospace, monospace'; function DirectEmpty() { const { t } = useTranslation(); const navigate = useNavigate(); return ( } title={ {t('Direct.no_direct_messages')} } content={ {t('Direct.no_direct_messages_desc')} } options={ } /> ); } function DirectFooterStatus() { const { t } = useTranslation(); return ( vojo.chat {t('Direct.status_e2ee')} ); } export function Direct() { const mx = useMatrixClient(); useNavToActivePathMapper('direct'); const scrollRef = useRef(null); const directs = useDirectRooms(); const notificationPreferences = useRoomsNotificationPreferencesContext(); // roomToUnreadAtom only changes on read/unread transitions and ignores own // events — covers incoming notifying messages but not own sends or muted // incoming. Kept as a subscribe-only re-render trigger. useAtomValue(roomToUnreadAtom); // Activity tick: bump on every live timeline event in any DM room so list // order, preview text and time labels refresh on own sends, muted incoming, // reactions — anything roomToUnread misses. Uses the 'Room.timeline' // literal (= RoomEvent.Timeline) to avoid named imports from matrix-js-sdk // that the project's moduleResolution flags as TS2614 (see // docs/known-tech-debt-lint/). const [, setActivityTick] = useState(0); useEffect(() => { const directsSet = new Set(directs); const handleTimeline = ( _ev: unknown, room: { roomId: string } | undefined, _start: unknown, _removed: unknown, data: { liveEvent?: boolean } | undefined ) => { if (!room || !data?.liveEvent) return; if (!directsSet.has(room.roomId)) return; setActivityTick((n) => n + 1); }; const emitter = mx as unknown as { on: (e: string, h: typeof handleTimeline) => void; removeListener: (e: string, h: typeof handleTimeline) => void; }; emitter.on('Room.timeline', handleTimeline); return () => { emitter.removeListener('Room.timeline', handleTimeline); }; }, [mx, directs]); const selectedRoomId = useSelectedRoom(); const noRoomToDisplay = directs.length === 0; // Sort each render — small list, getLastActiveTimestamp changes outside // React's dep model so memoising would need a manual trigger anyway. const sortedDirects = Array.from(directs).sort(factoryRoomIdByActivity(mx)); const virtualizer = useVirtualizer({ count: sortedDirects.length, getScrollElement: () => scrollRef.current, estimateSize: () => 68, overscan: 10, }); return ( {noRoomToDisplay ? ( ) : (
{virtualizer.getVirtualItems().map((vItem) => { const roomId = sortedDirects[vItem.index]; const room = mx.getRoom(roomId); if (!room) return null; const selected = selectedRoomId === roomId; return ( ); })}
)}
); }