refactor: activity manager (#17943)

This commit is contained in:
Daniel Dietzler 2025-04-30 17:50:38 +02:00 committed by GitHub
parent be5cc2cdf5
commit 436cff72b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 175 additions and 206 deletions

View file

@ -1,32 +1,24 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { AppRoute, timeBeforeShowLoadingSpinner } from '$lib/constants';
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
import { getAssetType } from '$lib/utils/asset-utils';
import { autoGrowHeight } from '$lib/actions/autogrow';
import { shortcut } from '$lib/actions/shortcut';
import Icon from '$lib/components/elements/icon.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AppRoute, timeBeforeShowLoadingSpinner } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte';
import { locale } from '$lib/stores/preferences.store';
import { getAssetThumbnailUrl } from '$lib/utils';
import { getAssetType } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { isTenMinutesApart } from '$lib/utils/timesince';
import {
ReactionType,
createActivity,
deleteActivity,
getActivities,
type ActivityResponseDto,
type AssetTypeEnum,
type UserResponseDto,
} from '@immich/sdk';
import { mdiClose, mdiDotsVertical, mdiHeart, mdiSend, mdiDeleteOutline } from '@mdi/js';
import { ReactionType, type ActivityResponseDto, type AssetTypeEnum, type UserResponseDto } from '@immich/sdk';
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiHeart, mdiSend } from '@mdi/js';
import * as luxon from 'luxon';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import UserAvatar from '../shared-components/user-avatar.svelte';
import { locale } from '$lib/stores/preferences.store';
import { shortcut } from '$lib/actions/shortcut';
import { t } from 'svelte-i18n';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
@ -48,34 +40,16 @@
};
interface Props {
reactions: ActivityResponseDto[];
user: UserResponseDto;
assetId?: string | undefined;
albumId: string;
assetType?: AssetTypeEnum | undefined;
albumOwnerId: string;
disabled: boolean;
isLiked: ActivityResponseDto | null;
onDeleteComment: () => void;
onDeleteLike: () => void;
onAddComment: () => void;
onClose: () => void;
}
let {
reactions = $bindable(),
user,
assetId = undefined,
albumId,
assetType = undefined,
albumOwnerId,
disabled,
isLiked,
onDeleteComment,
onDeleteLike,
onAddComment,
onClose,
}: Props = $props();
let { user, assetId = undefined, albumId, assetType = undefined, albumOwnerId, disabled, onClose }: Props = $props();
let innerHeight: number = $state(0);
let activityHeight: number = $state(0);
@ -85,36 +59,18 @@
let message = $state('');
let isSendingMessage = $state(false);
onMount(async () => {
await getReactions();
});
const getReactions = async () => {
try {
reactions = await getActivities({ assetId, albumId });
} catch (error) {
handleError(error, $t('errors.unable_to_load_asset_activity'));
}
};
const timeOptions = {
const timeOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
} as Intl.DateTimeFormatOptions;
};
const handleDeleteReaction = async (reaction: ActivityResponseDto, index: number) => {
try {
await deleteActivity({ id: reaction.id });
reactions.splice(index, 1);
if (isLiked && reaction.type === ReactionType.Like && reaction.id == isLiked.id) {
onDeleteLike();
} else {
onDeleteComment();
}
await activityManager.deleteActivity(reaction, index);
const deleteMessages: Record<ReactionType, string> = {
[ReactionType.Comment]: $t('comment_deleted'),
@ -135,13 +91,9 @@
}
const timeout = setTimeout(() => (isSendingMessage = true), timeBeforeShowLoadingSpinner);
try {
const data = await createActivity({
activityCreateDto: { albumId, assetId, type: ReactionType.Comment, comment: message },
});
reactions.push(data);
await activityManager.addActivity({ albumId, assetId, type: ReactionType.Comment, comment: message });
message = '';
onAddComment();
} catch (error) {
handleError(error, $t('errors.unable_to_add_comment'));
} finally {
@ -156,7 +108,6 @@
});
$effect(() => {
if (assetId && previousAssetId != assetId) {
handlePromiseError(getReactions());
previousAssetId = assetId;
}
});
@ -184,7 +135,7 @@
class="overflow-y-auto immich-scrollbar relative w-full px-2"
style="height: {divHeight}px;padding-bottom: {chatHeight}px"
>
{#each reactions as reaction, index (reaction.id)}
{#each activityManager.activities as reaction, index (reaction.id)}
{#if reaction.type === ReactionType.Comment}
<div class="flex dark:bg-gray-800 bg-gray-200 py-3 ps-3 mt-3 rounded-lg gap-4 justify-start">
<div class="flex items-center">
@ -221,7 +172,7 @@
{/if}
</div>
{#if (index != reactions.length - 1 && !shouldGroup(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1}
{#if (index != activityManager.activities.length - 1 && !shouldGroup(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
<div
class="pt-1 px-2 text-right w-full text-sm text-gray-500 dark:text-gray-300"
title={new Date(reaction.createdAt).toLocaleDateString(undefined, timeOptions)}
@ -273,7 +224,7 @@
</div>
{/if}
</div>
{#if (index != reactions.length - 1 && isTenMinutesApart(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1}
{#if (index != activityManager.activities.length - 1 && isTenMinutesApart(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
<div
class="pt-1 px-2 text-right w-full text-sm text-gray-500 dark:text-gray-300"
title={new Date(reaction.createdAt).toLocaleDateString(navigator.language, timeOptions)}

View file

@ -5,8 +5,8 @@
import NextAssetAction from '$lib/components/asset-viewer/actions/next-asset-action.svelte';
import PreviousAssetAction from '$lib/components/asset-viewer/actions/previous-asset-action.svelte';
import { AssetAction, ProjectionType } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { updateNumberOfComments } from '$lib/stores/activity.store';
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isShowDetail } from '$lib/stores/preferences.store';
@ -19,15 +19,9 @@
import {
AssetJobName,
AssetTypeEnum,
ReactionType,
createActivity,
deleteActivity,
getActivities,
getActivityStatistics,
getAllAlbums,
getStack,
runAssetJobs,
type ActivityResponseDto,
type AlbumResponseDto,
type AssetResponseDto,
type PersonResponseDto,
@ -61,7 +55,6 @@
person?: PersonResponseDto | null;
preAction?: PreAction | undefined;
onAction?: OnAction | undefined;
reactions?: ActivityResponseDto[];
showCloseButton?: boolean;
onClose: (dto: { asset: AssetResponseDto }) => void;
onNext: () => Promise<HasAsset>;
@ -80,7 +73,6 @@
person = null,
preAction = undefined,
onAction = undefined,
reactions = $bindable([]),
showCloseButton,
onClose,
onNext,
@ -107,8 +99,6 @@
let previewStackedAsset: AssetResponseDto | undefined = $state();
let isShowActivity = $state(false);
let isShowEditor = $state(false);
let isLiked: ActivityResponseDto | null = $state(null);
let numberOfComments = $state(0);
let fullscreenElement = $state<Element>();
let unsubscribes: (() => void)[] = [];
let selectedEditType: string = $state('');
@ -136,59 +126,20 @@
});
};
const handleAddComment = () => {
numberOfComments++;
updateNumberOfComments(1);
};
const handleRemoveComment = () => {
numberOfComments--;
updateNumberOfComments(-1);
};
const handleFavorite = async () => {
if (album && album.isActivityEnabled) {
try {
if (isLiked) {
const activityId = isLiked.id;
await deleteActivity({ id: activityId });
reactions = reactions.filter((reaction) => reaction.id !== activityId);
isLiked = null;
} else {
const data = await createActivity({
activityCreateDto: { albumId: album.id, assetId: asset.id, type: ReactionType.Like },
});
isLiked = data;
reactions = [...reactions, isLiked];
}
await activityManager.toggleLike();
} catch (error) {
handleError(error, $t('errors.unable_to_change_favorite'));
}
}
};
const getFavorite = async () => {
if (album && $user) {
try {
const data = await getActivities({
userId: $user.id,
assetId: asset.id,
albumId: album.id,
$type: ReactionType.Like,
});
isLiked = data.length > 0 ? data[0] : null;
} catch (error) {
handleError(error, $t('errors.unable_to_load_liked_status'));
}
}
};
const getNumberOfComments = async () => {
const updateComments = async () => {
if (album) {
try {
const { comments } = await getActivityStatistics({ assetId: asset.id, albumId: album.id });
numberOfComments = comments;
await activityManager.refreshActivities(album.id, asset.id);
} catch (error) {
handleError(error, $t('errors.unable_to_get_comments_number'));
}
@ -227,6 +178,10 @@
if (!sharedLink) {
await handleGetAllAlbums();
}
if (album) {
activityManager.init(album.id, asset.id);
}
});
onDestroy(() => {
@ -241,6 +196,8 @@
for (const unsubscribe of unsubscribes) {
unsubscribe();
}
activityManager.reset();
});
const handleGetAllAlbums = async () => {
@ -402,14 +359,13 @@
}
});
$effect(() => {
if (album && !album.isActivityEnabled && numberOfComments === 0) {
if (album && !album.isActivityEnabled && activityManager.commentCount === 0) {
isShowActivity = false;
}
});
$effect(() => {
if (isShared && asset.id) {
handlePromiseError(getFavorite());
handlePromiseError(getNumberOfComments());
handlePromiseError(updateComments());
}
});
$effect(() => {
@ -547,12 +503,12 @@
onVideoStarted={handleVideoStarted}
/>
{/if}
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || numberOfComments > 0)}
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || activityManager.commentCount > 0)}
<div class="z-[9999] absolute bottom-0 end-0 mb-20 me-8">
<ActivityStatus
disabled={!album?.isActivityEnabled}
{isLiked}
{numberOfComments}
isLiked={activityManager.isLiked}
numberOfComments={activityManager.commentCount}
onFavorite={handleFavorite}
onOpenActivityTab={handleOpenActivity}
/>
@ -642,11 +598,6 @@
albumOwnerId={album.ownerId}
albumId={album.id}
assetId={asset.id}
{isLiked}
bind:reactions
onAddComment={handleAddComment}
onDeleteComment={handleRemoveComment}
onDeleteLike={() => (isLiked = null)}
onClose={() => (isShowActivity = false)}
/>
</div>