1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

fix(server,web): correctly show album level like (#4916)

* fix: like in global activity

* refactor: rename isGlobal to ReactionLevel.Album

* chore: open api

* chore: e2e test for album vs comment duplicate like checking

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
martin 2023-11-10 03:32:31 +01:00 committed by GitHub
parent 986bbfa831
commit 92ec1ce77f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 120 additions and 20 deletions

View File

@ -2570,6 +2570,20 @@ export interface QueueStatusDto {
*/ */
'isPaused': boolean; 'isPaused': boolean;
} }
/**
*
* @export
* @enum {string}
*/
export const ReactionLevel = {
Album: 'album',
Asset: 'asset'
} as const;
export type ReactionLevel = typeof ReactionLevel[keyof typeof ReactionLevel];
/** /**
* *
* @export * @export
@ -5065,11 +5079,12 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat
* @param {string} albumId * @param {string} albumId
* @param {string} [assetId] * @param {string} [assetId]
* @param {ReactionType} [type] * @param {ReactionType} [type]
* @param {ReactionLevel} [level]
* @param {string} [userId] * @param {string} [userId]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getActivities: async (albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'albumId' is not null or undefined // verify required parameter 'albumId' is not null or undefined
assertParamExists('getActivities', 'albumId', albumId) assertParamExists('getActivities', 'albumId', albumId)
const localVarPath = `/activity`; const localVarPath = `/activity`;
@ -5105,6 +5120,10 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat
localVarQueryParameter['type'] = type; localVarQueryParameter['type'] = type;
} }
if (level !== undefined) {
localVarQueryParameter['level'] = level;
}
if (userId !== undefined) { if (userId !== undefined) {
localVarQueryParameter['userId'] = userId; localVarQueryParameter['userId'] = userId;
} }
@ -5205,12 +5224,13 @@ export const ActivityApiFp = function(configuration?: Configuration) {
* @param {string} albumId * @param {string} albumId
* @param {string} [assetId] * @param {string} [assetId]
* @param {ReactionType} [type] * @param {ReactionType} [type]
* @param {ReactionLevel} [level]
* @param {string} [userId] * @param {string} [userId]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getActivities(albumId: string, assetId?: string, type?: ReactionType, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> { async getActivities(albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, userId, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, level, userId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -5259,7 +5279,7 @@ export const ActivityApiFactory = function (configuration?: Configuration, baseP
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> { getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(axios, basePath)); return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@ -5328,6 +5348,13 @@ export interface ActivityApiGetActivitiesRequest {
*/ */
readonly type?: ReactionType readonly type?: ReactionType
/**
*
* @type {ReactionLevel}
* @memberof ActivityApiGetActivities
*/
readonly level?: ReactionLevel
/** /**
* *
* @type {string} * @type {string}
@ -5394,7 +5421,7 @@ export class ActivityApi extends BaseAPI {
* @memberof ActivityApi * @memberof ActivityApi
*/ */
public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) { public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
} }
/** /**

View File

@ -101,6 +101,7 @@ doc/PersonResponseDto.md
doc/PersonStatisticsResponseDto.md doc/PersonStatisticsResponseDto.md
doc/PersonUpdateDto.md doc/PersonUpdateDto.md
doc/QueueStatusDto.md doc/QueueStatusDto.md
doc/ReactionLevel.md
doc/ReactionType.md doc/ReactionType.md
doc/RecognitionConfig.md doc/RecognitionConfig.md
doc/ScanLibraryDto.md doc/ScanLibraryDto.md
@ -281,6 +282,7 @@ lib/model/person_response_dto.dart
lib/model/person_statistics_response_dto.dart lib/model/person_statistics_response_dto.dart
lib/model/person_update_dto.dart lib/model/person_update_dto.dart
lib/model/queue_status_dto.dart lib/model/queue_status_dto.dart
lib/model/reaction_level.dart
lib/model/reaction_type.dart lib/model/reaction_type.dart
lib/model/recognition_config.dart lib/model/recognition_config.dart
lib/model/scan_library_dto.dart lib/model/scan_library_dto.dart
@ -440,6 +442,7 @@ test/person_response_dto_test.dart
test/person_statistics_response_dto_test.dart test/person_statistics_response_dto_test.dart
test/person_update_dto_test.dart test/person_update_dto_test.dart
test/queue_status_dto_test.dart test/queue_status_dto_test.dart
test/reaction_level_test.dart
test/reaction_type_test.dart test/reaction_type_test.dart
test/recognition_config_test.dart test/recognition_config_test.dart
test/scan_library_dto_test.dart test/scan_library_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/doc/ReactionLevel.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -31,6 +31,14 @@
"$ref": "#/components/schemas/ReactionType" "$ref": "#/components/schemas/ReactionType"
} }
}, },
{
"name": "level",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/ReactionLevel"
}
},
{ {
"name": "userId", "name": "userId",
"required": false, "required": false,
@ -7728,6 +7736,13 @@
], ],
"type": "object" "type": "object"
}, },
"ReactionLevel": {
"enum": [
"album",
"asset"
],
"type": "string"
},
"ReactionType": { "ReactionType": {
"enum": [ "enum": [
"comment", "comment",

View File

@ -9,6 +9,11 @@ export enum ReactionType {
LIKE = 'like', LIKE = 'like',
} }
export enum ReactionLevel {
ALBUM = 'album',
ASSET = 'asset',
}
export type MaybeDuplicate<T> = { duplicate: boolean; value: T }; export type MaybeDuplicate<T> = { duplicate: boolean; value: T };
export class ActivityResponseDto { export class ActivityResponseDto {
@ -39,6 +44,11 @@ export class ActivitySearchDto extends ActivityDto {
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) @ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
type?: ReactionType; type?: ReactionType;
@IsEnum(ReactionLevel)
@Optional()
@ApiProperty({ enumName: 'ReactionLevel', enum: ReactionLevel })
level?: ReactionLevel;
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
userId?: string; userId?: string;
} }

View File

@ -10,6 +10,7 @@ import {
ActivitySearchDto, ActivitySearchDto,
ActivityStatisticsResponseDto, ActivityStatisticsResponseDto,
MaybeDuplicate, MaybeDuplicate,
ReactionLevel,
ReactionType, ReactionType,
mapActivity, mapActivity,
} from './activity.dto'; } from './activity.dto';
@ -30,7 +31,7 @@ export class ActivityService {
const activities = await this.repository.search({ const activities = await this.repository.search({
userId: dto.userId, userId: dto.userId,
albumId: dto.albumId, albumId: dto.albumId,
assetId: dto.assetId, assetId: dto.level === ReactionLevel.ALBUM ? null : dto.assetId,
isLiked: dto.type && dto.type === ReactionType.LIKE, isLiked: dto.type && dto.type === ReactionType.LIKE,
}); });
@ -54,11 +55,12 @@ export class ActivityService {
let activity: ActivityEntity | null = null; let activity: ActivityEntity | null = null;
let duplicate = false; let duplicate = false;
if (dto.type === 'like') { if (dto.type === ReactionType.LIKE) {
delete dto.comment; delete dto.comment;
[activity] = await this.repository.search({ [activity] = await this.repository.search({
...common, ...common,
isGlobal: !dto.assetId, // `null` will search for an album like
assetId: dto.assetId ?? null,
isLiked: true, isLiked: true,
}); });
duplicate = !!activity; duplicate = !!activity;

View File

@ -6,10 +6,9 @@ import { ActivityEntity } from '../entities/activity.entity';
export interface ActivitySearch { export interface ActivitySearch {
albumId?: string; albumId?: string;
assetId?: string; assetId?: string | null;
userId?: string; userId?: string;
isLiked?: boolean; isLiked?: boolean;
isGlobal?: boolean;
} }
@Injectable() @Injectable()
@ -17,11 +16,11 @@ export class ActivityRepository implements IActivityRepository {
constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {} constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {}
search(options: ActivitySearch): Promise<ActivityEntity[]> { search(options: ActivitySearch): Promise<ActivityEntity[]> {
const { userId, assetId, albumId, isLiked, isGlobal } = options; const { userId, assetId, albumId, isLiked } = options;
return this.repository.find({ return this.repository.find({
where: { where: {
userId, userId,
assetId: isGlobal ? IsNull() : assetId, assetId: assetId === null ? IsNull() : assetId,
albumId, albumId,
isLiked, isLiked,
}, },

View File

@ -247,6 +247,20 @@ describe(`${ActivityController.name} (e2e)`, () => {
expect(body).toEqual(reaction); expect(body).toEqual(reaction);
}); });
it('should not confuse an album like with an asset like', async () => {
const reaction = await api.activityApi.create(server, admin.accessToken, {
albumId: album.id,
assetId: asset.id,
type: ReactionType.LIKE,
});
const { status, body } = await request(server)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201);
expect(body.id).not.toEqual(reaction.id);
});
it('should add a comment to an asset', async () => { it('should add a comment to an asset', async () => {
const { status, body } = await request(server) const { status, body } = await request(server)
.post('/activity') .post('/activity')

View File

@ -2570,6 +2570,20 @@ export interface QueueStatusDto {
*/ */
'isPaused': boolean; 'isPaused': boolean;
} }
/**
*
* @export
* @enum {string}
*/
export const ReactionLevel = {
Album: 'album',
Asset: 'asset'
} as const;
export type ReactionLevel = typeof ReactionLevel[keyof typeof ReactionLevel];
/** /**
* *
* @export * @export
@ -5065,11 +5079,12 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat
* @param {string} albumId * @param {string} albumId
* @param {string} [assetId] * @param {string} [assetId]
* @param {ReactionType} [type] * @param {ReactionType} [type]
* @param {ReactionLevel} [level]
* @param {string} [userId] * @param {string} [userId]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getActivities: async (albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'albumId' is not null or undefined // verify required parameter 'albumId' is not null or undefined
assertParamExists('getActivities', 'albumId', albumId) assertParamExists('getActivities', 'albumId', albumId)
const localVarPath = `/activity`; const localVarPath = `/activity`;
@ -5105,6 +5120,10 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat
localVarQueryParameter['type'] = type; localVarQueryParameter['type'] = type;
} }
if (level !== undefined) {
localVarQueryParameter['level'] = level;
}
if (userId !== undefined) { if (userId !== undefined) {
localVarQueryParameter['userId'] = userId; localVarQueryParameter['userId'] = userId;
} }
@ -5205,12 +5224,13 @@ export const ActivityApiFp = function(configuration?: Configuration) {
* @param {string} albumId * @param {string} albumId
* @param {string} [assetId] * @param {string} [assetId]
* @param {ReactionType} [type] * @param {ReactionType} [type]
* @param {ReactionLevel} [level]
* @param {string} [userId] * @param {string} [userId]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getActivities(albumId: string, assetId?: string, type?: ReactionType, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> { async getActivities(albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, userId, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, level, userId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -5259,7 +5279,7 @@ export const ActivityApiFactory = function (configuration?: Configuration, baseP
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> { getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(axios, basePath)); return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@ -5328,6 +5348,13 @@ export interface ActivityApiGetActivitiesRequest {
*/ */
readonly type?: ReactionType readonly type?: ReactionType
/**
*
* @type {ReactionLevel}
* @memberof ActivityApiGetActivities
*/
readonly level?: ReactionLevel
/** /**
* *
* @type {string} * @type {string}
@ -5394,7 +5421,7 @@ export class ActivityApi extends BaseAPI {
* @memberof ActivityApi * @memberof ActivityApi
*/ */
public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) { public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
} }
/** /**

View File

@ -39,6 +39,7 @@
export let assetType: AssetTypeEnum | undefined = undefined; export let assetType: AssetTypeEnum | undefined = undefined;
export let albumOwnerId: string; export let albumOwnerId: string;
export let disabled: boolean; export let disabled: boolean;
export let isLiked: ActivityResponseDto | null;
let textArea: HTMLTextAreaElement; let textArea: HTMLTextAreaElement;
let innerHeight: number; let innerHeight: number;
@ -105,7 +106,7 @@
reactions.splice(index, 1); reactions.splice(index, 1);
showDeleteReaction.splice(index, 1); showDeleteReaction.splice(index, 1);
reactions = reactions; reactions = reactions;
if (reaction.type === 'like' && reaction.user.id === user.id) { if (isLiked && reaction.type === 'like' && reaction.id == isLiked.id) {
dispatch('deleteLike'); dispatch('deleteLike');
} else { } else {
dispatch('deleteComment'); dispatch('deleteComment');

View File

@ -756,6 +756,7 @@
albumOwnerId={album.ownerId} albumOwnerId={album.ownerId}
albumId={album.id} albumId={album.id}
assetId={asset.id} assetId={asset.id}
{isLiked}
bind:reactions bind:reactions
on:addComment={handleAddComment} on:addComment={handleAddComment}
on:deleteComment={handleRemoveComment} on:deleteComment={handleRemoveComment}

View File

@ -35,7 +35,7 @@
import { downloadArchive } from '$lib/utils/asset-utils'; import { downloadArchive } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { ActivityResponseDto, ReactionType, UserResponseDto, api } from '@api'; import { ActivityResponseDto, ReactionLevel, ReactionType, UserResponseDto, api } from '@api';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { clickOutside } from '$lib/utils/click-outside'; import { clickOutside } from '$lib/utils/click-outside';
@ -167,7 +167,6 @@
const { data } = await api.activityApi.createActivity({ const { data } = await api.activityApi.createActivity({
activityCreateDto: { albumId: album.id, type: ReactionType.Like }, activityCreateDto: { albumId: album.id, type: ReactionType.Like },
}); });
isLiked = data; isLiked = data;
reactions = [...reactions, isLiked]; reactions = [...reactions, isLiked];
} }
@ -183,6 +182,7 @@
userId: user.id, userId: user.id,
albumId: album.id, albumId: album.id,
type: ReactionType.Like, type: ReactionType.Like,
level: ReactionLevel.Album,
}); });
if (data.length > 0) { if (data.length > 0) {
isLiked = data[0]; isLiked = data[0];
@ -687,6 +687,7 @@
disabled={!album.isActivityEnabled} disabled={!album.isActivityEnabled}
albumOwnerId={album.ownerId} albumOwnerId={album.ownerId}
albumId={album.id} albumId={album.id}
{isLiked}
bind:reactions bind:reactions
on:addComment={() => updateNumberOfComments(1)} on:addComment={() => updateNumberOfComments(1)}
on:deleteComment={() => updateNumberOfComments(-1)} on:deleteComment={() => updateNumberOfComments(-1)}