1
0
mirror of https://github.com/immich-app/immich.git synced 2025-03-03 15:42:28 +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:
martin 2023-11-01 04:13:34 +01:00 committed by GitHub
parent 68f6446718
commit ce5966c23d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 4487 additions and 38 deletions

View File

@ -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

View File

@ -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

View File

@ -77,6 +77,10 @@ Class | Method | HTTP request | Description
*APIKeyApi* | [**getKey**](doc//APIKeyApi.md#getkey) | **GET** /api-key/{id} |
*APIKeyApi* | [**getKeys**](doc//APIKeyApi.md#getkeys) | **GET** /api-key |
*APIKeyApi* | [**updateKey**](doc//APIKeyApi.md#updatekey) | **PUT** /api-key/{id} |
*ActivityApi* | [**createActivity**](doc//ActivityApi.md#createactivity) | **POST** /activity |
*ActivityApi* | [**deleteActivity**](doc//ActivityApi.md#deleteactivity) | **DELETE** /activity/{id} |
*ActivityApi* | [**getActivities**](doc//ActivityApi.md#getactivities) | **GET** /activity |
*ActivityApi* | [**getActivityStatistics**](doc//ActivityApi.md#getactivitystatistics) | **GET** /activity/statistics |
*AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{id}/assets |
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{id}/users |
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album |
@ -204,6 +208,9 @@ Class | Method | HTTP request | Description
- [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md)
- [APIKeyResponseDto](doc//APIKeyResponseDto.md)
- [APIKeyUpdateDto](doc//APIKeyUpdateDto.md)
- [ActivityCreateDto](doc//ActivityCreateDto.md)
- [ActivityResponseDto](doc//ActivityResponseDto.md)
- [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md)
- [AddUsersDto](doc//AddUsersDto.md)
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
@ -284,6 +291,7 @@ Class | Method | HTTP request | Description
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
- [PersonUpdateDto](doc//PersonUpdateDto.md)
- [QueueStatusDto](doc//QueueStatusDto.md)
- [ReactionType](doc//ReactionType.md)
- [RecognitionConfig](doc//RecognitionConfig.md)
- [ScanLibraryDto](doc//ScanLibraryDto.md)
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
@ -339,6 +347,7 @@ Class | Method | HTTP request | Description
- [UpdateTagDto](doc//UpdateTagDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md)
- [UserDto](doc//UserDto.md)
- [UserResponseDto](doc//UserResponseDto.md)
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
- [VideoCodec](doc//VideoCodec.md)

242
mobile/openapi/doc/ActivityApi.md generated Normal file
View File

@ -0,0 +1,242 @@
# openapi.api.ActivityApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**createActivity**](ActivityApi.md#createactivity) | **POST** /activity |
[**deleteActivity**](ActivityApi.md#deleteactivity) | **DELETE** /activity/{id} |
[**getActivities**](ActivityApi.md#getactivities) | **GET** /activity |
[**getActivityStatistics**](ActivityApi.md#getactivitystatistics) | **GET** /activity/statistics |
# **createActivity**
> ActivityResponseDto createActivity(activityCreateDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ActivityApi();
final activityCreateDto = ActivityCreateDto(); // ActivityCreateDto |
try {
final result = api_instance.createActivity(activityCreateDto);
print(result);
} catch (e) {
print('Exception when calling ActivityApi->createActivity: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**activityCreateDto** | [**ActivityCreateDto**](ActivityCreateDto.md)| |
### Return type
[**ActivityResponseDto**](ActivityResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **deleteActivity**
> deleteActivity(id)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ActivityApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
api_instance.deleteActivity(id);
} catch (e) {
print('Exception when calling ActivityApi->deleteActivity: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getActivities**
> List<ActivityResponseDto> getActivities(albumId, assetId, type)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ActivityApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final type = ; // ReactionType |
try {
final result = api_instance.getActivities(albumId, assetId, type);
print(result);
} catch (e) {
print('Exception when calling ActivityApi->getActivities: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**assetId** | **String**| | [optional]
**type** | [**ReactionType**](.md)| | [optional]
### Return type
[**List<ActivityResponseDto>**](ActivityResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getActivityStatistics**
> ActivityStatisticsResponseDto getActivityStatistics(albumId, assetId)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = ActivityApi();
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
final result = api_instance.getActivityStatistics(albumId, assetId);
print(result);
} catch (e) {
print('Exception when calling ActivityApi->getActivityStatistics: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**albumId** | **String**| |
**assetId** | **String**| | [optional]
### Return type
[**ActivityStatisticsResponseDto**](ActivityStatisticsResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

18
mobile/openapi/doc/ActivityCreateDto.md generated Normal file
View File

@ -0,0 +1,18 @@
# openapi.model.ActivityCreateDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**albumId** | **String** | |
**assetId** | **String** | | [optional]
**comment** | **String** | | [optional]
**type** | [**ReactionType**](ReactionType.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -0,0 +1,20 @@
# openapi.model.ActivityResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetId** | **String** | |
**comment** | **String** | | [optional]
**createdAt** | [**DateTime**](DateTime.md) | |
**id** | **String** | |
**type** | **String** | |
**user** | [**UserDto**](UserDto.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -0,0 +1,15 @@
# openapi.model.ActivityStatisticsResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**comments** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

14
mobile/openapi/doc/ReactionType.md generated Normal file
View File

@ -0,0 +1,14 @@
# openapi.model.ReactionType
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

19
mobile/openapi/doc/UserDto.md generated Normal file
View File

@ -0,0 +1,19 @@
# openapi.model.UserDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**email** | **String** | |
**firstName** | **String** | |
**id** | **String** | |
**lastName** | **String** | |
**profileImagePath** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -29,6 +29,7 @@ part 'auth/http_basic_auth.dart';
part 'auth/http_bearer_auth.dart';
part 'api/api_key_api.dart';
part 'api/activity_api.dart';
part 'api/album_api.dart';
part 'api/asset_api.dart';
part 'api/audit_api.dart';
@ -49,6 +50,9 @@ part 'model/api_key_create_dto.dart';
part 'model/api_key_create_response_dto.dart';
part 'model/api_key_response_dto.dart';
part 'model/api_key_update_dto.dart';
part 'model/activity_create_dto.dart';
part 'model/activity_response_dto.dart';
part 'model/activity_statistics_response_dto.dart';
part 'model/add_users_dto.dart';
part 'model/admin_signup_response_dto.dart';
part 'model/album_count_response_dto.dart';
@ -129,6 +133,7 @@ part 'model/person_response_dto.dart';
part 'model/person_statistics_response_dto.dart';
part 'model/person_update_dto.dart';
part 'model/queue_status_dto.dart';
part 'model/reaction_type.dart';
part 'model/recognition_config.dart';
part 'model/scan_library_dto.dart';
part 'model/search_album_response_dto.dart';
@ -184,6 +189,7 @@ part 'model/update_stack_parent_dto.dart';
part 'model/update_tag_dto.dart';
part 'model/update_user_dto.dart';
part 'model/usage_by_user_dto.dart';
part 'model/user_dto.dart';
part 'model/user_response_dto.dart';
part 'model/validate_access_token_response_dto.dart';
part 'model/video_codec.dart';

227
mobile/openapi/lib/api/activity_api.dart generated Normal file
View File

@ -0,0 +1,227 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ActivityApi {
ActivityApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'POST /activity' operation and returns the [Response].
/// Parameters:
///
/// * [ActivityCreateDto] activityCreateDto (required):
Future<Response> createActivityWithHttpInfo(ActivityCreateDto activityCreateDto,) async {
// ignore: prefer_const_declarations
final path = r'/activity';
// ignore: prefer_final_locals
Object? postBody = activityCreateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [ActivityCreateDto] activityCreateDto (required):
Future<ActivityResponseDto?> createActivity(ActivityCreateDto activityCreateDto,) async {
final response = await createActivityWithHttpInfo(activityCreateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ActivityResponseDto',) as ActivityResponseDto;
}
return null;
}
/// Performs an HTTP 'DELETE /activity/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> deleteActivityWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/activity/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> deleteActivity(String id,) async {
final response = await deleteActivityWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'GET /activity' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
///
/// * [String] assetId:
///
/// * [ReactionType] type:
Future<Response> getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionType? type, }) async {
// ignore: prefer_const_declarations
final path = r'/activity';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'albumId', albumId));
if (assetId != null) {
queryParams.addAll(_queryParams('', 'assetId', assetId));
}
if (type != null) {
queryParams.addAll(_queryParams('', 'type', type));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] albumId (required):
///
/// * [String] assetId:
///
/// * [ReactionType] type:
Future<List<ActivityResponseDto>?> getActivities(String albumId, { String? assetId, ReactionType? type, }) async {
final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, type: type, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<ActivityResponseDto>') as List)
.cast<ActivityResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'GET /activity/statistics' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId (required):
///
/// * [String] assetId:
Future<Response> getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, }) async {
// ignore: prefer_const_declarations
final path = r'/activity/statistics';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'albumId', albumId));
if (assetId != null) {
queryParams.addAll(_queryParams('', 'assetId', assetId));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] albumId (required):
///
/// * [String] assetId:
Future<ActivityStatisticsResponseDto?> getActivityStatistics(String albumId, { String? assetId, }) async {
final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ActivityStatisticsResponseDto',) as ActivityStatisticsResponseDto;
}
return null;
}
}

View File

@ -189,6 +189,12 @@ class ApiClient {
return APIKeyResponseDto.fromJson(value);
case 'APIKeyUpdateDto':
return APIKeyUpdateDto.fromJson(value);
case 'ActivityCreateDto':
return ActivityCreateDto.fromJson(value);
case 'ActivityResponseDto':
return ActivityResponseDto.fromJson(value);
case 'ActivityStatisticsResponseDto':
return ActivityStatisticsResponseDto.fromJson(value);
case 'AddUsersDto':
return AddUsersDto.fromJson(value);
case 'AdminSignupResponseDto':
@ -349,6 +355,8 @@ class ApiClient {
return PersonUpdateDto.fromJson(value);
case 'QueueStatusDto':
return QueueStatusDto.fromJson(value);
case 'ReactionType':
return ReactionTypeTypeTransformer().decode(value);
case 'RecognitionConfig':
return RecognitionConfig.fromJson(value);
case 'ScanLibraryDto':
@ -459,6 +467,8 @@ class ApiClient {
return UpdateUserDto.fromJson(value);
case 'UsageByUserDto':
return UsageByUserDto.fromJson(value);
case 'UserDto':
return UserDto.fromJson(value);
case 'UserResponseDto':
return UserResponseDto.fromJson(value);
case 'ValidateAccessTokenResponseDto':

View File

@ -97,6 +97,9 @@ String parameterToString(dynamic value) {
if (value is PathType) {
return PathTypeTypeTransformer().encode(value).toString();
}
if (value is ReactionType) {
return ReactionTypeTypeTransformer().encode(value).toString();
}
if (value is SharedLinkType) {
return SharedLinkTypeTypeTransformer().encode(value).toString();
}

View File

@ -0,0 +1,140 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ActivityCreateDto {
/// Returns a new [ActivityCreateDto] instance.
ActivityCreateDto({
required this.albumId,
this.assetId,
this.comment,
required this.type,
});
String albumId;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? assetId;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? comment;
ReactionType type;
@override
bool operator ==(Object other) => identical(this, other) || other is ActivityCreateDto &&
other.albumId == albumId &&
other.assetId == assetId &&
other.comment == comment &&
other.type == type;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(albumId.hashCode) +
(assetId == null ? 0 : assetId!.hashCode) +
(comment == null ? 0 : comment!.hashCode) +
(type.hashCode);
@override
String toString() => 'ActivityCreateDto[albumId=$albumId, assetId=$assetId, comment=$comment, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'albumId'] = this.albumId;
if (this.assetId != null) {
json[r'assetId'] = this.assetId;
} else {
// json[r'assetId'] = null;
}
if (this.comment != null) {
json[r'comment'] = this.comment;
} else {
// json[r'comment'] = null;
}
json[r'type'] = this.type;
return json;
}
/// Returns a new [ActivityCreateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ActivityCreateDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ActivityCreateDto(
albumId: mapValueOfType<String>(json, r'albumId')!,
assetId: mapValueOfType<String>(json, r'assetId'),
comment: mapValueOfType<String>(json, r'comment'),
type: ReactionType.fromJson(json[r'type'])!,
);
}
return null;
}
static List<ActivityCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ActivityCreateDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ActivityCreateDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ActivityCreateDto> mapFromJson(dynamic json) {
final map = <String, ActivityCreateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ActivityCreateDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ActivityCreateDto-objects as value to a dart map
static Map<String, List<ActivityCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ActivityCreateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ActivityCreateDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'albumId',
'type',
};
}

View File

@ -0,0 +1,219 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ActivityResponseDto {
/// Returns a new [ActivityResponseDto] instance.
ActivityResponseDto({
required this.assetId,
this.comment,
required this.createdAt,
required this.id,
required this.type,
required this.user,
});
String? assetId;
String? comment;
DateTime createdAt;
String id;
ActivityResponseDtoTypeEnum type;
UserDto user;
@override
bool operator ==(Object other) => identical(this, other) || other is ActivityResponseDto &&
other.assetId == assetId &&
other.comment == comment &&
other.createdAt == createdAt &&
other.id == id &&
other.type == type &&
other.user == user;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetId == null ? 0 : assetId!.hashCode) +
(comment == null ? 0 : comment!.hashCode) +
(createdAt.hashCode) +
(id.hashCode) +
(type.hashCode) +
(user.hashCode);
@override
String toString() => 'ActivityResponseDto[assetId=$assetId, comment=$comment, createdAt=$createdAt, id=$id, type=$type, user=$user]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.assetId != null) {
json[r'assetId'] = this.assetId;
} else {
// json[r'assetId'] = null;
}
if (this.comment != null) {
json[r'comment'] = this.comment;
} else {
// json[r'comment'] = null;
}
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
json[r'id'] = this.id;
json[r'type'] = this.type;
json[r'user'] = this.user;
return json;
}
/// Returns a new [ActivityResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ActivityResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ActivityResponseDto(
assetId: mapValueOfType<String>(json, r'assetId'),
comment: mapValueOfType<String>(json, r'comment'),
createdAt: mapDateTime(json, r'createdAt', '')!,
id: mapValueOfType<String>(json, r'id')!,
type: ActivityResponseDtoTypeEnum.fromJson(json[r'type'])!,
user: UserDto.fromJson(json[r'user'])!,
);
}
return null;
}
static List<ActivityResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ActivityResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ActivityResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ActivityResponseDto> mapFromJson(dynamic json) {
final map = <String, ActivityResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ActivityResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ActivityResponseDto-objects as value to a dart map
static Map<String, List<ActivityResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ActivityResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ActivityResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetId',
'createdAt',
'id',
'type',
'user',
};
}
class ActivityResponseDtoTypeEnum {
/// Instantiate a new enum with the provided [value].
const ActivityResponseDtoTypeEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const comment = ActivityResponseDtoTypeEnum._(r'comment');
static const like = ActivityResponseDtoTypeEnum._(r'like');
/// List of all possible values in this [enum][ActivityResponseDtoTypeEnum].
static const values = <ActivityResponseDtoTypeEnum>[
comment,
like,
];
static ActivityResponseDtoTypeEnum? fromJson(dynamic value) => ActivityResponseDtoTypeEnumTypeTransformer().decode(value);
static List<ActivityResponseDtoTypeEnum>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <ActivityResponseDtoTypeEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ActivityResponseDtoTypeEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [ActivityResponseDtoTypeEnum] to String,
/// and [decode] dynamic data back to [ActivityResponseDtoTypeEnum].
class ActivityResponseDtoTypeEnumTypeTransformer {
factory ActivityResponseDtoTypeEnumTypeTransformer() => _instance ??= const ActivityResponseDtoTypeEnumTypeTransformer._();
const ActivityResponseDtoTypeEnumTypeTransformer._();
String encode(ActivityResponseDtoTypeEnum data) => data.value;
/// Decodes a [dynamic value][data] to a ActivityResponseDtoTypeEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
ActivityResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'comment': return ActivityResponseDtoTypeEnum.comment;
case r'like': return ActivityResponseDtoTypeEnum.like;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [ActivityResponseDtoTypeEnumTypeTransformer] instance.
static ActivityResponseDtoTypeEnumTypeTransformer? _instance;
}

View File

@ -0,0 +1,98 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ActivityStatisticsResponseDto {
/// Returns a new [ActivityStatisticsResponseDto] instance.
ActivityStatisticsResponseDto({
required this.comments,
});
int comments;
@override
bool operator ==(Object other) => identical(this, other) || other is ActivityStatisticsResponseDto &&
other.comments == comments;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(comments.hashCode);
@override
String toString() => 'ActivityStatisticsResponseDto[comments=$comments]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'comments'] = this.comments;
return json;
}
/// Returns a new [ActivityStatisticsResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ActivityStatisticsResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ActivityStatisticsResponseDto(
comments: mapValueOfType<int>(json, r'comments')!,
);
}
return null;
}
static List<ActivityStatisticsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ActivityStatisticsResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ActivityStatisticsResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ActivityStatisticsResponseDto> mapFromJson(dynamic json) {
final map = <String, ActivityStatisticsResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ActivityStatisticsResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ActivityStatisticsResponseDto-objects as value to a dart map
static Map<String, List<ActivityStatisticsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ActivityStatisticsResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ActivityStatisticsResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'comments',
};
}

View File

@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ReactionType {
/// Instantiate a new enum with the provided [value].
const ReactionType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const comment = ReactionType._(r'comment');
static const like = ReactionType._(r'like');
/// List of all possible values in this [enum][ReactionType].
static const values = <ReactionType>[
comment,
like,
];
static ReactionType? fromJson(dynamic value) => ReactionTypeTypeTransformer().decode(value);
static List<ReactionType>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <ReactionType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ReactionType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [ReactionType] to String,
/// and [decode] dynamic data back to [ReactionType].
class ReactionTypeTypeTransformer {
factory ReactionTypeTypeTransformer() => _instance ??= const ReactionTypeTypeTransformer._();
const ReactionTypeTypeTransformer._();
String encode(ReactionType data) => data.value;
/// Decodes a [dynamic value][data] to a ReactionType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
ReactionType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'comment': return ReactionType.comment;
case r'like': return ReactionType.like;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [ReactionTypeTypeTransformer] instance.
static ReactionTypeTypeTransformer? _instance;
}

130
mobile/openapi/lib/model/user_dto.dart generated Normal file
View File

@ -0,0 +1,130 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class UserDto {
/// Returns a new [UserDto] instance.
UserDto({
required this.email,
required this.firstName,
required this.id,
required this.lastName,
required this.profileImagePath,
});
String email;
String firstName;
String id;
String lastName;
String profileImagePath;
@override
bool operator ==(Object other) => identical(this, other) || other is UserDto &&
other.email == email &&
other.firstName == firstName &&
other.id == id &&
other.lastName == lastName &&
other.profileImagePath == profileImagePath;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(email.hashCode) +
(firstName.hashCode) +
(id.hashCode) +
(lastName.hashCode) +
(profileImagePath.hashCode);
@override
String toString() => 'UserDto[email=$email, firstName=$firstName, id=$id, lastName=$lastName, profileImagePath=$profileImagePath]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'email'] = this.email;
json[r'firstName'] = this.firstName;
json[r'id'] = this.id;
json[r'lastName'] = this.lastName;
json[r'profileImagePath'] = this.profileImagePath;
return json;
}
/// Returns a new [UserDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UserDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return UserDto(
email: mapValueOfType<String>(json, r'email')!,
firstName: mapValueOfType<String>(json, r'firstName')!,
id: mapValueOfType<String>(json, r'id')!,
lastName: mapValueOfType<String>(json, r'lastName')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
);
}
return null;
}
static List<UserDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UserDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UserDto> mapFromJson(dynamic json) {
final map = <String, UserDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UserDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UserDto-objects as value to a dart map
static Map<String, List<UserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = UserDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'email',
'firstName',
'id',
'lastName',
'profileImagePath',
};
}

View File

@ -0,0 +1,41 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
/// tests for ActivityApi
void main() {
// final instance = ActivityApi();
group('tests for ActivityApi', () {
//Future<ActivityResponseDto> createActivity(ActivityCreateDto activityCreateDto) async
test('test createActivity', () async {
// TODO
});
//Future deleteActivity(String id) async
test('test deleteActivity', () async {
// TODO
});
//Future<List<ActivityResponseDto>> getActivities(String albumId, { String assetId, ReactionType type }) async
test('test getActivities', () async {
// TODO
});
//Future<ActivityStatisticsResponseDto> getActivityStatistics(String albumId, { String assetId }) async
test('test getActivityStatistics', () async {
// TODO
});
});
}

View File

@ -0,0 +1,42 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for ActivityCreateDto
void main() {
// final instance = ActivityCreateDto();
group('test ActivityCreateDto', () {
// String albumId
test('to test the property `albumId`', () async {
// TODO
});
// String assetId
test('to test the property `assetId`', () async {
// TODO
});
// String comment
test('to test the property `comment`', () async {
// TODO
});
// ReactionType type
test('to test the property `type`', () async {
// TODO
});
});
}

View File

@ -0,0 +1,52 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for ActivityResponseDto
void main() {
// final instance = ActivityResponseDto();
group('test ActivityResponseDto', () {
// String assetId
test('to test the property `assetId`', () async {
// TODO
});
// String comment
test('to test the property `comment`', () async {
// TODO
});
// DateTime createdAt
test('to test the property `createdAt`', () async {
// TODO
});
// String id
test('to test the property `id`', () async {
// TODO
});
// String type
test('to test the property `type`', () async {
// TODO
});
// UserDto user
test('to test the property `user`', () async {
// TODO
});
});
}

View File

@ -0,0 +1,27 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for ActivityStatisticsResponseDto
void main() {
// final instance = ActivityStatisticsResponseDto();
group('test ActivityStatisticsResponseDto', () {
// int comments
test('to test the property `comments`', () async {
// TODO
});
});
}

View File

@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for ReactionType
void main() {
group('test ReactionType', () {
});
}

47
mobile/openapi/test/user_dto_test.dart generated Normal file
View File

@ -0,0 +1,47 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for UserDto
void main() {
// final instance = UserDto();
group('test UserDto', () {
// String email
test('to test the property `email`', () async {
// TODO
});
// String firstName
test('to test the property `firstName`', () async {
// TODO
});
// String id
test('to test the property `id`', () async {
// TODO
});
// String lastName
test('to test the property `lastName`', () async {
// TODO
});
// String profileImagePath
test('to test the property `profileImagePath`', () async {
// TODO
});
});
}

View File

@ -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",

View File

@ -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)) ||

View 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),
};
}

View 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);
}
}

View 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');
});
});
});

View File

@ -0,0 +1,2 @@
export * from './activity.dto';
export * from './activity.service';

View File

@ -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,

View File

@ -1,4 +1,5 @@
export * from './access';
export * from './activity';
export * from './album';
export * from './api-key';
export * from './asset';

View File

@ -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>;

View 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>;
}

View File

@ -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';

View File

@ -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,

View File

@ -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,

View 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);
}
}

View File

@ -1,3 +1,4 @@
export * from './activity.controller';
export * from './album.controller';
export * from './api-key.controller';
export * from './app.controller';

View 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;
}

View File

@ -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,

View File

@ -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 },

View 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"`);
}
}

View File

@ -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({

View 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,
},
});
}
}

View File

@ -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';

View 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);
},
};

View File

@ -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;
},
};

View File

@ -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,

View 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
View 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(),
}),
};

View File

@ -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(),

View 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(),
};
};

View File

@ -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);

View File

@ -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

View File

@ -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

View 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>

View File

@ -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>

View File

@ -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();
}
};

View File

@ -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()}

View File

@ -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>

View File

@ -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}

View File

@ -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;

View File

@ -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';
}
};

View 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;
};

View File

@ -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)}