import React, { MouseEventHandler, forwardRef, useState, MouseEvent } from 'react'; import { EventType, Room } from 'matrix-js-sdk'; import { Avatar, Box, Icon, IconButton, Icons, Text, Menu, MenuItem, config, PopOut, toRem, Line, RectCords, Badge, Spinner, Tooltip, TooltipProvider, } from 'folds'; import { useFocusWithin, useHover } from 'react-aria'; import FocusTrap from 'focus-trap-react'; import { useNavigate } from 'react-router-dom'; import { NavButton, NavItem, NavItemContent, NavItemOptions } from '../../components/nav'; import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; import { nameInitials } from '../../utils/common'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRoomUnread } from '../../state/hooks/unread'; import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { usePowerLevels } from '../../hooks/usePowerLevels'; import { copyToClipboard } from '../../utils/dom'; import { markAsRead } from '../../utils/notifications'; import { UseStateProvider } from '../../components/UseStateProvider'; import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers'; import { TypingIndicator } from '../../components/typing-indicator'; import { stopPropagation } from '../../utils/keyboard'; import { getMatrixToRoom } from '../../plugins/matrix-to'; import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix'; import { getViaServers } from '../../plugins/via-servers'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { useOpenRoomSettings } from '../../state/hooks/roomSettings'; import { useSpaceOptionally } from '../../hooks/useSpace'; import { getRoomNotificationModeIcon, RoomNotificationMode, } from '../../hooks/useRoomsNotificationPreferences'; import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher'; import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { useCallState } from '../../pages/client/call/CallProvider'; import { useCallMembers } from '../../hooks/useCallMemberships'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { RoomNavUser } from './RoomNavUser'; import { useRoomName } from '../../hooks/useRoomMeta'; type RoomNavItemMenuProps = { room: Room; requestClose: () => void; notificationMode?: RoomNotificationMode; }; const RoomNavItemMenu = forwardRef( ({ room, requestClose, notificationMode }, ref) => { const mx = useMatrixClient(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const powerLevels = usePowerLevels(room); const creators = useRoomCreators(room); const permissions = useRoomPermissions(creators, powerLevels); const canInvite = permissions.action('invite', mx.getSafeUserId()); const openRoomSettings = useOpenRoomSettings(); const space = useSpaceOptionally(); const [invitePrompt, setInvitePrompt] = useState(false); const handleMarkAsRead = () => { markAsRead(mx, room.roomId, hideActivity); requestClose(); }; const handleInvite = () => { setInvitePrompt(true); }; const handleCopyLink = () => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room); copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers)); requestClose(); }; const handleRoomSettings = () => { openRoomSettings(room.roomId, space?.roomId); requestClose(); }; return ( {invitePrompt && room && ( { setInvitePrompt(false); requestClose(); }} /> )} } radii="300" disabled={!unread} > Mark as Read {(handleOpen, opened, changing) => ( ) : ( ) } radii="300" aria-pressed={opened} onClick={handleOpen} > Notifications )} } radii="300" aria-pressed={invitePrompt} disabled={!canInvite} > Invite } radii="300" > Copy Link } radii="300" > Room Settings {(promptLeave, setPromptLeave) => ( <> setPromptLeave(true)} variant="Critical" fill="None" size="300" after={} radii="300" aria-pressed={promptLeave} > Leave Room {promptLeave && ( setPromptLeave(false)} /> )} )} ); } ); RoomNavItemMenu.displayName = 'RoomNavItemMenu'; type RoomNavItemProps = { room: Room; selected: boolean; linkPath: string; notificationMode?: RoomNotificationMode; showAvatar?: boolean; direct?: boolean; }; export function RoomNavItem({ room, selected, showAvatar, direct, notificationMode, linkPath, }: RoomNavItemProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const [hover, setHover] = useState(false); const { hoverProps } = useHover({ onHoverChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const [menuAnchor, setMenuAnchor] = useState(); const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const typingMember = useRoomTypingMember(room.roomId).filter( (receipt) => receipt.userId !== mx.getUserId() ); const { isActiveCallReady, activeCallRoomId, setActiveCallRoomId, setViewedCallRoomId, isChatOpen, toggleChat, hangUp, } = useCallState(); const isActiveCall = isActiveCallReady && activeCallRoomId === room.roomId; const callMemberships = useCallMembers(mx, room.roomId); const powerLevels = usePowerLevels(room); const creators = useRoomCreators(room); const roomName = useRoomName(room); const permissions = useRoomPermissions(creators, powerLevels); const canJoinCall = permissions.event(EventType.GroupCallMemberPrefix, mx.getSafeUserId()); const { navigateRoom } = useRoomNavigate(); const navigate = useNavigate(); const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const handleContextMenu: MouseEventHandler = (evt) => { evt.preventDefault(); setMenuAnchor({ x: evt.clientX, y: evt.clientY, width: 0, height: 0, }); }; const handleOpenMenu: MouseEventHandler = (evt) => { setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; const handleNavItemClick: MouseEventHandler = (evt) => { if (room.isCallRoom()) { if (!isMobile) { if (!isActiveCall && canJoinCall) { hangUp(); setActiveCallRoomId(room.roomId); } else { navigateRoom(room.roomId); } } else { evt.stopPropagation(); if (isChatOpen) toggleChat(); setViewedCallRoomId(room.roomId); navigateRoom(room.roomId); } } else { navigate(linkPath); } }; const handleChatButtonClick = (evt: MouseEvent) => { evt.stopPropagation(); if (!isChatOpen) toggleChat(); setViewedCallRoomId(room.roomId); navigate(linkPath); }; const optionsVisible = hover || !!menuAnchor; const ariaLabel = [ roomName, room.isCallRoom() ? [ 'Call Room', isActiveCall && 'Currently in Call', callMemberships.length && `${callMemberships.length} in Call`, ] : 'Text Room', unread?.total && `${unread.total} Messages`, ] .flat() .filter(Boolean) .join(', '); return ( {showAvatar ? ( ( {nameInitials(roomName)} )} /> ) : ( )} {roomName} {!optionsVisible && !unread && !selected && typingMember.length > 0 && ( )} {!optionsVisible && unread && ( 0} count={unread.total} /> )} {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && ( )} {optionsVisible && ( setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > setMenuAnchor(undefined)} notificationMode={notificationMode} /> } > {room.isCallRoom() && ( Open Chat } > {(triggerRef) => ( )} )} )} {room.isCallRoom() && ( {callMemberships.map((callMembership) => ( ))} )} ); }