import React, { MouseEventHandler, forwardRef, useCallback, useMemo, useRef, useState, } from 'react'; import { useAtom, useAtomValue } from 'jotai'; import { Avatar, Box, Button, Icon, IconButton, Icons, Line, Menu, MenuItem, PopOut, RectCords, Spinner, Text, color, 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 } 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 { Membership, 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'; import { useRoomCreators } from '../../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; import { ContainerColor } from '../../../styles/ContainerColor.css'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { BreakWord } from '../../../styles/Text.css'; 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 creators = useRoomCreators(room); const permissions = useRoomPermissions(creators, powerLevels); const canInvite = permissions.action('invite', mx.getSafeUserId()); 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)} /> } /> )} ); } type SpaceTombstoneProps = { roomId: string; replacementRoomId: string }; export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProps) { const mx = useMatrixClient(); const { navigateRoom } = useRoomNavigate(); const [joinState, handleJoin] = useAsyncCallback( useCallback(() => { const currentRoom = mx.getRoom(roomId); const via = currentRoom ? getViaServers(currentRoom) : []; return mx.joinRoom(replacementRoomId, { viaServers: via, }); }, [mx, roomId, replacementRoomId]) ); const replacementRoom = mx.getRoom(replacementRoomId); const handleOpen = () => { if (replacementRoom) navigateRoom(replacementRoom.roomId); if (joinState.status === AsyncStatus.Success) navigateRoom(joinState.data.roomId); }; return ( Space Upgraded This space has been replaced and is no longer active. {joinState.status === AsyncStatus.Error && ( {(joinState.error as any)?.message ?? 'Failed to join replacement space!'} )} {replacementRoom?.getMyMembership() === Membership.Join || joinState.status === AsyncStatus.Success ? ( ) : ( )} ); } 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 tombstoneEvent = useStateEvent(space, StateEvent.RoomTombstone); 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 ( {tombstoneEvent && ( )} 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 ( ); })}
); }