diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index a4a8ff275..c58a4a4c5 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -954,6 +954,12 @@ export interface CreateUserDto { * @memberof CreateUserDto */ 'lastName': string; + /** + * + * @type {boolean} + * @memberof CreateUserDto + */ + 'memoriesEnabled'?: boolean; /** * * @type {string} @@ -2995,6 +3001,12 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'lastName'?: string; + /** + * + * @type {boolean} + * @memberof UpdateUserDto + */ + 'memoriesEnabled'?: boolean; /** * * @type {string} @@ -3124,6 +3136,12 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'lastName': string; + /** + * + * @type {boolean} + * @memberof UserResponseDto + */ + 'memoriesEnabled': boolean; /** * * @type {string} diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 311bf98a7..a628df9bd 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -342,7 +342,10 @@ class HomePage extends HookConsumerWidget { listener: selectionListener, selectionActive: selectionEnabledHook.value, onRefresh: refreshAssets, - topWidget: const MemoryLane(), + topWidget: + (currentUser != null && currentUser.memoryEnabled) + ? const MemoryLane() + : const SizedBox(), ), error: (error, _) => Center(child: Text(error.toString())), loading: buildLoadingIndicator, diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index f2657e8f9..57723ed74 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -97,12 +97,11 @@ class AuthenticationNotifier extends StateNotifier { Future logout() async { var log = Logger('AuthenticationNotifier'); try { - String? userEmail = Store.tryGet(StoreKey.currentUser)?.email; _apiService.authenticationApi .logout() - .then((_) => log.info("Logout was successfull for $userEmail")) + .then((_) => log.info("Logout was successful for $userEmail")) .onError( (error, stackTrace) => log.severe("Error logging out $userEmail", error, stackTrace), @@ -186,8 +185,7 @@ class AuthenticationNotifier extends StateNotifier { user = User.fromDto(userResponseDto); retResult = true; - } - else { + } else { _log.severe("Unable to get user information from the server."); return false; } diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index 245192996..1aaef3af5 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/memories/providers/memory.provider.dart'; @@ -6,6 +7,9 @@ import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; +import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; class TabNavigationObserver extends AutoRouterObserver { @@ -46,6 +50,20 @@ class TabNavigationObserver extends AutoRouterObserver { if (route.name == 'HomeRoute') { ref.invalidate(memoryFutureProvider); + + // Update user info + try { + final userResponseDto = + await ref.read(apiServiceProvider).userApi.getMyUserInfo(); + + if (userResponseDto == null) { + return; + } + + Store.put(StoreKey.currentUser, User.fromDto(userResponseDto)); + } catch (e) { + debugPrint("Error refreshing user info $e"); + } } ref.watch(serverInfoProvider.notifier).getServerVersion(); } diff --git a/mobile/lib/shared/models/user.dart b/mobile/lib/shared/models/user.dart index 362adebc0..304150064 100644 --- a/mobile/lib/shared/models/user.dart +++ b/mobile/lib/shared/models/user.dart @@ -17,6 +17,7 @@ class User { this.isPartnerSharedBy = false, this.isPartnerSharedWith = false, this.profileImagePath = '', + this.memoryEnabled = true, }); Id get isarId => fastHash(id); @@ -30,7 +31,8 @@ class User { isPartnerSharedBy = false, isPartnerSharedWith = false, profileImagePath = dto.profileImagePath, - isAdmin = dto.isAdmin; + isAdmin = dto.isAdmin, + memoryEnabled = dto.memoriesEnabled; @Index(unique: true, replace: false, type: IndexType.hash) String id; @@ -42,6 +44,7 @@ class User { bool isPartnerSharedWith; bool isAdmin; String profileImagePath; + bool memoryEnabled; @Backlink(to: 'owner') final IsarLinks albums = IsarLinks(); @Backlink(to: 'sharedUsers') @@ -58,7 +61,8 @@ class User { isPartnerSharedBy == other.isPartnerSharedBy && isPartnerSharedWith == other.isPartnerSharedWith && profileImagePath == other.profileImagePath && - isAdmin == other.isAdmin; + isAdmin == other.isAdmin && + memoryEnabled == other.memoryEnabled; } @override @@ -72,5 +76,6 @@ class User { isPartnerSharedBy.hashCode ^ isPartnerSharedWith.hashCode ^ profileImagePath.hashCode ^ - isAdmin.hashCode; + isAdmin.hashCode ^ + memoryEnabled.hashCode; } diff --git a/mobile/lib/shared/models/user.g.dart b/mobile/lib/shared/models/user.g.dart index 26f20b985..461168f6e 100644 Binary files a/mobile/lib/shared/models/user.g.dart and b/mobile/lib/shared/models/user.g.dart differ diff --git a/mobile/makefile b/mobile/makefile index f4ce1450d..a04fe3621 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -1,8 +1,8 @@ build: - flutter packages pub run build_runner build --delete-conflicting-outputs + dart run build_runner build --delete-conflicting-outputs watch: - flutter packages pub run build_runner watch --delete-conflicting-outputs + dart run build_runner watch --delete-conflicting-outputs create_app_icon: flutter pub run flutter_launcher_icons:main diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md index 531e2cf19..0a25b4df4 100644 Binary files a/mobile/openapi/doc/CreateUserDto.md and b/mobile/openapi/doc/CreateUserDto.md differ diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index d3b41f3c3..ff6bc8d42 100644 Binary files a/mobile/openapi/doc/UpdateUserDto.md and b/mobile/openapi/doc/UpdateUserDto.md differ diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index 320827f98..6455c12d0 100644 Binary files a/mobile/openapi/doc/UserResponseDto.md and b/mobile/openapi/doc/UserResponseDto.md differ diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart index 0535d9406..1345ac995 100644 Binary files a/mobile/openapi/lib/model/create_user_dto.dart and b/mobile/openapi/lib/model/create_user_dto.dart differ diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index 71f3f16e8..6202c6d1e 100644 Binary files a/mobile/openapi/lib/model/update_user_dto.dart and b/mobile/openapi/lib/model/update_user_dto.dart differ diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 2720827d7..5ecc26b49 100644 Binary files a/mobile/openapi/lib/model/user_response_dto.dart and b/mobile/openapi/lib/model/user_response_dto.dart differ diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart index 872635e2f..9ce64c1e0 100644 Binary files a/mobile/openapi/test/create_user_dto_test.dart and b/mobile/openapi/test/create_user_dto_test.dart differ diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index 065436435..511de33af 100644 Binary files a/mobile/openapi/test/update_user_dto_test.dart and b/mobile/openapi/test/update_user_dto_test.dart differ diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart index cd6fa7694..9e2095607 100644 Binary files a/mobile/openapi/test/user_response_dto_test.dart and b/mobile/openapi/test/user_response_dto_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 1aac93051..bbf28e8d1 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -5396,6 +5396,9 @@ "lastName": { "type": "string" }, + "memoriesEnabled": { + "type": "boolean" + }, "password": { "type": "string" }, @@ -7004,6 +7007,9 @@ "lastName": { "type": "string" }, + "memoriesEnabled": { + "type": "boolean" + }, "password": { "type": "string" }, @@ -7092,6 +7098,9 @@ "lastName": { "type": "string" }, + "memoriesEnabled": { + "type": "boolean" + }, "oauthId": { "type": "string" }, @@ -7123,7 +7132,8 @@ "createdAt", "deletedAt", "updatedAt", - "oauthId" + "oauthId", + "memoriesEnabled" ], "type": "object" }, diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts index 50eed510d..febacbb38 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/domain/album/album.service.spec.ts @@ -176,6 +176,7 @@ describe(AlbumService.name, () => { deletedAt: null, updatedAt: new Date('2021-01-01'), externalPath: null, + memoriesEnabled: true, }, ownerId: 'admin_id', shared: false, diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index c8e048969..ac15abc8c 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -1,10 +1,11 @@ import { BadRequestException } from '@nestjs/common'; import { authStub, newPartnerRepositoryMock, partnerStub } from '@test'; +import { UserResponseDto } from '../index'; import { IPartnerRepository, PartnerDirection } from './partner.repository'; import { PartnerService } from './partner.service'; const responseDto = { - admin: { + admin: { email: 'admin@test.com', firstName: 'admin_first_name', id: 'admin_id', @@ -18,8 +19,9 @@ const responseDto = { deletedAt: null, updatedAt: new Date('2021-01-01'), externalPath: null, + memoriesEnabled: true, }, - user1: { + user1: { email: 'immich@test.com', firstName: 'immich_first_name', id: 'user-id', @@ -33,6 +35,7 @@ const responseDto = { deletedAt: null, updatedAt: new Date('2021-01-01'), externalPath: null, + memoriesEnabled: true, }, }; diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts index afb5aec23..c9a9945af 100644 --- a/server/src/domain/user/dto/create-user.dto.ts +++ b/server/src/domain/user/dto/create-user.dto.ts @@ -1,5 +1,5 @@ import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { toEmail, toSanitized } from '../../domain.util'; export class CreateUserDto { @@ -27,6 +27,10 @@ export class CreateUserDto { @IsOptional() @IsString() externalPath?: string | null; + + @IsOptional() + @IsBoolean() + memoriesEnabled?: boolean; } export class CreateAdminDto { diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts index 7eb98f538..a1e053855 100644 --- a/server/src/domain/user/dto/update-user.dto.ts +++ b/server/src/domain/user/dto/update-user.dto.ts @@ -45,4 +45,8 @@ export class UpdateUserDto { @IsOptional() @IsBoolean() shouldChangePassword?: boolean; + + @IsOptional() + @IsBoolean() + memoriesEnabled?: boolean; } diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts index a2bd50883..9a3372ad5 100644 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ b/server/src/domain/user/response-dto/user-response.dto.ts @@ -14,6 +14,7 @@ export class UserResponseDto { deletedAt!: Date | null; updatedAt!: Date; oauthId!: string; + memoriesEnabled!: boolean; } export function mapUser(entity: UserEntity): UserResponseDto { @@ -31,5 +32,6 @@ export function mapUser(entity: UserEntity): UserResponseDto { deletedAt: entity.deletedAt, updatedAt: entity.updatedAt, oauthId: entity.oauthId, + memoriesEnabled: entity.memoriesEnabled, }; } diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts index e7c0f7f2b..6c59d9ca7 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/domain/user/user.core.ts @@ -60,6 +60,7 @@ export class UserCore { dto.externalPath = null; } + console.log(dto.memoriesEnabled); return this.userRepository.update(id, dto); } catch (e) { Logger.error(e, 'Failed to update user info'); diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts index a573a8c20..ef1a07d17 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/domain/user/user.service.spec.ts @@ -16,6 +16,7 @@ import { ICryptoRepository } from '../crypto'; import { IJobRepository, JobName } from '../job'; import { IStorageRepository } from '../storage'; import { UpdateUserDto } from './dto/update-user.dto'; +import { UserResponseDto } from './response-dto'; import { IUserRepository } from './user.repository'; import { UserService } from './user.service'; @@ -54,6 +55,7 @@ const adminUser: UserEntity = Object.freeze({ assets: [], storageLabel: 'admin', externalPath: null, + memoriesEnabled: true, }); const immichUser: UserEntity = Object.freeze({ @@ -73,9 +75,10 @@ const immichUser: UserEntity = Object.freeze({ assets: [], storageLabel: null, externalPath: null, + memoriesEnabled: true, }); -const updatedImmichUser: UserEntity = Object.freeze({ +const updatedImmichUser = Object.freeze({ id: immichUserAuth.id, email: 'immich@test.com', password: 'immich_password', @@ -92,9 +95,10 @@ const updatedImmichUser: UserEntity = Object.freeze({ assets: [], storageLabel: null, externalPath: null, + memoriesEnabled: true, }); -const adminUserResponse = Object.freeze({ +const adminUserResponse = Object.freeze({ id: adminUserAuth.id, email: 'admin@test.com', firstName: 'admin_first_name', @@ -108,6 +112,7 @@ const adminUserResponse = Object.freeze({ updatedAt: new Date('2021-01-01'), storageLabel: 'admin', externalPath: null, + memoriesEnabled: true, }); describe(UserService.name, () => { @@ -158,6 +163,7 @@ describe(UserService.name, () => { updatedAt: new Date('2021-01-01'), storageLabel: 'admin', externalPath: null, + memoriesEnabled: true, }, ]); }); diff --git a/server/src/infra/entities/user.entity.ts b/server/src/infra/entities/user.entity.ts index 7cdac1f82..e6555153a 100644 --- a/server/src/infra/entities/user.entity.ts +++ b/server/src/infra/entities/user.entity.ts @@ -54,6 +54,9 @@ export class UserEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Column({ default: true }) + memoriesEnabled!: boolean; + @OneToMany(() => TagEntity, (tag) => tag.user) tags!: TagEntity[]; diff --git a/server/src/infra/migrations/1691600216749-UserMemoryPreference.ts b/server/src/infra/migrations/1691600216749-UserMemoryPreference.ts new file mode 100644 index 000000000..723874908 --- /dev/null +++ b/server/src/infra/migrations/1691600216749-UserMemoryPreference.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UserMemoryPreference1691600216749 implements MigrationInterface { + name = 'UserMemoryPreference1691600216749'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "memoriesEnabled" boolean NOT NULL DEFAULT true`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "memoriesEnabled"`); + } +} diff --git a/server/test/e2e/user.e2e-spec.ts b/server/test/e2e/user.e2e-spec.ts index 0ba37e33c..90c79ed96 100644 --- a/server/test/e2e/user.e2e-spec.ts +++ b/server/test/e2e/user.e2e-spec.ts @@ -143,6 +143,24 @@ describe(`${UserController.name}`, () => { }); expect(status).toBe(201); }); + + it('should create a user without memories enabled', async () => { + const { status, body } = await request(server) + .post(`/user`) + .send({ + email: 'no-memories@immich.app', + password: 'Password123', + firstName: 'No Memories', + lastName: 'User', + memoriesEnabled: false, + }) + .set('Authorization', `Bearer ${accessToken}`); + expect(body).toMatchObject({ + email: 'no-memories@immich.app', + memoriesEnabled: false, + }); + expect(status).toBe(201); + }); }); describe('PUT /user', () => { @@ -206,6 +224,21 @@ describe(`${UserController.name}`, () => { }); expect(before.updatedAt).not.toEqual(after.updatedAt); }); + + it('should update memories enabled', async () => { + const before = await api.userApi.get(server, accessToken, loginResponse.userId); + const after = await api.userApi.update(server, accessToken, { + id: before.id, + memoriesEnabled: false, + }); + + expect(after).toMatchObject({ + ...before, + updatedAt: expect.anything(), + memoriesEnabled: false, + }); + expect(before.updatedAt).not.toEqual(after.updatedAt); + }); }); describe('GET /user/count', () => { diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 4367bf60e..f2a8dcab8 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -17,6 +17,7 @@ export const userStub = { updatedAt: new Date('2021-01-01'), tags: [], assets: [], + memoriesEnabled: true, }), user1: Object.freeze({ ...authStub.user1, @@ -33,6 +34,7 @@ export const userStub = { updatedAt: new Date('2021-01-01'), tags: [], assets: [], + memoriesEnabled: true, }), user2: Object.freeze({ ...authStub.user2, @@ -49,6 +51,7 @@ export const userStub = { updatedAt: new Date('2021-01-01'), tags: [], assets: [], + memoriesEnabled: true, }), storageLabel: Object.freeze({ ...authStub.user1, @@ -65,5 +68,6 @@ export const userStub = { updatedAt: new Date('2021-01-01'), tags: [], assets: [], + memoriesEnabled: true, }), }; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index a4a8ff275..c58a4a4c5 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -954,6 +954,12 @@ export interface CreateUserDto { * @memberof CreateUserDto */ 'lastName': string; + /** + * + * @type {boolean} + * @memberof CreateUserDto + */ + 'memoriesEnabled'?: boolean; /** * * @type {string} @@ -2995,6 +3001,12 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'lastName'?: string; + /** + * + * @type {boolean} + * @memberof UpdateUserDto + */ + 'memoriesEnabled'?: boolean; /** * * @type {string} @@ -3124,6 +3136,12 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'lastName': string; + /** + * + * @type {boolean} + * @memberof UserResponseDto + */ + 'memoriesEnabled': boolean; /** * * @type {string} diff --git a/web/src/lib/components/user-settings-page/memories-settings.svelte b/web/src/lib/components/user-settings-page/memories-settings.svelte new file mode 100644 index 000000000..9e269fc09 --- /dev/null +++ b/web/src/lib/components/user-settings-page/memories-settings.svelte @@ -0,0 +1,49 @@ + + +
+
+
+
+
+ +
+
+ +
+
+
+
+
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index 76311414c..376c78e63 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -4,10 +4,11 @@ import { onMount } from 'svelte'; import SettingAccordion from '../admin-page/settings/setting-accordion.svelte'; import ChangePasswordSettings from './change-password-settings.svelte'; - import OAuthSettings from './oauth-settings.svelte'; - import UserAPIKeyList from './user-api-key-list.svelte'; import DeviceList from './device-list.svelte'; + import MemoriesSettings from './memories-settings.svelte'; + import OAuthSettings from './oauth-settings.svelte'; import PartnerSettings from './partner-settings.svelte'; + import UserAPIKeyList from './user-api-key-list.svelte'; import UserProfileSettings from './user-profile-settings.svelte'; export let user: UserResponseDto; @@ -39,6 +40,10 @@ + + + + {#if oauthEnabled} {#if assetCount} - + {#if data.user.memoriesEnabled} + + {/if} {:else} openFileUploadDialog()} /> diff --git a/web/src/test-data/factories/user-factory.ts b/web/src/test-data/factories/user-factory.ts index ebf280159..507242fe8 100644 --- a/web/src/test-data/factories/user-factory.ts +++ b/web/src/test-data/factories/user-factory.ts @@ -15,5 +15,6 @@ export const userFactory = Sync.makeFactory({ createdAt: Sync.each(() => faker.date.past().toISOString()), deletedAt: null, updatedAt: Sync.each(() => faker.date.past().toISOString()), + memoriesEnabled: true, oauthId: '', });