Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
dd2c658
docs(spec): per-user favorites overlay design (#763)
Deeds67 Jul 20, 2026
9c6e38d
docs(spec): fix review findings in per-user favorites design (#763)
Deeds67 Jul 20, 2026
f087092
docs(spec): favorites need own sync watermarks + audit table (#763)
Deeds67 Jul 20, 2026
d04995d
docs(plan): slice 0 implementation plan for per-user favorites (#763)
Deeds67 Jul 20, 2026
28febd2
feat(favorites): add asset_favorite + audit tables with backfill (#763)
Deeds67 Jul 20, 2026
41ccd46
feat(favorites): restore asset.isFavorite on revert-to-immich (#763)
Deeds67 Jul 20, 2026
46205a5
docs(plan): slice 1 read-path plan; reassign E10 to slice 4 (#763)
Deeds67 Jul 20, 2026
09cb61c
feat(favorites): add favoriteExistsFor overlay expression helper (#763)
Deeds67 Jul 20, 2026
2d137d3
feat(favorites): resolve timeline reads from the overlay (#763)
Deeds67 Jul 20, 2026
06cf83b
docs(spec): E31 decided — favorites are per-photo, never per-stack (#…
Deeds67 Jul 20, 2026
57b785d
fix(favorites): prune asset_favorite_audit in audit table cleanup (#763)
Deeds67 Jul 20, 2026
de79375
feat(favorites): resolve search and map favorite filters from the ove…
Deeds67 Jul 20, 2026
2ae3978
fix(favorites): regenerate SQL snapshots for the overlay-based search…
Deeds67 Jul 20, 2026
ec003a1
docs(plan): slice 1b — mapAsset per-user favorite projection (#763)
Deeds67 Jul 20, 2026
17ce00b
feat(favorites): map isFavorite from the per-user projection (#763)
Deeds67 Jul 20, 2026
77668fc
feat(favorites): project per-user favorite into asset responses (#763)
Deeds67 Jul 20, 2026
bd8ac82
docs(plan): slice 2 write-path plan (#763)
Deeds67 Jul 20, 2026
267a49c
feat(favorites): add AssetFavoriteRepository (#763)
Deeds67 Jul 20, 2026
43a9a93
feat(favorites): add PUT /assets/favorites and deprecate the isFavori…
Deeds67 Jul 20, 2026
2743336
docs(spec): slice 7 must precede slice 3 (#763)
Deeds67 Jul 20, 2026
f13e3bf
docs(plan): slice 7 secondary writes — runs before slice 3 (#763)
Deeds67 Jul 20, 2026
67313ac
feat(favorites): upload and copy write overlay rows (#763)
Deeds67 Jul 20, 2026
3144acf
feat(favorites): merge duplicates with a per-user favorite union (#763)
Deeds67 Jul 20, 2026
9285d79
docs(spec): slice 3 must run LAST, after slice 6 (#763)
Deeds67 Jul 20, 2026
b2a8171
docs(plan): slice 4 cross-scope favorites (#763)
Deeds67 Jul 20, 2026
ecd1284
feat(favorites): compose isFavorite with withSharedSpaces and withPar…
Deeds67 Jul 20, 2026
3f8dbd2
feat(favorites): resolve space scope for favorite-filtered map marker…
Deeds67 Jul 20, 2026
707a216
feat(favorites): surface favorited stack children standalone in favor…
Deeds67 Jul 20, 2026
97b572f
test(favorites): pin access-loss re-derivation for spaces, albums, pa…
Deeds67 Jul 20, 2026
ada881e
test(favorites): cross-scope favorites first-page performance budget …
Deeds67 Jul 20, 2026
8645d30
feat(web): keep partner/space scope active while filtering by favorit…
Deeds67 Jul 20, 2026
cfeb16d
docs(plan): slice 5 web heart un-gate (#763)
Deeds67 Jul 20, 2026
9cae7db
feat(web): un-gate the asset-viewer favorite action from ownership (#…
Deeds67 Jul 20, 2026
24133f2
test(web): pin ownership in the shared-link favorite gate test (#763)
Deeds67 Jul 20, 2026
6e74196
feat(web): favorite the whole selection — multi-select bar, cmdk, spa…
Deeds67 Jul 20, 2026
5c6d10f
feat(web): un-gate the favorite action on search, albums, recently-ad…
Deeds67 Jul 20, 2026
6baf252
test(e2e): space viewer favorites a non-owned photo via the web UI (#…
Deeds67 Jul 20, 2026
a66a0a4
docs: space members can favorite — permission matrix update (#763)
Deeds67 Jul 20, 2026
4c5afbc
docs(plan): slice 6 mobile sync + un-gate (#763)
Deeds67 Jul 20, 2026
e1599ea
feat(sync): resolve isFavorite per recipient in every asset sync stre…
Deeds67 Jul 20, 2026
2009750
feat(sync): asset_favorite as its own synced entity — upserts + tombs…
Deeds67 Jul 20, 2026
2654474
fix(sync): order AssetFavoritesV1 after every asset-delivering stream…
Deeds67 Jul 20, 2026
47da7c7
chore(sql): regenerate query snapshots for per-user favorites (#763)
Deeds67 Jul 20, 2026
e136e74
feat(mobile): sync per-user favorite upserts and tombstones into the …
Deeds67 Jul 20, 2026
e08b613
feat(mobile): per-user favorites — un-gate the heart, fix mixed-selec…
Deeds67 Jul 20, 2026
ba1dcf4
docs(plan): slice 3 column drop — the point of no return (#763)
Deeds67 Jul 20, 2026
81665ae
fix(mobile): fall back to the legacy favorite endpoint on pre-5.3 ser…
Deeds67 Jul 20, 2026
0d6f1e6
feat(favorites): resolve the plugin-facing favorite from the owner ov…
Deeds67 Jul 20, 2026
bd6946d
docs(test): correct the grep-gate caveat — job.service still reads th…
Deeds67 Jul 20, 2026
c189178
fix(favorites): resolve websocket payload favorites from the owner ov…
Deeds67 Jul 20, 2026
4df2485
feat(favorites): drop the legacy asset.isFavorite column — overlay is…
Deeds67 Jul 20, 2026
193c3e1
docs(test): grep-gate caveat — the payload staleness bug is fixed (#763)
Deeds67 Jul 20, 2026
c68843b
chore(sql): regenerate snapshots post column drop (#763)
Deeds67 Jul 20, 2026
895f342
test(favorites): pin concurrent favorite/unfavorite convergence (E27)…
Deeds67 Jul 20, 2026
ab7102c
fix(revert): guard the favorite backfill for pre-#763 schemas (#763)
Deeds67 Jul 20, 2026
a79e351
test(e2e): point mocked favorite intercepts at PUT /assets/favorites …
Deeds67 Jul 21, 2026
f43f381
test(web): Recently Added keeps partner scope under a favorites filte…
Deeds67 Jul 21, 2026
f14a636
fix(schema): declare asset_favorite_delete_audit so #763 doesn't rein…
Deeds67 Jul 22, 2026
d44e0db
fix(favorites): reconcile #763 with main's selection-capabilities eng…
Deeds67 Jul 30, 2026
db8e700
fix(favorites): reconcile #763 with Immich v3.1.0 (#763)
Deeds67 Jul 30, 2026
963aca0
fix(mobile): repoint the #763 capability filter and test onto v3.1.0'…
Deeds67 Jul 30, 2026
00adab4
chore(lint): apply v3.1.0 eslint-unicorn + prettier to the #763 files…
Deeds67 Jul 30, 2026
851650d
test(favorites): pin the access-revocation invariant for space favori…
Deeds67 Jul 30, 2026
c877f16
test(mobile): gate the consolidated update path against owner-only fa…
Deeds67 Jul 30, 2026
8f7e49f
fix(favorites): make the /favorites page cross-scope like the Photos …
Deeds67 Jul 30, 2026
5db5538
Revert "fix(favorites): make the /favorites page cross-scope like the…
Deeds67 Jul 30, 2026
b9357bd
fix: reconcile the favorites overlay with main's map, places and time…
Deeds67 Aug 15, 2026
5d774fc
fix: close the map e2e brace, declare the favorites PK index, format
Deeds67 Aug 15, 2026
dd07a58
fix(favorites): resolve the smart-facet caller instead of asserting i…
Deeds67 Sep 2, 2026
8cf0c4b
fix(favorites): make the /favorites page cross-scope so a member's fa…
Deeds67 Sep 5, 2026
c5dd083
chore: drop the #763 slice plans the rebase re-added (#763)
Deeds67 Sep 6, 2026
7fbe69c
fix(favorites): restore the overlay adaptations the rebase dropped, a…
Deeds67 Sep 6, 2026
561ec84
fix: repair the two rebase resolutions CI caught (#763)
Deeds67 Sep 6, 2026
bd89e65
fix(favorites): keep a favourite reachable when its space is hidden f…
Deeds67 Sep 6, 2026
7f93ff8
test(favorites): reverse the showInTimeline pin — favourites are exem…
Deeds67 Sep 6, 2026
ca1650d
fix(favorites): apply the timeline-hide carve-out to the search surfa…
Deeds67 Sep 6, 2026
e32adef
fix(favorites): keep the Favorites scope when filtering from a favori…
Deeds67 Sep 6, 2026
398be03
fix(web): stop a filtered timeline claiming the library is empty (#763)
Deeds67 Sep 6, 2026
6eabea0
chore(web): satisfy the web linter
Deeds67 Sep 6, 2026
6dba8d5
fix(favorites): reconcile #763 with the v3.2.0 base (#763)
Deeds67 Sep 11, 2026
6386e21
chore(sql): regenerate query snapshots against the v3.2.0 base (#763)
Deeds67 Sep 11, 2026
0f25e0d
fix(server,mobile): repair the #763 fallout the v3.2.0 rebase left be…
Deeds67 Sep 11, 2026
c2ce85d
fix(server): await the favourite sync sends, and pin the new getTimeB…
Deeds67 Sep 11, 2026
69921cb
fix(rebase): carry per-user favorites onto the v3.2.2 base
Deeds67 Sep 19, 2026
948f5af
fix(mobile): gate per-user favorites on the server's declared capabil…
Deeds67 Oct 5, 2026
b5fe2b2
fix(mobile): include partner and hidden-space favorites on the Favori…
Deeds67 Oct 5, 2026
2db40b9
fix(server): re-sync an owner's favorite change to apps without the f…
Deeds67 Oct 5, 2026
f5f4654
fix(server): never expose the link owner's favorites to a shared-link…
Deeds67 Oct 5, 2026
feb4ad7
test(e2e): pin per-user favorite access in the selection RBAC matrix …
Deeds67 Oct 5, 2026
3e7cbb8
chore(server): regenerate asset favorite query snapshot (#763)
Deeds67 Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/docs/guides/switch-back-to-immich.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This guide walks through switching a Gallery instance back to upstream [Immich](
- Pet detection results, and every individual pet you named through pet recognition
- Asset duplicate checksums
- Library sync state
- Favorites belonging to anyone other than the asset's owner (see note below)

Assets you uploaded through Gallery are preserved, as long as they exist in Immich-native tables (which is the normal case for every file uploaded via the web or mobile app).

Expand Down Expand Up @@ -115,8 +116,9 @@ Watch the server log. A successful boot ends with the usual Immich startup banne

Here is what the cleanup script changes:

- It drops the Gallery-only tables `shared_space*`, `album_space_asset*`, `library_user`, `library_audit`, `library_asset_audit`, `shared_space_library*`, `face_identity*`, `face_repair*`, `pet_search`, `user_group`, `user_group_member`, `classification_category`, `classification_prompt_embedding`, `storage_migration_log` and `asset_duplicate_checksum`.
- It drops the Gallery-only tables `shared_space*`, `album_space_asset*`, `library_user`, `library_audit`, `library_asset_audit`, `shared_space_library*`, `face_identity*`, `face_repair*`, `pet_search`, `user_group`, `user_group_member`, `classification_category`, `classification_prompt_embedding`, `storage_migration_log`, `asset_duplicate_checksum`, `asset_favorite` and `asset_favorite_audit`.
- It drops the Gallery-added columns `person.type`, `person.species`, `asset_job_status.petsDetectedAt`, `asset_job_status.classifiedAt` and `library.createId`.
- It restores `asset.isFavorite`, an upstream Immich column Gallery moved into the `asset_favorite` overlay so each space member can favorite independently, before dropping `asset_favorite`, and backfills it from the overlay — but only for the asset's owner. Favorites belonging to anyone else (for example, a shared-space viewer who favorited another member's photo) have no equivalent in plain Immich's single-favorite-flag model and are discarded. Your own favorites on your own assets are preserved.
- It drops the Gallery-only functions and triggers that reference the dropped tables.
- It strips the `classification` key out of the `system-config` row in `system_metadata`.
- It deletes fork migration rows from `kysely_migrations` and `migration_overrides`, so upstream Immich's migrator does not see them as unknown migrations.
Expand Down
65 changes: 64 additions & 1 deletion e2e/src/specs/server/api/asset-copy.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type LoginResponseDto } from '@immich/sdk';
import { type LoginResponseDto, SharedSpaceRole } from '@immich/sdk';
import { type Actor, authHeaders } from 'src/actors.js';
import { createUserDto } from 'src/fixtures.js';
import { errorDto } from 'src/responses.js';
Expand Down Expand Up @@ -203,6 +203,69 @@ describe('PUT /assets/copy', () => {
expect((after.body as { isFavorite: boolean }).isFavorite).toBe(false);
});

// #763 slice 7 (E20): favorite carries the ACTING user's overlay row, never every user who
// favorited the source. `other` gains read access to both assets via a shared space (viewer
// role is enough under Permission.AssetRead, per #763 slice 2) purely so they can favorite the
// source and later read the target back — `other` still can't call PUT /assets/copy itself,
// since Permission.AssetCopy stays owner-only (asset.service.spec.ts already pins that).
it("copies only the ACTING user's favorite of the source, not another user's (E20)", async () => {
const [sourceId, targetId] = await createOwnerPair();

const space = await utils.createSpace(owner.accessToken, { name: 't26-copy-favorite-space' });
await utils.addSpaceMember(owner.accessToken, space.id, { userId: other.userId, role: SharedSpaceRole.Viewer });
await utils.addSpaceAssets(owner.accessToken, space.id, [sourceId, targetId]);

// Both the owner (acting user) and `other` favorite the SOURCE — independent per-user facts.
const ownerFav = await request(app)
.put('/assets/favorites')
.set(asBearerAuth(owner.accessToken))
.send({ ids: [sourceId], isFavorite: true });
expect(ownerFav.status).toBe(204);

const otherFav = await request(app)
.put('/assets/favorites')
.set(asBearerAuth(other.accessToken))
.send({ ids: [sourceId], isFavorite: true });
expect(otherFav.status).toBe(204);

const copy = await request(app)
.put('/assets/copy')
.set(asBearerAuth(owner.accessToken))
.send({ sourceId, targetId, favorite: true });
expect(copy.status).toBe(204);

// The acting user (owner) gets the target favorited (E20 happy path).
const ownerView = await request(app).get(`/assets/${targetId}`).set(asBearerAuth(owner.accessToken));
expect((ownerView.body as { isFavorite: boolean }).isFavorite).toBe(true);

// `other` favorited the SOURCE but never acted on this copy — their view of the target must
// stay unfavorited. This is the negative the old global-column copy could not express.
const otherView = await request(app).get(`/assets/${targetId}`).set(asBearerAuth(other.accessToken));
expect((otherView.body as { isFavorite: boolean }).isFavorite).toBe(false);
});

it('clears the target favorite when the acting user did not favorite the source, even though favorite defaults to true', async () => {
const [sourceId, targetId] = await createOwnerPair();

// Owner favorites the TARGET directly (unrelated to the upcoming copy) but never favorites
// the source. A working copy still "copies the status" — including a false one — for the
// acting user, mirroring the old column-overwrite semantics scoped to the actor.
const preFav = await request(app)
.put('/assets/favorites')
.set(asBearerAuth(owner.accessToken))
.send({ ids: [targetId], isFavorite: true });
expect(preFav.status).toBe(204);

const copy = await request(app)
.put('/assets/copy')
.set(asBearerAuth(owner.accessToken))
.send({ sourceId, targetId });
expect(copy.status).toBe(204);

const after = await request(app).get(`/assets/${targetId}`).set(asBearerAuth(owner.accessToken));
expect((after.body as { isFavorite: boolean }).isFavorite).toBe(false);
});

it('owner can copy: album associations are propagated source → target', async () => {
const [sourceId, targetId] = await createOwnerPair();

Expand Down
Loading
Loading