import React, { MouseEventHandler, forwardRef, useCallback, useMemo, useRef, useState, } from 'react'; import { useAtom, useAtomValue } from 'jotai'; import { Avatar, Box, Icon, IconButton, Icons, Line, Menu, MenuItem, PopOut, RectCords, Text, config, toRem, } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import { JoinRule, Room } from 'matrix-js-sdk'; import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; import FocusTrap from 'focus-trap-react'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { mDirectAtom } from '../../../state/mDirectList'; import { NavCategory, NavCategoryHeader, NavItem, NavItemContent, NavLink, } from '../../../components/nav'; import { getSpaceLobbyPath, getSpaceRoomPath, getSpaceSearchPath } from '../../pathUtils'; import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { useSpaceLobbySelected, useSpaceSearchSelected, } from '../../../hooks/router/useSelectedSpace'; import { useSpace } from '../../../hooks/useSpace'; import { VirtualTile } from '../../../components/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 { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; import { useRoomName } from '../../../hooks/useRoomMeta'; import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy'; import { allRoomsAtom } from '../../../state/room-list/roomList'; import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page'; import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { openInviteUser } from '../../../../client/action/navigation'; import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList'; import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { markAsRead } from '../../../../client/action/notifications'; import { useRoomsUnread } from '../../../state/hooks/unread'; import { UseStateProvider } from '../../../components/UseStateProvider'; import { LeaveSpacePrompt } from '../../../components/leave-space-prompt'; import { copyToClipboard } from '../../../utils/dom'; import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories'; import { useStateEvent } from '../../../hooks/useStateEvent'; import { StateEvent } from '../../../../types/matrix/room'; import { stopPropagation } from '../../../utils/keyboard'; import { getMatrixToRoom } from '../../../plugins/matrix-to'; import { getViaServers } from '../../../plugins/via-servers'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { getRoomNotificationMode, useRoomsNotificationPreferencesContext, } from '../../../hooks/useRoomsNotificationPreferences'; import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; type SpaceMenuProps = { room: Room; requestClose: () => void; }; const SpaceMenu = forwardRef(({ room, requestClose }, ref) => { const mx = useMatrixClient(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [developerTools] = useSetting(settingsAtom, 'developerTools'); const roomToParents = useAtomValue(roomToParentsAtom); const powerLevels = usePowerLevels(room); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const openSpaceSettings = useOpenSpaceSettings(); const { navigateRoom } = useRoomNavigate(); const allChild = useSpaceChildren( allRoomsAtom, room.roomId, useRecursiveChildScopeFactory(mx, roomToParents) ); const unread = useRoomsUnread(allChild, roomToUnreadAtom); const handleMarkAsRead = () => { allChild.forEach((childRoomId) => markAsRead(mx, childRoomId, hideActivity)); requestClose(); }; const handleCopyLink = () => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room); copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers)); requestClose(); }; const handleInvite = () => { openInviteUser(room.roomId); requestClose(); }; const handleRoomSettings = () => { openSpaceSettings(room.roomId); requestClose(); }; const handleOpenTimeline = () => { navigateRoom(room.roomId); requestClose(); }; return ( } radii="300" disabled={!unread} > Mark as Read } radii="300" disabled={!canInvite} > Invite } radii="300" > Copy Link } radii="300" > Space Settings {developerTools && ( } radii="300" > Event Timeline )} {(promptLeave, setPromptLeave) => ( <> setPromptLeave(true)} variant="Critical" fill="None" size="300" after={} radii="300" aria-pressed={promptLeave} > Leave Space {promptLeave && ( setPromptLeave(false)} /> )} )} ); }); function SpaceHeader() { const space = useSpace(); const spaceName = useRoomName(space); const [menuAnchor, setMenuAnchor] = useState(); const joinRules = useStateEvent( space, StateEvent.RoomJoinRules )?.getContent(); const handleOpenMenu: MouseEventHandler = (evt) => { const cords = evt.currentTarget.getBoundingClientRect(); setMenuAnchor((currentState) => { if (currentState) return undefined; return cords; }); }; return ( <> {spaceName} {joinRules?.join_rule !== JoinRule.Public && } {menuAnchor && ( setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > setMenuAnchor(undefined)} /> } /> )} ); } export function Space() { const mx = useMatrixClient(); const space = useSpace(); useNavToActivePathMapper(space.roomId); const spaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId); const scrollRef = useRef(null); const mDirects = useAtomValue(mDirectAtom); const roomToUnread = useAtomValue(roomToUnreadAtom); const allRooms = useAtomValue(allRoomsAtom); const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const notificationPreferences = useRoomsNotificationPreferencesContext(); const selectedRoomId = useSelectedRoom(); const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias); const searchSelected = useSpaceSearchSelected(spaceIdOrAlias); const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom()); const getRoom = useCallback( (rId: string) => { if (allJoinedRooms.has(rId)) { return mx.getRoom(rId) ?? undefined; } return undefined; }, [mx, allJoinedRooms] ); const hierarchy = useSpaceJoinedHierarchy( space.roomId, getRoom, useCallback( (parentId, roomId) => { if (!closedCategories.has(makeNavCategoryId(space.roomId, parentId))) { return false; } const showRoom = roomToUnread.has(roomId) || roomId === selectedRoomId; if (showRoom) return false; return true; }, [space.roomId, closedCategories, roomToUnread, selectedRoomId] ), useCallback( (sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)), [closedCategories, space.roomId] ) ); const virtualizer = useVirtualizer({ count: hierarchy.length, getScrollElement: () => scrollRef.current, estimateSize: () => 0, overscan: 10, }); const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => closedCategories.has(categoryId) ); const getToLink = (roomId: string) => getSpaceRoomPath(spaceIdOrAlias, getCanonicalAliasOrRoomId(mx, roomId)); return ( Lobby Message Search {virtualizer.getVirtualItems().map((vItem) => { const { roomId } = hierarchy[vItem.index] ?? {}; const room = mx.getRoom(roomId); if (!room) return null; if (room.isSpaceRoom()) { const categoryId = makeNavCategoryId(space.roomId, roomId); return (
{roomId === space.roomId ? 'Rooms' : room?.name}
); } return ( ); })}
); }