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, Spinner, } from 'folds'; import { useNavigate } from 'react-router-dom'; import { JoinRule, Room } from 'matrix-js-sdk'; import { useAtomValue } from 'jotai'; 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 { useRoom } from '../../hooks/useRoom'; import { useSetSetting, useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { useSpaceOptionally } from '../../hooks/useSpace'; import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils'; import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '../../utils/matrix'; import { _SearchPathSearchParams } from '../../pages/paths'; import * as css from './RoomViewHeader.css'; import { useRoomUnread } from '../../state/hooks/unread'; import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { markAsRead } from '../../../client/action/notifications'; import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { openInviteUser } from '../../../client/action/navigation'; import { copyToClipboard } from '../../utils/dom'; import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { mDirectAtom } from '../../state/mDirectList'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { stopPropagation } from '../../utils/keyboard'; import { getMatrixToRoom } from '../../plugins/matrix-to'; import { getViaServers } from '../../plugins/via-servers'; 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'; 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 { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const notificationPreferences = useRoomsNotificationPreferencesContext(); const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId); const { navigateRoom } = useRoomNavigate(); const handleMarkAsRead = () => { markAsRead(mx, room.roomId, hideActivity); requestClose(); }; const handleInvite = () => { openInviteUser(room.roomId); requestClose(); }; const handleCopyLink = () => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room); copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers)); requestClose(); }; const openSettings = useOpenRoomSettings(); const parentSpace = useSpaceOptionally(); const handleOpenSettings = () => { openSettings(room.roomId, parentSpace?.roomId); requestClose(); }; return ( } radii="300" disabled={!unread} > Mark as Read {(handleOpen, opened, changing) => ( ) : ( ) } radii="300" aria-pressed={opened} onClick={handleOpen} > Notifications )} } radii="300" disabled={!canInvite} > Invite } radii="300" > Copy Link } 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)} /> )} )} {(promptLeave, setPromptLeave) => ( <> setPromptLeave(true)} variant="Critical" fill="None" size="300" after={} radii="300" aria-pressed={promptLeave} > Leave Room {promptLeave && ( setPromptLeave(false)} /> )} )} ); }); export function RoomViewHeader() { const navigate = useNavigate(); const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const screenSize = useScreenSizeContext(); const room = useRoom(); const space = useSpaceOptionally(); const [menuAnchor, setMenuAnchor] = useState(); const [pinMenuAnchor, setPinMenuAnchor] = useState(); const mDirects = useAtomValue(mDirectAtom); const pinnedEvents = useRoomPinnedEvents(room); const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption); const ecryptedRoom = !!encryptionEvent; const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId)); const name = useRoomName(room); const topic = useRoomTopic(room); const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined; const setPeopleDrawer = useSetSetting(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()); }; return ( {screenSize === ScreenSize.Mobile && ( {(onBack) => ( )} )} {screenSize !== ScreenSize.Mobile && ( ( )} /> )} {name} {topic && ( {(viewTopic, setViewTopic) => ( <> }> setViewTopic(false), escapeDeactivates: stopPropagation, }} > setViewTopic(false)} /> setViewTopic(true)} className={css.HeaderTopic} size="T200" priority="300" truncate > {topic} )} )} {!ecryptedRoom && ( 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)} /> } /> {screenSize === ScreenSize.Desktop && ( 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)} /> } /> ); }