mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(web,server): activity (#4682)
* feat: activity * regenerate api * fix: make asset owner unable to delete comment * fix: merge * fix: tests * feat: use textarea instead of input * fix: do actions only if the album is shared * fix: placeholder opacity * fix(web): improve messages UI * fix(web): improve input message UI * pr feedback * fix: tests * pr feedback * pr feedback * pr feedback * fix permissions * regenerate api * pr feedback * pr feedback * multiple improvements on web * fix: ui colors * WIP * chore: open api * pr feedback * fix: add comment * chore: clean up * pr feedback * refactor: endpoints * chore: open api * fix: filter by type * fix: e2e * feat: e2e remove own comment * fix: web tests * remove console.log * chore: cleanup * fix: ui tweaks * pr feedback * fix web test * fix: unit tests * chore: remove unused code * revert useless changes * fix: grouping messages * fix: remove nullable on updatedAt * fix: text overflow * styling --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
68f6446718
commit
ce5966c23d
577
cli/src/api/open-api/api.ts
generated
577
cli/src/api/open-api/api.ts
generated
@ -99,6 +99,103 @@ export interface APIKeyUpdateDto {
|
||||
*/
|
||||
'name': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityCreateDto
|
||||
*/
|
||||
export interface ActivityCreateDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'albumId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'assetId'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'comment'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {ReactionType}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'type': ReactionType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityResponseDto
|
||||
*/
|
||||
export interface ActivityResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'assetId': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'comment'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'createdAt': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'type': ActivityResponseDtoTypeEnum;
|
||||
/**
|
||||
*
|
||||
* @type {UserDto}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'user': UserDto;
|
||||
}
|
||||
|
||||
export const ActivityResponseDtoTypeEnum = {
|
||||
Comment: 'comment',
|
||||
Like: 'like'
|
||||
} as const;
|
||||
|
||||
export type ActivityResponseDtoTypeEnum = typeof ActivityResponseDtoTypeEnum[keyof typeof ActivityResponseDtoTypeEnum];
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityStatisticsResponseDto
|
||||
*/
|
||||
export interface ActivityStatisticsResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ActivityStatisticsResponseDto
|
||||
*/
|
||||
'comments': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -2490,6 +2587,20 @@ export interface QueueStatusDto {
|
||||
*/
|
||||
'isPaused': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const ReactionType = {
|
||||
Comment: 'comment',
|
||||
Like: 'like'
|
||||
} as const;
|
||||
|
||||
export type ReactionType = typeof ReactionType[keyof typeof ReactionType];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -4248,6 +4359,43 @@ export interface UsageByUserDto {
|
||||
*/
|
||||
'videos': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserDto
|
||||
*/
|
||||
export interface UserDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'email': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'firstName': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'lastName': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'profileImagePath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -4831,6 +4979,435 @@ export class APIKeyApi extends BaseAPI {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ActivityApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityCreateDto} activityCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createActivity: async (activityCreateDto: ActivityCreateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'activityCreateDto' is not null or undefined
|
||||
assertParamExists('createActivity', 'activityCreateDto', activityCreateDto)
|
||||
const localVarPath = `/activity`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(activityCreateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteActivity: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('deleteActivity', 'id', id)
|
||||
const localVarPath = `/activity/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {ReactionType} [type]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'albumId' is not null or undefined
|
||||
assertParamExists('getActivities', 'albumId', albumId)
|
||||
const localVarPath = `/activity`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (albumId !== undefined) {
|
||||
localVarQueryParameter['albumId'] = albumId;
|
||||
}
|
||||
|
||||
if (assetId !== undefined) {
|
||||
localVarQueryParameter['assetId'] = assetId;
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
localVarQueryParameter['type'] = type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivityStatistics: async (albumId: string, assetId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'albumId' is not null or undefined
|
||||
assertParamExists('getActivityStatistics', 'albumId', albumId)
|
||||
const localVarPath = `/activity/statistics`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (albumId !== undefined) {
|
||||
localVarQueryParameter['albumId'] = albumId;
|
||||
}
|
||||
|
||||
if (assetId !== undefined) {
|
||||
localVarQueryParameter['assetId'] = assetId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ActivityApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ActivityApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityCreateDto} activityCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createActivity(activityCreateDto: ActivityCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createActivity(activityCreateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteActivity(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteActivity(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {ReactionType} [type]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getActivities(albumId: string, assetId?: string, type?: ReactionType, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getActivityStatistics(albumId: string, assetId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityStatisticsResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivityStatistics(albumId, assetId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ActivityApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ActivityApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityResponseDto> {
|
||||
return localVarFp.createActivity(requestParameters.activityCreateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteActivity(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
|
||||
return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityStatisticsResponseDto> {
|
||||
return localVarFp.getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for createActivity operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiCreateActivityRequest
|
||||
*/
|
||||
export interface ActivityApiCreateActivityRequest {
|
||||
/**
|
||||
*
|
||||
* @type {ActivityCreateDto}
|
||||
* @memberof ActivityApiCreateActivity
|
||||
*/
|
||||
readonly activityCreateDto: ActivityCreateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for deleteActivity operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiDeleteActivityRequest
|
||||
*/
|
||||
export interface ActivityApiDeleteActivityRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiDeleteActivity
|
||||
*/
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getActivities operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiGetActivitiesRequest
|
||||
*/
|
||||
export interface ActivityApiGetActivitiesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly albumId: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly assetId?: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {ReactionType}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly type?: ReactionType
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getActivityStatistics operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiGetActivityStatisticsRequest
|
||||
*/
|
||||
export interface ActivityApiGetActivityStatisticsRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivityStatistics
|
||||
*/
|
||||
readonly albumId: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivityStatistics
|
||||
*/
|
||||
readonly assetId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ActivityApi - object-oriented interface
|
||||
* @export
|
||||
* @class ActivityApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ActivityApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).createActivity(requestParameters.activityCreateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).deleteActivity(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AlbumApi - axios parameter creator
|
||||
* @export
|
||||
|
18
mobile/openapi/.openapi-generator/FILES
generated
18
mobile/openapi/.openapi-generator/FILES
generated
@ -8,6 +8,10 @@ doc/APIKeyCreateDto.md
|
||||
doc/APIKeyCreateResponseDto.md
|
||||
doc/APIKeyResponseDto.md
|
||||
doc/APIKeyUpdateDto.md
|
||||
doc/ActivityApi.md
|
||||
doc/ActivityCreateDto.md
|
||||
doc/ActivityResponseDto.md
|
||||
doc/ActivityStatisticsResponseDto.md
|
||||
doc/AddUsersDto.md
|
||||
doc/AdminSignupResponseDto.md
|
||||
doc/AlbumApi.md
|
||||
@ -97,6 +101,7 @@ doc/PersonResponseDto.md
|
||||
doc/PersonStatisticsResponseDto.md
|
||||
doc/PersonUpdateDto.md
|
||||
doc/QueueStatusDto.md
|
||||
doc/ReactionType.md
|
||||
doc/RecognitionConfig.md
|
||||
doc/ScanLibraryDto.md
|
||||
doc/SearchAlbumResponseDto.md
|
||||
@ -158,11 +163,13 @@ doc/UpdateTagDto.md
|
||||
doc/UpdateUserDto.md
|
||||
doc/UsageByUserDto.md
|
||||
doc/UserApi.md
|
||||
doc/UserDto.md
|
||||
doc/UserResponseDto.md
|
||||
doc/ValidateAccessTokenResponseDto.md
|
||||
doc/VideoCodec.md
|
||||
git_push.sh
|
||||
lib/api.dart
|
||||
lib/api/activity_api.dart
|
||||
lib/api/album_api.dart
|
||||
lib/api/api_key_api.dart
|
||||
lib/api/asset_api.dart
|
||||
@ -187,6 +194,9 @@ lib/auth/authentication.dart
|
||||
lib/auth/http_basic_auth.dart
|
||||
lib/auth/http_bearer_auth.dart
|
||||
lib/auth/oauth.dart
|
||||
lib/model/activity_create_dto.dart
|
||||
lib/model/activity_response_dto.dart
|
||||
lib/model/activity_statistics_response_dto.dart
|
||||
lib/model/add_users_dto.dart
|
||||
lib/model/admin_signup_response_dto.dart
|
||||
lib/model/album_count_response_dto.dart
|
||||
@ -271,6 +281,7 @@ lib/model/person_response_dto.dart
|
||||
lib/model/person_statistics_response_dto.dart
|
||||
lib/model/person_update_dto.dart
|
||||
lib/model/queue_status_dto.dart
|
||||
lib/model/reaction_type.dart
|
||||
lib/model/recognition_config.dart
|
||||
lib/model/scan_library_dto.dart
|
||||
lib/model/search_album_response_dto.dart
|
||||
@ -326,10 +337,15 @@ lib/model/update_stack_parent_dto.dart
|
||||
lib/model/update_tag_dto.dart
|
||||
lib/model/update_user_dto.dart
|
||||
lib/model/usage_by_user_dto.dart
|
||||
lib/model/user_dto.dart
|
||||
lib/model/user_response_dto.dart
|
||||
lib/model/validate_access_token_response_dto.dart
|
||||
lib/model/video_codec.dart
|
||||
pubspec.yaml
|
||||
test/activity_api_test.dart
|
||||
test/activity_create_dto_test.dart
|
||||
test/activity_response_dto_test.dart
|
||||
test/activity_statistics_response_dto_test.dart
|
||||
test/add_users_dto_test.dart
|
||||
test/admin_signup_response_dto_test.dart
|
||||
test/album_api_test.dart
|
||||
@ -424,6 +440,7 @@ test/person_response_dto_test.dart
|
||||
test/person_statistics_response_dto_test.dart
|
||||
test/person_update_dto_test.dart
|
||||
test/queue_status_dto_test.dart
|
||||
test/reaction_type_test.dart
|
||||
test/recognition_config_test.dart
|
||||
test/scan_library_dto_test.dart
|
||||
test/search_album_response_dto_test.dart
|
||||
@ -485,6 +502,7 @@ test/update_tag_dto_test.dart
|
||||
test/update_user_dto_test.dart
|
||||
test/usage_by_user_dto_test.dart
|
||||
test/user_api_test.dart
|
||||
test/user_dto_test.dart
|
||||
test/user_response_dto_test.dart
|
||||
test/validate_access_token_response_dto_test.dart
|
||||
test/video_codec_test.dart
|
||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/ActivityApi.md
generated
Normal file
BIN
mobile/openapi/doc/ActivityApi.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/ActivityCreateDto.md
generated
Normal file
BIN
mobile/openapi/doc/ActivityCreateDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/ActivityResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/ActivityResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/ActivityStatisticsResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/ActivityStatisticsResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/ReactionType.md
generated
Normal file
BIN
mobile/openapi/doc/ReactionType.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/UserDto.md
generated
Normal file
BIN
mobile/openapi/doc/UserDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/activity_api.dart
generated
Normal file
BIN
mobile/openapi/lib/api/activity_api.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/activity_create_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/activity_create_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/activity_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/activity_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/activity_statistics_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/activity_statistics_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/reaction_type.dart
generated
Normal file
BIN
mobile/openapi/lib/model/reaction_type.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/user_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/user_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/activity_api_test.dart
generated
Normal file
BIN
mobile/openapi/test/activity_api_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/activity_create_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/activity_create_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/activity_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/activity_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/activity_statistics_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/activity_statistics_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/reaction_type_test.dart
generated
Normal file
BIN
mobile/openapi/test/reaction_type_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/user_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/user_dto_test.dart
generated
Normal file
Binary file not shown.
@ -1,6 +1,194 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"paths": {
|
||||
"/activity": {
|
||||
"get": {
|
||||
"operationId": "getActivities",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "albumId",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReactionType"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ActivityResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Activity"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createActivity",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ActivityCreateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ActivityResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Activity"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/activity/statistics": {
|
||||
"get": {
|
||||
"operationId": "getActivityStatistics",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "albumId",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ActivityStatisticsResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Activity"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/activity/{id}": {
|
||||
"delete": {
|
||||
"operationId": "deleteActivity",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Activity"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/album": {
|
||||
"get": {
|
||||
"operationId": "getAllAlbums",
|
||||
@ -5512,6 +5700,77 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivityCreateDto": {
|
||||
"properties": {
|
||||
"albumId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"assetId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"comment": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/ReactionType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"albumId",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivityResponseDto": {
|
||||
"properties": {
|
||||
"assetId": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"comment": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"comment",
|
||||
"like"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/UserDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"createdAt",
|
||||
"type",
|
||||
"user",
|
||||
"assetId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivityStatisticsResponseDto": {
|
||||
"properties": {
|
||||
"comments": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"comments"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AddUsersDto": {
|
||||
"properties": {
|
||||
"sharedUserIds": {
|
||||
@ -7432,6 +7691,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReactionType": {
|
||||
"enum": [
|
||||
"comment",
|
||||
"like"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RecognitionConfig": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@ -8757,6 +9023,33 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserDto": {
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"profileImagePath": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"profileImagePath"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserResponseDto": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
@ -8810,12 +9103,12 @@
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"email",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"profileImagePath",
|
||||
"storageLabel",
|
||||
"externalPath",
|
||||
"profileImagePath",
|
||||
"shouldChangePassword",
|
||||
"isAdmin",
|
||||
"createdAt",
|
||||
|
@ -3,6 +3,9 @@ import { AuthUserDto } from '../auth';
|
||||
import { IAccessRepository } from '../repositories';
|
||||
|
||||
export enum Permission {
|
||||
ACTIVITY_CREATE = 'activity.create',
|
||||
ACTIVITY_DELETE = 'activity.delete',
|
||||
|
||||
// ASSET_CREATE = 'asset.create',
|
||||
ASSET_READ = 'asset.read',
|
||||
ASSET_UPDATE = 'asset.update',
|
||||
@ -133,6 +136,20 @@ export class AccessCore {
|
||||
|
||||
private async hasOtherAccess(authUser: AuthUserDto, permission: Permission, id: string) {
|
||||
switch (permission) {
|
||||
// uses album id
|
||||
case Permission.ACTIVITY_CREATE:
|
||||
return (
|
||||
(await this.repository.album.hasOwnerAccess(authUser.id, id)) ||
|
||||
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
|
||||
);
|
||||
|
||||
// uses activity id
|
||||
case Permission.ACTIVITY_DELETE:
|
||||
return (
|
||||
(await this.repository.activity.hasOwnerAccess(authUser.id, id)) ||
|
||||
(await this.repository.activity.hasAlbumOwnerAccess(authUser.id, id))
|
||||
);
|
||||
|
||||
case Permission.ASSET_READ:
|
||||
return (
|
||||
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
|
||||
|
65
server/src/domain/activity/activity.dto.ts
Normal file
65
server/src/domain/activity/activity.dto.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { ActivityEntity } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
|
||||
import { Optional, ValidateUUID } from '../domain.util';
|
||||
import { UserDto, mapSimpleUser } from '../user/response-dto';
|
||||
|
||||
export enum ReactionType {
|
||||
COMMENT = 'comment',
|
||||
LIKE = 'like',
|
||||
}
|
||||
|
||||
export type MaybeDuplicate<T> = { duplicate: boolean; value: T };
|
||||
|
||||
export class ActivityResponseDto {
|
||||
id!: string;
|
||||
createdAt!: Date;
|
||||
type!: ReactionType;
|
||||
user!: UserDto;
|
||||
assetId!: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
export class ActivityStatisticsResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
comments!: number;
|
||||
}
|
||||
|
||||
export class ActivityDto {
|
||||
@ValidateUUID()
|
||||
albumId!: string;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export class ActivitySearchDto extends ActivityDto {
|
||||
@IsEnum(ReactionType)
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
|
||||
type?: ReactionType;
|
||||
}
|
||||
|
||||
const isComment = (dto: ActivityCreateDto) => dto.type === 'comment';
|
||||
|
||||
export class ActivityCreateDto extends ActivityDto {
|
||||
@IsEnum(ReactionType)
|
||||
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
|
||||
type!: ReactionType;
|
||||
|
||||
@ValidateIf(isComment)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export function mapActivity(activity: ActivityEntity): ActivityResponseDto {
|
||||
return {
|
||||
id: activity.id,
|
||||
assetId: activity.assetId,
|
||||
createdAt: activity.createdAt,
|
||||
comment: activity.comment,
|
||||
type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT,
|
||||
user: mapSimpleUser(activity.user),
|
||||
};
|
||||
}
|
80
server/src/domain/activity/activity.service.ts
Normal file
80
server/src/domain/activity/activity.service.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { ActivityEntity } from '@app/infra/entities';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore, Permission } from '../access';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { IAccessRepository, IActivityRepository } from '../repositories';
|
||||
import {
|
||||
ActivityCreateDto,
|
||||
ActivityDto,
|
||||
ActivityResponseDto,
|
||||
ActivitySearchDto,
|
||||
ActivityStatisticsResponseDto,
|
||||
MaybeDuplicate,
|
||||
ReactionType,
|
||||
mapActivity,
|
||||
} from './activity.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ActivityService {
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IActivityRepository) private repository: IActivityRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
|
||||
async getAll(authUser: AuthUserDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
|
||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, dto.albumId);
|
||||
const activities = await this.repository.search({
|
||||
albumId: dto.albumId,
|
||||
assetId: dto.assetId,
|
||||
isLiked: dto.type && dto.type === ReactionType.LIKE,
|
||||
});
|
||||
|
||||
return activities.map(mapActivity);
|
||||
}
|
||||
|
||||
async getStatistics(authUser: AuthUserDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
|
||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, dto.albumId);
|
||||
return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) };
|
||||
}
|
||||
|
||||
async create(authUser: AuthUserDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
|
||||
await this.access.requirePermission(authUser, Permission.ACTIVITY_CREATE, dto.albumId);
|
||||
|
||||
const common = {
|
||||
userId: authUser.id,
|
||||
assetId: dto.assetId,
|
||||
albumId: dto.albumId,
|
||||
};
|
||||
|
||||
let activity: ActivityEntity | null = null;
|
||||
let duplicate = false;
|
||||
|
||||
if (dto.type === 'like') {
|
||||
delete dto.comment;
|
||||
[activity] = await this.repository.search({
|
||||
...common,
|
||||
isLiked: true,
|
||||
});
|
||||
duplicate = !!activity;
|
||||
}
|
||||
|
||||
if (!activity) {
|
||||
activity = await this.repository.create({
|
||||
...common,
|
||||
isLiked: dto.type === ReactionType.LIKE,
|
||||
comment: dto.comment,
|
||||
});
|
||||
}
|
||||
|
||||
return { duplicate, value: mapActivity(activity) };
|
||||
}
|
||||
|
||||
async delete(authUser: AuthUserDto, id: string): Promise<void> {
|
||||
await this.access.requirePermission(authUser, Permission.ACTIVITY_DELETE, id);
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
}
|
168
server/src/domain/activity/activity.spec.ts
Normal file
168
server/src/domain/activity/activity.spec.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { authStub, IAccessRepositoryMock, newAccessRepositoryMock } from '@test';
|
||||
import { activityStub } from '@test/fixtures/activity.stub';
|
||||
import { newActivityRepositoryMock } from '@test/repositories/activity.repository.mock';
|
||||
import { IActivityRepository } from '../repositories';
|
||||
import { ReactionType } from './activity.dto';
|
||||
import { ActivityService } from './activity.service';
|
||||
|
||||
describe(ActivityService.name, () => {
|
||||
let sut: ActivityService;
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let activityMock: jest.Mocked<IActivityRepository>;
|
||||
|
||||
beforeEach(async () => {
|
||||
accessMock = newAccessRepositoryMock();
|
||||
activityMock = newActivityRepositoryMock();
|
||||
|
||||
sut = new ActivityService(accessMock, activityMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should get all', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
|
||||
await expect(sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id' })).resolves.toEqual([]);
|
||||
|
||||
expect(activityMock.search).toHaveBeenCalledWith({
|
||||
assetId: 'asset-id',
|
||||
albumId: 'album-id',
|
||||
isLiked: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by type=like', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id', type: ReactionType.LIKE }),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
expect(activityMock.search).toHaveBeenCalledWith({
|
||||
assetId: 'asset-id',
|
||||
albumId: 'album-id',
|
||||
isLiked: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by type=comment', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id', type: ReactionType.COMMENT }),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
expect(activityMock.search).toHaveBeenCalledWith({
|
||||
assetId: 'asset-id',
|
||||
albumId: 'album-id',
|
||||
isLiked: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatistics', () => {
|
||||
it('should get the comment count', async () => {
|
||||
activityMock.getStatistics.mockResolvedValue(1);
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
await expect(
|
||||
sut.getStatistics(authStub.admin, {
|
||||
assetId: 'asset-id',
|
||||
albumId: activityStub.oneComment.albumId,
|
||||
}),
|
||||
).resolves.toEqual({ comments: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('addComment', () => {
|
||||
it('should require access to the album', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(false);
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'comment',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should create a comment', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
activityMock.create.mockResolvedValue(activityStub.oneComment);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'comment',
|
||||
});
|
||||
|
||||
expect(activityMock.create).toHaveBeenCalledWith({
|
||||
userId: 'admin_id',
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
comment: 'comment',
|
||||
isLiked: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a like', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
activityMock.create.mockResolvedValue(activityStub.liked);
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
type: ReactionType.LIKE,
|
||||
});
|
||||
|
||||
expect(activityMock.create).toHaveBeenCalledWith({
|
||||
userId: 'admin_id',
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
isLiked: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip if like exists', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
activityMock.search.mockResolvedValue([activityStub.liked]);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
type: ReactionType.LIKE,
|
||||
});
|
||||
|
||||
expect(activityMock.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should require access', async () => {
|
||||
accessMock.activity.hasOwnerAccess.mockResolvedValue(false);
|
||||
await expect(sut.delete(authStub.admin, activityStub.oneComment.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(activityMock.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should let the activity owner delete a comment', async () => {
|
||||
accessMock.activity.hasOwnerAccess.mockResolvedValue(true);
|
||||
await sut.delete(authStub.admin, 'activity-id');
|
||||
expect(activityMock.delete).toHaveBeenCalledWith('activity-id');
|
||||
});
|
||||
|
||||
it('should let the album owner delete a comment', async () => {
|
||||
accessMock.activity.hasAlbumOwnerAccess.mockResolvedValue(true);
|
||||
await sut.delete(authStub.admin, 'activity-id');
|
||||
expect(activityMock.delete).toHaveBeenCalledWith('activity-id');
|
||||
});
|
||||
});
|
||||
});
|
2
server/src/domain/activity/index.ts
Normal file
2
server/src/domain/activity/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './activity.dto';
|
||||
export * from './activity.service';
|
@ -1,4 +1,5 @@
|
||||
import { DynamicModule, Global, Module, ModuleMetadata, OnApplicationShutdown, Provider } from '@nestjs/common';
|
||||
import { ActivityService } from './activity';
|
||||
import { AlbumService } from './album';
|
||||
import { APIKeyService } from './api-key';
|
||||
import { AssetService } from './asset';
|
||||
@ -21,6 +22,7 @@ import { TagService } from './tag';
|
||||
import { UserService } from './user';
|
||||
|
||||
const providers: Provider[] = [
|
||||
ActivityService,
|
||||
AlbumService,
|
||||
APIKeyService,
|
||||
AssetService,
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './access';
|
||||
export * from './activity';
|
||||
export * from './album';
|
||||
export * from './api-key';
|
||||
export * from './asset';
|
||||
|
@ -1,6 +1,10 @@
|
||||
export const IAccessRepository = 'IAccessRepository';
|
||||
|
||||
export interface IAccessRepository {
|
||||
activity: {
|
||||
hasOwnerAccess(userId: string, albumId: string): Promise<boolean>;
|
||||
hasAlbumOwnerAccess(userId: string, albumId: string): Promise<boolean>;
|
||||
};
|
||||
asset: {
|
||||
hasOwnerAccess(userId: string, assetId: string): Promise<boolean>;
|
||||
hasAlbumAccess(userId: string, assetId: string): Promise<boolean>;
|
||||
|
11
server/src/domain/repositories/activity.repository.ts
Normal file
11
server/src/domain/repositories/activity.repository.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ActivityEntity } from '@app/infra/entities/activity.entity';
|
||||
import { ActivitySearch } from '@app/infra/repositories';
|
||||
|
||||
export const IActivityRepository = 'IActivityRepository';
|
||||
|
||||
export interface IActivityRepository {
|
||||
search(options: ActivitySearch): Promise<ActivityEntity[]>;
|
||||
create(activity: Partial<ActivityEntity>): Promise<ActivityEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
getStatistics(assetId: string | undefined, albumId: string): Promise<number>;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export * from './access.repository';
|
||||
export * from './activity.repository';
|
||||
export * from './album.repository';
|
||||
export * from './api-key.repository';
|
||||
export * from './asset.repository';
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { UserEntity } from '@app/infra/entities';
|
||||
|
||||
export class UserResponseDto {
|
||||
export class UserDto {
|
||||
id!: string;
|
||||
email!: string;
|
||||
firstName!: string;
|
||||
lastName!: string;
|
||||
email!: string;
|
||||
profileImagePath!: string;
|
||||
}
|
||||
|
||||
export class UserResponseDto extends UserDto {
|
||||
storageLabel!: string | null;
|
||||
externalPath!: string | null;
|
||||
profileImagePath!: string;
|
||||
shouldChangePassword!: boolean;
|
||||
isAdmin!: boolean;
|
||||
createdAt!: Date;
|
||||
@ -17,15 +20,21 @@ export class UserResponseDto {
|
||||
memoriesEnabled?: boolean;
|
||||
}
|
||||
|
||||
export function mapUser(entity: UserEntity): UserResponseDto {
|
||||
export const mapSimpleUser = (entity: UserEntity): UserDto => {
|
||||
return {
|
||||
id: entity.id,
|
||||
email: entity.email,
|
||||
firstName: entity.firstName,
|
||||
lastName: entity.lastName,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
};
|
||||
};
|
||||
|
||||
export function mapUser(entity: UserEntity): UserResponseDto {
|
||||
return {
|
||||
...mapSimpleUser(entity),
|
||||
storageLabel: entity.storageLabel,
|
||||
externalPath: entity.externalPath,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
shouldChangePassword: entity.shouldChangePassword,
|
||||
isAdmin: entity.isAdmin,
|
||||
createdAt: entity.createdAt,
|
||||
|
@ -13,6 +13,7 @@ import { FileUploadInterceptor } from './app.interceptor';
|
||||
import { AppService } from './app.service';
|
||||
import {
|
||||
APIKeyController,
|
||||
ActivityController,
|
||||
AlbumController,
|
||||
AppController,
|
||||
AssetController,
|
||||
@ -39,6 +40,7 @@ import {
|
||||
TypeOrmModule.forFeature([AssetEntity]),
|
||||
],
|
||||
controllers: [
|
||||
ActivityController,
|
||||
AssetController,
|
||||
AssetControllerV1,
|
||||
AppController,
|
||||
|
52
server/src/immich/controllers/activity.controller.ts
Normal file
52
server/src/immich/controllers/activity.controller.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { AuthUserDto } from '@app/domain';
|
||||
import {
|
||||
ActivityDto,
|
||||
ActivitySearchDto,
|
||||
ActivityService,
|
||||
ActivityCreateDto as CreateDto,
|
||||
ActivityResponseDto as ResponseDto,
|
||||
ActivityStatisticsResponseDto as StatsResponseDto,
|
||||
} from '@app/domain/activity';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { AuthUser, Authenticated } from '../app.guard';
|
||||
import { UseValidation } from '../app.utils';
|
||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||
|
||||
@ApiTags('Activity')
|
||||
@Controller('activity')
|
||||
@Authenticated()
|
||||
@UseValidation()
|
||||
export class ActivityController {
|
||||
constructor(private service: ActivityService) {}
|
||||
|
||||
@Get()
|
||||
getActivities(@AuthUser() authUser: AuthUserDto, @Query() dto: ActivitySearchDto): Promise<ResponseDto[]> {
|
||||
return this.service.getAll(authUser, dto);
|
||||
}
|
||||
|
||||
@Get('statistics')
|
||||
getActivityStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: ActivityDto): Promise<StatsResponseDto> {
|
||||
return this.service.getStatistics(authUser, dto);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createActivity(
|
||||
@AuthUser() authUser: AuthUserDto,
|
||||
@Body() dto: CreateDto,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<ResponseDto> {
|
||||
const { duplicate, value } = await this.service.create(authUser, dto);
|
||||
if (duplicate) {
|
||||
res.status(HttpStatus.OK);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteActivity(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(authUser, id);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export * from './activity.controller';
|
||||
export * from './album.controller';
|
||||
export * from './api-key.controller';
|
||||
export * from './app.controller';
|
||||
|
51
server/src/infra/entities/activity.entity.ts
Normal file
51
server/src/infra/entities/activity.entity.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import {
|
||||
Check,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { AlbumEntity } from './album.entity';
|
||||
import { AssetEntity } from './asset.entity';
|
||||
import { UserEntity } from './user.entity';
|
||||
|
||||
@Entity('activity')
|
||||
@Index('IDX_activity_like', ['assetId', 'userId', 'albumId'], { unique: true, where: '("isLiked" = true)' })
|
||||
@Check(`("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false)`)
|
||||
export class ActivityEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column()
|
||||
albumId!: string;
|
||||
|
||||
@Column()
|
||||
userId!: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
assetId!: string | null;
|
||||
|
||||
@Column({ type: 'text', default: null })
|
||||
comment!: string | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isLiked!: boolean;
|
||||
|
||||
@ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||
asset!: AssetEntity | null;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
user!: UserEntity;
|
||||
|
||||
@ManyToOne(() => AlbumEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
album!: AlbumEntity;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { ActivityEntity } from './activity.entity';
|
||||
import { AlbumEntity } from './album.entity';
|
||||
import { APIKeyEntity } from './api-key.entity';
|
||||
import { AssetFaceEntity } from './asset-face.entity';
|
||||
@ -15,6 +16,7 @@ import { TagEntity } from './tag.entity';
|
||||
import { UserTokenEntity } from './user-token.entity';
|
||||
import { UserEntity } from './user.entity';
|
||||
|
||||
export * from './activity.entity';
|
||||
export * from './album.entity';
|
||||
export * from './api-key.entity';
|
||||
export * from './asset-face.entity';
|
||||
@ -33,6 +35,7 @@ export * from './user-token.entity';
|
||||
export * from './user.entity';
|
||||
|
||||
export const databaseEntities = [
|
||||
ActivityEntity,
|
||||
AlbumEntity,
|
||||
APIKeyEntity,
|
||||
AssetEntity,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
IAccessRepository,
|
||||
IActivityRepository,
|
||||
IAlbumRepository,
|
||||
IAssetRepository,
|
||||
IAuditRepository,
|
||||
@ -35,6 +36,7 @@ import { bullConfig, bullQueues } from './infra.config';
|
||||
import {
|
||||
APIKeyRepository,
|
||||
AccessRepository,
|
||||
ActivityRepository,
|
||||
AlbumRepository,
|
||||
AssetRepository,
|
||||
AuditRepository,
|
||||
@ -60,6 +62,7 @@ import {
|
||||
} from './repositories';
|
||||
|
||||
const providers: Provider[] = [
|
||||
{ provide: IActivityRepository, useClass: ActivityRepository },
|
||||
{ provide: IAccessRepository, useClass: AccessRepository },
|
||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||
|
22
server/src/infra/migrations/1698693294632-AddActivity.ts
Normal file
22
server/src/infra/migrations/1698693294632-AddActivity.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddActivity1698693294632 implements MigrationInterface {
|
||||
name = 'AddActivity1698693294632'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "activity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "assetId" uuid, "comment" text, "isLiked" boolean NOT NULL DEFAULT false, CONSTRAINT "CHK_2ab1e70f113f450eb40c1e3ec8" CHECK (("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false)), CONSTRAINT "PK_24625a1d6b1b089c8ae206fe467" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_activity_like" ON "activity" ("assetId", "userId", "albumId") WHERE ("isLiked" = true)`);
|
||||
await queryRunner.query(`ALTER TABLE "activity" ADD CONSTRAINT "FK_8091ea76b12338cb4428d33d782" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "activity" ADD CONSTRAINT "FK_1af8519996fbfb3684b58df280b" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_1af8519996fbfb3684b58df280b"`);
|
||||
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea"`);
|
||||
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_8091ea76b12338cb4428d33d782"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_activity_like"`);
|
||||
await queryRunner.query(`DROP TABLE "activity"`);
|
||||
}
|
||||
|
||||
}
|
@ -2,6 +2,7 @@ import { IAccessRepository } from '@app/domain';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
ActivityEntity,
|
||||
AlbumEntity,
|
||||
AssetEntity,
|
||||
LibraryEntity,
|
||||
@ -13,6 +14,7 @@ import {
|
||||
|
||||
export class AccessRepository implements IAccessRepository {
|
||||
constructor(
|
||||
@InjectRepository(ActivityEntity) private activityRepository: Repository<ActivityEntity>,
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
|
||||
@InjectRepository(LibraryEntity) private libraryRepository: Repository<LibraryEntity>,
|
||||
@ -22,6 +24,26 @@ export class AccessRepository implements IAccessRepository {
|
||||
@InjectRepository(UserTokenEntity) private tokenRepository: Repository<UserTokenEntity>,
|
||||
) {}
|
||||
|
||||
activity = {
|
||||
hasOwnerAccess: (userId: string, activityId: string): Promise<boolean> => {
|
||||
return this.activityRepository.exist({
|
||||
where: {
|
||||
id: activityId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
},
|
||||
hasAlbumOwnerAccess: (userId: string, activityId: string): Promise<boolean> => {
|
||||
return this.activityRepository.exist({
|
||||
where: {
|
||||
id: activityId,
|
||||
album: {
|
||||
ownerId: userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
library = {
|
||||
hasOwnerAccess: (userId: string, libraryId: string): Promise<boolean> => {
|
||||
return this.libraryRepository.exist({
|
||||
|
64
server/src/infra/repositories/activity.repository.ts
Normal file
64
server/src/infra/repositories/activity.repository.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { IActivityRepository } from '@app/domain';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ActivityEntity } from '../entities/activity.entity';
|
||||
|
||||
export interface ActivitySearch {
|
||||
albumId?: string;
|
||||
assetId?: string;
|
||||
userId?: string;
|
||||
isLiked?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ActivityRepository implements IActivityRepository {
|
||||
constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {}
|
||||
|
||||
search(options: ActivitySearch): Promise<ActivityEntity[]> {
|
||||
const { userId, assetId, albumId, isLiked } = options;
|
||||
return this.repository.find({
|
||||
where: {
|
||||
userId,
|
||||
assetId,
|
||||
albumId,
|
||||
isLiked,
|
||||
},
|
||||
relations: {
|
||||
user: true,
|
||||
},
|
||||
order: {
|
||||
createdAt: 'ASC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
create(entity: Partial<ActivityEntity>): Promise<ActivityEntity> {
|
||||
return this.save(entity);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
getStatistics(assetId: string, albumId: string): Promise<number> {
|
||||
return this.repository.count({
|
||||
where: { assetId, albumId, isLiked: false },
|
||||
relations: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async save(entity: Partial<ActivityEntity>) {
|
||||
const { id } = await this.repository.save(entity);
|
||||
return this.repository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export * from './access.repository';
|
||||
export * from './activity.repository';
|
||||
export * from './album.repository';
|
||||
export * from './api-key.repository';
|
||||
export * from './asset.repository';
|
||||
|
14
server/test/api/activity-api.ts
Normal file
14
server/test/api/activity-api.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { ActivityCreateDto, ActivityResponseDto } from '@app/domain';
|
||||
import request from 'supertest';
|
||||
|
||||
export const activityApi = {
|
||||
create: async (server: any, accessToken: string, dto: ActivityCreateDto) => {
|
||||
const res = await request(server).post('/activity').set('Authorization', `Bearer ${accessToken}`).send(dto);
|
||||
expect(res.status === 200 || res.status === 201).toBe(true);
|
||||
return res.body as ActivityResponseDto;
|
||||
},
|
||||
delete: async (server: any, accessToken: string, id: string) => {
|
||||
const res = await request(server).delete(`/activity/${id}`).set('Authorization', `Bearer ${accessToken}`);
|
||||
expect(res.status).toEqual(204);
|
||||
},
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { AlbumResponseDto, BulkIdResponseDto, BulkIdsDto, CreateAlbumDto } from '@app/domain';
|
||||
import { AddUsersDto, AlbumResponseDto, BulkIdResponseDto, BulkIdsDto, CreateAlbumDto } from '@app/domain';
|
||||
import request from 'supertest';
|
||||
|
||||
export const albumApi = {
|
||||
@ -15,4 +15,9 @@ export const albumApi = {
|
||||
expect(res.status).toEqual(200);
|
||||
return res.body as BulkIdResponseDto[];
|
||||
},
|
||||
addUsers: async (server: any, accessToken: string, id: string, dto: AddUsersDto) => {
|
||||
const res = await request(server).put(`/album/${id}/users`).set('Authorization', `Bearer ${accessToken}`).send(dto);
|
||||
expect(res.status).toEqual(200);
|
||||
return res.body as AlbumResponseDto;
|
||||
},
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { activityApi } from './activity-api';
|
||||
import { albumApi } from './album-api';
|
||||
import { assetApi } from './asset-api';
|
||||
import { authApi } from './auth-api';
|
||||
@ -6,6 +7,7 @@ import { sharedLinkApi } from './shared-link-api';
|
||||
import { userApi } from './user-api';
|
||||
|
||||
export const api = {
|
||||
activityApi,
|
||||
authApi,
|
||||
assetApi,
|
||||
libraryApi,
|
||||
|
376
server/test/e2e/activity.e2e-spec.ts
Normal file
376
server/test/e2e/activity.e2e-spec.ts
Normal file
@ -0,0 +1,376 @@
|
||||
import { AlbumResponseDto, LoginResponseDto, ReactionType } from '@app/domain';
|
||||
import { ActivityController } from '@app/immich';
|
||||
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
|
||||
import { api } from '@test/api';
|
||||
import { db } from '@test/db';
|
||||
import { errorStub, uuidStub } from '@test/fixtures';
|
||||
import { testApp } from '@test/test-utils';
|
||||
import request from 'supertest';
|
||||
|
||||
describe(`${ActivityController.name} (e2e)`, () => {
|
||||
let server: any;
|
||||
let admin: LoginResponseDto;
|
||||
let asset: AssetFileUploadResponseDto;
|
||||
let album: AlbumResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
[server] = await testApp.create();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.reset();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
asset = await api.assetApi.upload(server, admin.accessToken, 'example');
|
||||
album = await api.albumApi.create(server, admin.accessToken, { albumName: 'Album 1', assetIds: [asset.id] });
|
||||
});
|
||||
|
||||
describe('GET /activity', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).get('/activity');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
});
|
||||
|
||||
it('should require an albumId', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.get('/activity')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||
});
|
||||
|
||||
it('should reject an invalid albumId', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.get('/activity')
|
||||
.query({ albumId: uuidStub.invalid })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||
});
|
||||
|
||||
it('should reject an invalid assetId', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.get('/activity')
|
||||
.query({ albumId: uuidStub.notFound, assetId: uuidStub.invalid })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
|
||||
});
|
||||
|
||||
it('should start off empty', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.get('/activity')
|
||||
.query({ albumId: album.id })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([]);
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should filter by album id', async () => {
|
||||
const album2 = await api.albumApi.create(server, admin.accessToken, {
|
||||
albumName: 'Album 2',
|
||||
assetIds: [asset.id],
|
||||
});
|
||||
const [reaction] = await Promise.all([
|
||||
api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album.id,
|
||||
type: ReactionType.LIKE,
|
||||
}),
|
||||
api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album2.id,
|
||||
type: ReactionType.LIKE,
|
||||
}),
|
||||
]);
|
||||
|
||||
const { status, body } = await request(server)
|
||||
.get('/activity')
|
||||
.query({ albumId: album.id })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(200);
|
||||
expect(body.length).toBe(1);
|
||||
expect(body[0]).toEqual(reaction);
|
||||
});
|
||||
|
||||
it('should filter by type=comment', async () => {
|
||||
const [reaction] = await Promise.all([
|
||||
api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album.id,
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'comment',
|
||||
}),
|
||||
api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
|
||||
]);
|
||||
|
||||
const { status, body } = await request(server)
|
||||
.get('/activity')
|
||||
.query({ albumId: album.id, type: 'comment' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(200);
|
||||
expect(body.length).toBe(1);
|
||||
expect(body[0]).toEqual(reaction);
|
||||
});
|
||||
|
||||
it('should filter by type=like', async () => {
|
||||
const [reaction] = await Promise.all([
|
||||
api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
|
||||
api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album.id,
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'comment',
|
||||
}),
|
||||
]);
|
||||
|
||||
const { status, body } = await request(server)
|
||||
.get('/activity')
|
||||
.query({ albumId: album.id, type: 'like' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(200);
|
||||
expect(body.length).toBe(1);
|
||||
expect(body[0]).toEqual(reaction);
|
||||
});
|
||||
|
||||
it('should filter by assetId', async () => {
|
||||
const [reaction] = await Promise.all([
|
||||
api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
type: ReactionType.LIKE,
|
||||
}),
|
||||
api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
|
||||
]);
|
||||
|
||||
const { status, body } = await request(server)
|
||||
.get('/activity')
|
||||
.query({ albumId: album.id, assetId: asset.id })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(200);
|
||||
expect(body.length).toBe(1);
|
||||
expect(body[0]).toEqual(reaction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /activity', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).post('/activity');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
});
|
||||
|
||||
it('should require an albumId', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.post('/activity')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ albumId: uuidStub.invalid });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||
});
|
||||
|
||||
it('should require a comment when type is comment', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.post('/activity')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ albumId: uuidStub.notFound, type: 'comment', comment: null });
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorStub.badRequest(['comment must be a string', 'comment should not be empty']));
|
||||
});
|
||||
|
||||
it('should add a comment to an album', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.post('/activity')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ albumId: album.id, type: 'comment', comment: 'This is my first comment' });
|
||||
expect(status).toEqual(201);
|
||||
expect(body).toEqual({
|
||||
id: expect.any(String),
|
||||
assetId: null,
|
||||
createdAt: expect.any(String),
|
||||
type: 'comment',
|
||||
comment: 'This is my first comment',
|
||||
user: expect.objectContaining({ email: admin.userEmail }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a like to an album', async () => {
|
||||
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).toEqual({
|
||||
id: expect.any(String),
|
||||
assetId: null,
|
||||
createdAt: expect.any(String),
|
||||
type: 'like',
|
||||
comment: null,
|
||||
user: expect.objectContaining({ email: admin.userEmail }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a 200 for a duplicate like on the album', async () => {
|
||||
const reaction = await api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album.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(200);
|
||||
expect(body).toEqual(reaction);
|
||||
});
|
||||
|
||||
it('should add a comment to an asset', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.post('/activity')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ albumId: album.id, assetId: asset.id, type: 'comment', comment: 'This is my first comment' });
|
||||
expect(status).toEqual(201);
|
||||
expect(body).toEqual({
|
||||
id: expect.any(String),
|
||||
assetId: asset.id,
|
||||
createdAt: expect.any(String),
|
||||
type: 'comment',
|
||||
comment: 'This is my first comment',
|
||||
user: expect.objectContaining({ email: admin.userEmail }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a like to an asset', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.post('/activity')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
|
||||
expect(status).toEqual(201);
|
||||
expect(body).toEqual({
|
||||
id: expect.any(String),
|
||||
assetId: asset.id,
|
||||
createdAt: expect.any(String),
|
||||
type: 'like',
|
||||
comment: null,
|
||||
user: expect.objectContaining({ email: admin.userEmail }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a 200 for a duplicate like on an asset', 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, assetId: asset.id, type: 'like' });
|
||||
expect(status).toEqual(200);
|
||||
expect(body).toEqual(reaction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /activity/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).delete(`/activity/${uuidStub.notFound}`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.delete(`/activity/${uuidStub.invalid}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should remove a comment from an album', async () => {
|
||||
const reaction = await api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album.id,
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'This is a test comment',
|
||||
});
|
||||
const { status } = await request(server)
|
||||
.delete(`/activity/${reaction.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(204);
|
||||
});
|
||||
|
||||
it('should remove a like from an album', async () => {
|
||||
const reaction = await api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album.id,
|
||||
type: ReactionType.LIKE,
|
||||
});
|
||||
const { status } = await request(server)
|
||||
.delete(`/activity/${reaction.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(204);
|
||||
});
|
||||
|
||||
it('should let the owner remove a comment by another user', async () => {
|
||||
const { id: userId } = await api.userApi.create(server, admin.accessToken, {
|
||||
email: 'user1@immich.app',
|
||||
password: 'Password123',
|
||||
firstName: 'User 1',
|
||||
lastName: 'Test',
|
||||
});
|
||||
await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] });
|
||||
const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' });
|
||||
const reaction = await api.activityApi.create(server, nonOwner.accessToken, {
|
||||
albumId: album.id,
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'This is a test comment',
|
||||
});
|
||||
|
||||
const { status } = await request(server)
|
||||
.delete(`/activity/${reaction.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(204);
|
||||
});
|
||||
|
||||
it('should not let a user remove a comment by another user', async () => {
|
||||
const { id: userId } = await api.userApi.create(server, admin.accessToken, {
|
||||
email: 'user1@immich.app',
|
||||
password: 'Password123',
|
||||
firstName: 'User 1',
|
||||
lastName: 'Test',
|
||||
});
|
||||
await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] });
|
||||
const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' });
|
||||
const reaction = await api.activityApi.create(server, admin.accessToken, {
|
||||
albumId: album.id,
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'This is a test comment',
|
||||
});
|
||||
|
||||
const { status, body } = await request(server)
|
||||
.delete(`/activity/${reaction.id}`)
|
||||
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.badRequest('Not found or no activity.delete access'));
|
||||
});
|
||||
|
||||
it('should let a non-owner remove their own comment', async () => {
|
||||
const { id: userId } = await api.userApi.create(server, admin.accessToken, {
|
||||
email: 'user1@immich.app',
|
||||
password: 'Password123',
|
||||
firstName: 'User 1',
|
||||
lastName: 'Test',
|
||||
});
|
||||
await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] });
|
||||
const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' });
|
||||
const reaction = await api.activityApi.create(server, nonOwner.accessToken, {
|
||||
albumId: album.id,
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'This is a test comment',
|
||||
});
|
||||
|
||||
const { status } = await request(server)
|
||||
.delete(`/activity/${reaction.id}`)
|
||||
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
});
|
||||
});
|
||||
});
|
34
server/test/fixtures/activity.stub.ts
vendored
Normal file
34
server/test/fixtures/activity.stub.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
import { ActivityEntity } from '@app/infra/entities';
|
||||
import { albumStub } from './album.stub';
|
||||
import { assetStub } from './asset.stub';
|
||||
import { authStub } from './auth.stub';
|
||||
import { userStub } from './user.stub';
|
||||
|
||||
export const activityStub = {
|
||||
oneComment: Object.freeze<ActivityEntity>({
|
||||
id: 'activity-1',
|
||||
comment: 'comment',
|
||||
isLiked: false,
|
||||
userId: authStub.admin.id,
|
||||
user: userStub.admin,
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
albumId: albumStub.oneAsset.id,
|
||||
album: albumStub.oneAsset,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}),
|
||||
liked: Object.freeze<ActivityEntity>({
|
||||
id: 'activity-2',
|
||||
comment: null,
|
||||
isLiked: true,
|
||||
userId: authStub.admin.id,
|
||||
user: userStub.admin,
|
||||
assetId: assetStub.image.id,
|
||||
asset: assetStub.image,
|
||||
albumId: albumStub.oneAsset.id,
|
||||
album: albumStub.oneAsset,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}),
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { AccessCore, IAccessRepository } from '@app/domain';
|
||||
|
||||
export interface IAccessRepositoryMock {
|
||||
activity: jest.Mocked<IAccessRepository['activity']>;
|
||||
asset: jest.Mocked<IAccessRepository['asset']>;
|
||||
album: jest.Mocked<IAccessRepository['album']>;
|
||||
authDevice: jest.Mocked<IAccessRepository['authDevice']>;
|
||||
@ -15,6 +16,10 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock =>
|
||||
}
|
||||
|
||||
return {
|
||||
activity: {
|
||||
hasOwnerAccess: jest.fn(),
|
||||
hasAlbumOwnerAccess: jest.fn(),
|
||||
},
|
||||
asset: {
|
||||
hasOwnerAccess: jest.fn(),
|
||||
hasAlbumAccess: jest.fn(),
|
||||
|
10
server/test/repositories/activity.repository.mock.ts
Normal file
10
server/test/repositories/activity.repository.mock.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { IActivityRepository } from '@app/domain';
|
||||
|
||||
export const newActivityRepositoryMock = (): jest.Mocked<IActivityRepository> => {
|
||||
return {
|
||||
search: jest.fn(),
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
getStatistics: jest.fn(),
|
||||
};
|
||||
};
|
@ -20,12 +20,14 @@ import {
|
||||
UserApi,
|
||||
UserApiFp,
|
||||
AuditApi,
|
||||
ActivityApi,
|
||||
} from './open-api';
|
||||
import { BASE_PATH } from './open-api/base';
|
||||
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
|
||||
import type { ApiParams } from './types';
|
||||
|
||||
export class ImmichApi {
|
||||
public activityApi: ActivityApi;
|
||||
public albumApi: AlbumApi;
|
||||
public libraryApi: LibraryApi;
|
||||
public assetApi: AssetApi;
|
||||
@ -52,6 +54,7 @@ export class ImmichApi {
|
||||
constructor(params: ConfigurationParameters) {
|
||||
this.config = new Configuration(params);
|
||||
|
||||
this.activityApi = new ActivityApi(this.config);
|
||||
this.albumApi = new AlbumApi(this.config);
|
||||
this.auditApi = new AuditApi(this.config);
|
||||
this.libraryApi = new LibraryApi(this.config);
|
||||
|
577
web/src/api/open-api/api.ts
generated
577
web/src/api/open-api/api.ts
generated
@ -99,6 +99,103 @@ export interface APIKeyUpdateDto {
|
||||
*/
|
||||
'name': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityCreateDto
|
||||
*/
|
||||
export interface ActivityCreateDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'albumId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'assetId'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'comment'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {ReactionType}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'type': ReactionType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityResponseDto
|
||||
*/
|
||||
export interface ActivityResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'assetId': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'comment'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'createdAt': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'type': ActivityResponseDtoTypeEnum;
|
||||
/**
|
||||
*
|
||||
* @type {UserDto}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'user': UserDto;
|
||||
}
|
||||
|
||||
export const ActivityResponseDtoTypeEnum = {
|
||||
Comment: 'comment',
|
||||
Like: 'like'
|
||||
} as const;
|
||||
|
||||
export type ActivityResponseDtoTypeEnum = typeof ActivityResponseDtoTypeEnum[keyof typeof ActivityResponseDtoTypeEnum];
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityStatisticsResponseDto
|
||||
*/
|
||||
export interface ActivityStatisticsResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ActivityStatisticsResponseDto
|
||||
*/
|
||||
'comments': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -2490,6 +2587,20 @@ export interface QueueStatusDto {
|
||||
*/
|
||||
'isPaused': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const ReactionType = {
|
||||
Comment: 'comment',
|
||||
Like: 'like'
|
||||
} as const;
|
||||
|
||||
export type ReactionType = typeof ReactionType[keyof typeof ReactionType];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -4248,6 +4359,43 @@ export interface UsageByUserDto {
|
||||
*/
|
||||
'videos': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserDto
|
||||
*/
|
||||
export interface UserDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'email': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'firstName': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'lastName': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'profileImagePath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -4831,6 +4979,435 @@ export class APIKeyApi extends BaseAPI {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ActivityApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityCreateDto} activityCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createActivity: async (activityCreateDto: ActivityCreateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'activityCreateDto' is not null or undefined
|
||||
assertParamExists('createActivity', 'activityCreateDto', activityCreateDto)
|
||||
const localVarPath = `/activity`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(activityCreateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteActivity: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('deleteActivity', 'id', id)
|
||||
const localVarPath = `/activity/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {ReactionType} [type]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'albumId' is not null or undefined
|
||||
assertParamExists('getActivities', 'albumId', albumId)
|
||||
const localVarPath = `/activity`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (albumId !== undefined) {
|
||||
localVarQueryParameter['albumId'] = albumId;
|
||||
}
|
||||
|
||||
if (assetId !== undefined) {
|
||||
localVarQueryParameter['assetId'] = assetId;
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
localVarQueryParameter['type'] = type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivityStatistics: async (albumId: string, assetId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'albumId' is not null or undefined
|
||||
assertParamExists('getActivityStatistics', 'albumId', albumId)
|
||||
const localVarPath = `/activity/statistics`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (albumId !== undefined) {
|
||||
localVarQueryParameter['albumId'] = albumId;
|
||||
}
|
||||
|
||||
if (assetId !== undefined) {
|
||||
localVarQueryParameter['assetId'] = assetId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ActivityApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ActivityApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityCreateDto} activityCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createActivity(activityCreateDto: ActivityCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createActivity(activityCreateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteActivity(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteActivity(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {ReactionType} [type]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getActivities(albumId: string, assetId?: string, type?: ReactionType, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getActivityStatistics(albumId: string, assetId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityStatisticsResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivityStatistics(albumId, assetId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ActivityApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ActivityApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityResponseDto> {
|
||||
return localVarFp.createActivity(requestParameters.activityCreateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteActivity(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
|
||||
return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityStatisticsResponseDto> {
|
||||
return localVarFp.getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for createActivity operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiCreateActivityRequest
|
||||
*/
|
||||
export interface ActivityApiCreateActivityRequest {
|
||||
/**
|
||||
*
|
||||
* @type {ActivityCreateDto}
|
||||
* @memberof ActivityApiCreateActivity
|
||||
*/
|
||||
readonly activityCreateDto: ActivityCreateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for deleteActivity operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiDeleteActivityRequest
|
||||
*/
|
||||
export interface ActivityApiDeleteActivityRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiDeleteActivity
|
||||
*/
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getActivities operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiGetActivitiesRequest
|
||||
*/
|
||||
export interface ActivityApiGetActivitiesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly albumId: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly assetId?: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {ReactionType}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly type?: ReactionType
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getActivityStatistics operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiGetActivityStatisticsRequest
|
||||
*/
|
||||
export interface ActivityApiGetActivityStatisticsRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivityStatistics
|
||||
*/
|
||||
readonly albumId: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivityStatistics
|
||||
*/
|
||||
readonly assetId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ActivityApi - object-oriented interface
|
||||
* @export
|
||||
* @class ActivityApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ActivityApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).createActivity(requestParameters.activityCreateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).deleteActivity(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AlbumApi - axios parameter creator
|
||||
* @export
|
||||
|
@ -5,7 +5,7 @@
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import type { AlbumResponseDto, SharedLinkResponseDto } from '@api';
|
||||
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { dateFormats } from '../../constants';
|
||||
import { createAssetInteractionStore } from '../../stores/asset-interaction.store';
|
||||
@ -22,6 +22,7 @@
|
||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
||||
const album = sharedLink.album as AlbumResponseDto;
|
||||
|
||||
@ -138,7 +139,7 @@
|
||||
<main
|
||||
class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
|
||||
>
|
||||
<AssetGrid {assetStore} {assetInteractionStore}>
|
||||
<AssetGrid {album} {user} {assetStore} {assetInteractionStore}>
|
||||
<section class="pt-24">
|
||||
<!-- ALBUM TITLE -->
|
||||
<p
|
||||
|
289
web/src/lib/components/asset-viewer/activity-viewer.svelte
Normal file
289
web/src/lib/components/asset-viewer/activity-viewer.svelte
Normal file
@ -0,0 +1,289 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import { mdiClose, mdiHeart, mdiSend, mdiDotsVertical } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { ActivityResponseDto, api, AssetTypeEnum, ReactionType, type UserResponseDto } from '@api';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isTenMinutesApart } from '$lib/utils/timesince';
|
||||
import { clickOutside } from '$lib/utils/click-outside';
|
||||
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 { getAssetType } from '$lib/utils/asset-utils';
|
||||
import * as luxon from 'luxon';
|
||||
|
||||
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||
|
||||
const timeSince = (dateTime: luxon.DateTime) => {
|
||||
const diff = dateTime.diffNow().shiftTo(...units);
|
||||
const unit = units.find((unit) => diff.get(unit) !== 0) || 'second';
|
||||
|
||||
const relativeFormatter = new Intl.RelativeTimeFormat('en', {
|
||||
numeric: 'auto',
|
||||
});
|
||||
return relativeFormatter.format(Math.trunc(diff.as(unit)), unit);
|
||||
};
|
||||
|
||||
export let reactions: ActivityResponseDto[];
|
||||
export let user: UserResponseDto;
|
||||
export let assetId: string;
|
||||
export let albumId: string;
|
||||
export let assetType: AssetTypeEnum;
|
||||
export let albumOwnerId: string;
|
||||
|
||||
let textArea: HTMLTextAreaElement;
|
||||
let innerHeight: number;
|
||||
let activityHeight: number;
|
||||
let chatHeight: number;
|
||||
let divHeight: number;
|
||||
let previousAssetId: string | null;
|
||||
let message = '';
|
||||
let isSendingMessage = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: showDeleteReaction = Array(reactions.length).fill(false);
|
||||
$: {
|
||||
if (innerHeight && activityHeight) {
|
||||
divHeight = innerHeight - activityHeight;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (previousAssetId != assetId) {
|
||||
getReactions();
|
||||
previousAssetId = assetId;
|
||||
}
|
||||
}
|
||||
|
||||
const getReactions = async () => {
|
||||
try {
|
||||
const { data } = await api.activityApi.getActivities({ assetId, albumId });
|
||||
reactions = data;
|
||||
} catch (error) {
|
||||
handleError(error, 'Error when fetching reactions');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnter = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleSendComment();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const autoGrow = () => {
|
||||
textArea.style.height = '5px';
|
||||
textArea.style.height = textArea.scrollHeight + 'px';
|
||||
};
|
||||
|
||||
const timeOptions = {
|
||||
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 api.activityApi.deleteActivity({ id: reaction.id });
|
||||
reactions.splice(index, 1);
|
||||
showDeleteReaction.splice(index, 1);
|
||||
reactions = reactions;
|
||||
if (reaction.type === 'like' && reaction.user.id === user.id) {
|
||||
dispatch('deleteLike');
|
||||
} else {
|
||||
dispatch('deleteComment');
|
||||
}
|
||||
notificationController.show({
|
||||
message: `${reaction.type} deleted`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, `Can't remove ${reaction.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendComment = async () => {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => (isSendingMessage = true), 100);
|
||||
try {
|
||||
const { data } = await api.activityApi.createActivity({
|
||||
activityCreateDto: { albumId, assetId, type: ReactionType.Comment, comment: message },
|
||||
});
|
||||
reactions.push(data);
|
||||
textArea.style.height = '18px';
|
||||
message = '';
|
||||
dispatch('addComment');
|
||||
// Re-render the activity feed
|
||||
reactions = reactions;
|
||||
} catch (error) {
|
||||
handleError(error, "Can't add your comment");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
isSendingMessage = false;
|
||||
};
|
||||
|
||||
const showOptionsMenu = (index: number) => {
|
||||
showDeleteReaction[index] = !showDeleteReaction[index];
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="overflow-y-hidden relative h-full" bind:offsetHeight={innerHeight}>
|
||||
<div class="dark:bg-immich-dark-bg dark:text-immich-dark-fg w-full h-full">
|
||||
<div
|
||||
class="flex w-full h-fit dark:bg-immich-dark-bg dark:text-immich-dark-fg p-2 bg-white"
|
||||
bind:clientHeight={activityHeight}
|
||||
>
|
||||
<div class="flex place-items-center gap-2">
|
||||
<button
|
||||
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
on:click={() => dispatch('close')}
|
||||
>
|
||||
<Icon path={mdiClose} size="24" />
|
||||
</button>
|
||||
|
||||
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Activity</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if innerHeight}
|
||||
<div
|
||||
class="overflow-y-auto immich-scrollbar relative w-full"
|
||||
style="height: {divHeight}px;padding-bottom: {chatHeight}px"
|
||||
>
|
||||
{#each reactions as reaction, index (reaction.id)}
|
||||
{#if reaction.type === 'comment'}
|
||||
<div class="flex dark:bg-gray-800 bg-gray-200 p-3 mx-2 mt-3 rounded-lg gap-4 justify-start">
|
||||
<div>
|
||||
<UserAvatar user={reaction.user} size="sm" />
|
||||
</div>
|
||||
|
||||
<div class="w-full leading-4 overflow-hidden self-center break-words text-sm">{reaction.comment}</div>
|
||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||
<div class="flex items-start w-fit pt-[5px]" title="Delete comment">
|
||||
<button on:click={() => (!showDeleteReaction[index] ? showOptionsMenu(index) : '')}>
|
||||
<Icon path={mdiDotsVertical} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if showDeleteReaction[index]}
|
||||
<button
|
||||
class="absolute right-6 rounded-xl items-center bg-gray-300 dark:bg-slate-100 py-2 px-6 text-left text-sm font-medium text-immich-fg hover:bg-red-300 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg dark:hover:bg-red-300 transition-colors"
|
||||
use:clickOutside
|
||||
on:outclick={() => (showDeleteReaction[index] = false)}
|
||||
on:click={() => handleDeleteReaction(reaction, index)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if (index != reactions.length - 1 && isTenMinutesApart(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1}
|
||||
<div
|
||||
class=" px-2 text-right w-full text-sm text-gray-500 dark:text-gray-300"
|
||||
title={new Date(reaction.createdAt).toLocaleDateString(undefined, timeOptions)}
|
||||
>
|
||||
{timeSince(luxon.DateTime.fromISO(reaction.createdAt))}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if reaction.type === 'like'}
|
||||
<div class="relative">
|
||||
<div class="flex p-2 mx-2 mt-2 rounded-full gap-2 items-center text-sm">
|
||||
<div class="text-red-600"><Icon path={mdiHeart} size={20} /></div>
|
||||
|
||||
<div
|
||||
class="w-full"
|
||||
title={`${reaction.user.firstName} ${reaction.user.lastName} (${reaction.user.email})`}
|
||||
>
|
||||
{`${reaction.user.firstName} ${reaction.user.lastName} liked this ${getAssetType(
|
||||
assetType,
|
||||
).toLowerCase()}`}
|
||||
</div>
|
||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||
<div class="flex items-start w-fit" title="Delete like">
|
||||
<button on:click={() => (!showDeleteReaction[index] ? showOptionsMenu(index) : '')}>
|
||||
<Icon path={mdiDotsVertical} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if showDeleteReaction[index]}
|
||||
<button
|
||||
class="absolute top-2 right-6 rounded-xl items-center bg-gray-300 dark:bg-slate-100 p-3 text-left text-sm font-medium text-immich-fg hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg"
|
||||
use:clickOutside
|
||||
on:outclick={() => (showDeleteReaction[index] = false)}
|
||||
on:click={() => handleDeleteReaction(reaction, index)}
|
||||
>
|
||||
Delete Like
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if (index != reactions.length - 1 && isTenMinutesApart(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1}
|
||||
<div
|
||||
class=" px-2 text-right w-full text-sm text-gray-500 dark:text-gray-300"
|
||||
title={new Date(reaction.createdAt).toLocaleDateString(navigator.language, timeOptions)}
|
||||
>
|
||||
{timeSince(luxon.DateTime.fromISO(reaction.createdAt))}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="absolute w-full bottom-0">
|
||||
<div class="flex items-center justify-center p-2 mr-2" bind:clientHeight={chatHeight}>
|
||||
<div class="flex p-2 gap-4 h-fit bg-gray-200 text-immich-dark-gray rounded-3xl w-full">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" showTitle={false} />
|
||||
</div>
|
||||
<form class="flex w-full max-h-56 gap-1" on:submit|preventDefault={() => handleSendComment()}>
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<textarea
|
||||
bind:this={textArea}
|
||||
bind:value={message}
|
||||
placeholder="Say something"
|
||||
on:input={autoGrow}
|
||||
on:keypress={handleEnter}
|
||||
class="h-[18px] w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||
/>
|
||||
</div>
|
||||
{#if isSendingMessage}
|
||||
<div class="flex items-end place-items-center pb-2 ml-0">
|
||||
<div class="flex w-full place-items-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message}
|
||||
<div class="flex items-end w-fit ml-0 text-immich-primary dark:text-white">
|
||||
<CircleIconButton size="15" icon={mdiSend} />
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
::placeholder {
|
||||
color: rgb(60, 60, 60);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
::-ms-input-placeholder {
|
||||
/* Edge 12 -18 */
|
||||
color: white;
|
||||
}
|
||||
</style>
|
@ -1,6 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { AlbumResponseDto, api, AssetJobName, AssetResponseDto, AssetTypeEnum, SharedLinkResponseDto } from '@api';
|
||||
import {
|
||||
ActivityResponseDto,
|
||||
AlbumResponseDto,
|
||||
api,
|
||||
AssetJobName,
|
||||
AssetResponseDto,
|
||||
AssetTypeEnum,
|
||||
ReactionType,
|
||||
SharedLinkResponseDto,
|
||||
UserResponseDto,
|
||||
} from '@api';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
|
||||
@ -14,7 +24,7 @@
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte';
|
||||
import { isShowDetail } from '$lib/stores/preferences.store';
|
||||
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
|
||||
import { addAssetsToAlbum, downloadFile, getAssetType } from '$lib/utils/asset-utils';
|
||||
import NavigationArea from './navigation-area.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@ -23,10 +33,21 @@
|
||||
import ProgressBar, { ProgressBarStatus } from '../shared-components/progress-bar/progress-bar.svelte';
|
||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiImageBrokenVariant, mdiPause, mdiPlay } from '@mdi/js';
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiHeartOutline,
|
||||
mdiHeart,
|
||||
mdiCommentOutline,
|
||||
mdiChevronRight,
|
||||
mdiClose,
|
||||
mdiImageBrokenVariant,
|
||||
mdiPause,
|
||||
mdiPlay,
|
||||
} from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
||||
import ActivityViewer from './activity-viewer.svelte';
|
||||
|
||||
export let assetStore: AssetStore | null = null;
|
||||
export let asset: AssetResponseDto;
|
||||
@ -35,6 +56,11 @@
|
||||
$: isTrashEnabled = $featureFlags.trash;
|
||||
export let force = false;
|
||||
export let withStacked = false;
|
||||
export let isShared = true;
|
||||
export let user: UserResponseDto | null = null;
|
||||
export let album: AlbumResponseDto | null = null;
|
||||
|
||||
let reactions: ActivityResponseDto[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
archived: AssetResponseDto;
|
||||
@ -57,6 +83,9 @@
|
||||
let shouldShowDetailButton = asset.hasMetadata;
|
||||
let canCopyImagesToClipboard: boolean;
|
||||
let previewStackedAsset: AssetResponseDto | undefined;
|
||||
let isShowActivity = false;
|
||||
let isLiked: ActivityResponseDto | null = null;
|
||||
let numberOfComments: number;
|
||||
|
||||
$: {
|
||||
if (asset.stackCount && asset.stack) {
|
||||
@ -71,6 +100,62 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleFavorite = async () => {
|
||||
if (album) {
|
||||
try {
|
||||
if (isLiked) {
|
||||
const activityId = isLiked.id;
|
||||
await api.activityApi.deleteActivity({ id: activityId });
|
||||
reactions = reactions.filter((reaction) => reaction.id !== activityId);
|
||||
isLiked = null;
|
||||
} else {
|
||||
const { data } = await api.activityApi.createActivity({
|
||||
activityCreateDto: { albumId: album.id, assetId: asset.id, type: ReactionType.Like },
|
||||
});
|
||||
|
||||
isLiked = data;
|
||||
reactions = [...reactions, isLiked];
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Can't change favorite for asset");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getFavorite = async () => {
|
||||
if (album) {
|
||||
try {
|
||||
const { data } = await api.activityApi.getActivities({
|
||||
assetId: asset.id,
|
||||
albumId: album.id,
|
||||
type: ReactionType.Like,
|
||||
});
|
||||
if (data.length > 0) {
|
||||
isLiked = data[0];
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Can't get Favorite");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getNumberOfComments = async () => {
|
||||
if (album) {
|
||||
try {
|
||||
const { data } = await api.activityApi.getActivityStatistics({ assetId: asset.id, albumId: album.id });
|
||||
numberOfComments = data.comments;
|
||||
} catch (error) {
|
||||
handleError(error, "Can't get number of comments");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$: {
|
||||
if (isShared && asset.id) {
|
||||
getFavorite();
|
||||
getNumberOfComments();
|
||||
}
|
||||
}
|
||||
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo);
|
||||
|
||||
onMount(async () => {
|
||||
@ -116,6 +201,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenActivity = () => {
|
||||
if ($isShowDetail) {
|
||||
$isShowDetail = false;
|
||||
}
|
||||
isShowActivity = !isShowActivity;
|
||||
};
|
||||
|
||||
const handleKeyboardPress = (event: KeyboardEvent) => {
|
||||
if (shouldIgnoreShortcut(event)) {
|
||||
return;
|
||||
@ -157,6 +249,7 @@
|
||||
toggleFavorite();
|
||||
return;
|
||||
case 'i':
|
||||
isShowActivity = false;
|
||||
$isShowDetail = !$isShowDetail;
|
||||
return;
|
||||
}
|
||||
@ -193,6 +286,9 @@
|
||||
};
|
||||
|
||||
const showDetailInfoHandler = () => {
|
||||
if (isShowActivity) {
|
||||
isShowActivity = false;
|
||||
}
|
||||
$isShowDetail = !$isShowDetail;
|
||||
};
|
||||
|
||||
@ -317,17 +413,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const getAssetType = () => {
|
||||
switch (asset.type) {
|
||||
case 'IMAGE':
|
||||
return 'Photo';
|
||||
case 'VIDEO':
|
||||
return 'Video';
|
||||
default:
|
||||
return 'Asset';
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunJob = async (name: AssetJobName) => {
|
||||
try {
|
||||
await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
|
||||
@ -471,7 +556,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Asset Viewer -->
|
||||
<div class="col-span-4 col-start-1 row-span-full row-start-1">
|
||||
<div class="relative col-span-4 col-start-1 row-span-full row-start-1">
|
||||
{#if previewStackedAsset}
|
||||
{#key previewStackedAsset.id}
|
||||
{#if previewStackedAsset.type === AssetTypeEnum.Image}
|
||||
@ -517,6 +602,29 @@
|
||||
on:onVideoStarted={handleVideoStarted}
|
||||
/>
|
||||
{/if}
|
||||
{#if isShared}
|
||||
<div class="z-[9999] absolute bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||
<div
|
||||
class="w-full h-14 flex p-4 text-white items-center justify-center rounded-full gap-4 bg-immich-dark-bg bg-opacity-60"
|
||||
>
|
||||
<button on:click={handleFavorite}>
|
||||
<div class="items-center justify-center">
|
||||
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
|
||||
</div>
|
||||
</button>
|
||||
<button on:click={handleOpenActivity}>
|
||||
<div class="flex gap-2 items-center justify-center">
|
||||
<Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} />
|
||||
{#if numberOfComments}
|
||||
<div class="text-xl">{numberOfComments}</div>
|
||||
{:else if !isShowActivity && !$isShowDetail}
|
||||
<div class="text-lg">Say something</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
@ -582,6 +690,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isShared && album && isShowActivity && user}
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="activity-panel"
|
||||
class="z-[1002] row-start-1 row-span-5 w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg pl-4"
|
||||
translate="yes"
|
||||
>
|
||||
<ActivityViewer
|
||||
{user}
|
||||
assetType={asset.type}
|
||||
albumOwnerId={album.ownerId}
|
||||
albumId={album.id}
|
||||
assetId={asset.id}
|
||||
bind:reactions
|
||||
on:addComment={() => numberOfComments++}
|
||||
on:deleteComment={() => numberOfComments--}
|
||||
on:deleteLike={() => (isLiked = null)}
|
||||
on:close={() => (isShowActivity = false)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isShowAlbumPicker}
|
||||
<AlbumSelectionModal
|
||||
shared={addToSharedAlbum}
|
||||
@ -594,15 +724,15 @@
|
||||
|
||||
{#if isShowDeleteConfirmation}
|
||||
<ConfirmDialogue
|
||||
title="Delete {getAssetType()}"
|
||||
title="Delete {getAssetType(asset.type)}"
|
||||
confirmText="Delete"
|
||||
on:confirm={deleteAsset}
|
||||
on:cancel={() => (isShowDeleteConfirmation = false)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>
|
||||
Are you sure you want to delete this {getAssetType().toLowerCase()}? This will also remove it from its
|
||||
album(s).
|
||||
Are you sure you want to delete this {getAssetType(asset.type).toLowerCase()}? This will also remove it from
|
||||
its album(s).
|
||||
</p>
|
||||
<p><b>You cannot undo this action!</b></p>
|
||||
</svelte:fragment>
|
||||
|
@ -7,6 +7,7 @@
|
||||
import { useZoomImageWheel } from '@zoom-image/svelte';
|
||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let element: HTMLDivElement | undefined = undefined;
|
||||
@ -54,11 +55,14 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeypress = async ({ metaKey, ctrlKey, key }: KeyboardEvent) => {
|
||||
const handleKeypress = async (event: KeyboardEvent) => {
|
||||
if (shouldIgnoreShortcut(event)) {
|
||||
return;
|
||||
}
|
||||
if (window.getSelection()?.type === 'Range') {
|
||||
return;
|
||||
}
|
||||
if ((metaKey || ctrlKey) && key === 'c') {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'c') {
|
||||
await doCopy();
|
||||
}
|
||||
};
|
||||
|
@ -8,7 +8,7 @@
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { isSearchEnabled } from '$lib/stores/search.store';
|
||||
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import type { AlbumResponseDto, AssetResponseDto, UserResponseDto } from '@api';
|
||||
import { DateTime } from 'luxon';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||
@ -26,6 +26,9 @@
|
||||
export let assetInteractionStore: AssetInteractionStore;
|
||||
export let removeAction: AssetAction | null = null;
|
||||
export let withStacked = false;
|
||||
export let isShared = false;
|
||||
export let user: UserResponseDto | null = null;
|
||||
export let album: AlbumResponseDto | null = null;
|
||||
|
||||
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
|
||||
export let forceDelete = false;
|
||||
@ -391,10 +394,13 @@
|
||||
<Portal target="body">
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
{user}
|
||||
{withStacked}
|
||||
{assetStore}
|
||||
asset={$viewingAsset}
|
||||
force={forceDelete || !isTrashEnabled}
|
||||
{isShared}
|
||||
{album}
|
||||
on:previous={() => handlePrevious()}
|
||||
on:next={() => handleNext()}
|
||||
on:close={() => handleClose()}
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
|
||||
import { api, AssetResponseDto, SharedLinkResponseDto, UserResponseDto } from '@api';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let isOwned: boolean;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
|
||||
@ -102,6 +103,6 @@
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40">
|
||||
<GalleryViewer {assets} bind:selectedAssets />
|
||||
<GalleryViewer {user} {assets} bind:selectedAssets />
|
||||
</section>
|
||||
</section>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetResponseDto, ThumbnailFormat } from '@api';
|
||||
import { AssetResponseDto, ThumbnailFormat, UserResponseDto } from '@api';
|
||||
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
||||
@ -12,6 +12,7 @@
|
||||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
export let disableAssetSelect = false;
|
||||
export let showArchiveIcon = false;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
@ -103,6 +104,7 @@
|
||||
<!-- Overlay Asset Viewer -->
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
{user}
|
||||
asset={selectedAsset}
|
||||
on:previous={navigateAssetBackward}
|
||||
on:next={navigateAssetForward}
|
||||
|
@ -5,9 +5,17 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { imageLoad } from '$lib/utils/image-load';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { api } from '@api';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
interface User {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
profileImagePath: string;
|
||||
}
|
||||
|
||||
export let user: User;
|
||||
export let color: Color = 'primary';
|
||||
export let size: Size = 'full';
|
||||
export let rounded = true;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||
import { downloadManager } from '$lib/stores/download';
|
||||
import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto } from '@api';
|
||||
import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto, AssetTypeEnum } from '@api';
|
||||
import { handleError } from './handle-error';
|
||||
|
||||
export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> =>
|
||||
@ -192,3 +192,14 @@ export function isWebCompatibleImage(asset: AssetResponseDto): boolean {
|
||||
|
||||
return supportedImageExtensions.has(imgExtension);
|
||||
}
|
||||
|
||||
export const getAssetType = (type: AssetTypeEnum) => {
|
||||
switch (type) {
|
||||
case 'IMAGE':
|
||||
return 'Photo';
|
||||
case 'VIDEO':
|
||||
return 'Video';
|
||||
default:
|
||||
return 'Asset';
|
||||
}
|
||||
};
|
||||
|
9
web/src/lib/utils/timesince.ts
Normal file
9
web/src/lib/utils/timesince.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const isTenMinutesApart = (date1: string, date2: string): boolean => {
|
||||
if (!date1 || !date2) {
|
||||
return false;
|
||||
}
|
||||
const diffInMilliseconds = Math.abs(new Date(date1).getTime() - new Date(date2).getTime());
|
||||
const minutesDifference = diffInMilliseconds / (1000 * 60);
|
||||
|
||||
return minutesDifference >= 10;
|
||||
};
|
@ -424,11 +424,19 @@
|
||||
class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
|
||||
>
|
||||
{#if viewMode === ViewMode.SELECT_ASSETS}
|
||||
<AssetGrid assetStore={timelineStore} assetInteractionStore={timelineInteractionStore} isSelectionMode={true} />
|
||||
<AssetGrid
|
||||
user={data.user}
|
||||
assetStore={timelineStore}
|
||||
assetInteractionStore={timelineInteractionStore}
|
||||
isSelectionMode={true}
|
||||
/>
|
||||
{:else}
|
||||
<AssetGrid
|
||||
{album}
|
||||
user={data.user}
|
||||
{assetStore}
|
||||
{assetInteractionStore}
|
||||
isShared={album.sharedUsers.length > 0}
|
||||
isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL}
|
||||
singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL}
|
||||
on:select={({ detail: asset }) => handleUpdateThumbnail(asset.id)}
|
||||
|
Loading…
Reference in New Issue
Block a user