import React, { MouseEventHandler, forwardRef, useEffect, useMemo, useRef, useState } from 'react'; import { Avatar, Box, IconButton, Line, Menu, MenuItem, PopOut, RectCords, Text, Tooltip, TooltipProvider, config, toRem } from 'folds'; import { Icon, Icons } from '../../components/icons'; import FocusTrap from 'focus-trap-react'; import { PageHeader } from '../../components/page'; import { useSetSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { useRoomAvatar, useRoomName } from '../../hooks/useRoomMeta'; import { useSpace } from '../../hooks/useSpace'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { RoomAvatar } from '../../components/room-avatar'; import { nameInitials } from '../../utils/common'; import * as css from './LobbyHeader.css'; import { IPowerLevels } from '../../hooks/usePowerLevels'; import { UseStateProvider } from '../../components/UseStateProvider'; import { LeaveSpacePrompt } from '../../components/leave-space-prompt'; import { RemoveInaccessiblePrompt } from '../../components/remove-inaccessible-prompt'; import { stopPropagation } from '../../utils/keyboard'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { useBackRoute } from '../../hooks/useBackRoute'; import { mxcUrlToHttp } from '../../utils/matrix'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings'; import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { AddExistingModal } from '../add-existing'; import { SpeechBubble } from '../../components/speech-bubble/SpeechBubble'; import { SpaceOptionsMenu, type OpenAddExistingOptions } from '../space/SpaceOptionsMenu'; import type { HierarchyItemSpace } from '../../hooks/useSpaceHierarchy'; import { useNavigate } from 'react-router-dom'; import { getCanonicalAliasOrRoomId } from '../../utils/matrix'; import { getSpaceFeedPath } from '../../pages/pathUtils'; import { shouldShowForumLobby } from '../../utils/room'; import { useForumBoardContextOptionally } from '../forum/ForumBoardContext'; import { StateEvent } from '../../../types/matrix/room'; type LobbyMenuProps = { powerLevels: IPowerLevels; requestClose: () => void; }; const LobbyMenu = forwardRef( ({ powerLevels, requestClose }, ref) => { const mx = useMatrixClient(); const navigate = useNavigate(); const space = useSpace(); const isForum = shouldShowForumLobby(space); const creators = useRoomCreators(space); const permissions = useRoomPermissions(creators, powerLevels); const canInvite = permissions.action('invite', mx.getSafeUserId()); const canEditChildren = permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()); const openSpaceSettings = useOpenSpaceSettings(); const [invitePrompt, setInvitePrompt] = useState(false); const handleInvite = () => { setInvitePrompt(true); }; const handleRoomSettings = () => { openSpaceSettings(space.roomId); requestClose(); }; const handleOpenFeed = () => { navigate(getSpaceFeedPath(getCanonicalAliasOrRoomId(mx, space.roomId))); requestClose(); }; return ( {invitePrompt && ( { setInvitePrompt(false); requestClose(); }} /> )} {isForum && ( } radii="300" > Feed )} } radii="300" aria-pressed={invitePrompt} disabled={!canInvite} > Invite } radii="300" > Space Settings {canEditChildren && ( {(promptRemove, setPromptRemove) => ( <> setPromptRemove(true)} variant="Critical" fill="None" size="300" after={} radii="300" aria-pressed={promptRemove} > Remove Inaccessible {promptRemove && ( setPromptRemove(false)} /> )} )} )} {(promptLeave, setPromptLeave) => ( <> setPromptLeave(true)} variant="Critical" fill="None" size="300" after={} radii="300" aria-pressed={promptLeave} > Leave Space {promptLeave && ( setPromptLeave(false)} /> )} )} ); } ); type LobbyHeaderProps = { showProfile?: boolean; /** Space name on the left + add room/space + ⋮ menu (forum toolbar). */ showSpaceToolbar?: boolean; powerLevels: IPowerLevels; }; export function LobbyHeader({ showProfile, showSpaceToolbar, powerLevels }: LobbyHeaderProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const space = useSpace(); const creators = useRoomCreators(space); const spacePermissions = useRoomPermissions(creators, powerLevels); const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const [menuAnchor, setMenuAnchor] = useState(); const [addExisting, setAddExisting] = useState(null); const screenSize = useScreenSizeContext(); const { showInPageHeader, goBack } = useBackRoute(); const name = useRoomName(space); const avatarMxc = useRoomAvatar(space); const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined; const forumBoard = useForumBoardContextOptionally(); const menuWrapperRef = useRef(null); const menuTriggerElRef = useRef(null); const addParent = useMemo(() => { const rootId = space.roomId; if (!forumBoard) { return { roomId: rootId, showAddSpace: true }; } if (forumBoard.query.category) { const section = forumBoard.sections.find((s) => s.title === forumBoard.query.category); if (section?.spaceId) { return { roomId: section.spaceId, showAddSpace: false }; } } return { roomId: rootId, showAddSpace: true }; }, [forumBoard, space.roomId]); const addParentItem: HierarchyItemSpace = useMemo( () => ({ roomId: addParent.roomId, content: { via: [] }, ts: 0, space: true, parentId: addParent.roomId === space.roomId ? undefined : space.roomId, }), [addParent.roomId, space.roomId] ); const canEditChildren = Boolean( spacePermissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) ); const handleOpenMenu: MouseEventHandler = (evt) => { menuTriggerElRef.current = evt.currentTarget; setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; useEffect(() => { if (!showSpaceToolbar || !menuAnchor || addExisting) return; let outsideTimer: number | undefined; const clearOutsideTimer = () => { if (outsideTimer) { window.clearTimeout(outsideTimer); outsideTimer = undefined; } }; const closeMenu = () => { clearOutsideTimer(); setMenuAnchor(undefined); }; const isInside = (node: Node | null) => { if (!node) return false; return Boolean( menuWrapperRef.current?.contains(node) || menuTriggerElRef.current?.contains(node) ); }; const handlePointerDown = (evt: MouseEvent | TouchEvent) => { const node = evt.target as Node | null; if (!isInside(node)) { closeMenu(); } }; const handlePointerMove = (evt: PointerEvent | MouseEvent) => { const node = evt.target as Node | null; const inside = isInside(node); if (inside) { clearOutsideTimer(); return; } if (!outsideTimer) { outsideTimer = window.setTimeout(closeMenu, 3000); } }; document.addEventListener('mousedown', handlePointerDown, true); document.addEventListener('touchstart', handlePointerDown, true); document.addEventListener('pointermove', handlePointerMove, true); document.addEventListener('mousemove', handlePointerMove, true); return () => { clearOutsideTimer(); document.removeEventListener('mousedown', handlePointerDown, true); document.removeEventListener('touchstart', handlePointerDown, true); document.removeEventListener('pointermove', handlePointerMove, true); document.removeEventListener('mousemove', handlePointerMove, true); }; }, [addExisting, menuAnchor, showSpaceToolbar]); if (showSpaceToolbar) { return ( <> {showInPageHeader && ( )} {nameInitials(name)}} /> {name} More Options } > {(triggerRef) => ( )} setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} >
setMenuAnchor(undefined)} onOpenAddExisting={(options) => { setAddExisting(options); setMenuAnchor(undefined); }} forumAdd={ forumBoard && canEditChildren ? { item: addParentItem, showAddSpace: addParent.showAddSpace, } : undefined } />
} />
{addExisting && ( setAddExisting(null)} /> )} ); } return ( {showInPageHeader ? ( <> {showProfile && ( {name} )} ) : ( <> {showProfile && ( <> {nameInitials(name)}} /> {name} )} )} {screenSize !== ScreenSize.Mobile && ( Members } > {(triggerRef) => ( setPeopleDrawer((drawer) => !drawer)}> )} )} More Options } > {(triggerRef) => ( )} setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > setMenuAnchor(undefined)} /> } /> ); }