diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index ad0b814a587..27c478230db 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -14,25 +14,31 @@ class ActivityStatisticsResponseDto { /// Returns a new [ActivityStatisticsResponseDto] instance. ActivityStatisticsResponseDto({ required this.comments, + required this.likes, }); int comments; + int likes; + @override bool operator ==(Object other) => identical(this, other) || other is ActivityStatisticsResponseDto && - other.comments == comments; + other.comments == comments && + other.likes == likes; @override int get hashCode => // ignore: unnecessary_parenthesis - (comments.hashCode); + (comments.hashCode) + + (likes.hashCode); @override - String toString() => 'ActivityStatisticsResponseDto[comments=$comments]'; + String toString() => 'ActivityStatisticsResponseDto[comments=$comments, likes=$likes]'; Map toJson() { final json = {}; json[r'comments'] = this.comments; + json[r'likes'] = this.likes; return json; } @@ -46,6 +52,7 @@ class ActivityStatisticsResponseDto { return ActivityStatisticsResponseDto( comments: mapValueOfType(json, r'comments')!, + likes: mapValueOfType(json, r'likes')!, ); } return null; @@ -94,6 +101,7 @@ class ActivityStatisticsResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'comments', + 'likes', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 04cdf4f70b6..09c0143e805 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8482,10 +8482,14 @@ "properties": { "comments": { "type": "integer" + }, + "likes": { + "type": "integer" } }, "required": [ - "comments" + "comments", + "likes" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 3729ccd69fb..b390bf74779 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -38,6 +38,7 @@ export type ActivityCreateDto = { }; export type ActivityStatisticsResponseDto = { comments: number; + likes: number; }; export type NotificationCreateDto = { data?: object; diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index a97116cf359..d11fe8da7e3 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -29,6 +29,9 @@ export class ActivityResponseDto { export class ActivityStatisticsResponseDto { @ApiProperty({ type: 'integer' }) comments!: number; + + @ApiProperty({ type: 'integer' }) + likes!: number; } export class ActivityDto { diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 3db4ee6f5b0..c8b0c4315af 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -62,15 +62,21 @@ where -- ActivityRepository.getStatistics select - count(*) as "count" + count(*) filter ( + where + "activity"."isLiked" = $1 + ) as "comments", + count(*) filter ( + where + "activity"."isLiked" = $2 + ) as "likes" from "activity" inner join "users" on "users"."id" = "activity"."userId" and "users"."deletedAt" is null left join "assets" on "assets"."id" = "activity"."assetId" where - "activity"."assetId" = $1 - and "activity"."albumId" = $2 - and "activity"."isLiked" = $3 + "activity"."assetId" = $3 + and "activity"."albumId" = $4 and "assets"."deletedAt" is null and "assets"."visibility" != 'locked' diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index f8bbfdf8a65..aa302508689 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -67,19 +67,27 @@ export class ActivityRepository { } @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetId: DummyValue.UUID }] }) - async getStatistics({ albumId, assetId }: { albumId: string; assetId?: string }): Promise { - const { count } = await this.db + async getStatistics({ + albumId, + assetId, + }: { + albumId: string; + assetId?: string; + }): Promise<{ comments: number; likes: number }> { + const result = await this.db .selectFrom('activity') - .select((eb) => eb.fn.countAll().as('count')) + .select((eb) => [ + eb.fn.countAll().filterWhere('activity.isLiked', '=', false).as('comments'), + eb.fn.countAll().filterWhere('activity.isLiked', '=', true).as('likes'), + ]) .innerJoin('users', (join) => join.onRef('users.id', '=', 'activity.userId').on('users.deletedAt', 'is', null)) .leftJoin('assets', 'assets.id', 'activity.assetId') .$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!)) .where('activity.albumId', '=', albumId) - .where('activity.isLiked', '=', false) .where('assets.deletedAt', 'is', null) .where('assets.visibility', '!=', sql.lit(AssetVisibility.LOCKED)) .executeTakeFirstOrThrow(); - return count; + return result; } } diff --git a/server/src/services/activity.service.spec.ts b/server/src/services/activity.service.spec.ts index 82bd8bba89c..aea547e6dbc 100644 --- a/server/src/services/activity.service.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -54,13 +54,13 @@ describe(ActivityService.name, () => { }); describe('getStatistics', () => { - it('should get the comment count', async () => { + it('should get the comment and like count', async () => { const [albumId, assetId] = newUuids(); - mocks.activity.getStatistics.mockResolvedValue(1); + mocks.activity.getStatistics.mockResolvedValue({ comments: 1, likes: 3 }); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); - await expect(sut.getStatistics(factory.auth(), { assetId, albumId })).resolves.toEqual({ comments: 1 }); + await expect(sut.getStatistics(factory.auth(), { assetId, albumId })).resolves.toEqual({ comments: 1, likes: 3 }); }); }); diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index 6e3c3d70830..8256a34f02e 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -31,7 +31,7 @@ export class ActivityService extends BaseService { async getStatistics(auth: AuthDto, dto: ActivityDto): Promise { await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); - return { comments: await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId }) }; + return await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId }); } async create(auth: AuthDto, dto: ActivityCreateDto): Promise> { diff --git a/web/src/lib/components/asset-viewer/activity-status.svelte b/web/src/lib/components/asset-viewer/activity-status.svelte index 494c6fcbf7d..f240d9a6e95 100644 --- a/web/src/lib/components/asset-viewer/activity-status.svelte +++ b/web/src/lib/components/asset-viewer/activity-status.svelte @@ -7,25 +7,29 @@ interface Props { isLiked: ActivityResponseDto | null; numberOfComments: number | undefined; + numberOfLikes: number | undefined; disabled: boolean; onOpenActivityTab: () => void; onFavorite: () => void; } - let { isLiked, numberOfComments, disabled, onOpenActivityTab, onFavorite }: Props = $props(); + let { isLiked, numberOfComments, numberOfLikes, disabled, onOpenActivityTab, onFavorite }: Props = $props(); -
+
diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index 3c0775ad34d..c4aa7098d50 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -118,12 +118,9 @@ }; -
-
-
+
+
+
diff --git a/web/src/lib/managers/activity-manager.svelte.ts b/web/src/lib/managers/activity-manager.svelte.ts index a527778bb1e..c566f4f508b 100644 --- a/web/src/lib/managers/activity-manager.svelte.ts +++ b/web/src/lib/managers/activity-manager.svelte.ts @@ -17,6 +17,7 @@ class ActivityManager { #assetId = $state(); #activities = $state([]); #commentCount = $state(0); + #likeCount = $state(0); #isLiked = $state(null); get activities() { @@ -27,6 +28,10 @@ class ActivityManager { return this.#commentCount; } + get likeCount() { + return this.#likeCount; + } + get isLiked() { return this.#isLiked; } @@ -48,6 +53,10 @@ class ActivityManager { this.#commentCount++; } + if (activity.type === ReactionType.Like) { + this.#likeCount++; + } + handlePromiseError(this.refreshActivities(this.#albumId, this.#assetId)); return activity; } @@ -61,6 +70,10 @@ class ActivityManager { this.#commentCount--; } + if (activity.type === ReactionType.Like) { + this.#likeCount--; + } + this.#activities = index ? this.#activities.splice(index, 1) : this.#activities.filter(({ id }) => id !== activity.id); @@ -98,8 +111,9 @@ class ActivityManager { }); this.#isLiked = liked ?? null; - const { comments } = await getActivityStatistics({ albumId, assetId }); + const { comments, likes } = await getActivityStatistics({ albumId, assetId }); this.#commentCount = comments; + this.#likeCount = likes; } reset() { @@ -107,6 +121,7 @@ class ActivityManager { this.#assetId = undefined; this.#activities = []; this.#commentCount = 0; + this.#likeCount = 0; } } diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8f0252114cf..6e52b858ba2 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -576,6 +576,7 @@ disabled={!album.isActivityEnabled} isLiked={activityManager.isLiked} numberOfComments={activityManager.commentCount} + numberOfLikes={undefined} onFavorite={handleFavorite} onOpenActivityTab={handleOpenAndCloseActivityTab} />