feat: enhance room navigation with sub-room indicators and styling adjustments
This commit is contained in:
@@ -126,3 +126,7 @@ export const NavItemContent = style({
|
|||||||
export const NavItemOptions = style({
|
export const NavItemOptions = style({
|
||||||
paddingRight: config.space.S200,
|
paddingRight: config.space.S200,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ThreadItem = style({
|
||||||
|
minHeight: toRem(24),
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { useFocusWithin, useHover } from 'react-aria';
|
import { useFocusWithin, useHover } from 'react-aria';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
||||||
|
import * as css from '../../components/nav/styles.css';
|
||||||
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
||||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||||
import { getDirectRoomAvatarUrl, getRoomAvatarUrl, guessPerfectParent, getOrphanParents } from '../../utils/room';
|
import { getDirectRoomAvatarUrl, getRoomAvatarUrl, guessPerfectParent, getOrphanParents } from '../../utils/room';
|
||||||
@@ -279,6 +280,7 @@ type RoomNavItemProps = {
|
|||||||
notificationMode?: RoomNotificationMode;
|
notificationMode?: RoomNotificationMode;
|
||||||
showAvatar?: boolean;
|
showAvatar?: boolean;
|
||||||
direct?: boolean;
|
direct?: boolean;
|
||||||
|
hideIcon?: boolean;
|
||||||
};
|
};
|
||||||
export function RoomNavItem({
|
export function RoomNavItem({
|
||||||
room,
|
room,
|
||||||
@@ -287,6 +289,7 @@ export function RoomNavItem({
|
|||||||
direct,
|
direct,
|
||||||
notificationMode,
|
notificationMode,
|
||||||
linkPath,
|
linkPath,
|
||||||
|
hideIcon,
|
||||||
}: RoomNavItemProps) {
|
}: RoomNavItemProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
@@ -360,6 +363,7 @@ export function RoomNavItem({
|
|||||||
return (
|
return (
|
||||||
<Box direction="Column" style={{ width: '100%' }}>
|
<Box direction="Column" style={{ width: '100%' }}>
|
||||||
<NavItem
|
<NavItem
|
||||||
|
className={hideIcon ? css.ThreadItem : undefined}
|
||||||
variant="Background"
|
variant="Background"
|
||||||
radii="400"
|
radii="400"
|
||||||
highlight={unread !== undefined}
|
highlight={unread !== undefined}
|
||||||
@@ -371,9 +375,10 @@ export function RoomNavItem({
|
|||||||
>
|
>
|
||||||
<NavLink to={linkPath}>
|
<NavLink to={linkPath}>
|
||||||
<NavItemContent>
|
<NavItemContent>
|
||||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
<Box as="span" grow="Yes" alignItems="Center" gap={hideIcon ? "100" : "200"}>
|
||||||
<Avatar size="200" radii="400">
|
{!hideIcon && (
|
||||||
{showAvatar ? (
|
<Avatar size="200" radii="400">
|
||||||
|
{showAvatar ? (
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
roomId={room.roomId}
|
roomId={room.roomId}
|
||||||
src={
|
src={
|
||||||
@@ -396,7 +401,8 @@ export function RoomNavItem({
|
|||||||
joinRule={room.getJoinRule()}
|
joinRule={room.getJoinRule()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
)}
|
||||||
<Box as="span" grow="Yes" direction="Column" style={{ overflow: 'hidden', minWidth: 0 }}>
|
<Box as="span" grow="Yes" direction="Column" style={{ overflow: 'hidden', minWidth: 0 }}>
|
||||||
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
|
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
|
||||||
{room.name}
|
{room.name}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
|
||||||
Box,
|
Box,
|
||||||
Chip,
|
Chip,
|
||||||
Icon,
|
Icon,
|
||||||
@@ -28,13 +27,14 @@ function getServerFromRoomId(roomId: string): string {
|
|||||||
type UnjoinedSubRoomItemProps = {
|
type UnjoinedSubRoomItemProps = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
depth: number;
|
depth: number;
|
||||||
|
isLast?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays an unjoined sub-room with a Join button.
|
* Displays an unjoined sub-room with a Join button.
|
||||||
* Used when a parent room has sub-rooms that the user hasn't joined yet.
|
* 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 mx = useMatrixClient();
|
||||||
const viaServers = [getServerFromRoomId(roomId)].filter(Boolean);
|
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 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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{ paddingLeft, padding: `${config.space.S100} ${config.space.S200}` }}
|
style={{ paddingLeft, padding: `${config.space.S100} ${config.space.S200}`, minHeight: '1.5rem' }}
|
||||||
alignItems="Center"
|
alignItems="Center"
|
||||||
gap="200"
|
gap="200"
|
||||||
>
|
>
|
||||||
<Avatar size="200" radii="400">
|
{treeIcon && (
|
||||||
<Icon src={Icons.Hash} size="100" />
|
<span style={{
|
||||||
</Avatar>
|
paddingRight: '2px',
|
||||||
|
opacity: 0.5,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1',
|
||||||
|
userSelect: 'none',
|
||||||
|
marginLeft: '-10px'
|
||||||
|
}}>
|
||||||
|
{treeIcon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<Box grow="Yes" alignItems="Center" gap="100">
|
<Box grow="Yes" alignItems="Center" gap="100">
|
||||||
<Text size="T300" truncate style={{ opacity: 0.7 }}>
|
<Text size="T300" truncate style={{ opacity: 0.7 }}>
|
||||||
{roomId}
|
{roomId}
|
||||||
|
|||||||
@@ -236,8 +236,8 @@ function HomeEmpty() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RoomListItem =
|
type RoomListItem =
|
||||||
| { type: 'room'; roomId: string; room: Room; 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 };
|
| { type: 'unjoined-subroom'; roomId: string; depth: number; parentId: string; isLast?: boolean };
|
||||||
|
|
||||||
const DEFAULT_CATEGORY_ID = makeNavCategoryId('home', 'room');
|
const DEFAULT_CATEGORY_ID = makeNavCategoryId('home', 'room');
|
||||||
export function Home() {
|
export function Home() {
|
||||||
@@ -273,7 +273,7 @@ export function Home() {
|
|||||||
const items: RoomListItem[] = [];
|
const items: RoomListItem[] = [];
|
||||||
const processedRooms = new Set<string>();
|
const processedRooms = new Set<string>();
|
||||||
|
|
||||||
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
|
if (processedRooms.has(roomId)) return; // Prevent infinite loops
|
||||||
processedRooms.add(roomId);
|
processedRooms.add(roomId);
|
||||||
|
|
||||||
@@ -281,19 +281,19 @@ export function Home() {
|
|||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
// Joined room - show normally
|
// 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
|
// Get sub-rooms from room state
|
||||||
const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, '');
|
const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, '');
|
||||||
const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? [];
|
const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? [];
|
||||||
|
|
||||||
// Add sub-rooms recursively
|
// Add sub-rooms recursively
|
||||||
subRooms.forEach(subRoomId => {
|
subRooms.forEach((subRoomId, index) => {
|
||||||
addRoomAndSubRooms(subRoomId, depth + 1, roomId);
|
addRoomAndSubRooms(subRoomId, depth + 1, roomId, index === subRooms.length - 1);
|
||||||
});
|
});
|
||||||
} else if (parentId) {
|
} else if (parentId) {
|
||||||
// Unjoined sub-room - show with join button
|
// 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({
|
const virtualizer = useVirtualizer({
|
||||||
count: listItems.length,
|
count: listItems.length,
|
||||||
getScrollElement: () => scrollRef.current,
|
getScrollElement: () => scrollRef.current,
|
||||||
estimateSize: () => 38,
|
estimateSize: () => 32,
|
||||||
overscan: 10,
|
overscan: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,7 +336,7 @@ export function Home() {
|
|||||||
<HomeEmpty />
|
<HomeEmpty />
|
||||||
) : (
|
) : (
|
||||||
<PageNavContent scrollRef={scrollRef}>
|
<PageNavContent scrollRef={scrollRef}>
|
||||||
<Box direction="Column" gap="300">
|
<Box direction="Column" gap="100">
|
||||||
<NavCategory>
|
<NavCategory>
|
||||||
<NavItem variant="Background" radii="400" aria-selected={createRoomSelected}>
|
<NavItem variant="Background" radii="400" aria-selected={createRoomSelected}>
|
||||||
<NavButton onClick={() => navigate(getHomeCreatePath())}>
|
<NavButton onClick={() => navigate(getHomeCreatePath())}>
|
||||||
@@ -378,7 +378,9 @@ export function Home() {
|
|||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
onOpen={(roomIdOrAlias, viaServers, eventId) => {
|
onOpen={(roomIdOrAlias, viaServers, eventId) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
const searchParams: _RoomSearchParams = { viaServers, eventId };
|
const searchParams: _RoomSearchParams = {
|
||||||
|
viaServers: viaServers?.join(',')
|
||||||
|
};
|
||||||
navigate(
|
navigate(
|
||||||
withSearchParam(
|
withSearchParam(
|
||||||
getHomeRoomPath(roomIdOrAlias),
|
getHomeRoomPath(roomIdOrAlias),
|
||||||
@@ -435,13 +437,18 @@ export function Home() {
|
|||||||
key={vItem.index}
|
key={vItem.index}
|
||||||
ref={virtualizer.measureElement}
|
ref={virtualizer.measureElement}
|
||||||
>
|
>
|
||||||
<UnjoinedSubRoomItem roomId={item.roomId} depth={item.depth} />
|
<UnjoinedSubRoomItem roomId={item.roomId} depth={item.depth} isLast={item.isLast} />
|
||||||
</VirtualTile>
|
</VirtualTile>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = selectedRoomId === item.roomId;
|
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 (
|
return (
|
||||||
<VirtualTile
|
<VirtualTile
|
||||||
@@ -449,16 +456,31 @@ export function Home() {
|
|||||||
key={vItem.index}
|
key={vItem.index}
|
||||||
ref={virtualizer.measureElement}
|
ref={virtualizer.measureElement}
|
||||||
>
|
>
|
||||||
<div style={{ paddingLeft }}>
|
<div style={{ paddingLeft, display: 'flex', alignItems: 'center', minHeight: item.depth > 0 ? '1.5rem' : undefined }}>
|
||||||
<RoomNavItem
|
{treeIcon && (
|
||||||
room={item.room}
|
<span style={{
|
||||||
selected={selected}
|
paddingRight: '2px',
|
||||||
linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, item.roomId))}
|
opacity: 0.5,
|
||||||
notificationMode={getRoomNotificationMode(
|
fontFamily: 'monospace',
|
||||||
notificationPreferences,
|
fontSize: '14px',
|
||||||
item.room.roomId
|
lineHeight: '1',
|
||||||
)}
|
userSelect: 'none'
|
||||||
/>
|
}}>
|
||||||
|
{treeIcon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<RoomNavItem
|
||||||
|
room={item.room}
|
||||||
|
selected={selected}
|
||||||
|
hideIcon={item.depth > 0}
|
||||||
|
linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, item.roomId))}
|
||||||
|
notificationMode={getRoomNotificationMode(
|
||||||
|
notificationPreferences,
|
||||||
|
item.room.roomId
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VirtualTile>
|
</VirtualTile>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -445,8 +445,8 @@ export function Space() {
|
|||||||
// Flatten hierarchy to include sub-rooms
|
// Flatten hierarchy to include sub-rooms
|
||||||
type HierarchyItem =
|
type HierarchyItem =
|
||||||
| { type: 'hierarchy'; roomId: string }
|
| { type: 'hierarchy'; roomId: string }
|
||||||
| { type: 'subroom'; roomId: string; room: Room; depth: number }
|
| { type: 'subroom'; roomId: string; room: Room; depth: number; isLast?: boolean }
|
||||||
| { type: 'unjoined-subroom'; roomId: string; depth: number };
|
| { type: 'unjoined-subroom'; roomId: string; depth: number; isLast?: boolean };
|
||||||
|
|
||||||
const flattenedHierarchy = useMemo(() => {
|
const flattenedHierarchy = useMemo(() => {
|
||||||
const items: HierarchyItem[] = [];
|
const items: HierarchyItem[] = [];
|
||||||
@@ -459,19 +459,20 @@ export function Space() {
|
|||||||
const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, '');
|
const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, '');
|
||||||
const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? [];
|
const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? [];
|
||||||
|
|
||||||
subRooms.forEach(subRoomId => {
|
subRooms.forEach((subRoomId, index) => {
|
||||||
if (processedSubRooms.has(subRoomId)) return;
|
if (processedSubRooms.has(subRoomId)) return;
|
||||||
processedSubRooms.add(subRoomId);
|
processedSubRooms.add(subRoomId);
|
||||||
|
|
||||||
|
const isLast = index === subRooms.length - 1;
|
||||||
const subRoom = mx.getRoom(subRoomId);
|
const subRoom = mx.getRoom(subRoomId);
|
||||||
if (subRoom) {
|
if (subRoom) {
|
||||||
// Joined sub-room
|
// 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
|
// Recursively add sub-rooms of sub-rooms
|
||||||
addSubRooms(subRoomId, baseDepth + 1);
|
addSubRooms(subRoomId, baseDepth + 1);
|
||||||
} else {
|
} else {
|
||||||
// Unjoined sub-room - show with join button
|
// 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') {
|
if (item.type === 'unjoined-subroom') {
|
||||||
return (
|
return (
|
||||||
<VirtualTile virtualItem={vItem} key={vItem.index} ref={virtualizer.measureElement}>
|
<VirtualTile virtualItem={vItem} key={vItem.index} ref={virtualizer.measureElement}>
|
||||||
<UnjoinedSubRoomItem roomId={item.roomId} depth={item.depth} />
|
<UnjoinedSubRoomItem roomId={item.roomId} depth={item.depth} isLast={item.isLast} />
|
||||||
</VirtualTile>
|
</VirtualTile>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-room item (nested under parent)
|
// Sub-room item (nested under parent)
|
||||||
if (item.type === 'subroom') {
|
if (item.type === 'subroom') {
|
||||||
const paddingLeft = `${item.depth * 24}px`;
|
const paddingLeft = '30px';
|
||||||
|
|
||||||
|
let treeIcon = '';
|
||||||
|
if (item.depth > 0) {
|
||||||
|
treeIcon = item.isLast ? '╰' : '├';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualTile virtualItem={vItem} key={vItem.index} ref={virtualizer.measureElement}>
|
<VirtualTile virtualItem={vItem} key={vItem.index} ref={virtualizer.measureElement}>
|
||||||
<div style={{ paddingLeft }}>
|
<div style={{ paddingLeft, display: 'flex', alignItems: 'center', minHeight: '1.5rem' }}>
|
||||||
<RoomNavItem
|
{treeIcon && (
|
||||||
room={item.room}
|
<span style={{
|
||||||
selected={selectedRoomId === item.roomId}
|
paddingRight: '2px',
|
||||||
showAvatar={mDirects.has(item.roomId)}
|
opacity: 0.5,
|
||||||
direct={mDirects.has(item.roomId)}
|
fontFamily: 'monospace',
|
||||||
linkPath={getToLink(item.roomId)}
|
fontSize: '14px',
|
||||||
notificationMode={getRoomNotificationMode(notificationPreferences, item.room.roomId)}
|
lineHeight: '1',
|
||||||
/>
|
userSelect: 'none'
|
||||||
|
}}>
|
||||||
|
{treeIcon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<RoomNavItem
|
||||||
|
room={item.room}
|
||||||
|
selected={selectedRoomId === item.roomId}
|
||||||
|
hideIcon={item.depth > 0}
|
||||||
|
showAvatar={mDirects.has(item.roomId)}
|
||||||
|
direct={mDirects.has(item.roomId)}
|
||||||
|
linkPath={getToLink(item.roomId)}
|
||||||
|
notificationMode={getRoomNotificationMode(notificationPreferences, item.room.roomId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VirtualTile>
|
</VirtualTile>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user