import React, { MouseEventHandler, forwardRef, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Box, Avatar, Text, Overlay, OverlayCenter, OverlayBackdrop, IconButton, Icon, Icons, Tooltip, TooltipProvider, Menu, MenuItem, toRem, config, Line, PopOut, RectCords, Badge, Chip, Spinner, Button, } from 'folds'; import { useNavigate } from 'react-router-dom'; import { Room } from 'matrix-js-sdk'; import { useStateEvent } from '../../hooks/useStateEvent'; import { PageHeader } from '../../components/page'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { UseStateProvider } from '../../components/UseStateProvider'; import { RoomTopicViewer } from '../../components/room-topic-viewer'; import { StateEvent } from '../../../types/matrix/room'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useIsDirectRoom, useRoom } from '../../hooks/useRoom'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { useSpaceOptionally } from '../../hooks/useSpace'; import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils'; import { getCanonicalAliasOrRoomId, mxcUrlToHttp } from '../../utils/matrix'; import { getStateEvents } from '../../utils/room'; import { _SearchPathSearchParams } from '../../pages/paths'; import * as css from './RoomViewHeader.css'; import { useRoomUnread } from '../../state/hooks/unread'; import { usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { markAsRead } from '../../utils/notifications'; import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; import { useRoomAvatar, useLocalRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { stopPropagation } from '../../utils/keyboard'; import { BackRouteHandler } from '../../components/BackRouteHandler'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useRoomPinnedEvents } from '../../hooks/useRoomPinnedEvents'; import { RoomPinMenu } from './room-pin-menu'; import { useOpenRoomSettings } from '../../state/hooks/roomSettings'; import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher'; import { getRoomNotificationMode, getRoomNotificationModeIcon, useRoomsNotificationPreferencesContext, } from '../../hooks/useRoomsNotificationPreferences'; import { JumpToTime } from './jump-to-time'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { ReportRoomModal } from './ReportRoomModal'; import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { ContainerColor } from '../../styles/ContainerColor.css'; import { RoomSettingsPage } from '../../state/roomSettings'; import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed'; import { useLivekitSupport } from '../../hooks/useLivekitSupport'; import { webRTCSupported } from '../../utils/rtc'; type RoomMenuProps = { room: Room; requestClose: () => void; }; const RoomMenu = forwardRef(({ room, requestClose }, ref) => { const mx = useMatrixClient(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const powerLevels = usePowerLevelsContext(); const creators = useRoomCreators(room); const permissions = useRoomPermissions(creators, powerLevels); const canInvite = permissions.action('invite', mx.getSafeUserId()); const isServerNotice = room.getType() === 'm.server_notice'; const isCreator = creators.has(mx.getSafeUserId()); const notificationPreferences = useRoomsNotificationPreferencesContext(); const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId); const { navigateRoom } = useRoomNavigate(); const [invitePrompt, setInvitePrompt] = useState(false); const [reportRoomOpen, setReportRoomOpen] = useState(false); const handleMarkAsRead = () => { markAsRead(mx, room.roomId, hideActivity); requestClose(); }; const handleInvite = () => { setInvitePrompt(true); }; const openSettings = useOpenRoomSettings(); const parentSpace = useSpaceOptionally(); const handleOpenSettings = () => { openSettings(room.roomId, parentSpace?.roomId); requestClose(); }; return ( {invitePrompt && ( { setInvitePrompt(false); requestClose(); }} /> )} {reportRoomOpen && ( { setReportRoomOpen(false); requestClose(); }} /> )} } radii="300" disabled={!unread} > Mark as Read {(handleOpen, opened, changing) => ( ) : ( ) } radii="300" aria-pressed={opened} onClick={handleOpen} > Notifications )} {!isServerNotice && ( } radii="300" aria-pressed={invitePrompt} disabled={!canInvite} > Invite )} {!isServerNotice && ( } radii="300" > Room Settings )} {(promptJump, setPromptJump) => ( <> setPromptJump(true)} size="300" after={} radii="300" aria-pressed={promptJump} > Jump to Time {promptJump && ( { setPromptJump(false); navigateRoom(room.roomId, eventId); requestClose(); }} onCancel={() => setPromptJump(false)} /> )} )} {!isServerNotice && !isCreator && ( setReportRoomOpen(true)} variant="Critical" fill="None" size="300" after={} radii="300" aria-pressed={reportRoomOpen} > Report Room )} {(promptLeave, setPromptLeave) => ( <> setPromptLeave(true)} variant="Critical" fill="None" size="300" after={} radii="300" aria-pressed={promptLeave} > Leave Room {promptLeave && ( setPromptLeave(false)} /> )} )} ); }); type CallMenuProps = { onVoiceCall: () => void; onVideoCall: () => void; requestClose: () => void; }; const CallMenu = forwardRef( ({ requestClose, onVoiceCall, onVideoCall }, ref) => { const handleVoice = () => { onVoiceCall(); requestClose(); }; const handleVideo = () => { onVideoCall(); requestClose(); }; return ( Start Call ); }, ); function CallButton() { const room = useRoom(); const direct = useIsDirectRoom(); const callEmbed = useCallEmbed(); const startCall = useCallStart(direct); const callStarted = callEmbed && callEmbed.roomId === room.roomId; const inAnotherCall = callEmbed && !callStarted; const [menuAnchor, setMenuAnchor] = useState(); const handleOpenMenu: MouseEventHandler = (evt) => { setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; return ( <> {inAnotherCall ? ( Already in another call — End the current call to join! ) : ( Call )} } > {(triggerRef) => ( { evt.preventDefault(); startCall(room, { microphone: true, video: true, sound: true, }); }} disabled={inAnotherCall || callStarted} aria-pressed={!!menuAnchor} > )} setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > startCall(room, { microphone: true, video: true, sound: true })} onVoiceCall={() => startCall(room, { microphone: true, video: false, sound: true })} requestClose={() => setMenuAnchor(undefined)} /> } /> ); } export function RoomViewHeader({ callView }: { callView?: boolean }) { const navigate = useNavigate(); const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const screenSize = useScreenSizeContext(); const room = useRoom(); const space = useSpaceOptionally(); const powerLevels = usePowerLevelsContext(); const creators = useRoomCreators(room); const permissions = useRoomPermissions(creators, powerLevels); const hasCallPermission = permissions.stateEvent( StateEvent.GroupCallMemberPrefix, mx.getSafeUserId(), ); const livekitSupported = useLivekitSupport(); const rtcSupported = webRTCSupported(); const [menuAnchor, setMenuAnchor] = useState(); const [pinMenuAnchor, setPinMenuAnchor] = useState(); const direct = useIsDirectRoom(); const pinnedEvents = useRoomPinnedEvents(room); const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption); const encryptedRoom = !!encryptionEvent; const avatarMxc = useRoomAvatar(room, direct); const name = useLocalRoomName(room); const topic = useRoomTopic(room); const avatarUrl = avatarMxc ? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined) : undefined; const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const handleSearchClick = () => { const searchParams: _SearchPathSearchParams = { rooms: room.roomId, }; const path = space ? getSpaceSearchPath(getCanonicalAliasOrRoomId(mx, space.roomId)) : getHomeSearchPath(); navigate(withSearchParam(path, searchParams)); }; const handleOpenMenu: MouseEventHandler = (evt) => { setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; const handleOpenPinMenu: MouseEventHandler = (evt) => { setPinMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; const openSettings = useOpenRoomSettings(); const parentSpace = useSpaceOptionally(); const handleMemberToggle = () => { if (callView) { openSettings(room.roomId, parentSpace?.roomId, RoomSettingsPage.MembersPage); return; } setPeopleDrawer(!peopleDrawer); }; return ( {screenSize === ScreenSize.Mobile && ( {(onBack) => ( )} )} {screenSize !== ScreenSize.Mobile && ( ( )} /> )} {name} {room.getType() === 'm.server_notice' && ( System messages from your homeserver administrator. } > {(triggerRef) => ( Server Notice )} )} {topic && ( {(viewTopic, setViewTopic) => ( <> }> setViewTopic(false), escapeDeactivates: stopPropagation, }} > setViewTopic(false)} /> setViewTopic(true)} className={css.HeaderTopic} size="T200" priority="300" truncate > {topic.topic} )} )} {!encryptedRoom && ( Search } > {(triggerRef) => ( )} )} Pinned Messages } > {(triggerRef) => ( {pinnedEvents.length > 0 && ( {pinnedEvents.length} )} )} setPinMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > setPinMenuAnchor(undefined)} /> } /> {!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission && (direct || (room.getJoinRule() === 'invite' && getStateEvents(room, StateEvent.SpaceParent).length === 0)) && } {screenSize === ScreenSize.Desktop && ( {callView ? ( Members ) : ( {peopleDrawer ? 'Hide Members' : 'Show Members'} )} } > {(triggerRef) => ( )} )} More Options } > {(triggerRef) => ( )} setMenuAnchor(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', escapeDeactivates: stopPropagation, }} > setMenuAnchor(undefined)} /> } /> ); }