Skip to content

feat(spaces): show which spaces hold a photo on its thumbnail - #1249

Open
defts wants to merge 1 commit into
open-noodle:mainfrom
defts:feat/space-badge
Open

defts wants to merge 1 commit into
open-noodle:mainfrom
defts:feat/space-badge

Conversation

@defts

@defts defts commented Oct 27, 2026 •

Copy link
Copy Markdown

Summary

Nothing on a thumbnail tells you whether a photo is in a space. With a space shown on your timeline, your own photos, the ones you shared and the other members' photos all look the same in Photos, and finding out means opening each one.

This adds a small badge in the top-left corner of a thumbnail whose photo is in at least one of your spaces, on web and mobile. It is drawn like the thumbnail's other marks (favourite, stack): an icon on the photo, slightly see-through and with a thin shadow, so it hints without covering the photo.

  • one space: the Spaces icon in the space's colour;
  • several spaces: a white Spaces icon and how many. Several spaces have several colours, so the icon stays neutral, as the other marks. On small mobile tiles the count is left out.

Every path into a space counts (added to it, in a linked album, in a linked library), including spaces and albums you've hidden from your timeline: the photo is still in them and their members still see it. Only spaces you're a member of count, and hidden, locked and trashed photos are in none.

The badge stays off a space's own pages (photos, people, map, search), the trash, the locked folder, shared links and the viewer's stack strip. The selection check keeps the corner: the badge fades out while a photo is selected, and on web while you hover it.

Design doc: specs/2026-10-26-space-badge-design.md.

Screenshots

Web Web, on hover: the selection check takes the corner
web-en-timeline web-en-hover
Mobile Mobile, while selecting
mobile-en-timeline mobile-en-selected

How

  • Server: POST /assets/spaces in GalleryAssetController, next to POST /assets/editable. It takes 1 to 1000 asset IDs and returns, for each asset the caller can read, the IDs of the caller's spaces that show it; assets in none are left out, and a shared-link session gets nothing. SharedSpaceRepository.getMemberSpaceIdsByAssetIds crosses the requested assets with the caller's memberships and keeps the pairs sharedWithViewer admits with no album gate. The query runs first and the access check only on its hits, since most thumbnails are in no space.
  • Web: web/src/lib/gallery/space-badge/: SpaceBadge.svelte, mounted from Thumbnail.svelte, and a small cache that batches what the mounted thumbnails ask (50 ms, 500 IDs per request). Answers are kept and shown until a fresh one replaces them, so badges don't blink: space and album changes made in the app ask again about the photos they name, and moving to another page asks again about every mounted thumbnail. Colours come from the spaces list the sidebar already shares, loaded only once a photo is in a space. Editing a space now emits a SpaceUpdate event, which drops that list like the other space events do, so a new colour shows without a reload. Icon mdiAccountGroup, as the sidebar's Spaces entry.
  • Mobile: mobile/lib/gallery/space_badge.{provider,widget}.dart, in ThumbnailTile's indicator stack so it fades with the tile's other marks. No server call: one Drift query per tile over the same three paths, joined to the user's memberships and watched on the space tables, so it follows sync and works offline. Icon Icons.workspaces, as the Spaces tab. The Space person timeline now has the space origin, since every photo on it is in the space. A change the app makes to a space reached those tables only at the next sync, so a photo added from the timeline, or a new colour, showed late: SharedSpaceApiRepository now asks for a sync once the server has taken an add, a removal or an edit (queued behind a sync already running), below the action layer so every surface gets it, as linked albums already do.
  • Colour: the light end of the space card's gradient on each platform, which reads on dark photos while the shadow outlines it on bright ones; a light indigo for the default colour.
  • Docs: a "Seeing Which Photos Are in a Space" section in docs/docs/features/shared-spaces.md.

Known limits

  • The badge says a photo is in a space, not which one: hover hands the corner to the selection check, and screen readers skip it like the other thumbnail marks. Listing the spaces under "Appears in" in the info panel would be the natural follow-up; the endpoint already returns the IDs.
  • On web, other members' changes show when you move to another page; mobile follows them as it syncs.
  • On an album linked to a space, every photo carries that space's badge.
  • On mobile, a cross-owner contribution to an album linked to several spaces counts in each of them: the local album rows carry no space, as for the space timelines. The server counts only the space it went through.

Verification

  • Server: the service against a real database (asset-spaces.medium.spec.ts): each path on its own, two paths into one space, hidden spaces, links and albums, a contribution through one of two spaces, a deleted album, another user's space, archived photos in, hidden, locked and trashed ones out even for an elevated owner, a removed member, a shared link. The RBAC oracle snapshots the new read per actor. Unit tests for the route (1 to 1000 IDs) and the service.
  • Web: the cache, the badge, and a mounted badge against the real cache (appearing, following a change without blinking, asking again on another page, taking a space's new colour without a reload); updateSpaceDetails emits SpaceUpdate only on success. The e2e timeline mocks answer the new route.
  • Mobile: the query (each path, every change in and out, no repeated values), the widget (coloured icon and its place, white icon with a count, no count on small tiles, the colours, late answer, screen readers, excluded timelines, fading), a real ThumbnailTile fading it out while selected, and the repository asking for a sync after an add, a removal or an edit, not after a refusal.
  • Full suites: server unit (6792), the server medium specs for the oracles, utils and shared spaces (506), web (6701), mobile (4123); lint, tsc, svelte-check, dart analyze and the i18n check clean.
  • By hand on a local stack: web in Chromium and the app on the iOS simulator, with photos in one space, in two and in none (screenshots above).
  • server/src/queries/shared.space.repository.sql, the OpenAPI spec and the SDK regenerated.

Upstream files: grew by 6 lines

Against the Immich v3.3.5 tag:

File Before After
web/src/lib/components/assets/thumbnail/Thumbnail.svelte identical +2: the import and <SpaceBadge>
mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +5/−1 +7/−1: the import and SpaceBadge in the indicator stack
web/src/lib/managers/event-manager.svelte.ts +8 +9: the SpaceUpdate event
web/src/lib/stores/user.svelte.ts +54 +55: SpaceUpdate drops the spaces list, as the other space events do

The generated open-api/immich-openapi-specs.json and packages/sdk/src/fetch-client.ts gain the route. Everything else is in the fork namespaces, the fork-owned shared-space.repository.ts and its SQL, shared_space_timeline.repository.dart, tests, docs and the spec. No new strings, no migration.

A thumbnail whose photo is in one of your spaces gets a badge in its
top-left corner: the Spaces icon in the space's colour, or white with a
count when the photo is in several spaces. It counts every path into a
space and stays off a space's own pages, the trash and the locked folder.

Web asks a new POST /assets/spaces in batches and drops its spaces list
when a space is edited, so a new colour shows without a reload. Mobile
reads its local database and syncs once the server has taken an add, a
removal or an edit, so the badge follows a change made in the app.

Design: specs/2026-10-26-space-badge-design.md

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:feat Feature change for changelog documentation Improvements or additions to documentation 📱mobile 🗄️server 🖥️web

2 participants