From 33e31a6102f9efe664b4d8cea2db5a9612d8fbae Mon Sep 17 00:00:00 2001 From: Void Date: Sat, 14 Nov 2026 02:14:21 +0000 Subject: [PATCH 1/2] Files desk: name loose files the album list cannot show; pin the old publisher wire shape Co-Authored-By: Claude Sonnet 5.5 --- src/app/features/albums/AlbumView.tsx | 33 ++++++- src/app/features/albums/buildAlbums.ts | 34 ++++++- src/app/features/albums/publisherWire.test.ts | 99 +++++++++++++++++++ 3 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/app/features/albums/publisherWire.test.ts diff --git a/src/app/features/albums/AlbumView.tsx b/src/app/features/albums/AlbumView.tsx index d6ee37c9..f90138d6 100644 --- a/src/app/features/albums/AlbumView.tsx +++ b/src/app/features/albums/AlbumView.tsx @@ -14,7 +14,7 @@ import { markAsRead } from '../../utils/notifications'; import { getBlobSafeMimeType } from '../../utils/mimeTypes'; import { timeDayMonYear } from '../../utils/time'; import { getMemberDisplayName } from '../../utils/room'; -import { Album, AlbumFile, lastEvent } from './buildAlbums'; +import { Album, AlbumFile, TimelineEvent, lastEvent, looseFiles, looseNote } from './buildAlbums'; import { folderOf, listFolder, pathLabel } from './albumFolders'; import { FileReadBy } from './FileReadBy'; import { ShareSet, useShareEnabled } from './ShareSet'; @@ -106,6 +106,31 @@ function useMarkZoneRead(room: Room) { }, [mx, room, hideActivity]); } +/** The file events in the room's live timeline that no album thread holds, kept current as it syncs. */ +function useLooseFiles(room: Room): AlbumFile[] { + const [, bump] = useState(0); + useEffect(() => { + const on = () => bump((n) => n + 1); + room.on(RoomEvent.Timeline, on); + return () => { + room.off(RoomEvent.Timeline, on); + }; + }, [room]); + const events: TimelineEvent[] = room + .getLiveTimeline() + .getEvents() + .map((e) => ({ + event_id: e.getId(), + type: e.getType(), + sender: e.getSender(), + origin_server_ts: e.getTs(), + content: e.getContent(), + unsigned: e.getUnsigned() as Record, + threadRoot: e.threadRootId, + })); + return looseFiles(events); +} + type AlbumViewProps = { room: Room; onTimeline: () => void }; export function AlbumView({ room, onTimeline }: AlbumViewProps) { useMarkZoneRead(room); @@ -117,6 +142,7 @@ export function AlbumView({ room, onTimeline }: AlbumViewProps) { const authorOf = (a: Album) => getMemberDisplayName(room, a.author) ?? a.author; const encrypted = room.hasEncryptionStateEvent(); const canShare = useShareEnabled(room); + const loose = useLooseFiles(room); let body: React.ReactNode; if (loading) body = Loading albums…; @@ -206,6 +232,11 @@ export function AlbumView({ room, onTimeline }: AlbumViewProps) {
{body} + {!open && !encrypted && !loading && !error && loose.length > 0 && ( + + {looseNote(loose)} + + )} {/* Stays mounted while an album is open, so a running send keeps its progress. */} {canShare && (