Skip to content

Commit

Permalink
Add user avatars to web timelines
Browse files Browse the repository at this point in the history
  • Loading branch information
x24git committed Oct 16, 2024
1 parent 92e02b0 commit e5d0cbf
Show file tree
Hide file tree
Showing 12 changed files with 107 additions and 11 deletions.
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 (currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner && asset.ownerId != $user.id) || (asset.ownerId != $user.id && 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
26 changes: 21 additions & 5 deletions web/src/lib/components/assets/thumbnail/thumbnail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import { intersectionObserver } from '$lib/actions/intersection-observer';
import Icon from '$lib/components/elements/icon.svelte';
import { ProjectionType } from '$lib/constants';
import { getAssetThumbnailUrl, getUserInfo, 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 { user } from '$lib/stores/user.store';
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.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 @@ -20,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 Down Expand Up @@ -47,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 Down Expand Up @@ -76,6 +80,7 @@
let intersecting = false;
let lastRetrievedElement: HTMLElement | undefined;
let loaded = false;
let shareUser: UserResponseDto | undefined;
$: if (!retrieveElement) {
lastRetrievedElement = undefined;
Expand All @@ -84,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 @@ -161,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 @@ -270,9 +286,9 @@
</div>
{/if}

{#if isSharedLink() || asset.ownerId != user.userId}
{#if shareUser && showUserThumbnailsinViewer}
<div class="absolute bottom-2 left-2 z-10">
<UserAvatar user={$user} size="sm" />
<UserAvatar user={shareUser} size="sm" />
</div>
{/if}

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
3 changes: 3 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 All @@ -864,6 +866,7 @@
<AssetViewer
{withStacked}
{assetStore}
{showUserThumbnailsinViewer}
asset={$viewingAsset}
preloadAssets={$preloadAssets}
{isShared}
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
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>

0 comments on commit e5d0cbf

Please sign in to comment.