Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): Add Partner Sharing Avatars to Timeline and Info Cards #12413

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
} from '@mdi/js';
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { t } from 'svelte-i18n';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';

export let asset: AssetResponseDto;
export let album: AlbumResponseDto | null = null;
Expand Down Expand Up @@ -83,6 +84,11 @@
class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white"
data-testid="asset-viewer-navbar-actions"
>
{#if asset.owner && asset.owner.id != $user.id}
<div class="p-3 margin:auto">
<UserAvatar user={asset.owner} size="xs"></UserAvatar>
</div>
{/if}
{#if !asset.isTrashed && $user}
<ShareAction {asset} />
{/if}
Expand Down Expand Up @@ -125,7 +131,6 @@
title={$t('editor')}
/>
{/if} -->

{#if isOwner}
<DeleteAction {asset} {onAction} />

Expand Down
8 changes: 6 additions & 2 deletions web/src/lib/components/asset-viewer/detail-panel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,13 @@
</div>
{/if}

{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
{#if (asset.ownerId != $user?.id && asset.owner) || ($user === undefined && asset.owner)}
<section class="px-6 dark:text-immich-dark-fg mt-4">
<p class="text-sm">{$t('shared_by').toUpperCase()}</p>
{#if currentAlbum}
<p class="text-sm">{$t('shared_by').toUpperCase()}</p>
{:else}
<p class="text-sm">{$t('partner_sharing').toUpperCase()}</p>
{/if}
<div class="flex gap-4 pt-4">
<div>
<UserAvatar user={asset.owner} size="md" />
Expand Down
31 changes: 27 additions & 4 deletions web/src/lib/components/assets/thumbnail/thumbnail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
import { intersectionObserver } from '$lib/actions/intersection-observer';
import Icon from '$lib/components/elements/icon.svelte';
import { ProjectionType } from '$lib/constants';
import { getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
import { getAssetThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { getAltText } from '$lib/utils/thumbnail-util';
import { timeToSeconds } from '$lib/utils/date-time';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
import { user } from '$lib/stores/user.store';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type UserResponseDto } from '@immich/sdk';
import { locale, playVideoThumbnailOnHover, showUserThumbnails } from '$lib/stores/preferences.store';
import { getUserAndCacheResult } from '$lib/utils/users';
import { getAssetPlaybackUrl } from '$lib/utils';
import {
mdiArchiveArrowDownOutline,
Expand All @@ -19,6 +22,7 @@
} from '@mdi/js';

import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n';
import ImageThumbnail from './image-thumbnail.svelte';
import VideoThumbnail from './video-thumbnail.svelte';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
Expand All @@ -30,6 +34,7 @@
import { onDestroy } from 'svelte';
import { TUNABLES } from '$lib/utils/tunables';
import { thumbhash } from '$lib/actions/thumbhash';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';

export let asset: AssetResponseDto;
export let dateGroup: DateGroup | undefined = undefined;
Expand All @@ -45,6 +50,7 @@
export let showArchiveIcon = false;
export let showStackedIcon = true;
export let disableMouseOver = false;
export let showUserThumbnailsinViewer = true;
export let intersectionConfig: {
root?: HTMLElement;
bottom?: string;
Expand All @@ -64,7 +70,6 @@

let className = '';
export { className as class };

let {
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES;
Expand All @@ -75,6 +80,7 @@
let intersecting = false;
let lastRetrievedElement: HTMLElement | undefined;
let loaded = false;
let shareUser: UserResponseDto | undefined;

$: if (!retrieveElement) {
lastRetrievedElement = undefined;
Expand All @@ -83,6 +89,9 @@
lastRetrievedElement = element;
onRetrieveElement?.(element);
}
$: if ($showUserThumbnails && showUserThumbnailsinViewer && (isSharedLink() || asset.ownerId != $user.id)) {
handlePromiseError(getShareUser());
}

$: width = thumbnailSize || thumbnailWidth || 235;
$: height = thumbnailSize || thumbnailHeight || 235;
Expand Down Expand Up @@ -160,6 +169,14 @@
}
};

const getShareUser = async () => {
try {
shareUser = await getUserAndCacheResult(asset.ownerId);
} catch (error) {
handleError(error, $t('errors.unable_to_load_liked_status'));
}
};

onDestroy(() => {
assetStore?.taskManager.removeAllTasksForComponent(componentId);
});
Expand Down Expand Up @@ -269,6 +286,12 @@
</div>
{/if}

{#if shareUser && showUserThumbnailsinViewer}
<div class="absolute bottom-2 left-2 z-10">
<UserAvatar user={shareUser} size="sm" />
</div>
{/if}

{#if !isSharedLink() && showArchiveIcon && asset.isArchived}
<div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10">
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
Expand Down
2 changes: 2 additions & 0 deletions web/src/lib/components/photos-page/asset-date-group.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
export let singleSelect = false;
export let withStacked = false;
export let showArchiveIcon = false;
export let showUserThumbnailsinViewer = true;
export let assetGridElement: HTMLElement | undefined = undefined;
export let renderThumbsAtBottomMargin: string | undefined = undefined;
export let renderThumbsAtTopMargin: string | undefined = undefined;
Expand Down Expand Up @@ -207,6 +208,7 @@
onRetrieveElement={(element) => onRetrieveElement(dateGroup, asset, element)}
showStackedIcon={withStacked}
{showArchiveIcon}
{showUserThumbnailsinViewer}
{asset}
{groupIndex}
onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)}
Expand Down
2 changes: 2 additions & 0 deletions web/src/lib/components/photos-page/asset-grid.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
export let withStacked = false;
export let showArchiveIcon = false;
export let isShared = false;
export let showUserThumbnailsinViewer = true;
export let album: AlbumResponseDto | null = null;
export let isShowDeleteConfirmation = false;
export let onSelect: (asset: AssetResponseDto) => void = () => {};
Expand Down Expand Up @@ -839,6 +840,7 @@
renderThumbsAtBottomMargin={THUMBNAIL_INTERSECTION_ROOT_BOTTOM}
{withStacked}
{showArchiveIcon}
{showUserThumbnailsinViewer}
{assetStore}
{assetInteractionStore}
{isSelectionMode}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,6 @@
</ControlAppBar>
{/if}
<section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
<GalleryViewer {assets} bind:selectedAssets {viewport} />
<GalleryViewer {assets} bind:selectedAssets {viewport} showUserThumbnailsinViewer={false} />
</section>
</section>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
export let selectedAssets: Set<AssetResponseDto> = new Set();
export let disableAssetSelect = false;
export let showArchiveIcon = false;
export let showUserThumbnailsinViewer = true;
export let viewport: Viewport;
export let onIntersected: (() => void) | undefined = undefined;
export let showAssetName = false;
Expand Down Expand Up @@ -142,6 +143,7 @@
onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)}
selected={selectedAssets.has(asset)}
{showArchiveIcon}
{showUserThumbnailsinViewer}
thumbnailWidth={geometry.boxes[i].width}
thumbnailHeight={geometry.boxes[i].height}
/>
Expand Down
6 changes: 4 additions & 2 deletions web/src/lib/components/shared-components/user-avatar.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" context="module">
export type Size = 'full' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
export type Size = 'full' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
</script>

<script lang="ts">
Expand Down Expand Up @@ -56,6 +56,7 @@

const sizeClasses: Record<Size, string> = {
full: 'w-full h-full',
xs: 'w-6 h-6',
sm: 'w-7 h-7',
md: 'w-10 h-10',
lg: 'w-12 h-12',
Expand Down Expand Up @@ -90,7 +91,8 @@
{#if showFallback}
<span
class="flex h-full w-full select-none items-center justify-center font-medium"
class:text-xs={size === 'sm'}
class:text-xs={size === 'xs'}
class:text-sm={size === 'sm'}
class:text-lg={size === 'lg'}
class:text-xl={size === 'xl'}
class:text-2xl={size === 'xxl'}
Expand Down
9 changes: 9 additions & 0 deletions web/src/lib/components/user-settings-page/app-settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
loopVideo,
playVideoThumbnailOnHover,
showDeleteModal,
showUserThumbnails,
} from '$lib/stores/preferences.store';
import { findLocale } from '$lib/utils';
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
Expand Down Expand Up @@ -169,6 +170,14 @@
bind:checked={$showDeleteModal}
/>
</div>

<div class="ml-4">
<SettingSwitch
title={$t('show_user_thumbnails')}
subtitle={$t('show_user_thumbnails_description')}
bind:checked={$showUserThumbnails}
/>
</div>
</div>
</div>
</section>
2 changes: 2 additions & 0 deletions web/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,8 @@
"show_slideshow_transition": "Show slideshow transition",
"show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge",
"show_user_thumbnails": "Show user thumbnails",
"show_user_thumbnails_description": "Show user avatars on timelinle for shared albums and partners",
"shuffle": "Shuffle",
"sidebar": "Sidebar",
"sidebar_display_description": "Display a link to the view in the sidebar",
Expand Down
2 changes: 2 additions & 0 deletions web/src/lib/stores/preferences.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export const albumViewSettings = persisted<AlbumViewSettings>('album-view-settin

export const showDeleteModal = persisted<boolean>('delete-confirm-dialog', true, {});

export const showUserThumbnails = persisted<boolean>('show-user-thumbnails', true, {});

export const alwaysLoadOriginalFile = persisted<boolean>('always-load-original-file', false, {});

export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnail-on-hover', true, {});
Expand Down
35 changes: 35 additions & 0 deletions web/src/lib/stores/users.store.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need a complete store just for this. I'll let that to the web experts though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is another store that is worth integrating into, I don't think that would be an issue. We do need a store though, otherwise there would be API requests for User info for each thumbnail in the asset view.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type UserResponseDto } from '@immich/sdk';
import { writable } from 'svelte/store';

export const users = writable<{ [key: string]: UserResponseDto | undefined }>({});

export const userExistsInStore = (userId: string): boolean => {
let exists = false;
users.subscribe((userStore) => {
try {
exists = userId in userStore;
} catch {
exists = false;
}
})();
return exists;
};

export const updateUserInStore = ({ user, userId }: { user?: UserResponseDto; userId?: string }) => {
users.update((userStore) => {
if (user) {
userStore[user.id] = user;
} else if (userId) {
userStore[userId] = undefined;
}
return userStore;
});
};

export const getUserFromStore = (userId: string): UserResponseDto | undefined => {
let userInfo: UserResponseDto | undefined;
users.subscribe((userStore) => {
userInfo = userStore[userId];
})();
return userInfo;
};
16 changes: 16 additions & 0 deletions web/src/lib/utils/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getUserFromStore, updateUserInStore, userExistsInStore } from '$lib/stores/users.store';
import { getUser, type UserResponseDto } from '@immich/sdk';

export const getUserAndCacheResult = async (userId: string, skipCache: boolean = false): Promise<UserResponseDto> => {
let user: UserResponseDto;
if (!skipCache && userExistsInStore(userId)) {
user = getUserFromStore(userId)!;
} else {
//Add to store indicating a request to server is in-flight
updateUserInStore({ userId });
user = await getUser({ id: userId });
//Update store with results of server request
updateUserInStore({ user });
}
return user;
};
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@
</svelte:fragment>
</ControlAppBar>
{/if}
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} />
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} showUserThumbnailsinViewer={false} />
</main>
Loading