mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00: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:
@@ -20,12 +20,14 @@ import {
|
||||
UserApi,
|
||||
UserApiFp,
|
||||
AuditApi,
|
||||
ActivityApi,
|
||||
} from './open-api';
|
||||
import { BASE_PATH } from './open-api/base';
|
||||
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
|
||||
import type { ApiParams } from './types';
|
||||
|
||||
export class ImmichApi {
|
||||
public activityApi: ActivityApi;
|
||||
public albumApi: AlbumApi;
|
||||
public libraryApi: LibraryApi;
|
||||
public assetApi: AssetApi;
|
||||
@@ -52,6 +54,7 @@ export class ImmichApi {
|
||||
constructor(params: ConfigurationParameters) {
|
||||
this.config = new Configuration(params);
|
||||
|
||||
this.activityApi = new ActivityApi(this.config);
|
||||
this.albumApi = new AlbumApi(this.config);
|
||||
this.auditApi = new AuditApi(this.config);
|
||||
this.libraryApi = new LibraryApi(this.config);
|
||||
|
||||
577
web/src/api/open-api/api.ts
generated
577
web/src/api/open-api/api.ts
generated
@@ -99,6 +99,103 @@ export interface APIKeyUpdateDto {
|
||||
*/
|
||||
'name': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityCreateDto
|
||||
*/
|
||||
export interface ActivityCreateDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'albumId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'assetId'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'comment'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {ReactionType}
|
||||
* @memberof ActivityCreateDto
|
||||
*/
|
||||
'type': ReactionType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityResponseDto
|
||||
*/
|
||||
export interface ActivityResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'assetId': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'comment'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'createdAt': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'type': ActivityResponseDtoTypeEnum;
|
||||
/**
|
||||
*
|
||||
* @type {UserDto}
|
||||
* @memberof ActivityResponseDto
|
||||
*/
|
||||
'user': UserDto;
|
||||
}
|
||||
|
||||
export const ActivityResponseDtoTypeEnum = {
|
||||
Comment: 'comment',
|
||||
Like: 'like'
|
||||
} as const;
|
||||
|
||||
export type ActivityResponseDtoTypeEnum = typeof ActivityResponseDtoTypeEnum[keyof typeof ActivityResponseDtoTypeEnum];
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ActivityStatisticsResponseDto
|
||||
*/
|
||||
export interface ActivityStatisticsResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ActivityStatisticsResponseDto
|
||||
*/
|
||||
'comments': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -2490,6 +2587,20 @@ export interface QueueStatusDto {
|
||||
*/
|
||||
'isPaused': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const ReactionType = {
|
||||
Comment: 'comment',
|
||||
Like: 'like'
|
||||
} as const;
|
||||
|
||||
export type ReactionType = typeof ReactionType[keyof typeof ReactionType];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -4248,6 +4359,43 @@ export interface UsageByUserDto {
|
||||
*/
|
||||
'videos': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserDto
|
||||
*/
|
||||
export interface UserDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'email': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'firstName': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'lastName': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'profileImagePath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -4831,6 +4979,435 @@ export class APIKeyApi extends BaseAPI {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ActivityApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityCreateDto} activityCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createActivity: async (activityCreateDto: ActivityCreateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'activityCreateDto' is not null or undefined
|
||||
assertParamExists('createActivity', 'activityCreateDto', activityCreateDto)
|
||||
const localVarPath = `/activity`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(activityCreateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteActivity: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('deleteActivity', 'id', id)
|
||||
const localVarPath = `/activity/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {ReactionType} [type]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'albumId' is not null or undefined
|
||||
assertParamExists('getActivities', 'albumId', albumId)
|
||||
const localVarPath = `/activity`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (albumId !== undefined) {
|
||||
localVarQueryParameter['albumId'] = albumId;
|
||||
}
|
||||
|
||||
if (assetId !== undefined) {
|
||||
localVarQueryParameter['assetId'] = assetId;
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
localVarQueryParameter['type'] = type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivityStatistics: async (albumId: string, assetId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'albumId' is not null or undefined
|
||||
assertParamExists('getActivityStatistics', 'albumId', albumId)
|
||||
const localVarPath = `/activity/statistics`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (albumId !== undefined) {
|
||||
localVarQueryParameter['albumId'] = albumId;
|
||||
}
|
||||
|
||||
if (assetId !== undefined) {
|
||||
localVarQueryParameter['assetId'] = assetId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ActivityApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ActivityApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityCreateDto} activityCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createActivity(activityCreateDto: ActivityCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createActivity(activityCreateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteActivity(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteActivity(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {ReactionType} [type]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getActivities(albumId: string, assetId?: string, type?: ReactionType, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
* @param {string} [assetId]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getActivityStatistics(albumId: string, assetId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityStatisticsResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivityStatistics(albumId, assetId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ActivityApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const ActivityApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ActivityApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityResponseDto> {
|
||||
return localVarFp.createActivity(requestParameters.activityCreateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteActivity(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
|
||||
return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig): AxiosPromise<ActivityStatisticsResponseDto> {
|
||||
return localVarFp.getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for createActivity operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiCreateActivityRequest
|
||||
*/
|
||||
export interface ActivityApiCreateActivityRequest {
|
||||
/**
|
||||
*
|
||||
* @type {ActivityCreateDto}
|
||||
* @memberof ActivityApiCreateActivity
|
||||
*/
|
||||
readonly activityCreateDto: ActivityCreateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for deleteActivity operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiDeleteActivityRequest
|
||||
*/
|
||||
export interface ActivityApiDeleteActivityRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiDeleteActivity
|
||||
*/
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getActivities operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiGetActivitiesRequest
|
||||
*/
|
||||
export interface ActivityApiGetActivitiesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly albumId: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly assetId?: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {ReactionType}
|
||||
* @memberof ActivityApiGetActivities
|
||||
*/
|
||||
readonly type?: ReactionType
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for getActivityStatistics operation in ActivityApi.
|
||||
* @export
|
||||
* @interface ActivityApiGetActivityStatisticsRequest
|
||||
*/
|
||||
export interface ActivityApiGetActivityStatisticsRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivityStatistics
|
||||
*/
|
||||
readonly albumId: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ActivityApiGetActivityStatistics
|
||||
*/
|
||||
readonly assetId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ActivityApi - object-oriented interface
|
||||
* @export
|
||||
* @class ActivityApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ActivityApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiCreateActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public createActivity(requestParameters: ActivityApiCreateActivityRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).createActivity(requestParameters.activityCreateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiDeleteActivityRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public deleteActivity(requestParameters: ActivityApiDeleteActivityRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).deleteActivity(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivitiesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ActivityApiGetActivityStatisticsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ActivityApi
|
||||
*/
|
||||
public getActivityStatistics(requestParameters: ActivityApiGetActivityStatisticsRequest, options?: AxiosRequestConfig) {
|
||||
return ActivityApiFp(this.configuration).getActivityStatistics(requestParameters.albumId, requestParameters.assetId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AlbumApi - axios parameter creator
|
||||
* @export
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import type { AlbumResponseDto, SharedLinkResponseDto } from '@api';
|
||||
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { dateFormats } from '../../constants';
|
||||
import { createAssetInteractionStore } from '../../stores/asset-interaction.store';
|
||||
@@ -22,6 +22,7 @@
|
||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
||||
const album = sharedLink.album as AlbumResponseDto;
|
||||
|
||||
@@ -138,7 +139,7 @@
|
||||
<main
|
||||
class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
|
||||
>
|
||||
<AssetGrid {assetStore} {assetInteractionStore}>
|
||||
<AssetGrid {album} {user} {assetStore} {assetInteractionStore}>
|
||||
<section class="pt-24">
|
||||
<!-- ALBUM TITLE -->
|
||||
<p
|
||||
|
||||
289
web/src/lib/components/asset-viewer/activity-viewer.svelte
Normal file
289
web/src/lib/components/asset-viewer/activity-viewer.svelte
Normal file
@@ -0,0 +1,289 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import { mdiClose, mdiHeart, mdiSend, mdiDotsVertical } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { ActivityResponseDto, api, AssetTypeEnum, ReactionType, type UserResponseDto } from '@api';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isTenMinutesApart } from '$lib/utils/timesince';
|
||||
import { clickOutside } from '$lib/utils/click-outside';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import { getAssetType } from '$lib/utils/asset-utils';
|
||||
import * as luxon from 'luxon';
|
||||
|
||||
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||
|
||||
const timeSince = (dateTime: luxon.DateTime) => {
|
||||
const diff = dateTime.diffNow().shiftTo(...units);
|
||||
const unit = units.find((unit) => diff.get(unit) !== 0) || 'second';
|
||||
|
||||
const relativeFormatter = new Intl.RelativeTimeFormat('en', {
|
||||
numeric: 'auto',
|
||||
});
|
||||
return relativeFormatter.format(Math.trunc(diff.as(unit)), unit);
|
||||
};
|
||||
|
||||
export let reactions: ActivityResponseDto[];
|
||||
export let user: UserResponseDto;
|
||||
export let assetId: string;
|
||||
export let albumId: string;
|
||||
export let assetType: AssetTypeEnum;
|
||||
export let albumOwnerId: string;
|
||||
|
||||
let textArea: HTMLTextAreaElement;
|
||||
let innerHeight: number;
|
||||
let activityHeight: number;
|
||||
let chatHeight: number;
|
||||
let divHeight: number;
|
||||
let previousAssetId: string | null;
|
||||
let message = '';
|
||||
let isSendingMessage = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: showDeleteReaction = Array(reactions.length).fill(false);
|
||||
$: {
|
||||
if (innerHeight && activityHeight) {
|
||||
divHeight = innerHeight - activityHeight;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (previousAssetId != assetId) {
|
||||
getReactions();
|
||||
previousAssetId = assetId;
|
||||
}
|
||||
}
|
||||
|
||||
const getReactions = async () => {
|
||||
try {
|
||||
const { data } = await api.activityApi.getActivities({ assetId, albumId });
|
||||
reactions = data;
|
||||
} catch (error) {
|
||||
handleError(error, 'Error when fetching reactions');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnter = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleSendComment();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const autoGrow = () => {
|
||||
textArea.style.height = '5px';
|
||||
textArea.style.height = textArea.scrollHeight + 'px';
|
||||
};
|
||||
|
||||
const timeOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
} as Intl.DateTimeFormatOptions;
|
||||
|
||||
const handleDeleteReaction = async (reaction: ActivityResponseDto, index: number) => {
|
||||
try {
|
||||
await api.activityApi.deleteActivity({ id: reaction.id });
|
||||
reactions.splice(index, 1);
|
||||
showDeleteReaction.splice(index, 1);
|
||||
reactions = reactions;
|
||||
if (reaction.type === 'like' && reaction.user.id === user.id) {
|
||||
dispatch('deleteLike');
|
||||
} else {
|
||||
dispatch('deleteComment');
|
||||
}
|
||||
notificationController.show({
|
||||
message: `${reaction.type} deleted`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, `Can't remove ${reaction.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendComment = async () => {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => (isSendingMessage = true), 100);
|
||||
try {
|
||||
const { data } = await api.activityApi.createActivity({
|
||||
activityCreateDto: { albumId, assetId, type: ReactionType.Comment, comment: message },
|
||||
});
|
||||
reactions.push(data);
|
||||
textArea.style.height = '18px';
|
||||
message = '';
|
||||
dispatch('addComment');
|
||||
// Re-render the activity feed
|
||||
reactions = reactions;
|
||||
} catch (error) {
|
||||
handleError(error, "Can't add your comment");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
isSendingMessage = false;
|
||||
};
|
||||
|
||||
const showOptionsMenu = (index: number) => {
|
||||
showDeleteReaction[index] = !showDeleteReaction[index];
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="overflow-y-hidden relative h-full" bind:offsetHeight={innerHeight}>
|
||||
<div class="dark:bg-immich-dark-bg dark:text-immich-dark-fg w-full h-full">
|
||||
<div
|
||||
class="flex w-full h-fit dark:bg-immich-dark-bg dark:text-immich-dark-fg p-2 bg-white"
|
||||
bind:clientHeight={activityHeight}
|
||||
>
|
||||
<div class="flex place-items-center gap-2">
|
||||
<button
|
||||
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
on:click={() => dispatch('close')}
|
||||
>
|
||||
<Icon path={mdiClose} size="24" />
|
||||
</button>
|
||||
|
||||
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Activity</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if innerHeight}
|
||||
<div
|
||||
class="overflow-y-auto immich-scrollbar relative w-full"
|
||||
style="height: {divHeight}px;padding-bottom: {chatHeight}px"
|
||||
>
|
||||
{#each reactions as reaction, index (reaction.id)}
|
||||
{#if reaction.type === 'comment'}
|
||||
<div class="flex dark:bg-gray-800 bg-gray-200 p-3 mx-2 mt-3 rounded-lg gap-4 justify-start">
|
||||
<div>
|
||||
<UserAvatar user={reaction.user} size="sm" />
|
||||
</div>
|
||||
|
||||
<div class="w-full leading-4 overflow-hidden self-center break-words text-sm">{reaction.comment}</div>
|
||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||
<div class="flex items-start w-fit pt-[5px]" title="Delete comment">
|
||||
<button on:click={() => (!showDeleteReaction[index] ? showOptionsMenu(index) : '')}>
|
||||
<Icon path={mdiDotsVertical} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if showDeleteReaction[index]}
|
||||
<button
|
||||
class="absolute right-6 rounded-xl items-center bg-gray-300 dark:bg-slate-100 py-2 px-6 text-left text-sm font-medium text-immich-fg hover:bg-red-300 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg dark:hover:bg-red-300 transition-colors"
|
||||
use:clickOutside
|
||||
on:outclick={() => (showDeleteReaction[index] = false)}
|
||||
on:click={() => handleDeleteReaction(reaction, index)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if (index != reactions.length - 1 && isTenMinutesApart(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1}
|
||||
<div
|
||||
class=" px-2 text-right w-full text-sm text-gray-500 dark:text-gray-300"
|
||||
title={new Date(reaction.createdAt).toLocaleDateString(undefined, timeOptions)}
|
||||
>
|
||||
{timeSince(luxon.DateTime.fromISO(reaction.createdAt))}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if reaction.type === 'like'}
|
||||
<div class="relative">
|
||||
<div class="flex p-2 mx-2 mt-2 rounded-full gap-2 items-center text-sm">
|
||||
<div class="text-red-600"><Icon path={mdiHeart} size={20} /></div>
|
||||
|
||||
<div
|
||||
class="w-full"
|
||||
title={`${reaction.user.firstName} ${reaction.user.lastName} (${reaction.user.email})`}
|
||||
>
|
||||
{`${reaction.user.firstName} ${reaction.user.lastName} liked this ${getAssetType(
|
||||
assetType,
|
||||
).toLowerCase()}`}
|
||||
</div>
|
||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||
<div class="flex items-start w-fit" title="Delete like">
|
||||
<button on:click={() => (!showDeleteReaction[index] ? showOptionsMenu(index) : '')}>
|
||||
<Icon path={mdiDotsVertical} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if showDeleteReaction[index]}
|
||||
<button
|
||||
class="absolute top-2 right-6 rounded-xl items-center bg-gray-300 dark:bg-slate-100 p-3 text-left text-sm font-medium text-immich-fg hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg"
|
||||
use:clickOutside
|
||||
on:outclick={() => (showDeleteReaction[index] = false)}
|
||||
on:click={() => handleDeleteReaction(reaction, index)}
|
||||
>
|
||||
Delete Like
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if (index != reactions.length - 1 && isTenMinutesApart(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1}
|
||||
<div
|
||||
class=" px-2 text-right w-full text-sm text-gray-500 dark:text-gray-300"
|
||||
title={new Date(reaction.createdAt).toLocaleDateString(navigator.language, timeOptions)}
|
||||
>
|
||||
{timeSince(luxon.DateTime.fromISO(reaction.createdAt))}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="absolute w-full bottom-0">
|
||||
<div class="flex items-center justify-center p-2 mr-2" bind:clientHeight={chatHeight}>
|
||||
<div class="flex p-2 gap-4 h-fit bg-gray-200 text-immich-dark-gray rounded-3xl w-full">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" showTitle={false} />
|
||||
</div>
|
||||
<form class="flex w-full max-h-56 gap-1" on:submit|preventDefault={() => handleSendComment()}>
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<textarea
|
||||
bind:this={textArea}
|
||||
bind:value={message}
|
||||
placeholder="Say something"
|
||||
on:input={autoGrow}
|
||||
on:keypress={handleEnter}
|
||||
class="h-[18px] w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||
/>
|
||||
</div>
|
||||
{#if isSendingMessage}
|
||||
<div class="flex items-end place-items-center pb-2 ml-0">
|
||||
<div class="flex w-full place-items-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message}
|
||||
<div class="flex items-end w-fit ml-0 text-immich-primary dark:text-white">
|
||||
<CircleIconButton size="15" icon={mdiSend} />
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
::placeholder {
|
||||
color: rgb(60, 60, 60);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
::-ms-input-placeholder {
|
||||
/* Edge 12 -18 */
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { AlbumResponseDto, api, AssetJobName, AssetResponseDto, AssetTypeEnum, SharedLinkResponseDto } from '@api';
|
||||
import {
|
||||
ActivityResponseDto,
|
||||
AlbumResponseDto,
|
||||
api,
|
||||
AssetJobName,
|
||||
AssetResponseDto,
|
||||
AssetTypeEnum,
|
||||
ReactionType,
|
||||
SharedLinkResponseDto,
|
||||
UserResponseDto,
|
||||
} from '@api';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
|
||||
@@ -14,7 +24,7 @@
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte';
|
||||
import { isShowDetail } from '$lib/stores/preferences.store';
|
||||
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
|
||||
import { addAssetsToAlbum, downloadFile, getAssetType } from '$lib/utils/asset-utils';
|
||||
import NavigationArea from './navigation-area.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -23,10 +33,21 @@
|
||||
import ProgressBar, { ProgressBarStatus } from '../shared-components/progress-bar/progress-bar.svelte';
|
||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiImageBrokenVariant, mdiPause, mdiPlay } from '@mdi/js';
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiHeartOutline,
|
||||
mdiHeart,
|
||||
mdiCommentOutline,
|
||||
mdiChevronRight,
|
||||
mdiClose,
|
||||
mdiImageBrokenVariant,
|
||||
mdiPause,
|
||||
mdiPlay,
|
||||
} from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
||||
import ActivityViewer from './activity-viewer.svelte';
|
||||
|
||||
export let assetStore: AssetStore | null = null;
|
||||
export let asset: AssetResponseDto;
|
||||
@@ -35,6 +56,11 @@
|
||||
$: isTrashEnabled = $featureFlags.trash;
|
||||
export let force = false;
|
||||
export let withStacked = false;
|
||||
export let isShared = true;
|
||||
export let user: UserResponseDto | null = null;
|
||||
export let album: AlbumResponseDto | null = null;
|
||||
|
||||
let reactions: ActivityResponseDto[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
archived: AssetResponseDto;
|
||||
@@ -57,6 +83,9 @@
|
||||
let shouldShowDetailButton = asset.hasMetadata;
|
||||
let canCopyImagesToClipboard: boolean;
|
||||
let previewStackedAsset: AssetResponseDto | undefined;
|
||||
let isShowActivity = false;
|
||||
let isLiked: ActivityResponseDto | null = null;
|
||||
let numberOfComments: number;
|
||||
|
||||
$: {
|
||||
if (asset.stackCount && asset.stack) {
|
||||
@@ -71,6 +100,62 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleFavorite = async () => {
|
||||
if (album) {
|
||||
try {
|
||||
if (isLiked) {
|
||||
const activityId = isLiked.id;
|
||||
await api.activityApi.deleteActivity({ id: activityId });
|
||||
reactions = reactions.filter((reaction) => reaction.id !== activityId);
|
||||
isLiked = null;
|
||||
} else {
|
||||
const { data } = await api.activityApi.createActivity({
|
||||
activityCreateDto: { albumId: album.id, assetId: asset.id, type: ReactionType.Like },
|
||||
});
|
||||
|
||||
isLiked = data;
|
||||
reactions = [...reactions, isLiked];
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Can't change favorite for asset");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getFavorite = async () => {
|
||||
if (album) {
|
||||
try {
|
||||
const { data } = await api.activityApi.getActivities({
|
||||
assetId: asset.id,
|
||||
albumId: album.id,
|
||||
type: ReactionType.Like,
|
||||
});
|
||||
if (data.length > 0) {
|
||||
isLiked = data[0];
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Can't get Favorite");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getNumberOfComments = async () => {
|
||||
if (album) {
|
||||
try {
|
||||
const { data } = await api.activityApi.getActivityStatistics({ assetId: asset.id, albumId: album.id });
|
||||
numberOfComments = data.comments;
|
||||
} catch (error) {
|
||||
handleError(error, "Can't get number of comments");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$: {
|
||||
if (isShared && asset.id) {
|
||||
getFavorite();
|
||||
getNumberOfComments();
|
||||
}
|
||||
}
|
||||
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo);
|
||||
|
||||
onMount(async () => {
|
||||
@@ -116,6 +201,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenActivity = () => {
|
||||
if ($isShowDetail) {
|
||||
$isShowDetail = false;
|
||||
}
|
||||
isShowActivity = !isShowActivity;
|
||||
};
|
||||
|
||||
const handleKeyboardPress = (event: KeyboardEvent) => {
|
||||
if (shouldIgnoreShortcut(event)) {
|
||||
return;
|
||||
@@ -157,6 +249,7 @@
|
||||
toggleFavorite();
|
||||
return;
|
||||
case 'i':
|
||||
isShowActivity = false;
|
||||
$isShowDetail = !$isShowDetail;
|
||||
return;
|
||||
}
|
||||
@@ -193,6 +286,9 @@
|
||||
};
|
||||
|
||||
const showDetailInfoHandler = () => {
|
||||
if (isShowActivity) {
|
||||
isShowActivity = false;
|
||||
}
|
||||
$isShowDetail = !$isShowDetail;
|
||||
};
|
||||
|
||||
@@ -317,17 +413,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const getAssetType = () => {
|
||||
switch (asset.type) {
|
||||
case 'IMAGE':
|
||||
return 'Photo';
|
||||
case 'VIDEO':
|
||||
return 'Video';
|
||||
default:
|
||||
return 'Asset';
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunJob = async (name: AssetJobName) => {
|
||||
try {
|
||||
await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
|
||||
@@ -471,7 +556,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Asset Viewer -->
|
||||
<div class="col-span-4 col-start-1 row-span-full row-start-1">
|
||||
<div class="relative col-span-4 col-start-1 row-span-full row-start-1">
|
||||
{#if previewStackedAsset}
|
||||
{#key previewStackedAsset.id}
|
||||
{#if previewStackedAsset.type === AssetTypeEnum.Image}
|
||||
@@ -517,6 +602,29 @@
|
||||
on:onVideoStarted={handleVideoStarted}
|
||||
/>
|
||||
{/if}
|
||||
{#if isShared}
|
||||
<div class="z-[9999] absolute bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||
<div
|
||||
class="w-full h-14 flex p-4 text-white items-center justify-center rounded-full gap-4 bg-immich-dark-bg bg-opacity-60"
|
||||
>
|
||||
<button on:click={handleFavorite}>
|
||||
<div class="items-center justify-center">
|
||||
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
|
||||
</div>
|
||||
</button>
|
||||
<button on:click={handleOpenActivity}>
|
||||
<div class="flex gap-2 items-center justify-center">
|
||||
<Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} />
|
||||
{#if numberOfComments}
|
||||
<div class="text-xl">{numberOfComments}</div>
|
||||
{:else if !isShowActivity && !$isShowDetail}
|
||||
<div class="text-lg">Say something</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
@@ -582,6 +690,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isShared && album && isShowActivity && user}
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="activity-panel"
|
||||
class="z-[1002] row-start-1 row-span-5 w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg pl-4"
|
||||
translate="yes"
|
||||
>
|
||||
<ActivityViewer
|
||||
{user}
|
||||
assetType={asset.type}
|
||||
albumOwnerId={album.ownerId}
|
||||
albumId={album.id}
|
||||
assetId={asset.id}
|
||||
bind:reactions
|
||||
on:addComment={() => numberOfComments++}
|
||||
on:deleteComment={() => numberOfComments--}
|
||||
on:deleteLike={() => (isLiked = null)}
|
||||
on:close={() => (isShowActivity = false)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isShowAlbumPicker}
|
||||
<AlbumSelectionModal
|
||||
shared={addToSharedAlbum}
|
||||
@@ -594,15 +724,15 @@
|
||||
|
||||
{#if isShowDeleteConfirmation}
|
||||
<ConfirmDialogue
|
||||
title="Delete {getAssetType()}"
|
||||
title="Delete {getAssetType(asset.type)}"
|
||||
confirmText="Delete"
|
||||
on:confirm={deleteAsset}
|
||||
on:cancel={() => (isShowDeleteConfirmation = false)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>
|
||||
Are you sure you want to delete this {getAssetType().toLowerCase()}? This will also remove it from its
|
||||
album(s).
|
||||
Are you sure you want to delete this {getAssetType(asset.type).toLowerCase()}? This will also remove it from
|
||||
its album(s).
|
||||
</p>
|
||||
<p><b>You cannot undo this action!</b></p>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { useZoomImageWheel } from '@zoom-image/svelte';
|
||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let element: HTMLDivElement | undefined = undefined;
|
||||
@@ -54,11 +55,14 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeypress = async ({ metaKey, ctrlKey, key }: KeyboardEvent) => {
|
||||
const handleKeypress = async (event: KeyboardEvent) => {
|
||||
if (shouldIgnoreShortcut(event)) {
|
||||
return;
|
||||
}
|
||||
if (window.getSelection()?.type === 'Range') {
|
||||
return;
|
||||
}
|
||||
if ((metaKey || ctrlKey) && key === 'c') {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'c') {
|
||||
await doCopy();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { isSearchEnabled } from '$lib/stores/search.store';
|
||||
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import type { AlbumResponseDto, AssetResponseDto, UserResponseDto } from '@api';
|
||||
import { DateTime } from 'luxon';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||
@@ -26,6 +26,9 @@
|
||||
export let assetInteractionStore: AssetInteractionStore;
|
||||
export let removeAction: AssetAction | null = null;
|
||||
export let withStacked = false;
|
||||
export let isShared = false;
|
||||
export let user: UserResponseDto | null = null;
|
||||
export let album: AlbumResponseDto | null = null;
|
||||
|
||||
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
|
||||
export let forceDelete = false;
|
||||
@@ -391,10 +394,13 @@
|
||||
<Portal target="body">
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
{user}
|
||||
{withStacked}
|
||||
{assetStore}
|
||||
asset={$viewingAsset}
|
||||
force={forceDelete || !isTrashEnabled}
|
||||
{isShared}
|
||||
{album}
|
||||
on:previous={() => handlePrevious()}
|
||||
on:next={() => handleNext()}
|
||||
on:close={() => handleClose()}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
|
||||
import { api, AssetResponseDto, SharedLinkResponseDto, UserResponseDto } from '@api';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let isOwned: boolean;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
|
||||
@@ -102,6 +103,6 @@
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40">
|
||||
<GalleryViewer {assets} bind:selectedAssets />
|
||||
<GalleryViewer {user} {assets} bind:selectedAssets />
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetResponseDto, ThumbnailFormat } from '@api';
|
||||
import { AssetResponseDto, ThumbnailFormat, UserResponseDto } from '@api';
|
||||
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
||||
@@ -12,6 +12,7 @@
|
||||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
export let disableAssetSelect = false;
|
||||
export let showArchiveIcon = false;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
@@ -103,6 +104,7 @@
|
||||
<!-- Overlay Asset Viewer -->
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
{user}
|
||||
asset={selectedAsset}
|
||||
on:previous={navigateAssetBackward}
|
||||
on:next={navigateAssetForward}
|
||||
|
||||
@@ -5,9 +5,17 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { imageLoad } from '$lib/utils/image-load';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { api } from '@api';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
interface User {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
profileImagePath: string;
|
||||
}
|
||||
|
||||
export let user: User;
|
||||
export let color: Color = 'primary';
|
||||
export let size: Size = 'full';
|
||||
export let rounded = true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||
import { downloadManager } from '$lib/stores/download';
|
||||
import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto } from '@api';
|
||||
import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto, AssetTypeEnum } from '@api';
|
||||
import { handleError } from './handle-error';
|
||||
|
||||
export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> =>
|
||||
@@ -192,3 +192,14 @@ export function isWebCompatibleImage(asset: AssetResponseDto): boolean {
|
||||
|
||||
return supportedImageExtensions.has(imgExtension);
|
||||
}
|
||||
|
||||
export const getAssetType = (type: AssetTypeEnum) => {
|
||||
switch (type) {
|
||||
case 'IMAGE':
|
||||
return 'Photo';
|
||||
case 'VIDEO':
|
||||
return 'Video';
|
||||
default:
|
||||
return 'Asset';
|
||||
}
|
||||
};
|
||||
|
||||
9
web/src/lib/utils/timesince.ts
Normal file
9
web/src/lib/utils/timesince.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const isTenMinutesApart = (date1: string, date2: string): boolean => {
|
||||
if (!date1 || !date2) {
|
||||
return false;
|
||||
}
|
||||
const diffInMilliseconds = Math.abs(new Date(date1).getTime() - new Date(date2).getTime());
|
||||
const minutesDifference = diffInMilliseconds / (1000 * 60);
|
||||
|
||||
return minutesDifference >= 10;
|
||||
};
|
||||
@@ -424,11 +424,19 @@
|
||||
class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
|
||||
>
|
||||
{#if viewMode === ViewMode.SELECT_ASSETS}
|
||||
<AssetGrid assetStore={timelineStore} assetInteractionStore={timelineInteractionStore} isSelectionMode={true} />
|
||||
<AssetGrid
|
||||
user={data.user}
|
||||
assetStore={timelineStore}
|
||||
assetInteractionStore={timelineInteractionStore}
|
||||
isSelectionMode={true}
|
||||
/>
|
||||
{:else}
|
||||
<AssetGrid
|
||||
{album}
|
||||
user={data.user}
|
||||
{assetStore}
|
||||
{assetInteractionStore}
|
||||
isShared={album.sharedUsers.length > 0}
|
||||
isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL}
|
||||
singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL}
|
||||
on:select={({ detail: asset }) => handleUpdateThumbnail(asset.id)}
|
||||
|
||||
Reference in New Issue
Block a user