chore!: migrate album owner to album_user (#27467)

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
Daniel Dietzler
2026-04-22 16:52:23 +02:00
committed by GitHub
parent dfacde5af8
commit 4bfb8b36c2
75 changed files with 14750 additions and 1104 deletions
@@ -3,7 +3,7 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { getContextMenuPositionFromEvent, type ContextMenuPosition } from '$lib/utils/context-menu';
import { getShortDateRange } from '$lib/utils/date-time';
import type { AlbumResponseDto } from '@immich/sdk';
import { type AlbumResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiDotsVertical } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -85,12 +85,13 @@
{/if}
{#if showOwner}
{#if authManager.user.id === album.ownerId}
{@const owner = album.albumUsers[0].user}
{#if owner.id === authManager.user.id}
<p>{$t('owned')}</p>
{:else if album.owner}
<p>{$t('shared_by_user', { values: { user: album.owner.name } })}</p>
{:else}
<p>{$t('shared')}</p>
<p>
{$t('shared_by_user', { values: { user: owner.name } })}
</p>
{/if}
{:else if album.shared}
<p>{$t('shared')}</p>
@@ -20,7 +20,7 @@
import { getSelectedAlbumGroupOption, sortAlbums, stringToSortOrder, type AlbumGroup } from '$lib/utils/album-utils';
import type { ContextMenuPosition } from '$lib/utils/context-menu';
import { normalizeSearchString } from '$lib/utils/string-utils';
import { type AlbumResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
import { AlbumUserRole, type AlbumResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
import { modalManager } from '@immich/ui';
import { mdiDeleteOutline, mdiDownload, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
import { groupBy } from 'lodash-es';
@@ -98,24 +98,26 @@
/** Group by owner */
[AlbumGroupBy.Owner]: (order, albums): AlbumGroup[] => {
const currentUserId = authManager.user.id;
const groupedByOwnerIds = groupBy(albums, 'ownerId');
const groupedByOwnerIds = groupBy(albums, (album) => album.albumUsers[0].user.id);
const sortSign = order === SortOrder.Desc ? -1 : 1;
const sortedByOwnerNames = Object.entries(groupedByOwnerIds).sort(([ownerA, albumsA], [ownerB, albumsB]) => {
const sortedByOwnerNames = Object.entries(groupedByOwnerIds).sort(([ownerIdA, albumsA], [ownerIdB, albumsB]) => {
// We make sure owned albums stay either at the beginning or the end
// of the list
if (ownerA === currentUserId) {
if (ownerIdA === currentUserId) {
return -sortSign;
} else if (ownerB === currentUserId) {
} else if (ownerIdB === currentUserId) {
return sortSign;
} else {
return albumsA[0].owner.name.localeCompare(albumsB[0].owner.name, $locale) * sortSign;
const ownerA = albumsA[0].albumUsers[0].user;
const ownerB = albumsB[0].albumUsers[0].user;
return ownerA.name.localeCompare(ownerB.name, $locale) * sortSign;
}
});
return sortedByOwnerNames.map(([ownerId, albums]) => ({
id: ownerId,
name: ownerId === currentUserId ? $t('my_albums') : albums[0].owner.name,
name: ownerId === currentUserId ? $t('my_albums') : albums[0].albumUsers[0].user.name,
albums,
}));
},
@@ -130,7 +132,10 @@
return sharedAlbums;
}
default: {
const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== authManager.user.id);
const nonOwnedAlbums = sharedAlbums.filter(
(album) =>
album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role !== AlbumUserRole.Owner,
);
return nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
}
}
@@ -167,7 +172,9 @@
albumGroupIds = groupedAlbums.map(({ id }) => id);
});
let showFullContextMenu = $derived(allowEdit && selectedAlbum && selectedAlbum.ownerId === authManager.user.id);
let showFullContextMenu = $derived(
allowEdit && selectedAlbum && selectedAlbum.albumUsers[0].user.id === authManager.user.id,
);
onMount(async () => {
if (allowEdit) {
@@ -5,7 +5,7 @@
import { Route } from '$lib/route';
import { locale } from '$lib/stores/preferences.store';
import type { ContextMenuPosition } from '$lib/utils/context-menu';
import type { AlbumResponseDto } from '@immich/sdk';
import { AlbumUserRole, type AlbumResponseDto } from '@immich/sdk';
import { Icon } from '@immich/ui';
import { mdiShareVariantOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -43,9 +43,11 @@
icon={mdiShareVariantOutline}
size="16"
class="inline ms-1 opacity-70"
title={album.ownerId === authManager.user.id
title={album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role === AlbumUserRole.Owner
? $t('shared_by_you')
: $t('shared_by_user', { values: { user: album.owner.name } })}
: $t('shared_by_user', {
values: { user: album.albumUsers[0].user.name },
})}
/>
{/if}
</td>
@@ -12,7 +12,7 @@
import { getAssetType } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { isTenMinutesApart } from '$lib/utils/timesince';
import { ReactionType, type ActivityResponseDto, type AssetTypeEnum } from '@immich/sdk';
import { ReactionType, type ActivityResponseDto, type AlbumUserResponseDto, type AssetTypeEnum } from '@immich/sdk';
import { Icon, IconButton, LoadingSpinner, Textarea, toastManager } from '@immich/ui';
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiSend, mdiThumbUp } from '@mdi/js';
import * as luxon from 'luxon';
@@ -43,11 +43,11 @@
assetId?: string | undefined;
albumId: string;
assetType?: AssetTypeEnum | undefined;
albumOwnerId: string;
albumUsers: AlbumUserResponseDto[];
disabled: boolean;
}
let { assetId = undefined, albumId, assetType = undefined, albumOwnerId, disabled }: Props = $props();
let { assetId = undefined, albumId, assetType = undefined, albumUsers, disabled }: Props = $props();
let innerHeight: number = $state(0);
let activityHeight: number = $state(0);
@@ -56,6 +56,7 @@
let previousAssetId: string | undefined = $state(assetId);
let message = $state('');
let isSendingMessage = $state(false);
const isAlbumOwner = $derived(albumUsers[0].user.id === authManager.user.id);
const timeOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
@@ -147,7 +148,7 @@
/>
</a>
{/if}
{#if reaction.user.id === authManager.user.id || albumOwnerId === authManager.user.id}
{#if reaction.user.id === authManager.user.id || isAlbumOwner}
<div class="me-4">
<ButtonContextMenu
icon={mdiDotsVertical}
@@ -200,7 +201,7 @@
/>
</a>
{/if}
{#if reaction.user.id === authManager.user.id || albumOwnerId === authManager.user.id}
{#if reaction.user.id === authManager.user.id || isAlbumOwner}
<div class="me-4">
<ButtonContextMenu
icon={mdiDotsVertical}
@@ -638,7 +638,7 @@
<ActivityViewer
disabled={!album.isActivityEnabled}
assetType={asset.type}
albumOwnerId={album.ownerId}
albumUsers={album.albumUsers}
albumId={album.id}
assetId={asset.id}
/>
@@ -82,7 +82,7 @@
}: Props = $props();
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
const isAlbumOwner = $derived(authManager.authenticated && album?.ownerId === authManager.user.id);
const isAlbumOwner = $derived(authManager.authenticated && album?.albumUsers[0].user.id === authManager.user.id);
const isLocked = $derived(asset.visibility === AssetVisibility.Locked);
const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);