import type { Album, SharedLink, SharedLinkCreate, SharedLinkEdit } from './shared'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { toast } from 'sonner'; // Every mutation the /photos panels can perform, in one place. // // All of them are bulk-shaped even when the UI is acting on a single tile: Immich's own API is // `PUT /assets {ids, …}` / `DELETE /assets {ids}`, and having one code path means the selection bar and the // lightbox's single-asset buttons cannot drift into behaving differently. // // Invalidation is broad — the whole ['photos'] key. Favoriting an asset changes the favorites bucket index, // the timeline tile, the album cover and the asset detail; the queries are cheap and correctness is worth // more than a surgical cache patch. export function useAssetActions() { const { put, post, patch, delete: del } = useClient(); const qc = useQueryClient(); const invalidate = () => qc.invalidateQueries({ queryKey: ['photos'] }); const fail = (verb: string) => (err: unknown) => toast.error(`Could not ${verb}: ${err instanceof Error ? err.message : String(err)}`); const setFavorite = useMutation({ mutationFn: ({ ids, isFavorite }: { ids: string[]; isFavorite: boolean }) => put('/photos/_officer/assets', { ids, isFavorite }), onSuccess: invalidate, onError: fail('update favorites'), }); const setArchived = useMutation({ mutationFn: ({ ids, archived }: { ids: string[]; archived: boolean }) => put('/photos/_officer/assets', { ids, visibility: archived ? 'archive' : 'timeline' }), onSuccess: () => { invalidate(); toast.success('Moved'); }, onError: fail('archive'), }); // Immich's delete is a move to trash unless `force`. Keep those as two named actions rather than one with a // boolean at the call site — "delete" and "delete permanently" should not look alike in the code either. const trash = useMutation({ mutationFn: (ids: string[]) => del('/photos/_officer/assets', { ids }), onSuccess: (_data, ids) => { invalidate(); toast.success(`${ids.length} moved to trash`); }, onError: fail('delete'), }); const deleteForever = useMutation({ mutationFn: (ids: string[]) => del('/photos/_officer/assets', { ids, force: true }), onSuccess: (_data, ids) => { invalidate(); toast.success(`${ids.length} permanently deleted`); }, onError: fail('delete permanently'), }); const restore = useMutation({ mutationFn: (ids: string[]) => post('/photos/_officer/trash/restore/assets', { ids }), onSuccess: (_data, ids) => { invalidate(); toast.success(`${ids.length} restored`); }, onError: fail('restore'), }); const emptyTrash = useMutation({ mutationFn: () => post('/photos/_officer/trash/empty'), onSuccess: () => { invalidate(); toast.success('Trash emptied'); }, onError: fail('empty the trash'), }); const addToAlbum = useMutation({ mutationFn: ({ albumId, ids }: { albumId: string; ids: string[] }) => put(`/photos/_officer/albums/${albumId}/assets`, { ids }), onSuccess: (_data, { ids }) => { invalidate(); toast.success(`${ids.length} added to album`); }, onError: fail('add to the album'), }); const removeFromAlbum = useMutation({ mutationFn: ({ albumId, ids }: { albumId: string; ids: string[] }) => del(`/photos/_officer/albums/${albumId}/assets`, { ids }), onSuccess: () => { invalidate(); toast.success('Removed from album'); }, onError: fail('remove from the album'), }); const createAlbum = useMutation({ mutationFn: ({ albumName, assetIds }: { albumName: string; assetIds?: string[] }) => post('/photos/_officer/albums', { albumName, assetIds: assetIds ?? [] }), onSuccess: invalidate, onError: fail('create the album'), }); const deleteAlbum = useMutation({ mutationFn: (albumId: string) => del(`/photos/_officer/albums/${albumId}`), onSuccess: () => { invalidate(); toast.success('Album deleted'); }, onError: fail('delete the album'), }); // PATCH, not PUT: on Immich, PUT /albums/{id}/assets means "add assets" — the album itself is patched. const renameAlbum = useMutation({ mutationFn: ({ albumId, albumName }: { albumId: string; albumName: string }) => patch(`/photos/_officer/albums/${albumId}`, { albumName }), onSuccess: invalidate, onError: fail('rename the album'), }); // ── Shared links ── // // The only thing in this file that produces a PUBLIC address, which is why the create dialog sends every // flag explicitly instead of letting Immich's defaults apply. Immich defaults `allowDownload` and // `showMetadata` to true and the expiry to never; a form that silently inherits those is a form that // shares more than it appears to. const createSharedLink = useMutation({ mutationFn: (body: SharedLinkCreate) => post('/photos/_officer/shared-links', body), onSuccess: invalidate, onError: fail('create the link'), }); const updateSharedLink = useMutation({ mutationFn: ({ id, ...body }: SharedLinkEdit & { id: string }) => patch(`/photos/_officer/shared-links/${id}`, body), onSuccess: () => { invalidate(); toast.success('Link updated'); }, onError: fail('update the link'), }); // Revoke, not delete: the link stops working for everybody holding it, and nothing about the photos changes. const deleteSharedLink = useMutation({ mutationFn: (id: string) => del(`/photos/_officer/shared-links/${id}`), onSuccess: () => { invalidate(); toast.success('Link revoked'); }, onError: fail('revoke the link'), }); return { setFavorite, setArchived, trash, deleteForever, restore, emptyTrash, addToAlbum, removeFromAlbum, createAlbum, deleteAlbum, renameAlbum, createSharedLink, updateSharedLink, deleteSharedLink, }; }