Navigator: Desk and media pages pinned at the top of each Mind foldout #94
3 changed files with 60 additions and 25 deletions
|
|
@ -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 (
|
||||
<Text as={Link} to={getDeskPath(mind)} size="T200" priority="300">
|
||||
{`Desk · ${open} open`}
|
||||
</Text>
|
||||
<NavItem variant="Background" radii="400">
|
||||
<NavLink to={getDeskPath(mind)}>
|
||||
<NavItemContent>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||
<NavGutter>
|
||||
<Icon src={Icons.Bookmark} size="100" style={{ opacity: NAV_ICON_REST_OPACITY }} />
|
||||
</NavGutter>
|
||||
<Text className={NavLabel} as="span" size="Inherit" truncate>
|
||||
{open > 0 ? `Desk · ${open} open` : 'Desk'}
|
||||
</Text>
|
||||
</Box>
|
||||
</NavItemContent>
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<RoomNavCategoryButton closed={closed} onClick={handleClick}>
|
||||
{label}
|
||||
</RoomNavCategoryButton>
|
||||
{trailing}
|
||||
</NavCategoryHeader>
|
||||
{!closed && <NavLevel>{children}</NavLevel>}
|
||||
</NavCategory>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 (
|
||||
<Collapsible
|
||||
categoryId={`${tierId}|${group.mind}`}
|
||||
label={group.label}
|
||||
rooms={group.rooms}
|
||||
trailing={<DeskEntry mind={group.mind} />}
|
||||
>
|
||||
{group.rooms.map((r) => (
|
||||
<Collapsible categoryId={`${tierId}|${group.mind}`} label={group.label} rooms={group.rooms}>
|
||||
<DeskEntry mind={group.mind} />
|
||||
{rooms.map((r) => (
|
||||
<EstateRow key={r.roomId} room={r} />
|
||||
))}
|
||||
</Collapsible>
|
||||
|
|
|
|||
Loading…
Reference in a new issue