From d226ca45fbc0302e7c321bbcd28807b6a3341b8a Mon Sep 17 00:00:00 2001 From: Void Date: Wed, 11 Nov 2026 17:12:57 +0000 Subject: [PATCH] Media row: a joined file-zone room (by type or fileZoneRooms) is the row; alias or carried id only finds an unjoined one Co-Authored-By: Claude Sonnet 5.5 --- src/app/features/estate/EstateNavigator.tsx | 2 +- src/app/features/estate/MediaRow.tsx | 28 +++++++++++++++++++-- src/app/features/estate/mediaRoom.test.ts | 27 +++++++++++++++++++- src/app/features/estate/mediaRoom.ts | 10 ++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/app/features/estate/EstateNavigator.tsx b/src/app/features/estate/EstateNavigator.tsx index 2a38e1f0..8b8b3644 100644 --- a/src/app/features/estate/EstateNavigator.tsx +++ b/src/app/features/estate/EstateNavigator.tsx @@ -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 = diff --git a/src/app/features/estate/MediaRow.tsx b/src/app/features/estate/MediaRow.tsx index 684fa6c5..6277f09c 100644 --- a/src/app/features/estate/MediaRow.tsx +++ b/src/app/features/estate/MediaRow.tsx @@ -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(); 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); }; diff --git a/src/app/features/estate/mediaRoom.test.ts b/src/app/features/estate/mediaRoom.test.ts index 2cfbce81..af53c12b 100644 --- a/src/app/features/estate/mediaRoom.test.ts +++ b/src/app/features/estate/mediaRoom.test.ts @@ -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(); + }); }); diff --git a/src/app/features/estate/mediaRoom.ts b/src/app/features/estate/mediaRoom.ts index a99c3d9d..a5f09664 100644 --- a/src/app/features/estate/mediaRoom.ts +++ b/src/app/features/estate/mediaRoom.ts @@ -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[] }>; };