From 9d01885b584a370b492f22dc7d5e10dca33ef5f8 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Tue, 7 Nov 2023 05:37:21 +0100 Subject: [PATCH] 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 --- cli/src/api/open-api/api.ts | 12 +++ mobile/openapi/doc/AlbumResponseDto.md | Bin 1187 -> 1226 bytes mobile/openapi/doc/UpdateAlbumDto.md | Bin 526 -> 576 bytes .../openapi/lib/model/album_response_dto.dart | Bin 8568 -> 8920 bytes .../openapi/lib/model/update_album_dto.dart | Bin 4814 -> 5618 bytes .../openapi/test/album_response_dto_test.dart | Bin 2248 -> 2365 bytes .../openapi/test/update_album_dto_test.dart | Bin 802 -> 919 bytes server/immich-openapi-specs.json | 9 ++- server/src/domain/access/access.core.ts | 5 +- server/src/domain/activity/activity.spec.ts | 20 ++++- server/src/domain/album/album-response.dto.ts | 2 + server/src/domain/album/album.service.ts | 2 +- .../src/domain/album/dto/album-update.dto.ts | 6 +- .../domain/repositories/access.repository.ts | 5 +- server/src/infra/entities/album.entity.ts | 3 + .../1699268680508-DisableActivity.ts | 14 ++++ .../infra/repositories/access.repository.ts | 18 +++++ server/test/e2e/album.e2e-spec.ts | 1 + server/test/fixtures/album.stub.ts | 10 +++ server/test/fixtures/shared-link.stub.ts | 2 + .../repositories/access.repository.mock.ts | 1 + web/src/api/open-api/api.ts | 12 +++ .../album-page/album-options.svelte | 76 ++++++++++++++++++ .../asset-viewer/activity-status.svelte | 3 +- .../asset-viewer/activity-viewer.svelte | 8 +- .../asset-viewer/asset-viewer.svelte | 12 ++- web/src/lib/stores/activity.store.ts | 4 +- .../(user)/albums/[albumId]/+page.svelte | 46 ++++++++++- web/src/test-data/factories/album-factory.ts | 1 + 29 files changed, 252 insertions(+), 20 deletions(-) create mode 100644 server/src/infra/migrations/1699268680508-DisableActivity.ts create mode 100644 web/src/lib/components/album-page/album-options.svelte diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 9265d439fc..ec6e3f7243 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -331,6 +331,12 @@ export interface AlbumResponseDto { * @memberof AlbumResponseDto */ 'id': string; + /** + * + * @type {boolean} + * @memberof AlbumResponseDto + */ + 'isActivityEnabled': boolean; /** * * @type {string} @@ -4160,6 +4166,12 @@ export interface UpdateAlbumDto { * @memberof UpdateAlbumDto */ 'description'?: string; + /** + * + * @type {boolean} + * @memberof UpdateAlbumDto + */ + 'isActivityEnabled'?: boolean; } /** * diff --git a/mobile/openapi/doc/AlbumResponseDto.md b/mobile/openapi/doc/AlbumResponseDto.md index 93620b9fc64c50eaf1227805ad648253aa2266c4..bc00d30af1a6237d217a1f16c28ff97cb0a3c96f 100644 GIT binary patch delta 34 pcmZ3?d5UvGK9gW(v14*cW?5!QrE6YdQch~hWP2vD&G}3wOaR>C3(5ch delta 12 TcmX@bxtMc9KGWuDOeRbKA94gD diff --git a/mobile/openapi/doc/UpdateAlbumDto.md b/mobile/openapi/doc/UpdateAlbumDto.md index 283b8bc29af18ddff1856a58c7030c0ee10b1fbd..4ded87d1bfbc5a9bb6e1bf575925ea36006c730a 100644 GIT binary patch delta 42 xcmeBUIl!_ZlTl7fE3?=!xg@hJv!v2BFEJ@6HAPEHp+-SVD=9xeXL2axP5?5`4ru@Y delta 11 ScmX@W(#NtPlX3EF#-#umpaew# diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index cf2ad925236ecda24f58f8fd045986d69a3257a3..86e009e330866ac10b36732fbb2c49b4e8782601 100644 GIT binary patch delta 298 zcmez2bi;K+C)4EPOnl;*#g54(nPr(Jm9BY-Nja%0I$R1sF!`;J{N@!*cbPn@DYh!;LYw3HyO>QB6tar*^P-E? t(WO(=V-;)_N-{Ew_0Xj@KN1pR<3|&nd_qzY(=pE^962yVC(9{D0RWhZZjS%} delta 46 zcmV+}0MY;0MfgIni2}2(0{jBA#RWhGvpNfp0<-K54g#|v5QhV^;u8x7vpX6y2$P2* EQNZI6@c;k- diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index 6c0bf3eca692d523cdf7ce95b773519b498ddeb3..32d4d2a6081018e541f822d646b7b2d1a9269946 100644 GIT binary patch delta 425 zcmX@7`bm4kF-AEBg_4ZSV!g~_$K;aCvdof7*Sy4}oYWK@E(L{J&CS-#`HY**ng23v zc3_TW6i>>}&#_m)P&`?m?Y*gjni^0K5agF+q!wYCs9>i%%!zHPWp-Vkh!4`W6 zO@7ZQFNmRjasj94=EGc8OwJ0JuCh|dO)LmY%qdOvPYbCmNVNe4r=3PtaekhTLXkR# LVVghm+-3y;x;vHW delta 40 ycmV+@0N4NWE6ydb#sRb90d547$p`AQ=m-G;lPwC8v*ijB0<)S8Is&sH5YYu~D-YxV diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index c8417420055e30bb3872802de15a0d202c9f8115..933f77c19635bb527b772c760e6e6df5f7da713c 100644 GIT binary patch delta 61 zcmX>hxL0VyD%MHttb&=vj>#pNWtkNY;w#1Ag2TY diff --git a/mobile/openapi/test/update_album_dto_test.dart b/mobile/openapi/test/update_album_dto_test.dart index 7b8472ad3e568106bb6209ac43f4e99079f04d48..67ec80010ddb7cbe5cd95d3d01af518c948bf4e0 100644 GIT binary patch delta 70 zcmZ3)Hl2Ng1oLD)CU()J{QMk+%wosnlFYKql1kUS#H5_m$pK7Cg6M)NlR22wxVRJ) LYBjC7xN5loEeI8? delta 11 ScmbQvzKCsu1oPxxW-b60A_G4F diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 05aec878ad..7e4dc7cdbd 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -5894,6 +5894,9 @@ "id": { "type": "string" }, + "isActivityEnabled": { + "type": "boolean" + }, "lastModifiedAssetTimestamp": { "format": "date-time", "type": "string" @@ -5935,7 +5938,8 @@ "sharedUsers", "hasSharedLink", "assets", - "owner" + "owner", + "isActivityEnabled" ], "type": "object" }, @@ -8910,6 +8914,9 @@ }, "description": { "type": "string" + }, + "isActivityEnabled": { + "type": "boolean" } }, "type": "object" diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index 4142527784..88abd79b19 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -138,10 +138,7 @@ export class AccessCore { switch (permission) { // uses album id case Permission.ACTIVITY_CREATE: - return ( - (await this.repository.album.hasOwnerAccess(authUser.id, id)) || - (await this.repository.album.hasSharedAlbumAccess(authUser.id, id)) - ); + return await this.repository.activity.hasCreateAccess(authUser.id, id); // uses activity id case Permission.ACTIVITY_DELETE: diff --git a/server/src/domain/activity/activity.spec.ts b/server/src/domain/activity/activity.spec.ts index 968f7421a3..496d8978b7 100644 --- a/server/src/domain/activity/activity.spec.ts +++ b/server/src/domain/activity/activity.spec.ts @@ -94,7 +94,7 @@ describe(ActivityService.name, () => { }); it('should create a comment', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.activity.hasCreateAccess.mockResolvedValue(true); activityMock.create.mockResolvedValue(activityStub.oneComment); 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.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.search.mockResolvedValue([]); @@ -134,6 +149,7 @@ describe(ActivityService.name, () => { it('should skip if like exists', async () => { accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.activity.hasCreateAccess.mockResolvedValue(true); activityMock.search.mockResolvedValue([activityStub.liked]); await sut.create(authStub.admin, { diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts index b426bc37d6..671922408e 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/domain/album/album-response.dto.ts @@ -21,6 +21,7 @@ export class AlbumResponseDto { lastModifiedAssetTimestamp?: Date; startDate?: Date; endDate?: Date; + isActivityEnabled!: boolean; } export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => { @@ -61,6 +62,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons endDate, assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)), assetCount: entity.assets?.length || 0, + isActivityEnabled: entity.isActivityEnabled, }; }; diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index b8e789943e..37d44c33ac 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -125,12 +125,12 @@ export class AlbumService { throw new BadRequestException('Invalid album thumbnail'); } } - const updatedAlbum = await this.albumRepository.update({ id: album.id, albumName: dto.albumName, description: dto.description, albumThumbnailAssetId: dto.albumThumbnailAssetId, + isActivityEnabled: dto.isActivityEnabled, }); await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } }); diff --git a/server/src/domain/album/dto/album-update.dto.ts b/server/src/domain/album/dto/album-update.dto.ts index f574f2c23c..3b1858ba10 100644 --- a/server/src/domain/album/dto/album-update.dto.ts +++ b/server/src/domain/album/dto/album-update.dto.ts @@ -1,4 +1,4 @@ -import { IsString } from 'class-validator'; +import { IsBoolean, IsString } from 'class-validator'; import { Optional, ValidateUUID } from '../../domain.util'; export class UpdateAlbumDto { @@ -12,4 +12,8 @@ export class UpdateAlbumDto { @ValidateUUID({ optional: true }) albumThumbnailAssetId?: string; + + @Optional() + @IsBoolean() + isActivityEnabled?: boolean; } diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/domain/repositories/access.repository.ts index 43b53e605d..f9ceb6f520 100644 --- a/server/src/domain/repositories/access.repository.ts +++ b/server/src/domain/repositories/access.repository.ts @@ -2,8 +2,9 @@ export const IAccessRepository = 'IAccessRepository'; export interface IAccessRepository { activity: { - hasOwnerAccess(userId: string, albumId: string): Promise; - hasAlbumOwnerAccess(userId: string, albumId: string): Promise; + hasOwnerAccess(userId: string, activityId: string): Promise; + hasAlbumOwnerAccess(userId: string, activityId: string): Promise; + hasCreateAccess(userId: string, albumId: string): Promise; }; asset: { hasOwnerAccess(userId: string, assetId: string): Promise; diff --git a/server/src/infra/entities/album.entity.ts b/server/src/infra/entities/album.entity.ts index 38ce4310c9..fbc125351a 100644 --- a/server/src/infra/entities/album.entity.ts +++ b/server/src/infra/entities/album.entity.ts @@ -56,4 +56,7 @@ export class AlbumEntity { @OneToMany(() => SharedLinkEntity, (link) => link.album) sharedLinks!: SharedLinkEntity[]; + + @Column({ default: true }) + isActivityEnabled!: boolean; } diff --git a/server/src/infra/migrations/1699268680508-DisableActivity.ts b/server/src/infra/migrations/1699268680508-DisableActivity.ts new file mode 100644 index 0000000000..d860244f6d --- /dev/null +++ b/server/src/infra/migrations/1699268680508-DisableActivity.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DisableActivity1699268680508 implements MigrationInterface { + name = 'DisableActivity1699268680508' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" ADD "isActivityEnabled" boolean NOT NULL DEFAULT true`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "isActivityEnabled"`); + } + +} diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index 566514796d..aff498ac34 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -43,6 +43,24 @@ export class AccessRepository implements IAccessRepository { }, }); }, + hasCreateAccess: (userId: string, albumId: string): Promise => { + return this.albumRepository.exist({ + where: [ + { + id: albumId, + isActivityEnabled: true, + sharedUsers: { + id: userId, + }, + }, + { + id: albumId, + isActivityEnabled: true, + ownerId: userId, + }, + ], + }); + }, }; library = { hasOwnerAccess: (userId: string, libraryId: string): Promise => { diff --git a/server/test/e2e/album.e2e-spec.ts b/server/test/e2e/album.e2e-spec.ts index e10f5414f9..8348eff037 100644 --- a/server/test/e2e/album.e2e-spec.ts +++ b/server/test/e2e/album.e2e-spec.ts @@ -226,6 +226,7 @@ describe(`${AlbumController.name} (e2e)`, () => { assets: [], assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), + isActivityEnabled: true, }); }); }); diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index 48ed928173..fd4464d191 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -18,6 +18,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), sharedWithUser: Object.freeze({ id: 'album-2', @@ -33,6 +34,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [userStub.user1], + isActivityEnabled: true, }), sharedWithMultiple: Object.freeze({ id: 'album-3', @@ -48,6 +50,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [userStub.user1, userStub.user2], + isActivityEnabled: true, }), sharedWithAdmin: Object.freeze({ id: 'album-3', @@ -63,6 +66,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [userStub.admin], + isActivityEnabled: true, }), oneAsset: Object.freeze({ id: 'album-4', @@ -78,6 +82,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), twoAssets: Object.freeze({ id: 'album-4a', @@ -93,6 +98,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), emptyWithInvalidThumbnail: Object.freeze({ id: 'album-5', @@ -108,6 +114,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), emptyWithValidThumbnail: Object.freeze({ id: 'album-5', @@ -123,6 +130,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), oneAssetInvalidThumbnail: Object.freeze({ id: 'album-6', @@ -138,6 +146,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), oneAssetValidThumbnail: Object.freeze({ id: 'album-6', @@ -153,5 +162,6 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index dd6eb52334..56a0c10450 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -100,6 +100,7 @@ const albumResponse: AlbumResponseDto = { hasSharedLink: false, assets: [], assetCount: 1, + isActivityEnabled: true, }; export const sharedLinkStub = { @@ -179,6 +180,7 @@ export const sharedLinkStub = { albumThumbnailAssetId: null, sharedUsers: [], sharedLinks: [], + isActivityEnabled: true, assets: [ { id: 'id_1', diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 4f7992e86e..6abfc7c9e1 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -19,6 +19,7 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => activity: { hasOwnerAccess: jest.fn(), hasAlbumOwnerAccess: jest.fn(), + hasCreateAccess: jest.fn(), }, asset: { hasOwnerAccess: jest.fn(), diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 9265d439fc..ec6e3f7243 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -331,6 +331,12 @@ export interface AlbumResponseDto { * @memberof AlbumResponseDto */ 'id': string; + /** + * + * @type {boolean} + * @memberof AlbumResponseDto + */ + 'isActivityEnabled': boolean; /** * * @type {string} @@ -4160,6 +4166,12 @@ export interface UpdateAlbumDto { * @memberof UpdateAlbumDto */ 'description'?: string; + /** + * + * @type {boolean} + * @memberof UpdateAlbumDto + */ + 'isActivityEnabled'?: boolean; } /** * diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte new file mode 100644 index 0000000000..1a3e43892c --- /dev/null +++ b/web/src/lib/components/album-page/album-options.svelte @@ -0,0 +1,76 @@ + + + dispatch('close')}> +
+
+
+
+

Options

+
+ dispatch('close')} /> +
+
+ +
+
+

SHARING

+
+ dispatch('toggleEnableActivity')} + /> +
+
+
+
PEOPLE
+
+ +
+
+ +
+
{`${user.firstName} ${user.lastName}`}
+
Owner
+
+ {#each album.sharedUsers as user (user.id)} +
+
+ +
+
{`${user.firstName} ${user.lastName}`}
+
+ {/each} +
+
+
+
+
+
+
diff --git a/web/src/lib/components/asset-viewer/activity-status.svelte b/web/src/lib/components/asset-viewer/activity-status.svelte index 23fdb7a9d4..47e29ff974 100644 --- a/web/src/lib/components/asset-viewer/activity-status.svelte +++ b/web/src/lib/components/asset-viewer/activity-status.svelte @@ -7,6 +7,7 @@ export let isLiked: ActivityResponseDto | null; export let numberOfComments: number | undefined; export let isShowActivity: boolean | undefined; + export let disabled: boolean; const dispatch = createEventDispatcher(); @@ -14,7 +15,7 @@
-