From 922282b2b47c21b8fe77a16db34aa16ee75859f4 Mon Sep 17 00:00:00 2001 From: Chris Peckover Date: Sun, 30 Nov 2025 13:56:03 -0500 Subject: [PATCH] feat(web): Shared album owner labels (#21171) * - pass available album users along to the thumbnail through the asset-date-group - show a small user-avatar in bottom right of thumbnail * - change owner to their name in white text instead of the avatar * cleanup * - cleanup albumUsers creation - use font-light for the user's name * fix lint * format * - add toggle to show/hide asset owner names * update new Timeline with albumUsers * add @idubnori suggestion for the name font * Don't show 'view owners' button if the album doesn't have editors * add missing import * format * fix(web): #21171 (#24298) fix: Bind timelineManager to Timeline component --------- Co-authored-by: idubnori Co-authored-by: Alex --- .../assets/thumbnail/thumbnail.svelte | 14 +++++++++++++- .../lib/components/timeline/Timeline.svelte | 5 ++++- .../[[assetId=id]]/+page.svelte | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 0645541241..38d734fc22 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -4,7 +4,7 @@ import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils'; import { timeToSeconds } from '$lib/utils/date-time'; import { getAltText } from '$lib/utils/thumbnail-util'; - import { AssetMediaSize, AssetVisibility } from '@immich/sdk'; + import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk'; import { mdiArchiveArrowDownOutline, mdiCameraBurst, @@ -46,6 +46,7 @@ imageClass?: ClassValue; brokenAssetClass?: ClassValue; dimmed?: boolean; + albumUsers?: UserResponseDto[]; onClick?: (asset: TimelineAsset) => void; onSelect?: (asset: TimelineAsset) => void; onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void; @@ -64,6 +65,7 @@ readonly = false, showArchiveIcon = false, showStackedIcon = true, + albumUsers = [], onClick = undefined, onSelect = undefined, onMouseEvent = undefined, @@ -85,6 +87,8 @@ let width = $derived(thumbnailSize || thumbnailWidth || 235); let height = $derived(thumbnailSize || thumbnailHeight || 235); + let assetOwner = $derived(albumUsers?.find((user) => user.id === asset.ownerId) ?? null); + const onIconClickedHandler = (e?: MouseEvent) => { e?.stopPropagation(); e?.preventDefault(); @@ -268,6 +272,14 @@ {/if} + {#if !!assetOwner} +
+

+ {assetOwner.name} +

+
+ {/if} + {#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index d2873eca70..ba9cf37bff 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -23,7 +23,7 @@ import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { isAssetViewerRoute, navigate } from '$lib/utils/navigation'; import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util'; - import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk'; + import { type AlbumResponseDto, type PersonResponseDto, type UserResponseDto } from '@immich/sdk'; import { DateTime } from 'luxon'; import { onDestroy, onMount, type Snippet } from 'svelte'; import type { UpdatePayload } from 'vite'; @@ -49,6 +49,7 @@ showArchiveIcon?: boolean; isShared?: boolean; album?: AlbumResponseDto | null; + albumUsers?: UserResponseDto[]; person?: PersonResponseDto | null; isShowDeleteConfirmation?: boolean; onSelect?: (asset: TimelineAsset) => void; @@ -81,6 +82,7 @@ showArchiveIcon = false, isShared = false, album = null, + albumUsers = [], person = null, isShowDeleteConfirmation = $bindable(false), onSelect = () => {}, @@ -702,6 +704,7 @@ showStackedIcon={withStacked} {showArchiveIcon} {asset} + {albumUsers} {groupIndex} onClick={(asset) => { if (typeof onThumbnailClick === 'function') { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index b99260eee4..74b0f1d6b3 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -66,6 +66,7 @@ } from '@immich/sdk'; import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui'; import { + mdiAccountEyeOutline, mdiArrowLeft, mdiCogOutline, mdiDeleteOutline, @@ -100,6 +101,7 @@ let isCreatingSharedAlbum = $state(false); let isShowActivity = $state(false); let albumOrder: AssetOrder | undefined = $state(data.album.order); + let showAlbumUsers = $state(false); const assetInteraction = new AssetInteraction(); const timelineInteraction = new AssetInteraction(); @@ -290,6 +292,11 @@ let album = $derived(data.album); let albumId = $derived(album.id); + const containsEditors = $derived(album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor)); + const albumUsers = $derived( + showAlbumUsers && containsEditors ? [album.owner, ...album.albumUsers.map(({ user }) => user)] : [], + ); + $effect(() => { if (!album.isActivityEnabled && activityManager.commentCount === 0) { isShowActivity = false; @@ -418,6 +425,7 @@ + {#if containsEditors} + (showAlbumUsers = !showAlbumUsers)} + /> + {/if} + {#if isEditor}