import React, { useCallback, useEffect, useRef } from 'react'; import { Box, Line } from 'folds'; import { useParams } from 'react-router-dom'; import { isKeyHotkey } from 'is-hotkey'; import { useAtomValue, useSetAtom } from 'jotai'; import { RoomView } from './RoomView'; import { MembersDrawer } from './MembersDrawer'; import { MediaGallery } from './MediaGallery'; import { mediaGalleryAtom } from '../../state/mediaGallery'; import { WidgetsPanel } from './widgets/WidgetsPanel'; import { widgetsPanelAtom } from '../../state/widgetsPanel'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels'; import { useRoom } from '../../hooks/useRoom'; import { useKeyDown } from '../../hooks/useKeyDown'; import { markAsRead } from '../../utils/notifications'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRoomMembers } from '../../hooks/useRoomMembers'; import { CallView } from '../call/CallView'; import { RoomViewHeader } from './RoomViewHeader'; import { callChatAtom } from '../../state/callEmbed'; import { CallChatView } from './CallChatView'; import { useCallEmbed } from '../../hooks/useCallEmbed'; import { useCallMembers, useCallSession } from '../../hooks/useCall'; import { roomIdToActiveThreadIdAtomFamily } from '../../state/room/thread'; import { threadsListAtom } from '../../state/threadsList'; import { ThreadPanel } from './thread'; import { ThreadsListPanel } from './thread/ThreadsListPanel'; export function Room() { const { eventId } = useParams(); const room = useRoom(); const mx = useMatrixClient(); const callSession = useCallSession(room); const callMembers = useCallMembers(callSession); const callEmbed = useCallEmbed(); const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const activeThreadId = useAtomValue(roomIdToActiveThreadIdAtomFamily(room.roomId)); const setActiveThreadId = useSetAtom(roomIdToActiveThreadIdAtomFamily(room.roomId)); const galleryOpen = useAtomValue(mediaGalleryAtom); const setGalleryOpen = useSetAtom(mediaGalleryAtom); const widgetsOpen = useAtomValue(widgetsPanelAtom); const setWidgetsOpen = useSetAtom(widgetsPanelAtom); const threadsListOpen = useAtomValue(threadsListAtom); const setThreadsListOpen = useSetAtom(threadsListAtom); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const screenSize = useScreenSizeContext(); const powerLevels = usePowerLevels(room); const members = useRoomMembers(mx, room.roomId); const chat = useAtomValue(callChatAtom); useKeyDown( window, useCallback( (evt) => { if (isKeyHotkey('escape', evt)) { // Skip when a composer already consumed Escape (it preventDefaults). if (evt.defaultPrevented) return; // Skip while a thread panel is open: listener registration order // means this can run BEFORE the panel's own Escape handler, and the // user's intent there is "close the panel", not "mark room read". if (activeThreadId) return; markAsRead(mx, room.roomId, hideActivity); } }, [mx, room.roomId, hideActivity, activeThreadId], ), ); // Stable handlers for the threads-list panel so its document-level Escape // listener isn't torn down and re-added on every Room re-render. const closeThreadsList = useCallback(() => setThreadsListOpen(false), [setThreadsListOpen]); const openThreadFromList = useCallback( (threadId: string) => { setActiveThreadId(threadId); setThreadsListOpen(false); }, [setActiveThreadId, setThreadsListOpen], ); const callView = callEmbed?.roomId === room.roomId || room.isCallRoom() || callMembers.length > 0; // The content panels (thread / media gallery / widgets) are mutually exclusive // on every screen size: opening one closes the others. Detect the just-opened // transition so whichever was opened most recently wins. const prevThreadRef = useRef(activeThreadId); const prevGalleryRef = useRef(galleryOpen); const prevWidgetsRef = useRef(widgetsOpen); const prevThreadsListRef = useRef(threadsListOpen); useEffect(() => { const threadJustOpened = Boolean(activeThreadId) && !prevThreadRef.current; const galleryJustOpened = galleryOpen && !prevGalleryRef.current; const widgetsJustOpened = widgetsOpen && !prevWidgetsRef.current; const threadsListJustOpened = threadsListOpen && !prevThreadsListRef.current; if (threadJustOpened) { if (galleryOpen) setGalleryOpen(false); if (widgetsOpen) setWidgetsOpen(false); if (threadsListOpen) setThreadsListOpen(false); } else if (galleryJustOpened) { if (activeThreadId) setActiveThreadId(null); if (widgetsOpen) setWidgetsOpen(false); if (threadsListOpen) setThreadsListOpen(false); } else if (widgetsJustOpened) { if (activeThreadId) setActiveThreadId(null); if (galleryOpen) setGalleryOpen(false); if (threadsListOpen) setThreadsListOpen(false); } else if (threadsListJustOpened) { if (activeThreadId) setActiveThreadId(null); if (galleryOpen) setGalleryOpen(false); if (widgetsOpen) setWidgetsOpen(false); } prevThreadRef.current = activeThreadId; prevGalleryRef.current = galleryOpen; prevWidgetsRef.current = widgetsOpen; prevThreadsListRef.current = threadsListOpen; }, [ activeThreadId, galleryOpen, widgetsOpen, threadsListOpen, setGalleryOpen, setActiveThreadId, setWidgetsOpen, setThreadsListOpen, ]); // On non-desktop screens at most one right-side panel may show, priority // thread > gallery > widgets > members. On desktop thread + members may coexist // while the content panels stay mutually exclusive (via the effect above). const isDesktop = screenSize === ScreenSize.Desktop; const showThreadPanel = !callView && Boolean(activeThreadId); const showGallery = !callView && galleryOpen && (isDesktop || !activeThreadId); const showWidgets = !callView && widgetsOpen && (isDesktop || (!activeThreadId && !galleryOpen)); // The single-thread panel always replaces the list (they share the content slot). const showThreadsList = !callView && threadsListOpen && !activeThreadId && (isDesktop || (!galleryOpen && !widgetsOpen)); const showMembers = !callView && isDrawer && (isDesktop || (!activeThreadId && !galleryOpen && !widgetsOpen && !threadsListOpen)); return ( {callView && (screenSize === ScreenSize.Desktop || !chat) && ( )} {!callView && ( )} {callView && chat && ( <> {screenSize === ScreenSize.Desktop && ( )} )} {showGallery && ( <> {screenSize === ScreenSize.Desktop && ( )} setGalleryOpen(false)} /> )} {showWidgets && ( <> {screenSize === ScreenSize.Desktop && ( )} setWidgetsOpen(false)} /> )} {showThreadsList && ( <> {screenSize === ScreenSize.Desktop && ( )} )} {showThreadPanel && activeThreadId && ( <> {screenSize === ScreenSize.Desktop && ( )} setActiveThreadId(null)} /> )} {showMembers && ( <> {screenSize === ScreenSize.Desktop && ( )} )} ); }