From 12286f57f7da0c89ef4b00bc127af55e44a51769 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Sun, 15 Mar 2026 19:23:22 +1100 Subject: [PATCH] feat: enhance room navigation with sub-room indicators and styling adjustments --- src/app/components/nav/styles.css.ts | 4 ++ src/app/features/room-nav/RoomNavItem.tsx | 14 ++-- .../features/room-nav/UnjoinedSubRoomItem.tsx | 29 ++++++-- src/app/pages/client/home/Home.tsx | 66 ++++++++++++------- src/app/pages/client/space/Space.tsx | 53 ++++++++++----- 5 files changed, 117 insertions(+), 49 deletions(-) diff --git a/src/app/components/nav/styles.css.ts b/src/app/components/nav/styles.css.ts index 48b2a4d..dbb1459 100644 --- a/src/app/components/nav/styles.css.ts +++ b/src/app/components/nav/styles.css.ts @@ -126,3 +126,7 @@ export const NavItemContent = style({ export const NavItemOptions = style({ paddingRight: config.space.S200, }); + +export const ThreadItem = style({ + minHeight: toRem(24), +}); diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index 53e6aea..ef6b2dc 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -23,6 +23,7 @@ import { import { useFocusWithin, useHover } from 'react-aria'; import FocusTrap from 'focus-trap-react'; import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav'; +import * as css from '../../components/nav/styles.css'; import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl, guessPerfectParent, getOrphanParents } from '../../utils/room'; @@ -279,6 +280,7 @@ type RoomNavItemProps = { notificationMode?: RoomNotificationMode; showAvatar?: boolean; direct?: boolean; + hideIcon?: boolean; }; export function RoomNavItem({ room, @@ -287,6 +289,7 @@ export function RoomNavItem({ direct, notificationMode, linkPath, + hideIcon, }: RoomNavItemProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); @@ -360,6 +363,7 @@ export function RoomNavItem({ return ( - - - {showAvatar ? ( + + {!hideIcon && ( + + {showAvatar ? ( )} - + + )} {room.name} diff --git a/src/app/features/room-nav/UnjoinedSubRoomItem.tsx b/src/app/features/room-nav/UnjoinedSubRoomItem.tsx index 3250fc2..bdd6248 100644 --- a/src/app/features/room-nav/UnjoinedSubRoomItem.tsx +++ b/src/app/features/room-nav/UnjoinedSubRoomItem.tsx @@ -1,6 +1,5 @@ import React, { useCallback } from 'react'; import { - Avatar, Box, Chip, Icon, @@ -28,13 +27,14 @@ function getServerFromRoomId(roomId: string): string { type UnjoinedSubRoomItemProps = { roomId: string; depth: number; + isLast?: boolean; }; /** * Displays an unjoined sub-room with a Join button. * Used when a parent room has sub-rooms that the user hasn't joined yet. */ -export function UnjoinedSubRoomItem({ roomId, depth }: UnjoinedSubRoomItemProps) { +export function UnjoinedSubRoomItem({ roomId, depth, isLast = false }: UnjoinedSubRoomItemProps) { const mx = useMatrixClient(); const viaServers = [getServerFromRoomId(roomId)].filter(Boolean); @@ -43,17 +43,32 @@ export function UnjoinedSubRoomItem({ roomId, depth }: UnjoinedSubRoomItemProps) ); const canJoin = joinState.status === AsyncStatus.Idle || joinState.status === AsyncStatus.Error; - const paddingLeft = depth > 0 ? `${depth * 24}px` : undefined; + const paddingLeft = depth > 0 ? '30px' : undefined; + + let treeIcon = ''; + if (depth > 0) { + treeIcon = isLast ? '╰' : '├'; + } return ( - - - + {treeIcon && ( + + {treeIcon} + + )} {roomId} diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx index 40dc70b..548d1ec 100644 --- a/src/app/pages/client/home/Home.tsx +++ b/src/app/pages/client/home/Home.tsx @@ -236,8 +236,8 @@ function HomeEmpty() { } type RoomListItem = - | { type: 'room'; roomId: string; room: Room; depth: number; parentId?: string } - | { type: 'unjoined-subroom'; roomId: string; depth: number; parentId: string }; + | { type: 'room'; roomId: string; room: Room; depth: number; parentId?: string; isLast?: boolean } + | { type: 'unjoined-subroom'; roomId: string; depth: number; parentId: string; isLast?: boolean }; const DEFAULT_CATEGORY_ID = makeNavCategoryId('home', 'room'); export function Home() { @@ -273,7 +273,7 @@ export function Home() { const items: RoomListItem[] = []; const processedRooms = new Set(); - const addRoomAndSubRooms = (roomId: string, depth: number = 0, parentId?: string) => { + const addRoomAndSubRooms = (roomId: string, depth: number = 0, parentId?: string, isLast: boolean = false) => { if (processedRooms.has(roomId)) return; // Prevent infinite loops processedRooms.add(roomId); @@ -281,19 +281,19 @@ export function Home() { if (room) { // Joined room - show normally - items.push({ type: 'room', roomId, room, depth, parentId }); + items.push({ type: 'room', roomId, room, depth, parentId, isLast }); // Get sub-rooms from room state const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, ''); const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? []; // Add sub-rooms recursively - subRooms.forEach(subRoomId => { - addRoomAndSubRooms(subRoomId, depth + 1, roomId); + subRooms.forEach((subRoomId, index) => { + addRoomAndSubRooms(subRoomId, depth + 1, roomId, index === subRooms.length - 1); }); } else if (parentId) { // Unjoined sub-room - show with join button - items.push({ type: 'unjoined-subroom', roomId, depth, parentId }); + items.push({ type: 'unjoined-subroom', roomId, depth, parentId, isLast }); } }; @@ -321,7 +321,7 @@ export function Home() { const virtualizer = useVirtualizer({ count: listItems.length, getScrollElement: () => scrollRef.current, - estimateSize: () => 38, + estimateSize: () => 32, overscan: 10, }); @@ -336,7 +336,7 @@ export function Home() { ) : ( - + navigate(getHomeCreatePath())}> @@ -378,7 +378,9 @@ export function Home() { onCancel={() => setOpen(false)} onOpen={(roomIdOrAlias, viaServers, eventId) => { setOpen(false); - const searchParams: _RoomSearchParams = { viaServers, eventId }; + const searchParams: _RoomSearchParams = { + viaServers: viaServers?.join(',') + }; navigate( withSearchParam( getHomeRoomPath(roomIdOrAlias), @@ -435,13 +437,18 @@ export function Home() { key={vItem.index} ref={virtualizer.measureElement} > - + ); } const selected = selectedRoomId === item.roomId; - const paddingLeft = item.depth > 0 ? `${item.depth * 24}px` : undefined; + const paddingLeft = item.depth > 0 ? '30px' : undefined; + + let treeIcon = ''; + if (item.depth > 0) { + treeIcon = item.isLast ? '╰' : '├'; + } return ( -
- +
0 ? '1.5rem' : undefined }}> + {treeIcon && ( + + {treeIcon} + + )} +
+ 0} + linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, item.roomId))} + notificationMode={getRoomNotificationMode( + notificationPreferences, + item.room.roomId + )} + /> +
); diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 6858e89..a2d41ae 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -445,8 +445,8 @@ export function Space() { // Flatten hierarchy to include sub-rooms type HierarchyItem = | { type: 'hierarchy'; roomId: string } - | { type: 'subroom'; roomId: string; room: Room; depth: number } - | { type: 'unjoined-subroom'; roomId: string; depth: number }; + | { type: 'subroom'; roomId: string; room: Room; depth: number; isLast?: boolean } + | { type: 'unjoined-subroom'; roomId: string; depth: number; isLast?: boolean }; const flattenedHierarchy = useMemo(() => { const items: HierarchyItem[] = []; @@ -459,19 +459,20 @@ export function Space() { const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, ''); const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? []; - subRooms.forEach(subRoomId => { + subRooms.forEach((subRoomId, index) => { if (processedSubRooms.has(subRoomId)) return; processedSubRooms.add(subRoomId); + const isLast = index === subRooms.length - 1; const subRoom = mx.getRoom(subRoomId); if (subRoom) { // Joined sub-room - items.push({ type: 'subroom', roomId: subRoomId, room: subRoom, depth: baseDepth + 1 }); + items.push({ type: 'subroom', roomId: subRoomId, room: subRoom, depth: baseDepth + 1, isLast }); // Recursively add sub-rooms of sub-rooms addSubRooms(subRoomId, baseDepth + 1); } else { // Unjoined sub-room - show with join button - items.push({ type: 'unjoined-subroom', roomId: subRoomId, depth: baseDepth + 1 }); + items.push({ type: 'unjoined-subroom', roomId: subRoomId, depth: baseDepth + 1, isLast }); } }); }; @@ -562,26 +563,46 @@ export function Space() { if (item.type === 'unjoined-subroom') { return ( - + ); } // Sub-room item (nested under parent) if (item.type === 'subroom') { - const paddingLeft = `${item.depth * 24}px`; + const paddingLeft = '30px'; + + let treeIcon = ''; + if (item.depth > 0) { + treeIcon = item.isLast ? '╰' : '├'; + } return ( -
- +
+ {treeIcon && ( + + {treeIcon} + + )} +
+ 0} + showAvatar={mDirects.has(item.roomId)} + direct={mDirects.has(item.roomId)} + linkPath={getToLink(item.roomId)} + notificationMode={getRoomNotificationMode(notificationPreferences, item.room.roomId)} + /> +
);