Navigator: icon weights, solid unread dot, Media row detects by room type #97
6 changed files with 69 additions and 9 deletions
|
|
@ -32,10 +32,10 @@ export const NavLevel = style({
|
|||
paddingLeft: LEVEL_STEP,
|
||||
});
|
||||
|
||||
// Chevrons and room icons are matched in weight, so a foldout is not outweighed by its rooms.
|
||||
export const NAV_ICON_REST_OPACITY = 0.6;
|
||||
// Chevrons and room icons are matched in measured ink (a room glyph is about 1.7x a chevron's; unread rooms stay far above), so a foldout is not outweighed by its rooms.
|
||||
export const NAV_ICON_REST_OPACITY = 0.9;
|
||||
// A room's glyph is bigger and denser than the chevron, so it rests lighter to read as the same weight.
|
||||
export const NAV_ROOM_ICON_REST_OPACITY = 0.42;
|
||||
export const NAV_ROOM_ICON_REST_OPACITY = 0.26;
|
||||
export const NAV_ICON_UNREAD_OPACITY = 0.9;
|
||||
|
||||
export const NavCategory = style([
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ export function UnreadBadge({ highlight, count }: UnreadBadgeProps) {
|
|||
<Badge
|
||||
variant={highlight ? 'Success' : 'Secondary'}
|
||||
size={count > 0 ? '400' : '200'}
|
||||
// A mention is the one solid pill; plain unread is a soft tint, so a row's count is never its loudest part.
|
||||
fill={highlight ? 'Solid' : 'Soft'}
|
||||
// A mention and the bare unread dot are solid (a soft dot measured 1.6:1 against the ground); a plain unread
|
||||
// count is a soft tint, so a row's count is never its loudest part.
|
||||
fill={highlight || count === 0 ? 'Solid' : 'Soft'}
|
||||
radii="Pill"
|
||||
outlined={false}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ function MindMedia({ media }: { media: MediaRowState }) {
|
|||
}
|
||||
|
||||
function GroupSection({ tierId, group }: { tierId: string; group: EstateGroup }) {
|
||||
const media = useMindMedia(group.mind);
|
||||
const media = useMindMedia(group.mind, group.rooms);
|
||||
const hasMedia = MINDS.includes(group.mind);
|
||||
// The media room is the Media row; it is not listed a second time among the Mind's rooms.
|
||||
const rooms =
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { useAtomValue } from 'jotai';
|
|||
import { Avatar, Box, Icon, Icons, Text } from 'folds';
|
||||
import { NavGutter, NavItem, NavItemContent, NavLink } from '../../components/nav';
|
||||
import { NAV_ROOM_ICON_REST_OPACITY, NavLabel } from '../../components/nav/styles.css';
|
||||
import { EstateRoom } from './buildEstate';
|
||||
import { isFileZone } from '../albums/isFileZone';
|
||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
||||
import { MINDS } from '../../components/message/pose/poses';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||
|
|
@ -15,6 +18,7 @@ import {
|
|||
lookupMediaRoom,
|
||||
mediaRoomIdFromStewardships,
|
||||
mediaRow,
|
||||
typedMediaRoom,
|
||||
} from './mediaRoom';
|
||||
|
||||
const mediaIcon = (
|
||||
|
|
@ -58,12 +62,31 @@ export function MediaLink({ to }: { to: string }) {
|
|||
}
|
||||
|
||||
/** The Mind's media room as the navigator sees it right now. */
|
||||
export const useMindMedia = (mind: string): MediaRowState => {
|
||||
export const useMindMedia = (mind: string, rooms: readonly EstateRoom[]): MediaRowState => {
|
||||
const mx = useMatrixClient();
|
||||
const { rootId } = useEstateRootId();
|
||||
const joined = useAtomValue(allRoomsAtom);
|
||||
const invited = useAtomValue(allInvitesAtom);
|
||||
const config = useClientConfig();
|
||||
const server = mx.getDomain() ?? '';
|
||||
// A joined room whose type says it is a file zone is the Media row; the alias or carried id only finds a
|
||||
// room the client cannot type yet (not joined).
|
||||
const typed = typedMediaRoom(rooms, (roomId) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
return (
|
||||
!!room &&
|
||||
isFileZone(
|
||||
{
|
||||
roomId,
|
||||
aliases: [room.getCanonicalAlias(), ...room.getAltAliases()].filter(
|
||||
(a): a is string => !!a
|
||||
),
|
||||
createType: room.currentState.getStateEvents('m.room.create', '')?.getContent().type,
|
||||
},
|
||||
config
|
||||
)
|
||||
);
|
||||
});
|
||||
const carried = mediaRoomIdFromStewardships(
|
||||
rootId
|
||||
? mx.getRoom(rootId)?.currentState.getStateEvents(STEWARDSHIPS_KEY, '')?.getContent()
|
||||
|
|
@ -74,10 +97,11 @@ export const useMindMedia = (mind: string): MediaRowState => {
|
|||
queryKey: ['mind-media', mind, server, carried],
|
||||
queryFn: () => lookupMediaRoom(mx, mind, server, carried),
|
||||
staleTime: 60_000,
|
||||
enabled: MINDS.includes(mind),
|
||||
enabled: MINDS.includes(mind) && !typed,
|
||||
});
|
||||
const membership = new Map<string, 'join' | 'invite'>();
|
||||
invited.forEach((id) => membership.set(id, 'invite'));
|
||||
joined.forEach((id) => membership.set(id, 'join'));
|
||||
if (typed) return { kind: 'room', room: { ...typed, name: 'Media' } };
|
||||
return mediaRow(lookup.data, membership);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { lookupMediaRoom, mediaAlias, mediaRoomIdFromStewardships, mediaRow } from './mediaRoom';
|
||||
import {
|
||||
lookupMediaRoom,
|
||||
mediaAlias,
|
||||
mediaRoomIdFromStewardships,
|
||||
mediaRow,
|
||||
typedMediaRoom,
|
||||
} from './mediaRoom';
|
||||
|
||||
const notFound = Object.assign(new Error('Room alias not found'), {
|
||||
httpStatus: 404,
|
||||
|
|
@ -90,4 +96,23 @@ describe('the Media row of a Mind', () => {
|
|||
expect(mediaRow(undefined, new Map())).toEqual({ kind: 'checking' });
|
||||
expect(mediaRow({ found: false, reason: 'unset' }, new Map())).toEqual({ kind: 'unset' });
|
||||
});
|
||||
|
||||
it('takes the joined room whose type says file zone, whatever its alias, before any alias lookup', () => {
|
||||
const room = (roomId: string, state: 'joined' | 'joinable') => ({
|
||||
roomId,
|
||||
name: roomId,
|
||||
state,
|
||||
via: [],
|
||||
parentId: null,
|
||||
direct: false,
|
||||
});
|
||||
const rooms = [
|
||||
room('!chat', 'joined'),
|
||||
room('!old-files-slug', 'joined'),
|
||||
room('!unjoined', 'joinable'),
|
||||
];
|
||||
const zones = new Set(['!old-files-slug', '!unjoined']);
|
||||
expect(typedMediaRoom(rooms, (id) => zones.has(id))?.roomId).toBe('!old-files-slug');
|
||||
expect(typedMediaRoom(rooms, () => false)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ export const mediaRoomIdFromStewardships = (content: unknown, mind: string): str
|
|||
return typeof id === 'string' && id.startsWith('!') ? id : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* The Mind's media room among the rooms the client already knows: the first one whose room type (or the
|
||||
* fileZoneRooms config) says it is a file zone. The type is the authority; the alias is only how an unknown
|
||||
* room is found, below.
|
||||
*/
|
||||
export const typedMediaRoom = (
|
||||
rooms: readonly EstateRoom[],
|
||||
isZone: (roomId: string) => boolean
|
||||
): EstateRoom | undefined => rooms.find((r) => r.state === 'joined' && isZone(r.roomId));
|
||||
|
||||
type MediaClient = {
|
||||
getRoomIdForAlias(alias: string): Promise<{ room_id: string; servers?: string[] }>;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue