diff --git a/src/app/features/desk/DeskEntry.tsx b/src/app/features/desk/DeskEntry.tsx index a36ec8e4..8eab087f 100644 --- a/src/app/features/desk/DeskEntry.tsx +++ b/src/app/features/desk/DeskEntry.tsx @@ -1,16 +1,27 @@ import React from 'react'; -import { Link } from 'react-router-dom'; -import { Text } from 'folds'; +import { Box, Icon, Icons, Text } from 'folds'; +import { NavGutter, NavItem, NavItemContent, NavLink } from '../../components/nav'; +import { NAV_ICON_REST_OPACITY, NavLabel } from '../../components/nav/styles.css'; import { getDeskPath } from '../../pages/pathUtils'; import { deskFor } from './source'; -/** The way into a Mind's desk, set in its navigator group header; nothing at all when nothing is open. */ +/** A Mind's desk, pinned at the top of its navigator group: always there, with the open count when there is one. */ export function DeskEntry({ mind }: { mind: string }) { const open = deskFor(mind)?.open_count ?? 0; - if (open === 0) return null; return ( - - {`Desk · ${open} open`} - + + + + + + + + + {open > 0 ? `Desk · ${open} open` : 'Desk'} + + + + + ); } diff --git a/src/app/features/desk/DeskView.test.tsx b/src/app/features/desk/DeskView.test.tsx index ede59229..ca10ea66 100644 --- a/src/app/features/desk/DeskView.test.tsx +++ b/src/app/features/desk/DeskView.test.tsx @@ -139,12 +139,14 @@ describe('DeskEntry', () => { setDeskSource(() => ({ ...STATES.empty, open_count: 5 })); expect(entry('void')).toContain('Desk · 5 open'); }); - it('is absent at zero open, when nothing is published, and when only settled forks exist', () => { - setDeskSource(() => STATES.empty); - expect(entry('void')).toBe(''); - setDeskSource(() => undefined); - expect(entry('void')).toBe(''); - setDeskSource(() => ({ ...STATES.settled, open_count: 0 })); - expect(entry('void')).toBe(''); + it('is pinned even at zero open, when nothing is published, and when only settled forks exist', () => { + const bare = (html: string) => html.replace(/<[^>]*>/g, ''); + const states = [STATES.empty, undefined, { ...STATES.settled, open_count: 0 }]; + states.forEach((state) => { + setDeskSource(() => state); + const html = entry('void'); + expect(bare(html)).toBe('Desk'); + expect(html).toContain('href="/desk/void"'); + }); }); }); diff --git a/src/app/features/estate/EstateNavigator.tsx b/src/app/features/estate/EstateNavigator.tsx index 8066498d..c6285bc8 100644 --- a/src/app/features/estate/EstateNavigator.tsx +++ b/src/app/features/estate/EstateNavigator.tsx @@ -29,6 +29,8 @@ import { _RoomSearchParams } from '../../pages/paths'; import { searchModalAtom } from '../../state/searchModal'; import { getCanonicalAliasOrRoomId } from '../../utils/matrix'; import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useClientConfig } from '../../hooks/useClientConfig'; +import { isFileZone } from '../albums/isFileZone'; import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; import { useCallEmbed } from '../../hooks/useCallEmbed'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; @@ -140,11 +142,9 @@ type CollapsibleProps = { categoryId: string; label: string; rooms: EstateRoom[]; - /** Sits in the header beside the label, so a collapsed section still shows it. */ - trailing?: ReactNode; children: ReactNode; }; -function Collapsible({ categoryId, label, rooms, trailing, children }: CollapsibleProps) { +function Collapsible({ categoryId, label, rooms, children }: CollapsibleProps) { const mobile = useScreenSizeContext() === ScreenSize.Mobile; const selectedRoomId = useSelectedRoom(); const roomToUnread = useAtomValue(roomToUnreadAtom); @@ -168,22 +168,44 @@ function Collapsible({ categoryId, label, rooms, trailing, children }: Collapsib {label} - {trailing} {!closed && {children}} ); } +/** The Mind's media pages (file-zone rooms) first, then its other rooms in their given order. */ +function useMediaFirst(rooms: EstateRoom[]): EstateRoom[] { + const mx = useMatrixClient(); + const clientConfig = useClientConfig(); + return useMemo(() => { + const isMedia = (r: EstateRoom) => { + const mxRoom = mx.getRoom(r.roomId); + return ( + !!mxRoom && + isFileZone( + { + roomId: r.roomId, + aliases: [mxRoom.getCanonicalAlias(), ...mxRoom.getAltAliases()].filter( + (a): a is string => !!a + ), + createType: mxRoom.currentState.getStateEvents('m.room.create', '')?.getContent().type, + }, + clientConfig + ) + ); + }; + const media = rooms.filter(isMedia); + return [...media, ...rooms.filter((r) => !media.includes(r))]; + }, [mx, clientConfig, rooms]); +} + function GroupSection({ tierId, group }: { tierId: string; group: EstateGroup }) { + const rooms = useMediaFirst(group.rooms); return ( - } - > - {group.rooms.map((r) => ( + + + {rooms.map((r) => ( ))}