diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index a4a8ff2759d6f..c58a4a4c533a6 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 311bf98a79fda..a628df9bd0a99 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 f2657e8f94e21..57723ed745abc 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 24519299639bd..1aaef3af51c01 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 362adebc0b278..3041500641be6 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 26f20b98544d3..461168f6ebf35 100644 --- a/mobile/lib/shared/models/user.g.dart +++ b/mobile/lib/shared/models/user.g.dart @@ -52,8 +52,18 @@ const UserSchema = CollectionSchema( name: r'lastName', type: IsarType.string, ), - r'updatedAt': PropertySchema( + r'memoryEnabled': PropertySchema( id: 7, + name: r'memoryEnabled', + type: IsarType.bool, + ), + r'profileImagePath': PropertySchema( + id: 8, + name: r'profileImagePath', + type: IsarType.string, + ), + r'updatedAt': PropertySchema( + id: 9, name: r'updatedAt', type: IsarType.dateTime, ) @@ -111,6 +121,7 @@ int _userEstimateSize( bytesCount += 3 + object.firstName.length * 3; bytesCount += 3 + object.id.length * 3; bytesCount += 3 + object.lastName.length * 3; + bytesCount += 3 + object.profileImagePath.length * 3; return bytesCount; } @@ -127,7 +138,9 @@ void _userSerialize( writer.writeBool(offsets[4], object.isPartnerSharedBy); writer.writeBool(offsets[5], object.isPartnerSharedWith); writer.writeString(offsets[6], object.lastName); - writer.writeDateTime(offsets[7], object.updatedAt); + writer.writeBool(offsets[7], object.memoryEnabled); + writer.writeString(offsets[8], object.profileImagePath); + writer.writeDateTime(offsets[9], object.updatedAt); } User _userDeserialize( @@ -144,7 +157,9 @@ User _userDeserialize( isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false, isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false, lastName: reader.readString(offsets[6]), - updatedAt: reader.readDateTime(offsets[7]), + memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, + profileImagePath: reader.readStringOrNull(offsets[8]) ?? '', + updatedAt: reader.readDateTime(offsets[9]), ); return object; } @@ -171,6 +186,10 @@ P _userDeserializeProp

( case 6: return (reader.readString(offset)) as P; case 7: + return (reader.readBoolOrNull(offset) ?? true) as P; + case 8: + return (reader.readStringOrNull(offset) ?? '') as P; + case 9: return (reader.readDateTime(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -960,6 +979,146 @@ extension UserQueryFilter on QueryBuilder { }); } + QueryBuilder memoryEnabledEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'memoryEnabled', + value: value, + )); + }); + } + + QueryBuilder profileImagePathEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'profileImagePath', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder profileImagePathGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'profileImagePath', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder profileImagePathLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'profileImagePath', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder profileImagePathBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'profileImagePath', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder profileImagePathStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'profileImagePath', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder profileImagePathEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'profileImagePath', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder profileImagePathContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'profileImagePath', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder profileImagePathMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'profileImagePath', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder profileImagePathIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'profileImagePath', + value: '', + )); + }); + } + + QueryBuilder profileImagePathIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'profileImagePath', + value: '', + )); + }); + } + QueryBuilder updatedAtEqualTo( DateTime value) { return QueryBuilder.apply(this, (query) { @@ -1214,6 +1373,30 @@ extension UserQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByMemoryEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'memoryEnabled', Sort.asc); + }); + } + + QueryBuilder sortByMemoryEnabledDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'memoryEnabled', Sort.desc); + }); + } + + QueryBuilder sortByProfileImagePath() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'profileImagePath', Sort.asc); + }); + } + + QueryBuilder sortByProfileImagePathDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'profileImagePath', Sort.desc); + }); + } + QueryBuilder sortByUpdatedAt() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'updatedAt', Sort.asc); @@ -1324,6 +1507,30 @@ extension UserQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByMemoryEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'memoryEnabled', Sort.asc); + }); + } + + QueryBuilder thenByMemoryEnabledDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'memoryEnabled', Sort.desc); + }); + } + + QueryBuilder thenByProfileImagePath() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'profileImagePath', Sort.asc); + }); + } + + QueryBuilder thenByProfileImagePathDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'profileImagePath', Sort.desc); + }); + } + QueryBuilder thenByUpdatedAt() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'updatedAt', Sort.asc); @@ -1384,6 +1591,20 @@ extension UserQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByMemoryEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'memoryEnabled'); + }); + } + + QueryBuilder distinctByProfileImagePath( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'profileImagePath', + caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByUpdatedAt() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'updatedAt'); @@ -1440,6 +1661,18 @@ extension UserQueryProperty on QueryBuilder { }); } + QueryBuilder memoryEnabledProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'memoryEnabled'); + }); + } + + QueryBuilder profileImagePathProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'profileImagePath'); + }); + } + QueryBuilder updatedAtProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'updatedAt'); diff --git a/mobile/makefile b/mobile/makefile index f4ce1450d54b4..a04fe362156db 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 531e2cf199c38..0a25b4df4d803 100644 --- a/mobile/openapi/doc/CreateUserDto.md +++ b/mobile/openapi/doc/CreateUserDto.md @@ -12,6 +12,7 @@ Name | Type | Description | Notes **externalPath** | **String** | | [optional] **firstName** | **String** | | **lastName** | **String** | | +**memoriesEnabled** | **bool** | | [optional] **password** | **String** | | **storageLabel** | **String** | | [optional] diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index d3b41f3c38a79..ff6bc8d4299ef 100644 --- a/mobile/openapi/doc/UpdateUserDto.md +++ b/mobile/openapi/doc/UpdateUserDto.md @@ -14,6 +14,7 @@ Name | Type | Description | Notes **id** | **String** | | **isAdmin** | **bool** | | [optional] **lastName** | **String** | | [optional] +**memoriesEnabled** | **bool** | | [optional] **password** | **String** | | [optional] **shouldChangePassword** | **bool** | | [optional] **storageLabel** | **String** | | [optional] diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index 320827f984dbd..6455c12d0378a 100644 --- a/mobile/openapi/doc/UserResponseDto.md +++ b/mobile/openapi/doc/UserResponseDto.md @@ -16,6 +16,7 @@ Name | Type | Description | Notes **id** | **String** | | **isAdmin** | **bool** | | **lastName** | **String** | | +**memoriesEnabled** | **bool** | | **oauthId** | **String** | | **profileImagePath** | **String** | | **shouldChangePassword** | **bool** | | diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart index 0535d940604fa..1345ac995cfff 100644 --- a/mobile/openapi/lib/model/create_user_dto.dart +++ b/mobile/openapi/lib/model/create_user_dto.dart @@ -17,6 +17,7 @@ class CreateUserDto { this.externalPath, required this.firstName, required this.lastName, + this.memoriesEnabled, required this.password, this.storageLabel, }); @@ -29,6 +30,14 @@ class CreateUserDto { String lastName; + /// + /// 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? memoriesEnabled; + String password; String? storageLabel; @@ -39,6 +48,7 @@ class CreateUserDto { other.externalPath == externalPath && other.firstName == firstName && other.lastName == lastName && + other.memoriesEnabled == memoriesEnabled && other.password == password && other.storageLabel == storageLabel; @@ -49,11 +59,12 @@ class CreateUserDto { (externalPath == null ? 0 : externalPath!.hashCode) + (firstName.hashCode) + (lastName.hashCode) + + (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (password.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, firstName=$firstName, lastName=$lastName, password=$password, storageLabel=$storageLabel]'; + String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, firstName=$firstName, lastName=$lastName, memoriesEnabled=$memoriesEnabled, password=$password, storageLabel=$storageLabel]'; Map toJson() { final json = {}; @@ -65,6 +76,11 @@ class CreateUserDto { } json[r'firstName'] = this.firstName; json[r'lastName'] = this.lastName; + if (this.memoriesEnabled != null) { + json[r'memoriesEnabled'] = this.memoriesEnabled; + } else { + // json[r'memoriesEnabled'] = null; + } json[r'password'] = this.password; if (this.storageLabel != null) { json[r'storageLabel'] = this.storageLabel; @@ -86,6 +102,7 @@ class CreateUserDto { externalPath: mapValueOfType(json, r'externalPath'), firstName: mapValueOfType(json, r'firstName')!, lastName: mapValueOfType(json, r'lastName')!, + memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), password: mapValueOfType(json, r'password')!, storageLabel: mapValueOfType(json, r'storageLabel'), ); diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index 71f3f16e8a745..6202c6d1eff08 100644 --- a/mobile/openapi/lib/model/update_user_dto.dart +++ b/mobile/openapi/lib/model/update_user_dto.dart @@ -19,6 +19,7 @@ class UpdateUserDto { required this.id, this.isAdmin, this.lastName, + this.memoriesEnabled, this.password, this.shouldChangePassword, this.storageLabel, @@ -66,6 +67,14 @@ class UpdateUserDto { /// String? lastName; + /// + /// 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? memoriesEnabled; + /// /// 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 @@ -98,6 +107,7 @@ class UpdateUserDto { other.id == id && other.isAdmin == isAdmin && other.lastName == lastName && + other.memoriesEnabled == memoriesEnabled && other.password == password && other.shouldChangePassword == shouldChangePassword && other.storageLabel == storageLabel; @@ -111,12 +121,13 @@ class UpdateUserDto { (id.hashCode) + (isAdmin == null ? 0 : isAdmin!.hashCode) + (lastName == null ? 0 : lastName!.hashCode) + + (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (password == null ? 0 : password!.hashCode) + (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'UpdateUserDto[email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; + String toString() => 'UpdateUserDto[email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, memoriesEnabled=$memoriesEnabled, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; @@ -146,6 +157,11 @@ class UpdateUserDto { } else { // json[r'lastName'] = null; } + if (this.memoriesEnabled != null) { + json[r'memoriesEnabled'] = this.memoriesEnabled; + } else { + // json[r'memoriesEnabled'] = null; + } if (this.password != null) { json[r'password'] = this.password; } else { @@ -178,6 +194,7 @@ class UpdateUserDto { id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin'), lastName: mapValueOfType(json, r'lastName'), + memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), password: mapValueOfType(json, r'password'), shouldChangePassword: mapValueOfType(json, r'shouldChangePassword'), storageLabel: mapValueOfType(json, r'storageLabel'), diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 2720827d799f5..5ecc26b492adc 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -21,6 +21,7 @@ class UserResponseDto { required this.id, required this.isAdmin, required this.lastName, + required this.memoriesEnabled, required this.oauthId, required this.profileImagePath, required this.shouldChangePassword, @@ -44,6 +45,8 @@ class UserResponseDto { String lastName; + bool memoriesEnabled; + String oauthId; String profileImagePath; @@ -64,6 +67,7 @@ class UserResponseDto { other.id == id && other.isAdmin == isAdmin && other.lastName == lastName && + other.memoriesEnabled == memoriesEnabled && other.oauthId == oauthId && other.profileImagePath == profileImagePath && other.shouldChangePassword == shouldChangePassword && @@ -81,6 +85,7 @@ class UserResponseDto { (id.hashCode) + (isAdmin.hashCode) + (lastName.hashCode) + + (memoriesEnabled.hashCode) + (oauthId.hashCode) + (profileImagePath.hashCode) + (shouldChangePassword.hashCode) + @@ -88,7 +93,7 @@ class UserResponseDto { (updatedAt.hashCode); @override - String toString() => 'UserResponseDto[createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + String toString() => 'UserResponseDto[createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, memoriesEnabled=$memoriesEnabled, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -108,6 +113,7 @@ class UserResponseDto { json[r'id'] = this.id; json[r'isAdmin'] = this.isAdmin; json[r'lastName'] = this.lastName; + json[r'memoriesEnabled'] = this.memoriesEnabled; json[r'oauthId'] = this.oauthId; json[r'profileImagePath'] = this.profileImagePath; json[r'shouldChangePassword'] = this.shouldChangePassword; @@ -136,6 +142,7 @@ class UserResponseDto { id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, lastName: mapValueOfType(json, r'lastName')!, + memoriesEnabled: mapValueOfType(json, r'memoriesEnabled')!, oauthId: mapValueOfType(json, r'oauthId')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, @@ -196,6 +203,7 @@ class UserResponseDto { 'id', 'isAdmin', 'lastName', + 'memoriesEnabled', 'oauthId', 'profileImagePath', 'shouldChangePassword', diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart index 872635e2fb959..9ce64c1e054ad 100644 --- a/mobile/openapi/test/create_user_dto_test.dart +++ b/mobile/openapi/test/create_user_dto_test.dart @@ -36,6 +36,11 @@ void main() { // TODO }); + // bool memoriesEnabled + test('to test the property `memoriesEnabled`', () async { + // TODO + }); + // String password test('to test the property `password`', () async { // TODO diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index 06543643549f3..511de33af03a0 100644 --- a/mobile/openapi/test/update_user_dto_test.dart +++ b/mobile/openapi/test/update_user_dto_test.dart @@ -46,6 +46,11 @@ void main() { // TODO }); + // bool memoriesEnabled + test('to test the property `memoriesEnabled`', () async { + // TODO + }); + // String password test('to test the property `password`', () async { // TODO diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart index cd6fa7694f43a..9e2095607b083 100644 --- a/mobile/openapi/test/user_response_dto_test.dart +++ b/mobile/openapi/test/user_response_dto_test.dart @@ -56,6 +56,11 @@ void main() { // TODO }); + // bool memoriesEnabled + test('to test the property `memoriesEnabled`', () async { + // TODO + }); + // String oauthId test('to test the property `oauthId`', () async { // TODO diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 1aac93051b66a..bbf28e8d19823 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 50eed510d6d1f..febacbb385e1c 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 c8e04896950b7..ac15abc8c0727 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 afb5aec237ebf..c9a9945af452f 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 7eb98f5384904..a1e053855e8e4 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 a2bd508837bc4..9a3372ad55358 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 e7c0f7f2b1488..6c59d9ca71d33 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 a573a8c20d62a..ef1a07d1770a9 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 7cdac1f824ad6..e6555153aef83 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 0000000000000..72387490804ea --- /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 0ba37e33cc79d..90c79ed966a9c 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 4367bf60ef2e4..f2a8dcab81b43 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 a4a8ff2759d6f..c58a4a4c533a6 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 0000000000000..9e269fc092320 --- /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 76311414c30c4..376c78e63e2ea 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 ebf280159e09c..507242fe8b0d7 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: '', });