mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web,server): user memory settings (#3628)
* feat(web,server): user preference for time-based memories * chore: open api * dev: mobile * fix: update * mobile work --------- Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: Alex Tran <Alex.Tran@conductix.com>
This commit is contained in:
		
							
								
								
									
										18
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -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} | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -97,12 +97,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|   Future<void> 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<AuthenticationState> { | ||||
|         user = User.fromDto(userResponseDto); | ||||
|  | ||||
|         retResult = true; | ||||
|       } | ||||
|       else { | ||||
|       } else { | ||||
|         _log.severe("Unable to get user information from the server."); | ||||
|         return false; | ||||
|       } | ||||
|   | ||||
| @@ -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(); | ||||
|   } | ||||
|   | ||||
| @@ -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<Album> albums = IsarLinks<Album>(); | ||||
|   @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; | ||||
| } | ||||
|   | ||||
| @@ -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<P>( | ||||
|     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<User, User, QFilterCondition> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> memoryEnabledEqualTo( | ||||
|       bool value) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'memoryEnabled', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> profileImagePathEqualTo( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'profileImagePath', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> 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<User, User, QAfterFilterCondition> 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<User, User, QAfterFilterCondition> 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<User, User, QAfterFilterCondition> profileImagePathStartsWith( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.startsWith( | ||||
|         property: r'profileImagePath', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> profileImagePathEndsWith( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.endsWith( | ||||
|         property: r'profileImagePath', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> profileImagePathContains( | ||||
|       String value, | ||||
|       {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.contains( | ||||
|         property: r'profileImagePath', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> profileImagePathMatches( | ||||
|       String pattern, | ||||
|       {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.matches( | ||||
|         property: r'profileImagePath', | ||||
|         wildcard: pattern, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> profileImagePathIsEmpty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'profileImagePath', | ||||
|         value: '', | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> profileImagePathIsNotEmpty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         property: r'profileImagePath', | ||||
|         value: '', | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterFilterCondition> updatedAtEqualTo( | ||||
|       DateTime value) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
| @@ -1214,6 +1373,30 @@ extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> sortByMemoryEnabled() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'memoryEnabled', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> sortByMemoryEnabledDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'memoryEnabled', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> sortByProfileImagePath() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'profileImagePath', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> sortByProfileImagePathDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'profileImagePath', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> sortByUpdatedAt() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'updatedAt', Sort.asc); | ||||
| @@ -1324,6 +1507,30 @@ extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> thenByMemoryEnabled() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'memoryEnabled', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> thenByMemoryEnabledDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'memoryEnabled', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> thenByProfileImagePath() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'profileImagePath', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> thenByProfileImagePathDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'profileImagePath', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QAfterSortBy> thenByUpdatedAt() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'updatedAt', Sort.asc); | ||||
| @@ -1384,6 +1591,20 @@ extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QDistinct> distinctByMemoryEnabled() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addDistinctBy(r'memoryEnabled'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QDistinct> distinctByProfileImagePath( | ||||
|       {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addDistinctBy(r'profileImagePath', | ||||
|           caseSensitive: caseSensitive); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, User, QDistinct> distinctByUpdatedAt() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addDistinctBy(r'updatedAt'); | ||||
| @@ -1440,6 +1661,18 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, bool, QQueryOperations> memoryEnabledProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'memoryEnabled'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, String, QQueryOperations> profileImagePathProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'profileImagePath'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<User, DateTime, QQueryOperations> updatedAtProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'updatedAt'); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/CreateUserDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/CreateUserDto.md
									
									
									
										generated
									
									
									
								
							| @@ -12,6 +12,7 @@ Name | Type | Description | Notes | ||||
| **externalPath** | **String** |  | [optional]  | ||||
| **firstName** | **String** |  |  | ||||
| **lastName** | **String** |  |  | ||||
| **memoriesEnabled** | **bool** |  | [optional]  | ||||
| **password** | **String** |  |  | ||||
| **storageLabel** | **String** |  | [optional]  | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/UpdateUserDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/UpdateUserDto.md
									
									
									
										generated
									
									
									
								
							| @@ -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]  | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/UserResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/UserResponseDto.md
									
									
									
										generated
									
									
									
								
							| @@ -16,6 +16,7 @@ Name | Type | Description | Notes | ||||
| **id** | **String** |  |  | ||||
| **isAdmin** | **bool** |  |  | ||||
| **lastName** | **String** |  |  | ||||
| **memoriesEnabled** | **bool** |  |  | ||||
| **oauthId** | **String** |  |  | ||||
| **profileImagePath** | **String** |  |  | ||||
| **shouldChangePassword** | **bool** |  |  | ||||
|   | ||||
							
								
								
									
										19
									
								
								mobile/openapi/lib/model/create_user_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								mobile/openapi/lib/model/create_user_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -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<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -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<String>(json, r'externalPath'), | ||||
|         firstName: mapValueOfType<String>(json, r'firstName')!, | ||||
|         lastName: mapValueOfType<String>(json, r'lastName')!, | ||||
|         memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'), | ||||
|         password: mapValueOfType<String>(json, r'password')!, | ||||
|         storageLabel: mapValueOfType<String>(json, r'storageLabel'), | ||||
|       ); | ||||
|   | ||||
							
								
								
									
										19
									
								
								mobile/openapi/lib/model/update_user_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								mobile/openapi/lib/model/update_user_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -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<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -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<String>(json, r'id')!, | ||||
|         isAdmin: mapValueOfType<bool>(json, r'isAdmin'), | ||||
|         lastName: mapValueOfType<String>(json, r'lastName'), | ||||
|         memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'), | ||||
|         password: mapValueOfType<String>(json, r'password'), | ||||
|         shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'), | ||||
|         storageLabel: mapValueOfType<String>(json, r'storageLabel'), | ||||
|   | ||||
							
								
								
									
										10
									
								
								mobile/openapi/lib/model/user_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/lib/model/user_response_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -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<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -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<String>(json, r'id')!, | ||||
|         isAdmin: mapValueOfType<bool>(json, r'isAdmin')!, | ||||
|         lastName: mapValueOfType<String>(json, r'lastName')!, | ||||
|         memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled')!, | ||||
|         oauthId: mapValueOfType<String>(json, r'oauthId')!, | ||||
|         profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, | ||||
|         shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!, | ||||
| @@ -196,6 +203,7 @@ class UserResponseDto { | ||||
|     'id', | ||||
|     'isAdmin', | ||||
|     'lastName', | ||||
|     'memoriesEnabled', | ||||
|     'oauthId', | ||||
|     'profileImagePath', | ||||
|     'shouldChangePassword', | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/create_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/create_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/update_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/update_user_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/user_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/user_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -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 | ||||
|   | ||||
| @@ -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" | ||||
|       }, | ||||
|   | ||||
| @@ -176,6 +176,7 @@ describe(AlbumService.name, () => { | ||||
|           deletedAt: null, | ||||
|           updatedAt: new Date('2021-01-01'), | ||||
|           externalPath: null, | ||||
|           memoriesEnabled: true, | ||||
|         }, | ||||
|         ownerId: 'admin_id', | ||||
|         shared: false, | ||||
|   | ||||
| @@ -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: <UserResponseDto>{ | ||||
|     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: <UserResponseDto>{ | ||||
|     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, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -45,4 +45,8 @@ export class UpdateUserDto { | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   shouldChangePassword?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   memoriesEnabled?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -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<UserEntity>({ | ||||
|   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<UserResponseDto>({ | ||||
|   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, | ||||
|         }, | ||||
|       ]); | ||||
|     }); | ||||
|   | ||||
| @@ -54,6 +54,9 @@ export class UserEntity { | ||||
|   @UpdateDateColumn({ type: 'timestamptz' }) | ||||
|   updatedAt!: Date; | ||||
|  | ||||
|   @Column({ default: true }) | ||||
|   memoriesEnabled!: boolean; | ||||
|  | ||||
|   @OneToMany(() => TagEntity, (tag) => tag.user) | ||||
|   tags!: TagEntity[]; | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,13 @@ | ||||
| import { MigrationInterface, QueryRunner } from 'typeorm'; | ||||
|  | ||||
| export class UserMemoryPreference1691600216749 implements MigrationInterface { | ||||
|   name = 'UserMemoryPreference1691600216749'; | ||||
|  | ||||
|   public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|     await queryRunner.query(`ALTER TABLE "users" ADD "memoriesEnabled" boolean NOT NULL DEFAULT true`); | ||||
|   } | ||||
|  | ||||
|   public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|     await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "memoriesEnabled"`); | ||||
|   } | ||||
| } | ||||
| @@ -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', () => { | ||||
|   | ||||
							
								
								
									
										4
									
								
								server/test/fixtures/user.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								server/test/fixtures/user.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -17,6 +17,7 @@ export const userStub = { | ||||
|     updatedAt: new Date('2021-01-01'), | ||||
|     tags: [], | ||||
|     assets: [], | ||||
|     memoriesEnabled: true, | ||||
|   }), | ||||
|   user1: Object.freeze<UserEntity>({ | ||||
|     ...authStub.user1, | ||||
| @@ -33,6 +34,7 @@ export const userStub = { | ||||
|     updatedAt: new Date('2021-01-01'), | ||||
|     tags: [], | ||||
|     assets: [], | ||||
|     memoriesEnabled: true, | ||||
|   }), | ||||
|   user2: Object.freeze<UserEntity>({ | ||||
|     ...authStub.user2, | ||||
| @@ -49,6 +51,7 @@ export const userStub = { | ||||
|     updatedAt: new Date('2021-01-01'), | ||||
|     tags: [], | ||||
|     assets: [], | ||||
|     memoriesEnabled: true, | ||||
|   }), | ||||
|   storageLabel: Object.freeze<UserEntity>({ | ||||
|     ...authStub.user1, | ||||
| @@ -65,5 +68,6 @@ export const userStub = { | ||||
|     updatedAt: new Date('2021-01-01'), | ||||
|     tags: [], | ||||
|     assets: [], | ||||
|     memoriesEnabled: true, | ||||
|   }), | ||||
| }; | ||||
|   | ||||
							
								
								
									
										18
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -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} | ||||
|   | ||||
| @@ -0,0 +1,49 @@ | ||||
| <script lang="ts"> | ||||
|   import { | ||||
|     notificationController, | ||||
|     NotificationType, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import { api, UserResponseDto } from '@api'; | ||||
|   import { fade } from 'svelte/transition'; | ||||
|   import { handleError } from '../../utils/handle-error'; | ||||
|   import SettingSwitch from '../admin-page/settings/setting-switch.svelte'; | ||||
|   import Button from '../elements/buttons/button.svelte'; | ||||
|  | ||||
|   export let user: UserResponseDto; | ||||
|  | ||||
|   const handleSave = async () => { | ||||
|     try { | ||||
|       const { data } = await api.userApi.updateUser({ | ||||
|         updateUserDto: { | ||||
|           id: user.id, | ||||
|           memoriesEnabled: user.memoriesEnabled, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       Object.assign(user, data); | ||||
|  | ||||
|       notificationController.show({ message: 'Saved settings', type: NotificationType.Info }); | ||||
|     } catch (error) { | ||||
|       handleError(error, 'Unable to update settings'); | ||||
|     } | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <section class="my-4"> | ||||
|   <div in:fade={{ duration: 500 }}> | ||||
|     <form autocomplete="off" on:submit|preventDefault> | ||||
|       <div class="ml-4 mt-4 flex flex-col gap-4"> | ||||
|         <div class="ml-4"> | ||||
|           <SettingSwitch | ||||
|             title="Time-based memories" | ||||
|             subtitle="Photos from previous years" | ||||
|             bind:checked={user.memoriesEnabled} | ||||
|           /> | ||||
|         </div> | ||||
|         <div class="flex justify-end"> | ||||
|           <Button type="submit" size="sm" on:click={() => handleSave()}>Save</Button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
|   </div> | ||||
| </section> | ||||
| @@ -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 @@ | ||||
|   <DeviceList /> | ||||
| </SettingAccordion> | ||||
|  | ||||
| <SettingAccordion title="Memories" subtitle="Manage what you see in your memories."> | ||||
|   <MemoriesSettings {user} /> | ||||
| </SettingAccordion> | ||||
|  | ||||
| {#if oauthEnabled} | ||||
|   <SettingAccordion | ||||
|     title="OAuth" | ||||
|   | ||||
| @@ -59,7 +59,9 @@ | ||||
|   <svelte:fragment slot="content"> | ||||
|     {#if assetCount} | ||||
|       <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE}> | ||||
|         <MemoryLane /> | ||||
|         {#if data.user.memoriesEnabled} | ||||
|           <MemoryLane /> | ||||
|         {/if} | ||||
|       </AssetGrid> | ||||
|     {:else} | ||||
|       <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={() => openFileUploadDialog()} /> | ||||
|   | ||||
| @@ -15,5 +15,6 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({ | ||||
|   createdAt: Sync.each(() => faker.date.past().toISOString()), | ||||
|   deletedAt: null, | ||||
|   updatedAt: Sync.each(() => faker.date.past().toISOString()), | ||||
|   memoriesEnabled: true, | ||||
|   oauthId: '', | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user