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) => (
))}