import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Avatar, Box, Button, Icon, IconButton, Icons, Menu, MenuItem, PopOut, RectCords, Text, config, toRem, } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useAtom, useAtomValue } from 'jotai'; import FocusTrap from 'focus-trap-react'; import { Room } from 'matrix-js-sdk'; import { factoryRoomIdByActivity, factoryRoomIdByAtoZ } from '../../../utils/sort'; import { NavButton, NavCategory, NavCategoryHeader, NavEmptyCenter, NavEmptyLayout, NavItem, NavItemContent, NavLink, } from '../../../components/nav'; import { encodeSearchParamValueArray, getExplorePath, getHomeCreatePath, getHomeRoomPath, getHomeSearchPath, withSearchParam, } from '../../pathUtils'; import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { useHomeCreateSelected, useHomeSearchSelected, } from '../../../hooks/router/useHomeSelected'; import { useHomeRooms } from './useHomeRooms'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { VirtualTile } from '../../../components/virtualizer'; import { RoomNavCategoryButton, RoomNavItem, UnjoinedSubRoomItem } from '../../../features/room-nav'; import { makeNavCategoryId } from '../../../state/closedNavCategories'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { useCategoryHandler } from '../../../hooks/useCategoryHandler'; import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page'; import { useRoomsUnread } from '../../../state/hooks/unread'; import { useMarkRoomsAsRead } from '../../../hooks/useMarkAsRead'; import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories'; import { stopPropagation } from '../../../utils/keyboard'; import { PluginNavSlot } from '../../../features/settings/plugins/PluginNavSlot'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { getRoomNotificationMode, useRoomsNotificationPreferencesContext, } from '../../../hooks/useRoomsNotificationPreferences'; import { UseStateProvider } from '../../../components/UseStateProvider'; import { JoinAddressPrompt } from '../../../components/join-address-prompt'; import { ManageRoomsPrompt } from '../../../components/manage-rooms-prompt'; import { _RoomSearchParams } from '../../paths'; import { StateEvent } from '../../../../types/matrix/room'; type HomeMenuProps = { requestClose: () => void; onManageRooms: () => void; }; const HomeMenu = forwardRef( ({ requestClose, onManageRooms }, ref) => { const orphanRooms = useHomeRooms(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom); const mx = useMatrixClient(); const markRoomsAsRead = useMarkRoomsAsRead(mx); const handleMarkAsRead = () => { if (!unread) return; markRoomsAsRead(orphanRooms, hideActivity); requestClose(); }; const handleManageRooms = () => { requestClose(); onManageRooms(); }; return ( } radii="300" aria-disabled={!unread} > Mark as Read } radii="300" aria-disabled={orphanRooms.length === 0} > Manage Rooms ); } ); function HomeHeader() { const [menuAnchor, setMenuAnchor] = useState(); const [showManageRooms, setShowManageRooms] = useState(false); const mx = useMatrixClient(); const orphanRooms = useHomeRooms(); const rooms = useMemo(() => { return orphanRooms .map((roomId) => mx.getRoom(roomId)) .filter((room): room is Room => room !== null); }, [mx, orphanRooms]); const handleOpenMenu: MouseEventHandler = (evt) => { const cords = evt.currentTarget.getBoundingClientRect(); setMenuAnchor((currentState) => { if (currentState) return undefined; return cords; }); }; return ( <> Home setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > setMenuAnchor(undefined)} onManageRooms={() => setShowManageRooms(true)} /> } /> {showManageRooms && ( setShowManageRooms(false)} onCancel={() => setShowManageRooms(false)} /> )} ); } function HomeEmpty() { const navigate = useNavigate(); return ( } title={ No Rooms } content={ You do not have any rooms yet. } options={ <> } /> ); } type RoomListItem = | { type: 'room'; roomId: string; room: Room; depth: number; parentId?: string; isLast?: boolean } | { type: 'unjoined-subroom'; roomId: string; depth: number; parentId: string; isLast?: boolean }; const DEFAULT_CATEGORY_ID = makeNavCategoryId('home', 'room'); export function Home() { const mx = useMatrixClient(); useNavToActivePathMapper('home'); const scrollRef = useRef(null); const rooms = useHomeRooms(); const notificationPreferences = useRoomsNotificationPreferencesContext(); const roomToUnread = useAtomValue(roomToUnreadAtom); const navigate = useNavigate(); const selectedRoomId = useSelectedRoom(); const createRoomSelected = useHomeCreateSelected(); const searchSelected = useHomeSearchSelected(); const noRoomToDisplay = rooms.length === 0; const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom()); const sortedRooms = useMemo(() => { const items = Array.from(rooms).sort( closedCategories.has(DEFAULT_CATEGORY_ID) ? factoryRoomIdByActivity(mx) : factoryRoomIdByAtoZ(mx) ); if (closedCategories.has(DEFAULT_CATEGORY_ID)) { return items.filter((rId) => roomToUnread.has(rId) || rId === selectedRoomId); } return items; }, [mx, rooms, closedCategories, roomToUnread, selectedRoomId]); // Flatten rooms and their sub-rooms into a hierarchical list const listItems = useMemo(() => { const items: RoomListItem[] = []; const processedRooms = new Set(); const addRoomAndSubRooms = (roomId: string, depth: number = 0, parentId?: string, isLast: boolean = false) => { if (processedRooms.has(roomId)) return; // Prevent infinite loops processedRooms.add(roomId); const room = mx.getRoom(roomId); if (room) { // Joined room - show normally items.push({ type: 'room', roomId, room, depth, parentId, isLast }); // Get sub-rooms from room state const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, ''); const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? []; // Filter to only joined sub-rooms const joinedSubRooms = subRooms.filter(subRoomId => mx.getRoom(subRoomId)); // Add sub-rooms recursively joinedSubRooms.forEach((subRoomId, index) => { addRoomAndSubRooms(subRoomId, depth + 1, roomId, index === joinedSubRooms.length - 1); }); } }; // Build set of all sub-room IDs to identify top-level rooms const allSubRoomIds = new Set(); sortedRooms.forEach(roomId => { const room = mx.getRoom(roomId); if (room) { const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, ''); const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? []; subRooms.forEach(id => allSubRoomIds.add(id)); } }); // Only add top-level rooms (rooms that aren't sub-rooms of another) sortedRooms.forEach(roomId => { if (!allSubRoomIds.has(roomId)) { addRoomAndSubRooms(roomId); } }); return items; }, [mx, sortedRooms]); const virtualizer = useVirtualizer({ count: listItems.length, getScrollElement: () => scrollRef.current, estimateSize: () => 32, overscan: 10, }); const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => closedCategories.has(categoryId) ); return ( {noRoomToDisplay ? ( ) : ( navigate(getHomeCreatePath())}> Create Room {(open, setOpen) => ( <> setOpen(true)}> Join with Address {open && ( setOpen(false)} onOpen={(roomIdOrAlias, viaServers, eventId) => { setOpen(false); const searchParams: _RoomSearchParams = { viaServers: viaServers?.join(',') }; navigate( withSearchParam( getHomeRoomPath(roomIdOrAlias), searchParams ) ); }} /> )} )} Message Search Rooms
{virtualizer.getVirtualItems().map((vItem) => { const item = listItems[vItem.index]; if (!item) return null; if (item.type === 'unjoined-subroom') { return ( ); } const selected = selectedRoomId === item.roomId; const paddingLeft = item.depth > 0 ? '30px' : undefined; let treeIcon = ''; if (item.depth > 0) { treeIcon = item.isLast ? '╰' : '├'; } return (
0 ? '1.5rem' : undefined }}> {treeIcon && ( {treeIcon} )}
0} linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, item.roomId))} notificationMode={getRoomNotificationMode( notificationPreferences, item.room.roomId )} />
); })}
)}
); }