refactor: web activity queries (#33038)
This commit is contained in:
1 parent
a8786e5f17
commit
6520f7820b
8 files changed
+191
-272
No files matched your search
@@ -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();
|
||||
@@ -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' } },
|
||||
});
|
||||
+38
-31
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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] } });
|
||||
});
|
||||
Reference in new issue
Block a user