import React, { MouseEventHandler, forwardRef, useMemo, useState } from 'react'; import { useAtom, useAtomValue } from 'jotai'; import { Avatar, Box, Button, IconButton, Menu, MenuItem, PopOut, RectCords, Text, config, toRem } from 'folds'; import { Icon, Icons } from '../icons'; import { useVirtualizer } from '@tanstack/react-virtual'; import FocusTrap from 'focus-trap-react'; import { useNavigate } from 'react-router-dom'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { factoryRoomIdByActivity } from '../../utils/sort'; import { NavButton, NavCategory, NavCategoryHeader, NavEmptyCenter, NavEmptyLayout, NavItem, NavItemContent, } from '../nav'; import { getDirectCreatePath, getDirectRoomPath } from '../../pages/pathUtils'; import { getCanonicalAliasOrRoomId } from '../../utils/matrix'; import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; import { VirtualTile } from '../virtualizer'; import { RoomNavCategoryButton, RoomNavItem } from '../../features/room-nav'; import { makeNavCategoryId } from '../../state/closedNavCategories'; import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { useCategoryHandler } from '../../hooks/useCategoryHandler'; import { useDirectRooms } from '../../pages/client/direct/useDirectRooms'; import { useClosedNavCategoriesAtom } from '../../state/hooks/closedNavCategories'; import { useRoomsUnread } from '../../state/hooks/unread'; import { useMarkRoomsAsRead } from '../../hooks/useMarkAsRead'; import { stopPropagation } from '../../utils/keyboard'; import { PluginButtonSlot } from '../../features/settings/plugins/PluginButtonSlot'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { getRoomNotificationMode, useRoomsNotificationPreferencesContext, } from '../../hooks/useRoomsNotificationPreferences'; import { useDirectCreateSelected } from '../../hooks/router/useDirectSelected'; import { useRoomListReorder, useListReorderAnimation } from '../../hooks/useRoomListReorder'; /** * Props for the DirectMenu component */ type DirectMenuProps = { requestClose: () => void; }; /** * Menu component for Direct Messages category actions */ const DirectMenu = forwardRef(({ requestClose }, ref) => { const mx = useMatrixClient(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const orphanRooms = useDirectRooms(); const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom); const markRoomsAsRead = useMarkRoomsAsRead(mx); const handleMarkAsRead = () => { if (!unread) return; markRoomsAsRead(orphanRooms, hideActivity); requestClose(); }; return ( } radii="300" aria-disabled={!unread} > Mark as Read ); }); /** * Props for the DirectListHeader component */ type DirectListHeaderProps = { title?: string; }; /** * Header component for the Direct Messages list with menu */ export function DirectListHeader({ title = 'Direct Messages' }: DirectListHeaderProps) { const [menuAnchor, setMenuAnchor] = useState(); const handleOpenMenu: MouseEventHandler = (evt) => { const cords = evt.currentTarget.getBoundingClientRect(); setMenuAnchor((currentState) => { if (currentState) return undefined; return cords; }); }; return ( <> {title} setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > setMenuAnchor(undefined)} /> } /> ); } /** * Empty state component when there are no direct messages */ export function DirectListEmpty() { const navigate = useNavigate(); return ( } title={ No Direct Messages } content={ You do not have any direct messages yet. } options={ } /> ); } const DEFAULT_CATEGORY_ID = makeNavCategoryId('direct', 'direct'); /** * Props for the DirectList component */ export type DirectListProps = { scrollRef: React.RefObject; showCreateButton?: boolean; showCategoryHeader?: boolean; }; /** * Reusable component that renders the virtualized list of direct message rooms. * Can be embedded in any PageNavContent context. */ export function DirectList({ scrollRef, showCreateButton = true, showCategoryHeader = true, }: DirectListProps) { const mx = useMatrixClient(); const directs = useDirectRooms(); const notificationPreferences = useRoomsNotificationPreferencesContext(); const roomToUnread = useAtomValue(roomToUnreadAtom); const navigate = useNavigate(); const createDirectSelected = useDirectCreateSelected(); const selectedRoomId = useSelectedRoom(); const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom()); const reorderTrigger = useRoomListReorder(mx, directs); const sortedDirects = useMemo(() => { const items = Array.from(directs).sort(factoryRoomIdByActivity(mx)); if (closedCategories.has(DEFAULT_CATEGORY_ID)) { return items.filter((rId) => roomToUnread.has(rId) || rId === selectedRoomId); } return items; }, [mx, directs, closedCategories, roomToUnread, selectedRoomId, reorderTrigger]); const animationRef = useListReorderAnimation(sortedDirects); const virtualizer = useVirtualizer({ count: sortedDirects.length, getScrollElement: () => scrollRef.current, estimateSize: () => 38, overscan: 10, }); const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => closedCategories.has(categoryId) ); const noRoomToDisplay = directs.length === 0; if (noRoomToDisplay) { return ; } return ( {showCreateButton && ( navigate(getDirectCreatePath())}> Create Chat )} {showCategoryHeader && ( Chats )}
{virtualizer.getVirtualItems().map((vItem) => { const roomId = sortedDirects[vItem.index]; const room = mx.getRoom(roomId); if (!room) return null; const selected = selectedRoomId === roomId; return ( ); })}
); }