diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 0960cf59..17b004b5 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -10,7 +10,6 @@ import { useAtom, useAtomValue } from 'jotai'; import { Avatar, Box, - Button, Icon, IconButton, Icons, @@ -22,11 +21,13 @@ import { Text, config, toRem, -} from 'folds'; // Assuming 'folds' is your UI library +} from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import { JoinRule, Room } from 'matrix-js-sdk'; import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; import FocusTrap from 'focus-trap-react'; +import { logger } from 'matrix-js-sdk/lib/logger'; +import { WidgetApiToWidgetAction } from 'matrix-widget-api'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { mDirectAtom } from '../../../state/mDirectList'; import { @@ -46,6 +47,7 @@ import { import { useSpace } from '../../../hooks/useSpace'; import { VirtualTile } from '../../../components/virtualizer'; import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav'; +// Using the original name for clarity when generating space category IDs import { makeNavCategoryId as makeSpaceNavCategoryId } from '../../../state/closedNavCategories'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { useCategoryHandler } from '../../../hooks/useCategoryHandler'; @@ -76,26 +78,12 @@ import { useRoomsNotificationPreferencesContext, } from '../../../hooks/useRoomsNotificationPreferences'; import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings'; -import { useCallState } from '../CallProvider'; // Assuming path -import { WidgetApiToWidgetAction } from 'matrix-widget-api'; -import { logger } from 'matrix-js-sdk/lib/logger'; - -// --- Helper Functions --- - -// Determine if a room is a voice room (assuming Room object has this method) -const isVoiceRoom = (room: Room): boolean => room.isCallRoom?.() ?? false; -// Determine if a room is a text room -const isTextRoom = (room: Room): boolean => !isVoiceRoom(room); - -// Helper function to generate unique category IDs for channel type headers -const makeChannelTypeId = (parentId: string, type: 'text' | 'voice'): string => { - return `${parentId}_${type}_channels`; -}; +import { useCallState } from '../CallProvider'; /** * Processes the raw hierarchy from useSpaceJoinedHierarchy into a flat list * suitable for the virtualizer, including collapsible headers for text/voice channels. - * Removes the top-level "Channels" category header. + * Removes the top-level "Rooms" category header. * * @param hierarchy - The raw hierarchy data (array of { roomId: string }). * @param mx - The Matrix client instance. @@ -111,25 +99,21 @@ const processHierarchyForVirtualizer = ( ): Array<{ type: string; key: string; [key: string]: any }> => { const processed: Array<{ type: string; key: string; [key: string]: any }> = []; let currentCategoryRooms = { text: [], voice: [] }; - // Start with the root space as the initial parent context let currentParentId: string = spaceRoomId; - // Function to add collected text/voice rooms under their respective headers const addCollectedRoomsToProcessed = (parentId: string) => { - const textCategoryId = makeChannelTypeId(parentId, 'text'); - const voiceCategoryId = makeChannelTypeId(parentId, 'voice'); + const textCategoryId = `${parentId}_text_rooms`; + const voiceCategoryId = `${parentId}_call_rooms`; const isTextClosed = closedCategories.has(textCategoryId); - const isVoiceClosed = closedCategories.has(voiceCategoryId); + const isCallClosed = closedCategories.has(voiceCategoryId); - // Add Text Channels Header and Rooms (if any exist) if (currentCategoryRooms.text.length > 0) { processed.push({ - type: 'channel_header', // Use specific type for collapsible channel headers - title: 'Text Channels', - categoryId: textCategoryId, // ID used for collapse state + type: 'channel_header', + title: 'Text Rooms', + categoryId: textCategoryId, key: `${parentId}-text-header`, }); - // Only add room items if this category is not closed if (!isTextClosed) { currentCategoryRooms.text.forEach((room) => processed.push({ type: 'room', room, key: room.roomId }) @@ -137,76 +121,55 @@ const processHierarchyForVirtualizer = ( } } - // Add Voice Channels Header and Rooms (if any exist) if (currentCategoryRooms.voice.length > 0) { processed.push({ - type: 'channel_header', // Use specific type - title: 'Voice Channels', - categoryId: voiceCategoryId, // ID used for collapse state + type: 'channel_header', + title: 'Call Rooms', + categoryId: voiceCategoryId, key: `${parentId}-voice-header`, }); - // Only add room items if this category is not closed - if (!isVoiceClosed) { + if (!isCallClosed) { currentCategoryRooms.voice.forEach((room) => processed.push({ type: 'room', room, key: room.roomId }) ); } } - // Reset collected rooms for the next category/space currentCategoryRooms = { text: [], voice: [] }; }; - // Iterate through the raw hierarchy provided by the hook hierarchy.forEach((item) => { const room = mx.getRoom(item.roomId); if (!room) { logger.warn(`processHierarchyForVirtualizer: Room not found for ID ${item.roomId}`); - return; // Skip if room data isn't available } if (room.isSpaceRoom()) { - // When encountering a new space, first process the rooms collected under the *previous* parent addCollectedRoomsToProcessed(currentParentId); - - // Now, set the current parent context to this new space currentParentId = room.roomId; - - // Add the space category item itself to the processed list, - // *UNLESS* it's the root space (we want to skip the top-level "Channels" header) if (room.roomId !== spaceRoomId) { - const spaceCategoryId = makeSpaceNavCategoryId(spaceRoomId, room.roomId); // Use original ID generator for spaces + const spaceCategoryId = makeSpaceNavCategoryId(spaceRoomId, room.roomId); processed.push({ - type: 'category', // Type for main space categories + type: 'category', room, - categoryId: spaceCategoryId, // ID for this space's collapse state + categoryId: spaceCategoryId, key: room.roomId, }); } - // Note: We assume the `hierarchy` list is already filtered based on closed *space* categories. + } else if (room.isCallRoom()) { + currentCategoryRooms.voice.push(room); + } else if (!room.isCallRoom()) { + currentCategoryRooms.text.push(room); } else { - // This is a regular room (not a space). Add it to the appropriate list (text/voice) - // for the *current* parent space. - if (isVoiceRoom(room)) { - currentCategoryRooms.voice.push(room); - } else if (isTextRoom(room)) { - currentCategoryRooms.text.push(room); - } else { - // Fallback or handle unexpected room types if necessary - logger.warn( - `processHierarchyForVirtualizer: Room ${room.roomId} is neither text nor voice.` - ); - currentCategoryRooms.text.push(room); // Default to text for now - } + logger.warn(`processHierarchyForVirtualizer: Room ${room.roomId} is neither text nor voice.`); + currentCategoryRooms.text.push(room); } }); - // After iterating through all items, process any remaining rooms collected under the last parent addCollectedRoomsToProcessed(currentParentId); return processed; }; -// --- Space Menu Component (Remains Unchanged) --- type SpaceMenuProps = { room: Room; requestClose: () => void; @@ -333,7 +296,6 @@ const SpaceMenu = forwardRef(({ room, requestClo ); }); -// --- Space Header Component (Remains Unchanged) --- function SpaceHeader() { const space = useSpace(); const spaceName = useRoomName(space); @@ -395,14 +357,13 @@ function SpaceHeader() { ); } -// --- Fixed Bottom Nav Area Component (Remains Unchanged) --- function FixedBottomNavArea() { const { sendWidgetAction, activeCallRoomId } = useCallState(); const mx = useMatrixClient(); const userName = mx.getUser(mx.getUserId() ?? '')?.displayName ?? mx.getUserId() ?? 'User'; const handleSendMessageClick = () => { - const action = 'my.custom.action'; // Replace with your actual action + const action = 'my.custom.action'; const data = { message: `Hello from ${userName}!` }; logger.debug(`FixedBottomNavArea: Sending action '${action}'`); sendWidgetAction(action, data) @@ -412,7 +373,7 @@ function FixedBottomNavArea() { const handleToggleMuteClick = () => { const action = WidgetApiToWidgetAction.SetAudioInputMuted; - const data = {}; // Sending empty data might imply toggle for some widgets + const data = {}; logger.debug(`FixedBottomNavArea: Sending action '${action}'`); sendWidgetAction(action, data) .then(() => logger.info(`FixedBottomNavArea: Action '${action}' sent.`)) @@ -425,7 +386,7 @@ function FixedBottomNavArea() { direction="Column" gap="200" padding="300" - style={{ flexShrink: 0, borderTop: `1px solid ${config?.color?.LineStrong ?? '#ccc'}` }} // Use theme color if possible + style={{ flexShrink: 0, borderTop: `1px solid ${config?.color?.LineStrong ?? '#ccc'}` }} > No active call @@ -436,7 +397,7 @@ function FixedBottomNavArea() { return ( - {mx.getRoom(activeCallRoomId)?.normalizedName} + {} ); } /* @@ -457,10 +418,9 @@ function FixedBottomNavArea() { */ -// --- Main Space Component (Updated Rendering Logic) --- export function Space() { const mx = useMatrixClient(); - const space = useSpace(); // The current top-level space being viewed + const space = useSpace(); useNavToActivePathMapper(space.roomId); const spaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId); const scrollRef = useRef(null); @@ -474,10 +434,8 @@ export function Space() { const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias); const searchSelected = useSpaceSearchSelected(spaceIdOrAlias); - // State for managing collapsed categories (includes spaces and channel types) const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom()); - // Memoized callback to get room objects const getRoom = useCallback( (rId: string): Room | undefined => { if (allJoinedRooms.has(rId)) { @@ -488,75 +446,53 @@ export function Space() { [mx, allJoinedRooms] ); - // Fetch the raw hierarchy using the hook - // Note: The filtering callbacks passed here primarily affect *which* rooms/spaces - // are included in the raw list *before* processing. const hierarchy = useSpaceJoinedHierarchy( space.roomId, getRoom, - // isRoomHidden callback: Hides room if parent space category is closed, unless room is unread/selected. useCallback( (parentId, roomId) => { - // Generate the category ID for the parent *space* const parentSpaceCategoryId = makeSpaceNavCategoryId(space.roomId, parentId); - // If the parent space category is not closed, the room is not hidden by this rule. if (!closedCategories.has(parentSpaceCategoryId)) { return false; } - // Parent space is closed. Hide the room unless it's unread or currently selected. const showRoomAnyway = roomToUnread.has(roomId) || roomId === selectedRoomId; - return !showRoomAnyway; // Return true to hide, false to show + return !showRoomAnyway; }, - [space.roomId, closedCategories, roomToUnread, selectedRoomId] // Dependencies + [space.roomId, closedCategories, roomToUnread, selectedRoomId] ), - // isSubCategoryClosed callback: Checks if a *space* subcategory is closed. + useCallback( (subCategoryId) => closedCategories.has(makeSpaceNavCategoryId(space.roomId, subCategoryId)), - [closedCategories, space.roomId] // Dependencies + [closedCategories, space.roomId] ) ); - // Process the raw hierarchy into a list with collapsible channel headers const processedHierarchy = useMemo( - () => - processHierarchyForVirtualizer( - hierarchy, - mx, - space.roomId, - closedCategories // Pass closed state to the processing function - ), - [hierarchy, mx, space.roomId, closedCategories] // Dependencies for memoization + () => processHierarchyForVirtualizer(hierarchy, mx, space.roomId, closedCategories), + [hierarchy, mx, space.roomId, closedCategories] ); - // Setup the virtualizer with the processed list const virtualizer = useVirtualizer({ count: processedHierarchy.length, getScrollElement: () => scrollRef.current, - estimateSize: () => 32, // Adjust based on average item height - overscan: 10, // Render items slightly outside the viewport + estimateSize: () => 32, + overscan: 10, }); - // Click handler for toggling category collapse state (works for spaces and channel types) const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => closedCategories.has(categoryId) ); - // Function to generate navigation links for rooms const getToLink = (roomId: string) => getSpaceRoomPath(spaceIdOrAlias, getCanonicalAliasOrRoomId(mx, roomId)); - // --- Render --- return ( - {/* Fixed Header */} - - {/* Scrollable Content Area */} - {/* Static Top Links (Lobby, Search) */} @@ -593,38 +529,29 @@ export function Space() { - - {/* Virtualized List Area */} {virtualizer.getVirtualItems().map((vItem) => { const item = processedHierarchy[vItem.index]; - if (!item) return null; // Should not happen with correct processing - - // --- Render Logic based on Item Type --- + if (!item) return null; const renderContent = () => { switch (item.type) { - // Render a main space category header (for nested spaces) case 'category': { - // item has: room, categoryId, key const { room, categoryId } = item; - // Determine name: Use the room name for nested spaces const name = room.name; - // Add padding above subsequent categories - // Removed index === 0 check as root category is gone const paddingTop = config?.space?.S400 ?? '1rem'; return (
{name} @@ -632,21 +559,16 @@ export function Space() {
); } - // Render a collapsible header for Text or Voice channels case 'channel_header': { - // item has: title, categoryId, key const { title, categoryId } = item; return ( - // Add indentation and padding for visual hierarchy - {' '} - {/* Use subtle variant if available */} {title} @@ -654,12 +576,9 @@ export function Space() { ); } - // Render a regular room item (text or voice channel) case 'room': { - // item has: room, key const { room } = item; return ( - // Add indentation for rooms under headers {renderContent()} @@ -691,8 +608,6 @@ export function Space() { })}
- - {/* Fixed Bottom Section (Remains Unchanged) */}
);