Skip to content

fix(spaces): offer a specific space album from every add-to-collection entry point (#965) - #970

Open
Deeds67 wants to merge 3 commits into
mainfrom
fix/965-space-album-add-parity
Open

fix(spaces): offer a specific space album from every add-to-collection entry point (#965)#970
Deeds67 wants to merge 3 commits into
mainfrom
fix/965-space-album-add-parity

Conversation

@Deeds67

@Deeds67 Deeds67 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #965.

The bug

Whether "add this photo to that Shared Space album" was possible depended on which screen you started from — and web and mobile disagreed about which screens worked.

Entry point Personal album Space pool Album inside a space
Web — any surface yes yes no
Mobile — main timeline / search / person yes yes yes
Mobile — inside a personal album yes no no
Mobile — favorites, archive, on-device album yes no no
Mobile — asset viewer + → Album yes no no

Two independent causes:

  1. Web never listed space-linked albums at all. CollectionPickerModal loads personal albums plus writable spaces and stops. The only place it listed a space's linked albums was restrictToSpaceId mode, reached solely from a space surface with a non-owned selection ([bug] Contradictory notification when adding photo to album as non admin Editor ("Successful" but "File cannot be added") #764). So the target simply did not exist on any web surface.
  2. Mobile has two pickers and only one knows about spaces. The fork's CollectionPicker was wired into general_bottom_sheet and space_bottom_sheet; every other add-to-collection surface still mounted upstream's bare AlbumSelector.

The fix

No server change. Permission.AlbumAssetCreate already has a checkSpaceLinkedAlbumAccess arm, so a space Owner/Editor holds add-permission on every album linked to that space. A space-linked album dispatches through POST /albums/:id/assets exactly like a personal one.

Web — a space row with albumCount > 0 becomes expandable and opens into an "Add to space" child plus one row per linked album, mirroring mobile's SpaceCollectionSection. Albums are fetched lazily on expand and cached for the life of the modal, so opening the picker still costs two requests however many spaces you're in, and one space is open at a time. Children join the same flat keyboard-navigable row sequence. A space with no linked albums clicks straight through to the pool, exactly as before, and restrictToSpaceId mode is untouched.

MobileCollectionPicker is now mounted on the album, favorites, archive and on-device-album sheets, and on the asset viewer's +. It gained source (the viewer dispatches as ActionSource.viewer), assets (the viewer has no multiselect, and falling back to the empty one would read as "nothing non-owned" and offer space targets for someone else's photo), and onCompleted (so the viewer keeps refreshing "Appears in" and popping its sheet now that the picker owns the dispatch). Each rewired surface also loses its bespoke addToAlbum copy.

Second commit: fixes from review

A multi-agent review of the first commit found a bug in the exact flow this issue is about, plus several smaller ones. All are fixed in af110f4, each with a regression test that was confirmed to go red against the unfixed code.

  • Multi-selecting a space album silently added nothing. findByKey resolved keys against personal albums + spaces only — but a space album owned by another member has no album_user row for the caller, so getAllAlbums never returns it and it lives only in the expand-time cache. The row ticked, the footer counted it, then submit dropped it and called onClose(undefined), which both wrappers read as cancelled. Keys now resolve against the cache too.
  • The keyboard caret was cleared on every expand/collapse, so a keyboard user could reveal a space's albums and then had no way to arrow into them. Now re-anchored on the space row — only when the caret was already in use, so a mouse click doesn't acquire a highlight.
  • The pool child's multi-select state was computed and asserted but never rendered. It's now a component alongside the other row types, with its own checkbox, the shared scroll-into-view action, and an indent that lines up with the album children.
  • Collapse-then-re-expand during an in-flight request fired a second identical one.
  • The chevron was painted over by the hover checkbox.
  • Mobile: ref.invalidate ran before the context.mounted guard; and the space-albums loading state was conflated with "this space has no albums", flashing that message on every expand — web already distinguished the two.
  • Mobile test gaps closed: addToSpaceAlbum (the literal subject of this issue) had no coverage, and neither did the source pass-through on the album path — whose regression would make the asset viewer a silent no-op with a lying toast.

Correction: why the mobile partner sheet is still skipped

The first version of this PR claimed a partner's asset "can never reach any space target" and that "web hides the + entirely there". Both were wrong, and the code settles it:

  • Permission.AssetShare is owner ∪ partneraccess.ts:127-131 unions checkPartnerAccess. The space pool accepts a partner's assets.
  • Web's partner route never consults getSelectionCapabilities; it renders <ActionButton action={Actions.AddToAlbum} /> unconditionally (partners/[userId]/…/+page.svelte:99).

The sheet is still skipped, but for the real reason: mobile's own selectionHasNonOwned rule treats any non-owned asset as unreachable, so the Spaces section there would always be collapsed behind a notice — and that notice is itself stricter than the server. Relaxing the rule needs mobile to know which owners are partners, which changes every surface the rule already governs. Out of scope here; recorded as follow-up.

One side effect worth flagging: viewing a partner's photo in the asset viewer now shows that notice where previously there was no Spaces section at all. Consistent with the timeline's existing behaviour, but it's the clearest surviving parity gap.

Other behaviour changes worth calling out

  • Web: clicking a space row that has linked albums now expands it instead of adding to the pool. The pool is one click away ("Add to space"), and the row checkbox still means the pool.
  • Mobile favorites: local-only assets are now uploaded and added instead of being skipped with a toast. (add_to_album_bottom_sheet_some_local_assets is now an orphaned i18n key; the branch was already dead, since a favorites selection is remote-only.)
  • Mobile asset viewer: when nothing landed and the server reported failures, the sheet stays open instead of popping, so the user can retry.

Out of scope

  • The issue's closing note about extending the Album filter to "filter by a specific album" is a separate feature request.
  • Selections above MAX_SPACE_ASSETS_PER_REQUEST (50 000) still hide the whole spaces section, with the existing notice.
  • Mobile has no add-to-collection on the space-album page, or on an album you can edit but don't own, where web does. Recorded in the design note with the other remaining divergences.
  • Album sorting options in Shared Space are inconsistent between Web and Mobile #966 (album sort options differing between web and mobile inside a Space) is untouched.

Verification

Local: web 4363 unit tests, check:typescript / check:svelte (586 files) / eslint / prettier clean; mobile 3030 tests, dart analyze --fatal-infos lib test clean, dart format clean on every touched file.

Design notes: docs/superpowers/specs/2026-08-10-965-space-album-add-parity-design.md.

…n entry point (#965)

Adding a photo to an album inside a Shared Space depended on which screen you
started from, and web and mobile disagreed about which screens worked.

Web never listed space-linked albums at all: `CollectionPickerModal` loaded
personal albums plus writable spaces and stopped there, so no web surface could
target an album inside a space. A space row with linked albums now expands, on
click, into an "Add to space" child plus one row per linked album — fetched
lazily and cached for the life of the modal, so opening the picker still costs
two requests however many spaces the user is in. The rows stay one flat
keyboard-navigable sequence, and a space with no linked albums behaves exactly
as before.

Mobile had the right picker already but mounted it on only two surfaces; the
rest still used upstream's album-only `AlbumSelector`. `CollectionPicker` is now
wired into the album, favorites, archive and on-device-album sheets and into the
asset viewer's "+". To make the viewer work it gained `source` (it dispatches as
`ActionSource.viewer`) and `assets` — the viewer has no multiselect, and falling
back to the empty one would read as "nothing non-owned" and offer space targets
for someone else's photo.

No server change: `Permission.AlbumAssetCreate` already grants a space
Owner/Editor add-permission on every album linked to that space, so a
space-linked album dispatches exactly like a personal one.

The partner-detail sheet is deliberately left on the album-only selector — a
partner's asset can never reach any space target, and web hides its "+" there
for the same reason.
@Deeds67 Deeds67 added the changelog:fix Bug fix for changelog label Aug 10, 2026
…usable (#965)

Review of the first commit turned up a bug in the exact flow the issue is
about, plus several smaller ones.

Multi-selecting a space-linked album silently added nothing. `findByKey`
resolved keys against `allCollections` only — personal albums plus spaces — but
a space album owned by another member has no `album_user` row for the caller,
so `getAllAlbums` never returns it and it lives only in the expand-time cache.
The row ticked, the footer counted it, then `submitMulti` dropped it and called
`onClose(undefined)`, which both wrappers read as "cancelled": modal closed, no
request, no error. Keys now resolve against the cache too.

The keyboard caret was cleared whenever a space opened or closed, so a keyboard
user could reveal a space's albums and then had no way to arrow into them. It is
now re-anchored on the space row itself — but only when the caret was already in
use, so a mouse click does not acquire a selection highlight.

The pool child's multi-select state was computed and asserted but never
rendered. It is now a component alongside the other row types, with its own
checkbox, the shared scroll-into-view action, and an indent that lines up with
the album children.

Also: collapsing and re-expanding while the first request was in flight fired a
second identical one; the chevron was painted over by the hover checkbox; and on
mobile `ref.invalidate` ran before the `context.mounted` guard, and the space
albums' loading state was conflated with "this space has no albums", flashing
that message on every expand — web already distinguished the two.

Tests: each fix has a regression test, and each was confirmed to go red against
the unfixed code. Mobile gained the two dispatch tests that were missing — the
`source` pass-through on the album path, and `addToSpaceAlbum`, which is the
literal subject of the issue and had no coverage at all.

The design note's justification for skipping the mobile partner sheet was wrong:
`Permission.AssetShare` is owner UNION partner, and web's partner route renders
add-to-album unconditionally rather than via `getSelectionCapabilities`. The
sheet is still skipped, but for the real reason — mobile's own
`selectionHasNonOwned` rule is stricter than the server, and relaxing it is a
change to every surface that rule governs. Recorded, with the other remaining
web/mobile divergences, in the design note.
Three errors CI's Lint Web caught: two `unicorn/no-await-expression-member`
from indexing straight into `await findAllByTestId(...)` in the new specs, and
`svelte/prefer-svelte-reactivity` on the plain `Set` guarding in-flight
space-album fetches. The Set is only a dedupe guard and nothing renders from it,
but the rule forbids mutable built-in Sets in components, so it is now a
`SvelteSet`.

Worth noting for next time: `pnpm exec eslint` on the touched directories
reported success here while these errors existed, so a local eslint pass on this
tree cannot be trusted on its own.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent ability to add photos to a specific Shared Space album depending on entry point (Web/Mobile)

1 participant