refactor: web activity queries (#33038)

This commit is contained in:
Jason Rasmussen authored and Codex committed 2026-11-06 21:37:35 +03:00
1 parent a8786e5f17
commit 6520f7820b
8 files changed
+191 -272

No files matched your search

-1
View File
@@ -1080,7 +1080,6 @@
"unable_to_empty_trash": "Unable to empty trash",
"unable_to_enter_fullscreen": "Unable to enter fullscreen",
"unable_to_exit_fullscreen": "Unable to exit fullscreen",
"unable_to_get_comments_number": "Unable to get number of comments",
"unable_to_get_shared_link": "Failed to get shared link",
"unable_to_hide_person": "Unable to hide person",
"unable_to_leave_cluster_group": "Unable to leave cluster group",
@@ -1,17 +1,18 @@
<script lang="ts">
import { shortcut } from '#lib/actions/shortcut.ts';
import { timeBeforeShowLoadingSpinner } from '#lib/constants.ts';
import { activityManager } from '#lib/managers/activity-manager.svelte.ts';
import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts';
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { createActivityMutation, deleteActivityMutation, getActivitiesQuery } from '#lib/queries/activity.query.ts';
import { executeMutation } from '#lib/queries/client.ts';
import { Route } from '#lib/route.ts';
import { locale } from '#lib/stores/preferences.store.ts';
import { getAssetMediaUrl } from '#lib/utils.ts';
import { handleError } from '#lib/utils/handle-error.ts';
import { isTenMinutesApart } from '#lib/utils/timesince.ts';
import { AssetTypeEnum, ReactionType, type ActivityResponseDto, type AlbumUserResponseDto } from '@immich/sdk';
import { ContextMenuButton, Icon, IconButton, LoadingSpinner, Textarea, toastManager } from '@immich/ui';
import { ContextMenuButton, Icon, IconButton, LoadingSpinner, Textarea } from '@immich/ui';
import { mdiClose, mdiSend, mdiThumbUp, mdiTrashCanOutline } from '@mdi/js';
import { createQuery } from '@tanstack/svelte-query';
import * as luxon from 'luxon';
import { t } from 'svelte-i18n';
import { fromAction } from 'svelte/attachments';
@@ -46,6 +47,9 @@
let { assetId = undefined, albumId, assetType = undefined, albumUsers, disabled }: Props = $props();
const query = createQuery(() => getActivitiesQuery({ albumId, assetId }));
const activities = $derived(query.data ?? []);
let innerHeight: number = $state(0);
let activityHeight: number = $state(0);
let chatHeight: number = $state(0);
@@ -66,18 +70,16 @@
hour12: false,
};
const handleDeleteReaction = async (reaction: ActivityResponseDto) => {
try {
await activityManager.deleteActivity(reaction);
const deleteMessages: Record<ReactionType, string> = {
[ReactionType.Comment]: $t('comment_deleted'),
[ReactionType.Like]: $t('like_deleted'),
};
toastManager.primary(deleteMessages[reaction.type]);
} catch (error) {
handleError(error, $t('errors.unable_to_remove_reaction'));
}
const handleDeleteReaction = (reaction: ActivityResponseDto) => {
const successToast = reaction.type === ReactionType.Comment ? 'comment_deleted' : 'like_deleted';
return executeMutation(
() =>
deleteActivityMutation({
success: { toast: successToast },
error: { toast: 'errors.unable_to_remove_reaction' },
}),
[{ id: reaction.id }],
);
};
const handleSendComment = async () => {
@@ -85,15 +87,12 @@
return;
}
const timeout = setTimeout(() => (isSendingMessage = true), timeBeforeShowLoadingSpinner);
try {
await activityManager.addActivity({ albumId, assetId, type: ReactionType.Comment, comment: message });
message = '';
} catch (error) {
handleError(error, $t('errors.unable_to_add_comment'));
} finally {
clearTimeout(timeout);
}
await executeMutation(
createActivityMutation,
[{ activityCreateDto: { albumId, assetId, type: ReactionType.Comment, comment: message } }],
{ onSuccess: () => (message = '') },
);
clearTimeout(timeout);
isSendingMessage = false;
};
@@ -105,7 +104,7 @@
});
$effect(() => {
if (scrollContainer && activityManager.activities.length > 0 && !hasScrolledToBottom) {
if (scrollContainer && activities.length > 0 && !hasScrolledToBottom) {
hasScrolledToBottom = true;
scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
@@ -158,7 +157,7 @@
class="relative w-full immich-scrollbar overflow-y-auto px-2"
style="height: {divHeight}px;padding-bottom: {chatHeight}px"
>
{#each activityManager.activities as reaction, index (reaction.id)}
{#each activities as reaction, index (reaction.id)}
{#if reaction.type === ReactionType.Comment}
<div class="mt-3 flex justify-start gap-4 rounded-lg bg-gray-200 p-3 dark:bg-gray-800">
<div class="flex items-center">
@@ -178,7 +177,7 @@
{@render reactionMenu(reaction, $t('comment_options'))}
</div>
{#if (index !== activityManager.activities.length - 1 && !shouldGroup(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
{#if (index !== activities.length - 1 && !shouldGroup(activities[index].createdAt, activities[index + 1].createdAt)) || index === activities.length - 1}
<div
class="w-full px-2 pt-1 text-right text-sm text-gray-500 dark:text-gray-300"
title={new Date(reaction.createdAt).toLocaleDateString(undefined, timeOptions)}
@@ -208,7 +207,7 @@
{/if}
{@render reactionMenu(reaction, $t('reaction_options'))}
</div>
{#if (index !== activityManager.activities.length - 1 && isTenMinutesApart(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
{#if (index !== activities.length - 1 && isTenMinutesApart(activities[index].createdAt, activities[index + 1].createdAt)) || index === activities.length - 1}
<div
class="w-full px-2 pt-1 text-right text-sm text-gray-500 dark:text-gray-300"
title={new Date(reaction.createdAt).toLocaleDateString(navigator.language, timeOptions)}
@@ -8,13 +8,18 @@
import PreviousAssetAction from '#lib/components/asset-viewer/actions/PreviousAssetAction.svelte';
import type { Action, OnAction, PreAction } from '#lib/components/asset-viewer/actions/action.ts';
import { AssetAction, ProjectionType } from '#lib/constants.ts';
import { activityManager } from '#lib/managers/activity-manager.svelte.ts';
import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts';
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { EditToolType, editManager } from '#lib/managers/edit/edit-manager.svelte.ts';
import { eventManager } from '#lib/managers/event-manager.svelte.ts';
import type { TimelineAsset } from '#lib/managers/timeline-manager/types.ts';
import { queryClient } from '#lib/queries/client.ts';
import {
createActivityMutation,
deleteActivityMutation,
getActivityStatisticsQuery,
getMyLikeQuery,
} from '#lib/queries/activity.query.ts';
import { executeMutation, queryClient } from '#lib/queries/client.ts';
import { getStackQuery } from '#lib/queries/stack.query.ts';
import { getAssetActions } from '#lib/services/asset.service.ts';
import { faceManager } from '#lib/stores/face.svelte.ts';
@@ -31,6 +36,7 @@
import { toTimelineAsset } from '#lib/utils/timeline-util.ts';
import {
AssetTypeEnum,
ReactionType,
getAssetInfo,
type AlbumResponseDto,
type AssetResponseDto,
@@ -38,7 +44,7 @@
type StackResponseDto,
} from '@immich/sdk';
import { CommandPaletteDefaultProvider } from '@immich/ui';
import { createQuery } from '@tanstack/svelte-query';
import { createQuery, type MutationMeta } from '@tanstack/svelte-query';
import { onDestroy, onMount, untrack } from 'svelte';
import type { SwipeCustomEvent } from 'svelte-gestures';
import { t } from 'svelte-i18n';
@@ -108,13 +114,25 @@
const asset = $derived(previewStackedAsset ?? cursor.current);
const stackId = $derived(withStacked && !authManager.isSharedLink ? cursor.current.stack?.id : undefined);
const stackQuery = createQuery(
() => ({ ...getStackQuery(stackId ?? ''), enabled: !!stackId }),
() => queryClient,
);
const activityParams = $derived({ albumId: album?.id ?? '', assetId: asset.id });
const isActivityEnabled = $derived(!!album && isShared && authManager.authenticated);
const queries = {
stack: createQuery(
() => ({ ...getStackQuery(stackId ?? ''), enabled: !!stackId }),
() => queryClient,
),
activityStatistics: createQuery(() => ({
...getActivityStatisticsQuery(activityParams),
enabled: isActivityEnabled,
})),
myLike: createQuery(() => ({ ...getMyLikeQuery(activityParams), enabled: isActivityEnabled })),
};
const stack = $derived(
stackQuery.data?.assets.some(({ id }) => id === cursor.current.id) ? stackQuery.data : undefined,
queries.stack.data?.assets.some(({ id }) => id === cursor.current.id) ? queries.stack.data : undefined,
);
const commentCount = $derived(queries.activityStatistics.data?.comments ?? 0);
const likeCount = $derived(queries.activityStatistics.data?.likes ?? 0);
const myLike = $derived(queries.myLike.data ?? null);
const nextAsset = $derived(cursor.nextAsset);
const previousAsset = $derived(cursor.previousAsset);
let sharedLink = getSharedLink();
@@ -132,11 +150,13 @@
return;
}
try {
await activityManager.toggleLike();
} catch (error) {
handleError(error, $t('errors.unable_to_change_favorite'));
}
const meta: MutationMeta = { error: { toast: 'errors.unable_to_change_favorite' } };
await (myLike
? executeMutation(() => deleteActivityMutation(meta), [{ id: myLike.id }])
: executeMutation(
() => createActivityMutation(meta),
[{ activityCreateDto: { ...activityParams, type: ReactionType.Like } }],
));
};
const onAssetUpdate = (updatedAsset: AssetResponseDto) => {
@@ -200,7 +220,6 @@
});
onDestroy(() => {
activityManager.reset();
assetViewerManager.resetPanelState();
syncAssetViewerOpenClass(false);
preloadManager.destroy();
@@ -363,15 +382,10 @@
let isFullScreen = $derived(!!fullscreenElement);
$effect(() => {
if (album && !album.isActivityEnabled && activityManager.commentCount === 0) {
if (album && !album.isActivityEnabled && commentCount === 0) {
assetViewerManager.closeActivityPanel();
}
});
$effect(() => {
if (album && isShared && asset.id) {
handlePromiseError(activityManager.init(album.id, asset.id));
}
});
const syncAssetViewerOpenClass = (isOpen: boolean) => {
if (browser) {
@@ -434,8 +448,8 @@
const showActivityStatus = $derived(
$slideshowState === SlideshowState.None &&
isShared &&
((album && album.isActivityEnabled) || activityManager.commentCount > 0) &&
!activityManager.isLoading,
((album && album.isActivityEnabled) || commentCount > 0) &&
!queries.activityStatistics.isLoading,
);
const showOcrButton = $derived(
$slideshowState === SlideshowState.None &&
@@ -581,9 +595,9 @@
<div class="absolute inset-e-0 bottom-0 me-8 mb-20">
<ActivityStatus
disabled={!album?.isActivityEnabled}
isLiked={activityManager.isLiked}
numberOfComments={activityManager.commentCount}
numberOfLikes={activityManager.likeCount}
isLiked={myLike}
numberOfComments={commentCount}
numberOfLikes={likeCount}
onFavorite={handleFavorite}
/>
</div>
@@ -1,187 +0,0 @@
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { handlePromiseError } from '#lib/utils.ts';
import { handleError } from '#lib/utils/handle-error.ts';
import {
ReactionLevel,
ReactionType,
createActivity,
deleteActivity,
getActivities,
getActivityStatistics,
type ActivityCreateDto,
type ActivityResponseDto,
} from '@immich/sdk';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
type CacheKey = string;
type ActivityCache = {
activities: ActivityResponseDto[];
commentCount: number;
likeCount: number;
isLiked: ActivityResponseDto | null;
};
class ActivityManager {
#albumId = $state<string | undefined>();
#assetId = $state<string | undefined>();
#activities = $state<ActivityResponseDto[]>([]);
#commentCount = $state(0);
#likeCount = $state(0);
#isLiked = $state<ActivityResponseDto | null>(null);
#cache = new Map<CacheKey, ActivityCache>();
isLoading = $state(false);
get assetId() {
return this.#assetId;
}
get activities() {
return this.#activities;
}
get commentCount() {
return this.#commentCount;
}
get likeCount() {
return this.#likeCount;
}
get isLiked() {
return this.#isLiked;
}
#getCacheKey(albumId: string, assetId: string = '') {
return `${albumId}:${assetId}`;
}
async init(albumId: string, assetId?: string) {
if (assetId && assetId === this.#assetId) {
return;
}
this.#albumId = albumId;
this.#assetId = assetId;
try {
await activityManager.refreshActivities(albumId, assetId);
} catch (error) {
handleError(error, get(t)('errors.unable_to_get_comments_number'));
}
}
#invalidateCache(albumId: string, assetId?: string) {
this.#cache.delete(this.#getCacheKey(albumId));
this.#cache.delete(this.#getCacheKey(albumId, assetId));
}
async addActivity(dto: ActivityCreateDto) {
if (this.#albumId === undefined) {
return;
}
const activity = await createActivity({ activityCreateDto: dto });
this.#activities = [...this.#activities, activity];
if (activity.type === ReactionType.Comment) {
this.#commentCount++;
}
if (activity.type === ReactionType.Like) {
this.#likeCount++;
}
this.#invalidateCache(this.#albumId, this.#assetId);
handlePromiseError(this.refreshActivities(this.#albumId, this.#assetId));
return activity;
}
async deleteActivity(activity: ActivityResponseDto) {
if (!this.#albumId) {
return;
}
if (activity.type === ReactionType.Comment) {
this.#commentCount--;
}
if (activity.type === ReactionType.Like) {
this.#likeCount--;
}
this.#activities = this.#activities.filter(({ id }) => id !== activity.id);
await deleteActivity({ id: activity.id });
this.#invalidateCache(this.#albumId, this.#assetId);
handlePromiseError(this.refreshActivities(this.#albumId, this.#assetId));
}
async toggleLike() {
if (!this.#albumId) {
return;
}
if (this.#isLiked) {
await this.deleteActivity(this.#isLiked);
this.#isLiked = null;
} else {
this.#isLiked = (await this.addActivity({
albumId: this.#albumId,
assetId: this.#assetId,
type: ReactionType.Like,
}))!;
}
}
async refreshActivities(albumId: string, assetId?: string) {
this.isLoading = true;
const cacheKey = this.#getCacheKey(albumId, assetId);
if (this.#cache.has(cacheKey)) {
const cached = this.#cache.get(cacheKey)!;
this.#activities = cached.activities;
this.#commentCount = cached.commentCount;
this.#likeCount = cached.likeCount;
this.#isLiked = cached.isLiked ?? null;
this.isLoading = false;
return;
}
this.#activities = await getActivities({ albumId, assetId });
const [liked] = await getActivities({
albumId,
assetId,
userId: authManager.user.id,
$type: ReactionType.Like,
level: assetId ? undefined : ReactionLevel.Album,
});
this.#isLiked = liked ?? null;
const { comments, likes } = await getActivityStatistics({ albumId, assetId });
this.#commentCount = comments;
this.#likeCount = likes;
this.#cache.set(cacheKey, {
activities: this.#activities,
commentCount: this.#commentCount,
likeCount: this.#likeCount,
isLiked: this.#isLiked,
});
this.isLoading = false;
}
reset() {
this.#albumId = undefined;
this.#assetId = undefined;
this.#activities = [];
this.#commentCount = 0;
this.#likeCount = 0;
}
}
export const activityManager = new ActivityManager();
+66
View File
@@ -0,0 +1,66 @@
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { asMutation, queryClient } from '#lib/queries/client.ts';
import {
ReactionType,
createActivity,
deleteActivity,
getActivities,
getActivityStatistics,
type ActivityResponseDto,
} from '@immich/sdk';
import { queryOptions, type MutationMeta } from '@tanstack/svelte-query';
export type ActivityParams = { albumId: string; assetId?: string };
export const activitiesQueryKey = ['activities'];
const isMyLike = (activity: ActivityResponseDto, assetId?: string) =>
activity.type === ReactionType.Like &&
activity.user.id === authManager.user.id &&
activity.assetId === (assetId ?? null);
export const invalidateActivities = ({ albumId }: { albumId: string }) =>
queryClient.invalidateQueries({ queryKey: [...activitiesQueryKey, albumId] });
export const getActivitiesQuery = ({ albumId, assetId }: ActivityParams) =>
queryOptions({
queryKey: [...activitiesQueryKey, albumId, assetId ?? null],
queryFn: ({ signal }) => getActivities({ albumId, assetId }, { signal }),
});
export const getMyLikeQuery = ({ albumId, assetId }: ActivityParams) =>
queryOptions({
...getActivitiesQuery({ albumId, assetId }),
select: (activities) => activities.find((activity) => isMyLike(activity, assetId)) ?? null,
});
export const getActivityStatisticsQuery = ({ albumId, assetId }: ActivityParams) =>
queryOptions({
queryKey: [...activitiesQueryKey, albumId, assetId ?? null, 'statistics'],
queryFn: ({ signal }) => getActivityStatistics({ albumId, assetId }, { signal }),
});
export const createActivityMutation = (meta?: MutationMeta) =>
asMutation(createActivity, {
onSuccess: async (activity, [{ activityCreateDto }]) => {
const { albumId, assetId } = activityCreateDto;
queryClient.setQueryData(
getActivitiesQuery({ albumId, assetId }).queryKey,
(activities) => activities && [...activities, activity],
);
await invalidateActivities({ albumId });
},
meta: meta ?? { error: { toast: 'errors.unable_to_add_comment' } },
});
export const deleteActivityMutation = (meta?: MutationMeta) =>
asMutation(deleteActivity, {
onSuccess: async (_, [{ id }]) => {
queryClient.setQueriesData<ActivityResponseDto[]>(
{ queryKey: activitiesQueryKey, predicate: ({ state }) => Array.isArray(state.data) },
(activities) => activities?.filter((activity) => activity.id !== id),
);
await queryClient.invalidateQueries({ queryKey: activitiesQueryKey });
},
meta: meta ?? { error: { toast: 'errors.unable_to_remove_reaction' } },
});
@@ -22,12 +22,18 @@
import SelectAllAssets from '#lib/components/timeline/actions/SelectAllAction.svelte';
import SetVisibilityAction from '#lib/components/timeline/actions/SetVisibilityAction.svelte';
import { AlbumPageViewMode } from '#lib/constants.ts';
import { activityManager } from '#lib/managers/activity-manager.svelte.ts';
import { AssetMultiSelectManager, assetMultiSelectManager } from '#lib/managers/asset-multi-select-manager.svelte.ts';
import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts';
import { authManager } from '#lib/managers/auth-manager.svelte.ts';
import { TimelineManager } from '#lib/managers/timeline-manager/timeline-manager.svelte.ts';
import type { TimelineAsset } from '#lib/managers/timeline-manager/types.ts';
import {
createActivityMutation,
deleteActivityMutation,
getActivityStatisticsQuery,
getMyLikeQuery,
invalidateActivities,
} from '#lib/queries/activity.query.ts';
import {
deleteAlbumMutation,
getAlbumInfoQuery,
@@ -43,9 +49,8 @@
import { SlideshowNavigation, SlideshowState, slideshowStore } from '#lib/stores/slideshow.store.ts';
import { handlePromiseError } from '#lib/utils.ts';
import { delay } from '#lib/utils/asset-utils.ts';
import { handleError } from '#lib/utils/handle-error.ts';
import { isAlbumsRoute, navigate, type AssetGridRouteSearchParams } from '#lib/utils/navigation.ts';
import { AlbumUserRole, AssetVisibility } from '@immich/sdk';
import { AlbumUserRole, AssetVisibility, ReactionType } from '@immich/sdk';
import { ActionButton, CommandPaletteDefaultProvider, Icon, IconButton } from '@immich/ui';
import {
mdiAccountEye,
@@ -59,7 +64,7 @@
mdiPlus,
mdiPresentationPlay,
} from '@mdi/js';
import { onDestroy } from 'svelte';
import { createQuery, type MutationMeta } from '@tanstack/svelte-query';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
import { goto, onNavigate } from '$app/navigation';
@@ -69,26 +74,37 @@
let { params }: PageProps = $props();
const albumQuery = createPreloadedQuery(() => getAlbumInfoQuery(params.albumId));
const album = $derived(albumQuery.data);
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
const isShared = $derived(viewMode !== AlbumPageViewMode.SELECT_ASSETS && album.albumUsers.length > 1);
const queries = {
config: createPreloadedQuery(getUserConfigQuery),
album: createPreloadedQuery(() => getAlbumInfoQuery(params.albumId)),
activityStatistics: createQuery(() => ({
...getActivityStatisticsQuery({ albumId: album.id }),
enabled: isShared,
})),
myLike: createQuery(() => ({ ...getMyLikeQuery({ albumId: album.id }), enabled: isShared })),
};
const config = $derived(queries.config.data);
const album = $derived(queries.album.data);
const commentCount = $derived(queries.activityStatistics.data?.comments ?? 0);
const myLike = $derived(queries.myLike.data ?? null);
let { slideshowState, slideshowNavigation } = slideshowStore;
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
let timelineManager = $state<TimelineManager>() as TimelineManager;
let showAlbumUsers = $derived(timelineManager?.showAssetOwners ?? false);
const timelineMultiSelectManager = new AssetMultiSelectManager();
const handleFavorite = async () => {
try {
await activityManager.toggleLike();
} catch (error) {
handleError(error, $t('errors.cant_change_asset_favorite'));
}
const meta: MutationMeta = { error: { toast: 'errors.cant_change_asset_favorite' } };
await (myLike
? executeMutation(() => deleteActivityMutation(meta), [{ id: myLike.id }])
: executeMutation(
() => createActivityMutation(meta),
[{ activityCreateDto: { albumId: album.id, type: ReactionType.Like } }],
));
};
const handleStartSlideshow = async () => {
@@ -147,6 +163,7 @@
const onAlbumRemoveAssets = async ({ albumIds }: { assetIds: string[]; albumIds: string[] }) => {
if (albumIds.includes(album.id)) {
await invalidateActivities({ albumId: album.id });
await delay(1000);
await refreshAlbum();
}
@@ -208,7 +225,7 @@
const albumUsers = $derived(showAlbumUsers && containsEditors ? album.albumUsers.map(({ user }) => user) : []);
$effect(() => {
if (!album.isActivityEnabled && activityManager.commentCount === 0) {
if (!album.isActivityEnabled && commentCount === 0) {
assetViewerManager.closeActivityPanel();
}
});
@@ -224,24 +241,10 @@
return { albumId, order: album.order };
});
const isShared = $derived(viewMode !== AlbumPageViewMode.SELECT_ASSETS && album.albumUsers.length > 1);
$effect(() => {
if (assetViewerManager.isViewing || !isShared) {
return;
}
handlePromiseError(activityManager.init(album.id));
});
onDestroy(() => activityManager.reset());
const isOwned = $derived(isAlbumOwner(album));
let showActivityStatus = $derived(
album.albumUsers.length > 1 &&
!assetViewerManager.isViewing &&
(album.isActivityEnabled || activityManager.commentCount > 0),
album.albumUsers.length > 1 && !assetViewerManager.isViewing && (album.isActivityEnabled || commentCount > 0),
);
const isEditor = $derived(isAlbumEditor(album));
@@ -283,7 +286,11 @@
});
</script>
<OnEvents {onAlbumAddAssets} {onAlbumRemoveAssets} onAssetsDelete={() => refreshAlbum()} />
<OnEvents
{onAlbumAddAssets}
{onAlbumRemoveAssets}
onAssetsDelete={() => Promise.all([refreshAlbum(), invalidateActivities({ albumId: album.id })])}
/>
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload, Close, ...Object.values(Actions)]} />
@@ -382,8 +389,8 @@
<div class="absolute inset-e-0 bottom-0 z-2 me-12 mb-6">
<ActivityStatus
disabled={!album.isActivityEnabled}
isLiked={activityManager.isLiked}
numberOfComments={activityManager.commentCount}
isLiked={myLike}
numberOfComments={commentCount}
numberOfLikes={undefined}
onFavorite={handleFavorite}
/>
+4
View File
@@ -385,6 +385,10 @@ export class MockData {
removeFromAlbum(albumId: string, assetIds: string[]) {
const members = this.albumAssets.get(albumId);
const removed = [...this.activities.values()].filter(({ assetId }) => assetId && assetIds.includes(assetId));
for (const { id } of removed) {
this.activities.delete(id);
}
return assetIds.map((id) => ({ id, success: members?.delete(id) ?? false }));
}
@@ -81,3 +81,20 @@ test('trashing a liked asset removes the like from the album activity', async ({
});
expect(api.deleteAssets).toHaveBeenCalledWith({ body: { ids: [asset.id], force: false } });
});
test('removing an asset from the album updates the comment count', async ({ page, data, api, app }) => {
const { album, asset, other } = setup(data);
data.addActivities([
factory.activity({ user: other, assetId: asset.id, comment: 'on the asset' }),
factory.activity({ user: other, comment: 'on the album' }),
]);
await page.goto(Route.viewAlbum(album));
await expect(app.album.activity.commentsButton).toHaveText('2');
await app.timeline.select(asset);
await app.timeline.selectionBar.removeFromAlbum();
await expect(app.timeline.thumbnail(asset)).toHaveCount(0);
await expect(app.album.activity.commentsButton).toHaveText('1');
expect(api.removeAssetFromAlbum).toHaveBeenCalledWith({ params: { id: album.id }, body: { ids: [asset.id] } });
});