mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:05:39 -04:00
feat(server, web): Album's options (#4870)
* feat: disable activity * fix: disable reactions * fix: tests * fix: tests * fix: tests * pr feedback * pr feedback * chore: styling & wording * refactor component --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
ace0a5911c
commit
9d01885b58
12
cli/src/api/open-api/api.ts
generated
12
cli/src/api/open-api/api.ts
generated
@ -331,6 +331,12 @@ export interface AlbumResponseDto {
|
|||||||
* @memberof AlbumResponseDto
|
* @memberof AlbumResponseDto
|
||||||
*/
|
*/
|
||||||
'id': string;
|
'id': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AlbumResponseDto
|
||||||
|
*/
|
||||||
|
'isActivityEnabled': boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -4160,6 +4166,12 @@ export interface UpdateAlbumDto {
|
|||||||
* @memberof UpdateAlbumDto
|
* @memberof UpdateAlbumDto
|
||||||
*/
|
*/
|
||||||
'description'?: string;
|
'description'?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof UpdateAlbumDto
|
||||||
|
*/
|
||||||
|
'isActivityEnabled'?: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
1
mobile/openapi/doc/AlbumResponseDto.md
generated
1
mobile/openapi/doc/AlbumResponseDto.md
generated
@ -17,6 +17,7 @@ Name | Type | Description | Notes
|
|||||||
**endDate** | [**DateTime**](DateTime.md) | | [optional]
|
**endDate** | [**DateTime**](DateTime.md) | | [optional]
|
||||||
**hasSharedLink** | **bool** | |
|
**hasSharedLink** | **bool** | |
|
||||||
**id** | **String** | |
|
**id** | **String** | |
|
||||||
|
**isActivityEnabled** | **bool** | |
|
||||||
**lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional]
|
**lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional]
|
||||||
**owner** | [**UserResponseDto**](UserResponseDto.md) | |
|
**owner** | [**UserResponseDto**](UserResponseDto.md) | |
|
||||||
**ownerId** | **String** | |
|
**ownerId** | **String** | |
|
||||||
|
1
mobile/openapi/doc/UpdateAlbumDto.md
generated
1
mobile/openapi/doc/UpdateAlbumDto.md
generated
@ -11,6 +11,7 @@ Name | Type | Description | Notes
|
|||||||
**albumName** | **String** | | [optional]
|
**albumName** | **String** | | [optional]
|
||||||
**albumThumbnailAssetId** | **String** | | [optional]
|
**albumThumbnailAssetId** | **String** | | [optional]
|
||||||
**description** | **String** | | [optional]
|
**description** | **String** | | [optional]
|
||||||
|
**isActivityEnabled** | **bool** | | [optional]
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.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)
|
||||||
|
|
||||||
|
10
mobile/openapi/lib/model/album_response_dto.dart
generated
10
mobile/openapi/lib/model/album_response_dto.dart
generated
@ -22,6 +22,7 @@ class AlbumResponseDto {
|
|||||||
this.endDate,
|
this.endDate,
|
||||||
required this.hasSharedLink,
|
required this.hasSharedLink,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.isActivityEnabled,
|
||||||
this.lastModifiedAssetTimestamp,
|
this.lastModifiedAssetTimestamp,
|
||||||
required this.owner,
|
required this.owner,
|
||||||
required this.ownerId,
|
required this.ownerId,
|
||||||
@ -55,6 +56,8 @@ class AlbumResponseDto {
|
|||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
|
bool isActivityEnabled;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// 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
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@ -92,6 +95,7 @@ class AlbumResponseDto {
|
|||||||
other.endDate == endDate &&
|
other.endDate == endDate &&
|
||||||
other.hasSharedLink == hasSharedLink &&
|
other.hasSharedLink == hasSharedLink &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
|
other.isActivityEnabled == isActivityEnabled &&
|
||||||
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
|
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
|
||||||
other.owner == owner &&
|
other.owner == owner &&
|
||||||
other.ownerId == ownerId &&
|
other.ownerId == ownerId &&
|
||||||
@ -112,6 +116,7 @@ class AlbumResponseDto {
|
|||||||
(endDate == null ? 0 : endDate!.hashCode) +
|
(endDate == null ? 0 : endDate!.hashCode) +
|
||||||
(hasSharedLink.hashCode) +
|
(hasSharedLink.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
|
(isActivityEnabled.hashCode) +
|
||||||
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
|
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
|
||||||
(owner.hashCode) +
|
(owner.hashCode) +
|
||||||
(ownerId.hashCode) +
|
(ownerId.hashCode) +
|
||||||
@ -121,7 +126,7 @@ class AlbumResponseDto {
|
|||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]';
|
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -142,6 +147,7 @@ class AlbumResponseDto {
|
|||||||
}
|
}
|
||||||
json[r'hasSharedLink'] = this.hasSharedLink;
|
json[r'hasSharedLink'] = this.hasSharedLink;
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
|
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||||
if (this.lastModifiedAssetTimestamp != null) {
|
if (this.lastModifiedAssetTimestamp != null) {
|
||||||
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
|
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
|
||||||
} else {
|
} else {
|
||||||
@ -177,6 +183,7 @@ class AlbumResponseDto {
|
|||||||
endDate: mapDateTime(json, r'endDate', ''),
|
endDate: mapDateTime(json, r'endDate', ''),
|
||||||
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
|
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||||
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', ''),
|
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', ''),
|
||||||
owner: UserResponseDto.fromJson(json[r'owner'])!,
|
owner: UserResponseDto.fromJson(json[r'owner'])!,
|
||||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
@ -239,6 +246,7 @@ class AlbumResponseDto {
|
|||||||
'description',
|
'description',
|
||||||
'hasSharedLink',
|
'hasSharedLink',
|
||||||
'id',
|
'id',
|
||||||
|
'isActivityEnabled',
|
||||||
'owner',
|
'owner',
|
||||||
'ownerId',
|
'ownerId',
|
||||||
'shared',
|
'shared',
|
||||||
|
23
mobile/openapi/lib/model/update_album_dto.dart
generated
23
mobile/openapi/lib/model/update_album_dto.dart
generated
@ -16,6 +16,7 @@ class UpdateAlbumDto {
|
|||||||
this.albumName,
|
this.albumName,
|
||||||
this.albumThumbnailAssetId,
|
this.albumThumbnailAssetId,
|
||||||
this.description,
|
this.description,
|
||||||
|
this.isActivityEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -42,21 +43,31 @@ class UpdateAlbumDto {
|
|||||||
///
|
///
|
||||||
String? description;
|
String? description;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
bool? isActivityEnabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto &&
|
||||||
other.albumName == albumName &&
|
other.albumName == albumName &&
|
||||||
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
||||||
other.description == description;
|
other.description == description &&
|
||||||
|
other.isActivityEnabled == isActivityEnabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(albumName == null ? 0 : albumName!.hashCode) +
|
(albumName == null ? 0 : albumName!.hashCode) +
|
||||||
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
|
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
|
||||||
(description == null ? 0 : description!.hashCode);
|
(description == null ? 0 : description!.hashCode) +
|
||||||
|
(isActivityEnabled == null ? 0 : isActivityEnabled!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, description=$description]';
|
String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, description=$description, isActivityEnabled=$isActivityEnabled]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -75,6 +86,11 @@ class UpdateAlbumDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'description'] = null;
|
// json[r'description'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.isActivityEnabled != null) {
|
||||||
|
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||||
|
} else {
|
||||||
|
// json[r'isActivityEnabled'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +105,7 @@ class UpdateAlbumDto {
|
|||||||
albumName: mapValueOfType<String>(json, r'albumName'),
|
albumName: mapValueOfType<String>(json, r'albumName'),
|
||||||
albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
|
albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
|
||||||
description: mapValueOfType<String>(json, r'description'),
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
|
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
5
mobile/openapi/test/album_response_dto_test.dart
generated
5
mobile/openapi/test/album_response_dto_test.dart
generated
@ -61,6 +61,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// bool isActivityEnabled
|
||||||
|
test('to test the property `isActivityEnabled`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// DateTime lastModifiedAssetTimestamp
|
// DateTime lastModifiedAssetTimestamp
|
||||||
test('to test the property `lastModifiedAssetTimestamp`', () async {
|
test('to test the property `lastModifiedAssetTimestamp`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
5
mobile/openapi/test/update_album_dto_test.dart
generated
5
mobile/openapi/test/update_album_dto_test.dart
generated
@ -31,6 +31,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// bool isActivityEnabled
|
||||||
|
test('to test the property `isActivityEnabled`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5894,6 +5894,9 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"isActivityEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"lastModifiedAssetTimestamp": {
|
"lastModifiedAssetTimestamp": {
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -5935,7 +5938,8 @@
|
|||||||
"sharedUsers",
|
"sharedUsers",
|
||||||
"hasSharedLink",
|
"hasSharedLink",
|
||||||
"assets",
|
"assets",
|
||||||
"owner"
|
"owner",
|
||||||
|
"isActivityEnabled"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@ -8910,6 +8914,9 @@
|
|||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isActivityEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
@ -138,10 +138,7 @@ export class AccessCore {
|
|||||||
switch (permission) {
|
switch (permission) {
|
||||||
// uses album id
|
// uses album id
|
||||||
case Permission.ACTIVITY_CREATE:
|
case Permission.ACTIVITY_CREATE:
|
||||||
return (
|
return await this.repository.activity.hasCreateAccess(authUser.id, id);
|
||||||
(await this.repository.album.hasOwnerAccess(authUser.id, id)) ||
|
|
||||||
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
|
|
||||||
);
|
|
||||||
|
|
||||||
// uses activity id
|
// uses activity id
|
||||||
case Permission.ACTIVITY_DELETE:
|
case Permission.ACTIVITY_DELETE:
|
||||||
|
@ -94,7 +94,7 @@ describe(ActivityService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a comment', async () => {
|
it('should create a comment', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.activity.hasCreateAccess.mockResolvedValue(true);
|
||||||
activityMock.create.mockResolvedValue(activityStub.oneComment);
|
activityMock.create.mockResolvedValue(activityStub.oneComment);
|
||||||
|
|
||||||
await sut.create(authStub.admin, {
|
await sut.create(authStub.admin, {
|
||||||
@ -113,8 +113,23 @@ describe(ActivityService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a like', async () => {
|
it('should fail because activity is disabled for the album', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
accessMock.activity.hasCreateAccess.mockResolvedValue(false);
|
||||||
|
activityMock.create.mockResolvedValue(activityStub.oneComment);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.create(authStub.admin, {
|
||||||
|
albumId: 'album-id',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: ReactionType.COMMENT,
|
||||||
|
comment: 'comment',
|
||||||
|
}),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a like', async () => {
|
||||||
|
accessMock.activity.hasCreateAccess.mockResolvedValue(true);
|
||||||
activityMock.create.mockResolvedValue(activityStub.liked);
|
activityMock.create.mockResolvedValue(activityStub.liked);
|
||||||
activityMock.search.mockResolvedValue([]);
|
activityMock.search.mockResolvedValue([]);
|
||||||
|
|
||||||
@ -134,6 +149,7 @@ describe(ActivityService.name, () => {
|
|||||||
|
|
||||||
it('should skip if like exists', async () => {
|
it('should skip if like exists', async () => {
|
||||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||||
|
accessMock.activity.hasCreateAccess.mockResolvedValue(true);
|
||||||
activityMock.search.mockResolvedValue([activityStub.liked]);
|
activityMock.search.mockResolvedValue([activityStub.liked]);
|
||||||
|
|
||||||
await sut.create(authStub.admin, {
|
await sut.create(authStub.admin, {
|
||||||
|
@ -21,6 +21,7 @@ export class AlbumResponseDto {
|
|||||||
lastModifiedAssetTimestamp?: Date;
|
lastModifiedAssetTimestamp?: Date;
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
|
isActivityEnabled!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
|
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
|
||||||
@ -61,6 +62,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
|
|||||||
endDate,
|
endDate,
|
||||||
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
|
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
|
||||||
assetCount: entity.assets?.length || 0,
|
assetCount: entity.assets?.length || 0,
|
||||||
|
isActivityEnabled: entity.isActivityEnabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -125,12 +125,12 @@ export class AlbumService {
|
|||||||
throw new BadRequestException('Invalid album thumbnail');
|
throw new BadRequestException('Invalid album thumbnail');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedAlbum = await this.albumRepository.update({
|
const updatedAlbum = await this.albumRepository.update({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
albumName: dto.albumName,
|
albumName: dto.albumName,
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
||||||
|
isActivityEnabled: dto.isActivityEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IsString } from 'class-validator';
|
import { IsBoolean, IsString } from 'class-validator';
|
||||||
import { Optional, ValidateUUID } from '../../domain.util';
|
import { Optional, ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class UpdateAlbumDto {
|
export class UpdateAlbumDto {
|
||||||
@ -12,4 +12,8 @@ export class UpdateAlbumDto {
|
|||||||
|
|
||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID({ optional: true })
|
||||||
albumThumbnailAssetId?: string;
|
albumThumbnailAssetId?: string;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActivityEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,9 @@ export const IAccessRepository = 'IAccessRepository';
|
|||||||
|
|
||||||
export interface IAccessRepository {
|
export interface IAccessRepository {
|
||||||
activity: {
|
activity: {
|
||||||
hasOwnerAccess(userId: string, albumId: string): Promise<boolean>;
|
hasOwnerAccess(userId: string, activityId: string): Promise<boolean>;
|
||||||
hasAlbumOwnerAccess(userId: string, albumId: string): Promise<boolean>;
|
hasAlbumOwnerAccess(userId: string, activityId: string): Promise<boolean>;
|
||||||
|
hasCreateAccess(userId: string, albumId: string): Promise<boolean>;
|
||||||
};
|
};
|
||||||
asset: {
|
asset: {
|
||||||
hasOwnerAccess(userId: string, assetId: string): Promise<boolean>;
|
hasOwnerAccess(userId: string, assetId: string): Promise<boolean>;
|
||||||
|
@ -56,4 +56,7 @@ export class AlbumEntity {
|
|||||||
|
|
||||||
@OneToMany(() => SharedLinkEntity, (link) => link.album)
|
@OneToMany(() => SharedLinkEntity, (link) => link.album)
|
||||||
sharedLinks!: SharedLinkEntity[];
|
sharedLinks!: SharedLinkEntity[];
|
||||||
|
|
||||||
|
@Column({ default: true })
|
||||||
|
isActivityEnabled!: boolean;
|
||||||
}
|
}
|
||||||
|
14
server/src/infra/migrations/1699268680508-DisableActivity.ts
Normal file
14
server/src/infra/migrations/1699268680508-DisableActivity.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class DisableActivity1699268680508 implements MigrationInterface {
|
||||||
|
name = 'DisableActivity1699268680508'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "albums" ADD "isActivityEnabled" boolean NOT NULL DEFAULT true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "isActivityEnabled"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -43,6 +43,24 @@ export class AccessRepository implements IAccessRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
hasCreateAccess: (userId: string, albumId: string): Promise<boolean> => {
|
||||||
|
return this.albumRepository.exist({
|
||||||
|
where: [
|
||||||
|
{
|
||||||
|
id: albumId,
|
||||||
|
isActivityEnabled: true,
|
||||||
|
sharedUsers: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: albumId,
|
||||||
|
isActivityEnabled: true,
|
||||||
|
ownerId: userId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
library = {
|
library = {
|
||||||
hasOwnerAccess: (userId: string, libraryId: string): Promise<boolean> => {
|
hasOwnerAccess: (userId: string, libraryId: string): Promise<boolean> => {
|
||||||
|
@ -226,6 +226,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
|
|||||||
assets: [],
|
assets: [],
|
||||||
assetCount: 0,
|
assetCount: 0,
|
||||||
owner: expect.objectContaining({ email: user1.userEmail }),
|
owner: expect.objectContaining({ email: user1.userEmail }),
|
||||||
|
isActivityEnabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
10
server/test/fixtures/album.stub.ts
vendored
10
server/test/fixtures/album.stub.ts
vendored
@ -18,6 +18,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
sharedWithUser: Object.freeze<AlbumEntity>({
|
sharedWithUser: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-2',
|
id: 'album-2',
|
||||||
@ -33,6 +34,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [userStub.user1],
|
sharedUsers: [userStub.user1],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
sharedWithMultiple: Object.freeze<AlbumEntity>({
|
sharedWithMultiple: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-3',
|
id: 'album-3',
|
||||||
@ -48,6 +50,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [userStub.user1, userStub.user2],
|
sharedUsers: [userStub.user1, userStub.user2],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
sharedWithAdmin: Object.freeze<AlbumEntity>({
|
sharedWithAdmin: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-3',
|
id: 'album-3',
|
||||||
@ -63,6 +66,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [userStub.admin],
|
sharedUsers: [userStub.admin],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
oneAsset: Object.freeze<AlbumEntity>({
|
oneAsset: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-4',
|
id: 'album-4',
|
||||||
@ -78,6 +82,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
twoAssets: Object.freeze<AlbumEntity>({
|
twoAssets: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-4a',
|
id: 'album-4a',
|
||||||
@ -93,6 +98,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
|
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-5',
|
id: 'album-5',
|
||||||
@ -108,6 +114,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
|
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-5',
|
id: 'album-5',
|
||||||
@ -123,6 +130,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
|
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-6',
|
id: 'album-6',
|
||||||
@ -138,6 +146,7 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
|
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
|
||||||
id: 'album-6',
|
id: 'album-6',
|
||||||
@ -153,5 +162,6 @@ export const albumStub = {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
|
isActivityEnabled: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
2
server/test/fixtures/shared-link.stub.ts
vendored
2
server/test/fixtures/shared-link.stub.ts
vendored
@ -100,6 +100,7 @@ const albumResponse: AlbumResponseDto = {
|
|||||||
hasSharedLink: false,
|
hasSharedLink: false,
|
||||||
assets: [],
|
assets: [],
|
||||||
assetCount: 1,
|
assetCount: 1,
|
||||||
|
isActivityEnabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sharedLinkStub = {
|
export const sharedLinkStub = {
|
||||||
@ -179,6 +180,7 @@ export const sharedLinkStub = {
|
|||||||
albumThumbnailAssetId: null,
|
albumThumbnailAssetId: null,
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
|
isActivityEnabled: true,
|
||||||
assets: [
|
assets: [
|
||||||
{
|
{
|
||||||
id: 'id_1',
|
id: 'id_1',
|
||||||
|
@ -19,6 +19,7 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock =>
|
|||||||
activity: {
|
activity: {
|
||||||
hasOwnerAccess: jest.fn(),
|
hasOwnerAccess: jest.fn(),
|
||||||
hasAlbumOwnerAccess: jest.fn(),
|
hasAlbumOwnerAccess: jest.fn(),
|
||||||
|
hasCreateAccess: jest.fn(),
|
||||||
},
|
},
|
||||||
asset: {
|
asset: {
|
||||||
hasOwnerAccess: jest.fn(),
|
hasOwnerAccess: jest.fn(),
|
||||||
|
12
web/src/api/open-api/api.ts
generated
12
web/src/api/open-api/api.ts
generated
@ -331,6 +331,12 @@ export interface AlbumResponseDto {
|
|||||||
* @memberof AlbumResponseDto
|
* @memberof AlbumResponseDto
|
||||||
*/
|
*/
|
||||||
'id': string;
|
'id': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AlbumResponseDto
|
||||||
|
*/
|
||||||
|
'isActivityEnabled': boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -4160,6 +4166,12 @@ export interface UpdateAlbumDto {
|
|||||||
* @memberof UpdateAlbumDto
|
* @memberof UpdateAlbumDto
|
||||||
*/
|
*/
|
||||||
'description'?: string;
|
'description'?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof UpdateAlbumDto
|
||||||
|
*/
|
||||||
|
'isActivityEnabled'?: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
76
web/src/lib/components/album-page/album-options.svelte
Normal file
76
web/src/lib/components/album-page/album-options.svelte
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { mdiClose, mdiPlus } from '@mdi/js';
|
||||||
|
import SettingSwitch from '../admin-page/settings/setting-switch.svelte';
|
||||||
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
|
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import type { AlbumResponseDto, UserResponseDto } from '../../../api/open-api';
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
|
||||||
|
export let album: AlbumResponseDto;
|
||||||
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
close: void;
|
||||||
|
toggleEnableActivity: void;
|
||||||
|
showSelectSharedUser: void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FullScreenModal on:clickOutside={() => dispatch('close')}>
|
||||||
|
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden p-2 md:p-0">
|
||||||
|
<div
|
||||||
|
class="w-[550px] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">Options</h1>
|
||||||
|
<div>
|
||||||
|
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" items-center justify-center p-4">
|
||||||
|
<div class="py-2">
|
||||||
|
<h2 class="text-gray text-sm mb-3">SHARING</h2>
|
||||||
|
<div class="p-2">
|
||||||
|
<SettingSwitch
|
||||||
|
title="Comments & likes"
|
||||||
|
subtitle="Let others respond"
|
||||||
|
checked={album.isActivityEnabled}
|
||||||
|
on:toggle={() => dispatch('toggleEnableActivity')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="text-gray text-sm mb-3">PEOPLE</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
||||||
|
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||||
|
<div><Icon path={mdiPlus} size="25" /></div>
|
||||||
|
</div>
|
||||||
|
<div>Invite People</div>
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-2 py-2 mt-2">
|
||||||
|
<div>
|
||||||
|
<UserAvatar {user} size="md" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">{`${user.firstName} ${user.lastName}`}</div>
|
||||||
|
<div>Owner</div>
|
||||||
|
</div>
|
||||||
|
{#each album.sharedUsers as user (user.id)}
|
||||||
|
<div class="flex items-center gap-2 py-2">
|
||||||
|
<div>
|
||||||
|
<UserAvatar {user} size="md" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">{`${user.firstName} ${user.lastName}`}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FullScreenModal>
|
@ -7,6 +7,7 @@
|
|||||||
export let isLiked: ActivityResponseDto | null;
|
export let isLiked: ActivityResponseDto | null;
|
||||||
export let numberOfComments: number | undefined;
|
export let numberOfComments: number | undefined;
|
||||||
export let isShowActivity: boolean | undefined;
|
export let isShowActivity: boolean | undefined;
|
||||||
|
export let disabled: boolean;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
@ -14,7 +15,7 @@
|
|||||||
<div
|
<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"
|
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={() => dispatch('favorite')}>
|
<button class={disabled ? 'cursor-not-allowed' : ''} on:click={() => dispatch('favorite')} {disabled}>
|
||||||
<!-- svelte-ignore missing-declaration -->
|
<!-- svelte-ignore missing-declaration -->
|
||||||
<div class="items-center justify-center">
|
<div class="items-center justify-center">
|
||||||
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
|
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
export let albumId: string;
|
export let albumId: string;
|
||||||
export let assetType: AssetTypeEnum | undefined = undefined;
|
export let assetType: AssetTypeEnum | undefined = undefined;
|
||||||
export let albumOwnerId: string;
|
export let albumOwnerId: string;
|
||||||
|
export let disabled: boolean;
|
||||||
|
|
||||||
let textArea: HTMLTextAreaElement;
|
let textArea: HTMLTextAreaElement;
|
||||||
let innerHeight: number;
|
let innerHeight: number;
|
||||||
@ -280,12 +281,15 @@
|
|||||||
<form class="flex w-full max-h-56 gap-1" on:submit|preventDefault={() => handleSendComment()}>
|
<form class="flex w-full max-h-56 gap-1" on:submit|preventDefault={() => handleSendComment()}>
|
||||||
<div class="flex w-full items-center gap-4">
|
<div class="flex w-full items-center gap-4">
|
||||||
<textarea
|
<textarea
|
||||||
|
{disabled}
|
||||||
bind:this={textArea}
|
bind:this={textArea}
|
||||||
bind:value={message}
|
bind:value={message}
|
||||||
placeholder="Say something"
|
placeholder={disabled ? 'Comments are disabled' : 'Say something'}
|
||||||
on:input={autoGrow}
|
on:input={autoGrow}
|
||||||
on:keypress={handleEnter}
|
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"
|
class="h-[18px] {disabled
|
||||||
|
? 'cursor-not-allowed'
|
||||||
|
: ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if isSendingMessage}
|
{#if isSendingMessage}
|
||||||
|
@ -104,6 +104,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (album && !album.isActivityEnabled && numberOfComments === 0) {
|
||||||
|
isShowActivity = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleAddComment = () => {
|
const handleAddComment = () => {
|
||||||
numberOfComments++;
|
numberOfComments++;
|
||||||
updateNumberOfComments(1);
|
updateNumberOfComments(1);
|
||||||
@ -115,7 +121,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFavorite = async () => {
|
const handleFavorite = async () => {
|
||||||
if (album) {
|
if (album && album.isActivityEnabled) {
|
||||||
try {
|
try {
|
||||||
if (isLiked) {
|
if (isLiked) {
|
||||||
const activityId = isLiked.id;
|
const activityId = isLiked.id;
|
||||||
@ -661,9 +667,10 @@
|
|||||||
on:onVideoStarted={handleVideoStarted}
|
on:onVideoStarted={handleVideoStarted}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $slideshowState === SlideshowState.None && isShared}
|
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || numberOfComments > 0)}
|
||||||
<div class="z-[9999] absolute bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
<div class="z-[9999] absolute bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||||
<ActivityStatus
|
<ActivityStatus
|
||||||
|
disabled={!album?.isActivityEnabled}
|
||||||
{isLiked}
|
{isLiked}
|
||||||
{numberOfComments}
|
{numberOfComments}
|
||||||
{isShowActivity}
|
{isShowActivity}
|
||||||
@ -744,6 +751,7 @@
|
|||||||
>
|
>
|
||||||
<ActivityViewer
|
<ActivityViewer
|
||||||
{user}
|
{user}
|
||||||
|
disabled={!album.isActivityEnabled}
|
||||||
assetType={asset.type}
|
assetType={asset.type}
|
||||||
albumOwnerId={album.ownerId}
|
albumOwnerId={album.ownerId}
|
||||||
albumId={album.id}
|
albumId={album.id}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const numberOfComments = writable<number | undefined>(undefined);
|
export const numberOfComments = writable<number>(0);
|
||||||
|
|
||||||
export const setNumberOfComments = (number: number) => {
|
export const setNumberOfComments = (number: number) => {
|
||||||
numberOfComments.set(number);
|
numberOfComments.set(number);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateNumberOfComments = (addOrRemove: 1 | -1) => {
|
export const updateNumberOfComments = (addOrRemove: 1 | -1) => {
|
||||||
numberOfComments.update((n) => (n ? n + addOrRemove : undefined));
|
numberOfComments.update((n) => n + addOrRemove);
|
||||||
};
|
};
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
|
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
|
||||||
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
|
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
|
||||||
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
|
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
|
||||||
|
import AlbumOptions from '$lib/components/album-page/album-options.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
@ -64,6 +65,12 @@
|
|||||||
let album = data.album;
|
let album = data.album;
|
||||||
$: album = data.album;
|
$: album = data.album;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (!album.isActivityEnabled && $numberOfComments === 0) {
|
||||||
|
isShowActivity = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum ViewMode {
|
enum ViewMode {
|
||||||
CONFIRM_DELETE = 'confirm-delete',
|
CONFIRM_DELETE = 'confirm-delete',
|
||||||
LINK_SHARING = 'link-sharing',
|
LINK_SHARING = 'link-sharing',
|
||||||
@ -73,6 +80,7 @@
|
|||||||
ALBUM_OPTIONS = 'album-options',
|
ALBUM_OPTIONS = 'album-options',
|
||||||
VIEW_USERS = 'view-users',
|
VIEW_USERS = 'view-users',
|
||||||
VIEW = 'view',
|
VIEW = 'view',
|
||||||
|
OPTIONS = 'options',
|
||||||
}
|
}
|
||||||
|
|
||||||
let backUrl: string = AppRoute.ALBUMS;
|
let backUrl: string = AppRoute.ALBUMS;
|
||||||
@ -107,6 +115,8 @@
|
|||||||
assetGridWidth = globalWidth;
|
assetGridWidth = globalWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$: showActivityStatus =
|
||||||
|
album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
|
||||||
|
|
||||||
afterNavigate(({ from }) => {
|
afterNavigate(({ from }) => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetViewingStore.showAssetViewer(false);
|
||||||
@ -128,6 +138,24 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleToggleEnableActivity = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.albumApi.updateAlbumInfo({
|
||||||
|
id: album.id,
|
||||||
|
updateAlbumDto: {
|
||||||
|
isActivityEnabled: !album.isActivityEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
album = data;
|
||||||
|
notificationController.show({
|
||||||
|
type: NotificationType.Info,
|
||||||
|
message: `Activity is ${album.isActivityEnabled ? 'enabled' : 'disabled'}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, `Can't ${!album.isActivityEnabled ? 'enable' : 'disable'} activity`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFavorite = async () => {
|
const handleFavorite = async () => {
|
||||||
try {
|
try {
|
||||||
if (isLiked) {
|
if (isLiked) {
|
||||||
@ -374,6 +402,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
currentAlbumName = album.albumName;
|
currentAlbumName = album.albumName;
|
||||||
|
notificationController.show({ type: NotificationType.Info, message: 'New album name has been saved' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to update album name');
|
handleError(error, 'Unable to update album name');
|
||||||
}
|
}
|
||||||
@ -455,6 +484,7 @@
|
|||||||
<MenuOption on:click={handleStartSlideshow} text="Slideshow" />
|
<MenuOption on:click={handleStartSlideshow} text="Slideshow" />
|
||||||
{/if}
|
{/if}
|
||||||
<MenuOption on:click={() => (viewMode = ViewMode.SELECT_THUMBNAIL)} text="Set album cover" />
|
<MenuOption on:click={() => (viewMode = ViewMode.SELECT_THUMBNAIL)} text="Set album cover" />
|
||||||
|
<MenuOption on:click={() => (viewMode = ViewMode.OPTIONS)} text="Options" />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{/if}
|
{/if}
|
||||||
</CircleIconButton>
|
</CircleIconButton>
|
||||||
@ -630,9 +660,10 @@
|
|||||||
</AssetGrid>
|
</AssetGrid>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if album.sharedUsers.length > 0 && !$showAssetViewer}
|
{#if showActivityStatus}
|
||||||
<div class="absolute z-[2] bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
<div class="absolute z-[2] bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||||
<ActivityStatus
|
<ActivityStatus
|
||||||
|
disabled={!album.isActivityEnabled}
|
||||||
{isLiked}
|
{isLiked}
|
||||||
numberOfComments={$numberOfComments}
|
numberOfComments={$numberOfComments}
|
||||||
{isShowActivity}
|
{isShowActivity}
|
||||||
@ -648,11 +679,12 @@
|
|||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 150 }}
|
transition:fly={{ duration: 150 }}
|
||||||
id="activity-panel"
|
id="activity-panel"
|
||||||
class="z-[1002] w-[360px] md: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"
|
class="z-[2] w-[360px] md: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"
|
translate="yes"
|
||||||
>
|
>
|
||||||
<ActivityViewer
|
<ActivityViewer
|
||||||
{user}
|
{user}
|
||||||
|
disabled={!album.isActivityEnabled}
|
||||||
albumOwnerId={album.ownerId}
|
albumOwnerId={album.ownerId}
|
||||||
albumId={album.id}
|
albumId={album.id}
|
||||||
bind:reactions
|
bind:reactions
|
||||||
@ -700,6 +732,16 @@
|
|||||||
</ConfirmDialogue>
|
</ConfirmDialogue>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if viewMode === ViewMode.OPTIONS}
|
||||||
|
<AlbumOptions
|
||||||
|
{album}
|
||||||
|
{user}
|
||||||
|
on:close={() => (viewMode = ViewMode.VIEW)}
|
||||||
|
on:toggleEnableActivity={handleToggleEnableActivity}
|
||||||
|
on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if isEditingDescription}
|
{#if isEditingDescription}
|
||||||
<EditDescriptionModal
|
<EditDescriptionModal
|
||||||
{album}
|
{album}
|
||||||
|
@ -17,4 +17,5 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
|||||||
shared: false,
|
shared: false,
|
||||||
sharedUsers: [],
|
sharedUsers: [],
|
||||||
hasSharedLink: false,
|
hasSharedLink: false,
|
||||||
|
isActivityEnabled: true,
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user