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:
Jason Rasmussen 2023-08-09 22:01:16 -04:00 committed by GitHub
parent 343087e2b4
commit a6eb227330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 519 additions and 25 deletions

View File

@ -954,6 +954,12 @@ export interface CreateUserDto {
* @memberof CreateUserDto * @memberof CreateUserDto
*/ */
'lastName': string; 'lastName': string;
/**
*
* @type {boolean}
* @memberof CreateUserDto
*/
'memoriesEnabled'?: boolean;
/** /**
* *
* @type {string} * @type {string}
@ -2995,6 +3001,12 @@ export interface UpdateUserDto {
* @memberof UpdateUserDto * @memberof UpdateUserDto
*/ */
'lastName'?: string; 'lastName'?: string;
/**
*
* @type {boolean}
* @memberof UpdateUserDto
*/
'memoriesEnabled'?: boolean;
/** /**
* *
* @type {string} * @type {string}
@ -3124,6 +3136,12 @@ export interface UserResponseDto {
* @memberof UserResponseDto * @memberof UserResponseDto
*/ */
'lastName': string; 'lastName': string;
/**
*
* @type {boolean}
* @memberof UserResponseDto
*/
'memoriesEnabled': boolean;
/** /**
* *
* @type {string} * @type {string}

View File

@ -342,7 +342,10 @@ class HomePage extends HookConsumerWidget {
listener: selectionListener, listener: selectionListener,
selectionActive: selectionEnabledHook.value, selectionActive: selectionEnabledHook.value,
onRefresh: refreshAssets, onRefresh: refreshAssets,
topWidget: const MemoryLane(), topWidget:
(currentUser != null && currentUser.memoryEnabled)
? const MemoryLane()
: const SizedBox(),
), ),
error: (error, _) => Center(child: Text(error.toString())), error: (error, _) => Center(child: Text(error.toString())),
loading: buildLoadingIndicator, loading: buildLoadingIndicator,

View File

@ -97,12 +97,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Future<void> logout() async { Future<void> logout() async {
var log = Logger('AuthenticationNotifier'); var log = Logger('AuthenticationNotifier');
try { try {
String? userEmail = Store.tryGet(StoreKey.currentUser)?.email; String? userEmail = Store.tryGet(StoreKey.currentUser)?.email;
_apiService.authenticationApi _apiService.authenticationApi
.logout() .logout()
.then((_) => log.info("Logout was successfull for $userEmail")) .then((_) => log.info("Logout was successful for $userEmail"))
.onError( .onError(
(error, stackTrace) => (error, stackTrace) =>
log.severe("Error logging out $userEmail", error, stackTrace), log.severe("Error logging out $userEmail", error, stackTrace),
@ -186,8 +185,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
user = User.fromDto(userResponseDto); user = User.fromDto(userResponseDto);
retResult = true; retResult = true;
} } else {
else {
_log.severe("Unable to get user information from the server."); _log.severe("Unable to get user information from the server.");
return false; return false;
} }

View File

@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/memories/providers/memory.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/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.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'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
class TabNavigationObserver extends AutoRouterObserver { class TabNavigationObserver extends AutoRouterObserver {
@ -46,6 +50,20 @@ class TabNavigationObserver extends AutoRouterObserver {
if (route.name == 'HomeRoute') { if (route.name == 'HomeRoute') {
ref.invalidate(memoryFutureProvider); 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(); ref.watch(serverInfoProvider.notifier).getServerVersion();
} }

View File

@ -17,6 +17,7 @@ class User {
this.isPartnerSharedBy = false, this.isPartnerSharedBy = false,
this.isPartnerSharedWith = false, this.isPartnerSharedWith = false,
this.profileImagePath = '', this.profileImagePath = '',
this.memoryEnabled = true,
}); });
Id get isarId => fastHash(id); Id get isarId => fastHash(id);
@ -30,7 +31,8 @@ class User {
isPartnerSharedBy = false, isPartnerSharedBy = false,
isPartnerSharedWith = false, isPartnerSharedWith = false,
profileImagePath = dto.profileImagePath, profileImagePath = dto.profileImagePath,
isAdmin = dto.isAdmin; isAdmin = dto.isAdmin,
memoryEnabled = dto.memoriesEnabled;
@Index(unique: true, replace: false, type: IndexType.hash) @Index(unique: true, replace: false, type: IndexType.hash)
String id; String id;
@ -42,6 +44,7 @@ class User {
bool isPartnerSharedWith; bool isPartnerSharedWith;
bool isAdmin; bool isAdmin;
String profileImagePath; String profileImagePath;
bool memoryEnabled;
@Backlink(to: 'owner') @Backlink(to: 'owner')
final IsarLinks<Album> albums = IsarLinks<Album>(); final IsarLinks<Album> albums = IsarLinks<Album>();
@Backlink(to: 'sharedUsers') @Backlink(to: 'sharedUsers')
@ -58,7 +61,8 @@ class User {
isPartnerSharedBy == other.isPartnerSharedBy && isPartnerSharedBy == other.isPartnerSharedBy &&
isPartnerSharedWith == other.isPartnerSharedWith && isPartnerSharedWith == other.isPartnerSharedWith &&
profileImagePath == other.profileImagePath && profileImagePath == other.profileImagePath &&
isAdmin == other.isAdmin; isAdmin == other.isAdmin &&
memoryEnabled == other.memoryEnabled;
} }
@override @override
@ -72,5 +76,6 @@ class User {
isPartnerSharedBy.hashCode ^ isPartnerSharedBy.hashCode ^
isPartnerSharedWith.hashCode ^ isPartnerSharedWith.hashCode ^
profileImagePath.hashCode ^ profileImagePath.hashCode ^
isAdmin.hashCode; isAdmin.hashCode ^
memoryEnabled.hashCode;
} }

View File

@ -52,8 +52,18 @@ const UserSchema = CollectionSchema(
name: r'lastName', name: r'lastName',
type: IsarType.string, type: IsarType.string,
), ),
r'updatedAt': PropertySchema( r'memoryEnabled': PropertySchema(
id: 7, 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', name: r'updatedAt',
type: IsarType.dateTime, type: IsarType.dateTime,
) )
@ -111,6 +121,7 @@ int _userEstimateSize(
bytesCount += 3 + object.firstName.length * 3; bytesCount += 3 + object.firstName.length * 3;
bytesCount += 3 + object.id.length * 3; bytesCount += 3 + object.id.length * 3;
bytesCount += 3 + object.lastName.length * 3; bytesCount += 3 + object.lastName.length * 3;
bytesCount += 3 + object.profileImagePath.length * 3;
return bytesCount; return bytesCount;
} }
@ -127,7 +138,9 @@ void _userSerialize(
writer.writeBool(offsets[4], object.isPartnerSharedBy); writer.writeBool(offsets[4], object.isPartnerSharedBy);
writer.writeBool(offsets[5], object.isPartnerSharedWith); writer.writeBool(offsets[5], object.isPartnerSharedWith);
writer.writeString(offsets[6], object.lastName); 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( User _userDeserialize(
@ -144,7 +157,9 @@ User _userDeserialize(
isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false, isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false,
isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false, isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false,
lastName: reader.readString(offsets[6]), 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; return object;
} }
@ -171,6 +186,10 @@ P _userDeserializeProp<P>(
case 6: case 6:
return (reader.readString(offset)) as P; return (reader.readString(offset)) as P;
case 7: 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; return (reader.readDateTime(offset)) as P;
default: default:
throw IsarError('Unknown property with id $propertyId'); 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( QueryBuilder<User, User, QAfterFilterCondition> updatedAtEqualTo(
DateTime value) { DateTime value) {
return QueryBuilder.apply(this, (query) { 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() { QueryBuilder<User, User, QAfterSortBy> sortByUpdatedAt() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.asc); 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() { QueryBuilder<User, User, QAfterSortBy> thenByUpdatedAt() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.asc); 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() { QueryBuilder<User, User, QDistinct> distinctByUpdatedAt() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'updatedAt'); 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() { QueryBuilder<User, DateTime, QQueryOperations> updatedAtProperty() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'updatedAt'); return query.addPropertyName(r'updatedAt');

View File

@ -1,8 +1,8 @@
build: build:
flutter packages pub run build_runner build --delete-conflicting-outputs dart run build_runner build --delete-conflicting-outputs
watch: watch:
flutter packages pub run build_runner watch --delete-conflicting-outputs dart run build_runner watch --delete-conflicting-outputs
create_app_icon: create_app_icon:
flutter pub run flutter_launcher_icons:main flutter pub run flutter_launcher_icons:main

View File

@ -12,6 +12,7 @@ Name | Type | Description | Notes
**externalPath** | **String** | | [optional] **externalPath** | **String** | | [optional]
**firstName** | **String** | | **firstName** | **String** | |
**lastName** | **String** | | **lastName** | **String** | |
**memoriesEnabled** | **bool** | | [optional]
**password** | **String** | | **password** | **String** | |
**storageLabel** | **String** | | [optional] **storageLabel** | **String** | | [optional]

View File

@ -14,6 +14,7 @@ Name | Type | Description | Notes
**id** | **String** | | **id** | **String** | |
**isAdmin** | **bool** | | [optional] **isAdmin** | **bool** | | [optional]
**lastName** | **String** | | [optional] **lastName** | **String** | | [optional]
**memoriesEnabled** | **bool** | | [optional]
**password** | **String** | | [optional] **password** | **String** | | [optional]
**shouldChangePassword** | **bool** | | [optional] **shouldChangePassword** | **bool** | | [optional]
**storageLabel** | **String** | | [optional] **storageLabel** | **String** | | [optional]

View File

@ -16,6 +16,7 @@ Name | Type | Description | Notes
**id** | **String** | | **id** | **String** | |
**isAdmin** | **bool** | | **isAdmin** | **bool** | |
**lastName** | **String** | | **lastName** | **String** | |
**memoriesEnabled** | **bool** | |
**oauthId** | **String** | | **oauthId** | **String** | |
**profileImagePath** | **String** | | **profileImagePath** | **String** | |
**shouldChangePassword** | **bool** | | **shouldChangePassword** | **bool** | |

View File

@ -17,6 +17,7 @@ class CreateUserDto {
this.externalPath, this.externalPath,
required this.firstName, required this.firstName,
required this.lastName, required this.lastName,
this.memoriesEnabled,
required this.password, required this.password,
this.storageLabel, this.storageLabel,
}); });
@ -29,6 +30,14 @@ class CreateUserDto {
String lastName; 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 password;
String? storageLabel; String? storageLabel;
@ -39,6 +48,7 @@ class CreateUserDto {
other.externalPath == externalPath && other.externalPath == externalPath &&
other.firstName == firstName && other.firstName == firstName &&
other.lastName == lastName && other.lastName == lastName &&
other.memoriesEnabled == memoriesEnabled &&
other.password == password && other.password == password &&
other.storageLabel == storageLabel; other.storageLabel == storageLabel;
@ -49,11 +59,12 @@ class CreateUserDto {
(externalPath == null ? 0 : externalPath!.hashCode) + (externalPath == null ? 0 : externalPath!.hashCode) +
(firstName.hashCode) + (firstName.hashCode) +
(lastName.hashCode) + (lastName.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
(password.hashCode) + (password.hashCode) +
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -65,6 +76,11 @@ class CreateUserDto {
} }
json[r'firstName'] = this.firstName; json[r'firstName'] = this.firstName;
json[r'lastName'] = this.lastName; json[r'lastName'] = this.lastName;
if (this.memoriesEnabled != null) {
json[r'memoriesEnabled'] = this.memoriesEnabled;
} else {
// json[r'memoriesEnabled'] = null;
}
json[r'password'] = this.password; json[r'password'] = this.password;
if (this.storageLabel != null) { if (this.storageLabel != null) {
json[r'storageLabel'] = this.storageLabel; json[r'storageLabel'] = this.storageLabel;
@ -86,6 +102,7 @@ class CreateUserDto {
externalPath: mapValueOfType<String>(json, r'externalPath'), externalPath: mapValueOfType<String>(json, r'externalPath'),
firstName: mapValueOfType<String>(json, r'firstName')!, firstName: mapValueOfType<String>(json, r'firstName')!,
lastName: mapValueOfType<String>(json, r'lastName')!, lastName: mapValueOfType<String>(json, r'lastName')!,
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
password: mapValueOfType<String>(json, r'password')!, password: mapValueOfType<String>(json, r'password')!,
storageLabel: mapValueOfType<String>(json, r'storageLabel'), storageLabel: mapValueOfType<String>(json, r'storageLabel'),
); );

View File

@ -19,6 +19,7 @@ class UpdateUserDto {
required this.id, required this.id,
this.isAdmin, this.isAdmin,
this.lastName, this.lastName,
this.memoriesEnabled,
this.password, this.password,
this.shouldChangePassword, this.shouldChangePassword,
this.storageLabel, this.storageLabel,
@ -66,6 +67,14 @@ class UpdateUserDto {
/// ///
String? lastName; 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 /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
@ -98,6 +107,7 @@ class UpdateUserDto {
other.id == id && other.id == id &&
other.isAdmin == isAdmin && other.isAdmin == isAdmin &&
other.lastName == lastName && other.lastName == lastName &&
other.memoriesEnabled == memoriesEnabled &&
other.password == password && other.password == password &&
other.shouldChangePassword == shouldChangePassword && other.shouldChangePassword == shouldChangePassword &&
other.storageLabel == storageLabel; other.storageLabel == storageLabel;
@ -111,12 +121,13 @@ class UpdateUserDto {
(id.hashCode) + (id.hashCode) +
(isAdmin == null ? 0 : isAdmin!.hashCode) + (isAdmin == null ? 0 : isAdmin!.hashCode) +
(lastName == null ? 0 : lastName!.hashCode) + (lastName == null ? 0 : lastName!.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
(password == null ? 0 : password!.hashCode) + (password == null ? 0 : password!.hashCode) +
(shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) + (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) +
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -146,6 +157,11 @@ class UpdateUserDto {
} else { } else {
// json[r'lastName'] = null; // json[r'lastName'] = null;
} }
if (this.memoriesEnabled != null) {
json[r'memoriesEnabled'] = this.memoriesEnabled;
} else {
// json[r'memoriesEnabled'] = null;
}
if (this.password != null) { if (this.password != null) {
json[r'password'] = this.password; json[r'password'] = this.password;
} else { } else {
@ -178,6 +194,7 @@ class UpdateUserDto {
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isAdmin: mapValueOfType<bool>(json, r'isAdmin'), isAdmin: mapValueOfType<bool>(json, r'isAdmin'),
lastName: mapValueOfType<String>(json, r'lastName'), lastName: mapValueOfType<String>(json, r'lastName'),
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
password: mapValueOfType<String>(json, r'password'), password: mapValueOfType<String>(json, r'password'),
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'), shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword'),
storageLabel: mapValueOfType<String>(json, r'storageLabel'), storageLabel: mapValueOfType<String>(json, r'storageLabel'),

View File

@ -21,6 +21,7 @@ class UserResponseDto {
required this.id, required this.id,
required this.isAdmin, required this.isAdmin,
required this.lastName, required this.lastName,
required this.memoriesEnabled,
required this.oauthId, required this.oauthId,
required this.profileImagePath, required this.profileImagePath,
required this.shouldChangePassword, required this.shouldChangePassword,
@ -44,6 +45,8 @@ class UserResponseDto {
String lastName; String lastName;
bool memoriesEnabled;
String oauthId; String oauthId;
String profileImagePath; String profileImagePath;
@ -64,6 +67,7 @@ class UserResponseDto {
other.id == id && other.id == id &&
other.isAdmin == isAdmin && other.isAdmin == isAdmin &&
other.lastName == lastName && other.lastName == lastName &&
other.memoriesEnabled == memoriesEnabled &&
other.oauthId == oauthId && other.oauthId == oauthId &&
other.profileImagePath == profileImagePath && other.profileImagePath == profileImagePath &&
other.shouldChangePassword == shouldChangePassword && other.shouldChangePassword == shouldChangePassword &&
@ -81,6 +85,7 @@ class UserResponseDto {
(id.hashCode) + (id.hashCode) +
(isAdmin.hashCode) + (isAdmin.hashCode) +
(lastName.hashCode) + (lastName.hashCode) +
(memoriesEnabled.hashCode) +
(oauthId.hashCode) + (oauthId.hashCode) +
(profileImagePath.hashCode) + (profileImagePath.hashCode) +
(shouldChangePassword.hashCode) + (shouldChangePassword.hashCode) +
@ -88,7 +93,7 @@ class UserResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -108,6 +113,7 @@ class UserResponseDto {
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isAdmin'] = this.isAdmin; json[r'isAdmin'] = this.isAdmin;
json[r'lastName'] = this.lastName; json[r'lastName'] = this.lastName;
json[r'memoriesEnabled'] = this.memoriesEnabled;
json[r'oauthId'] = this.oauthId; json[r'oauthId'] = this.oauthId;
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
json[r'shouldChangePassword'] = this.shouldChangePassword; json[r'shouldChangePassword'] = this.shouldChangePassword;
@ -136,6 +142,7 @@ class UserResponseDto {
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!, isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
lastName: mapValueOfType<String>(json, r'lastName')!, lastName: mapValueOfType<String>(json, r'lastName')!,
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled')!,
oauthId: mapValueOfType<String>(json, r'oauthId')!, oauthId: mapValueOfType<String>(json, r'oauthId')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!, shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
@ -196,6 +203,7 @@ class UserResponseDto {
'id', 'id',
'isAdmin', 'isAdmin',
'lastName', 'lastName',
'memoriesEnabled',
'oauthId', 'oauthId',
'profileImagePath', 'profileImagePath',
'shouldChangePassword', 'shouldChangePassword',

View File

@ -36,6 +36,11 @@ void main() {
// TODO // TODO
}); });
// bool memoriesEnabled
test('to test the property `memoriesEnabled`', () async {
// TODO
});
// String password // String password
test('to test the property `password`', () async { test('to test the property `password`', () async {
// TODO // TODO

View File

@ -46,6 +46,11 @@ void main() {
// TODO // TODO
}); });
// bool memoriesEnabled
test('to test the property `memoriesEnabled`', () async {
// TODO
});
// String password // String password
test('to test the property `password`', () async { test('to test the property `password`', () async {
// TODO // TODO

View File

@ -56,6 +56,11 @@ void main() {
// TODO // TODO
}); });
// bool memoriesEnabled
test('to test the property `memoriesEnabled`', () async {
// TODO
});
// String oauthId // String oauthId
test('to test the property `oauthId`', () async { test('to test the property `oauthId`', () async {
// TODO // TODO

View File

@ -5396,6 +5396,9 @@
"lastName": { "lastName": {
"type": "string" "type": "string"
}, },
"memoriesEnabled": {
"type": "boolean"
},
"password": { "password": {
"type": "string" "type": "string"
}, },
@ -7004,6 +7007,9 @@
"lastName": { "lastName": {
"type": "string" "type": "string"
}, },
"memoriesEnabled": {
"type": "boolean"
},
"password": { "password": {
"type": "string" "type": "string"
}, },
@ -7092,6 +7098,9 @@
"lastName": { "lastName": {
"type": "string" "type": "string"
}, },
"memoriesEnabled": {
"type": "boolean"
},
"oauthId": { "oauthId": {
"type": "string" "type": "string"
}, },
@ -7123,7 +7132,8 @@
"createdAt", "createdAt",
"deletedAt", "deletedAt",
"updatedAt", "updatedAt",
"oauthId" "oauthId",
"memoriesEnabled"
], ],
"type": "object" "type": "object"
}, },

View File

@ -176,6 +176,7 @@ describe(AlbumService.name, () => {
deletedAt: null, deletedAt: null,
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
externalPath: null, externalPath: null,
memoriesEnabled: true,
}, },
ownerId: 'admin_id', ownerId: 'admin_id',
shared: false, shared: false,

View File

@ -1,10 +1,11 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { authStub, newPartnerRepositoryMock, partnerStub } from '@test'; import { authStub, newPartnerRepositoryMock, partnerStub } from '@test';
import { UserResponseDto } from '../index';
import { IPartnerRepository, PartnerDirection } from './partner.repository'; import { IPartnerRepository, PartnerDirection } from './partner.repository';
import { PartnerService } from './partner.service'; import { PartnerService } from './partner.service';
const responseDto = { const responseDto = {
admin: { admin: <UserResponseDto>{
email: 'admin@test.com', email: 'admin@test.com',
firstName: 'admin_first_name', firstName: 'admin_first_name',
id: 'admin_id', id: 'admin_id',
@ -18,8 +19,9 @@ const responseDto = {
deletedAt: null, deletedAt: null,
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
externalPath: null, externalPath: null,
memoriesEnabled: true,
}, },
user1: { user1: <UserResponseDto>{
email: 'immich@test.com', email: 'immich@test.com',
firstName: 'immich_first_name', firstName: 'immich_first_name',
id: 'user-id', id: 'user-id',
@ -33,6 +35,7 @@ const responseDto = {
deletedAt: null, deletedAt: null,
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
externalPath: null, externalPath: null,
memoriesEnabled: true,
}, },
}; };

View File

@ -1,5 +1,5 @@
import { Transform } from 'class-transformer'; 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'; import { toEmail, toSanitized } from '../../domain.util';
export class CreateUserDto { export class CreateUserDto {
@ -27,6 +27,10 @@ export class CreateUserDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
externalPath?: string | null; externalPath?: string | null;
@IsOptional()
@IsBoolean()
memoriesEnabled?: boolean;
} }
export class CreateAdminDto { export class CreateAdminDto {

View File

@ -45,4 +45,8 @@ export class UpdateUserDto {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
@IsOptional()
@IsBoolean()
memoriesEnabled?: boolean;
} }

View File

@ -14,6 +14,7 @@ export class UserResponseDto {
deletedAt!: Date | null; deletedAt!: Date | null;
updatedAt!: Date; updatedAt!: Date;
oauthId!: string; oauthId!: string;
memoriesEnabled!: boolean;
} }
export function mapUser(entity: UserEntity): UserResponseDto { export function mapUser(entity: UserEntity): UserResponseDto {
@ -31,5 +32,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
deletedAt: entity.deletedAt, deletedAt: entity.deletedAt,
updatedAt: entity.updatedAt, updatedAt: entity.updatedAt,
oauthId: entity.oauthId, oauthId: entity.oauthId,
memoriesEnabled: entity.memoriesEnabled,
}; };
} }

View File

@ -60,6 +60,7 @@ export class UserCore {
dto.externalPath = null; dto.externalPath = null;
} }
console.log(dto.memoriesEnabled);
return this.userRepository.update(id, dto); return this.userRepository.update(id, dto);
} catch (e) { } catch (e) {
Logger.error(e, 'Failed to update user info'); Logger.error(e, 'Failed to update user info');

View File

@ -16,6 +16,7 @@ import { ICryptoRepository } from '../crypto';
import { IJobRepository, JobName } from '../job'; import { IJobRepository, JobName } from '../job';
import { IStorageRepository } from '../storage'; import { IStorageRepository } from '../storage';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { UserResponseDto } from './response-dto';
import { IUserRepository } from './user.repository'; import { IUserRepository } from './user.repository';
import { UserService } from './user.service'; import { UserService } from './user.service';
@ -54,6 +55,7 @@ const adminUser: UserEntity = Object.freeze({
assets: [], assets: [],
storageLabel: 'admin', storageLabel: 'admin',
externalPath: null, externalPath: null,
memoriesEnabled: true,
}); });
const immichUser: UserEntity = Object.freeze({ const immichUser: UserEntity = Object.freeze({
@ -73,9 +75,10 @@ const immichUser: UserEntity = Object.freeze({
assets: [], assets: [],
storageLabel: null, storageLabel: null,
externalPath: null, externalPath: null,
memoriesEnabled: true,
}); });
const updatedImmichUser: UserEntity = Object.freeze({ const updatedImmichUser = Object.freeze<UserEntity>({
id: immichUserAuth.id, id: immichUserAuth.id,
email: 'immich@test.com', email: 'immich@test.com',
password: 'immich_password', password: 'immich_password',
@ -92,9 +95,10 @@ const updatedImmichUser: UserEntity = Object.freeze({
assets: [], assets: [],
storageLabel: null, storageLabel: null,
externalPath: null, externalPath: null,
memoriesEnabled: true,
}); });
const adminUserResponse = Object.freeze({ const adminUserResponse = Object.freeze<UserResponseDto>({
id: adminUserAuth.id, id: adminUserAuth.id,
email: 'admin@test.com', email: 'admin@test.com',
firstName: 'admin_first_name', firstName: 'admin_first_name',
@ -108,6 +112,7 @@ const adminUserResponse = Object.freeze({
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
storageLabel: 'admin', storageLabel: 'admin',
externalPath: null, externalPath: null,
memoriesEnabled: true,
}); });
describe(UserService.name, () => { describe(UserService.name, () => {
@ -158,6 +163,7 @@ describe(UserService.name, () => {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
storageLabel: 'admin', storageLabel: 'admin',
externalPath: null, externalPath: null,
memoriesEnabled: true,
}, },
]); ]);
}); });

View File

@ -54,6 +54,9 @@ export class UserEntity {
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: Date;
@Column({ default: true })
memoriesEnabled!: boolean;
@OneToMany(() => TagEntity, (tag) => tag.user) @OneToMany(() => TagEntity, (tag) => tag.user)
tags!: TagEntity[]; tags!: TagEntity[];

View File

@ -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"`);
}
}

View File

@ -143,6 +143,24 @@ describe(`${UserController.name}`, () => {
}); });
expect(status).toBe(201); 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', () => { describe('PUT /user', () => {
@ -206,6 +224,21 @@ describe(`${UserController.name}`, () => {
}); });
expect(before.updatedAt).not.toEqual(after.updatedAt); 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', () => { describe('GET /user/count', () => {

View File

@ -17,6 +17,7 @@ export const userStub = {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
tags: [], tags: [],
assets: [], assets: [],
memoriesEnabled: true,
}), }),
user1: Object.freeze<UserEntity>({ user1: Object.freeze<UserEntity>({
...authStub.user1, ...authStub.user1,
@ -33,6 +34,7 @@ export const userStub = {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
tags: [], tags: [],
assets: [], assets: [],
memoriesEnabled: true,
}), }),
user2: Object.freeze<UserEntity>({ user2: Object.freeze<UserEntity>({
...authStub.user2, ...authStub.user2,
@ -49,6 +51,7 @@ export const userStub = {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
tags: [], tags: [],
assets: [], assets: [],
memoriesEnabled: true,
}), }),
storageLabel: Object.freeze<UserEntity>({ storageLabel: Object.freeze<UserEntity>({
...authStub.user1, ...authStub.user1,
@ -65,5 +68,6 @@ export const userStub = {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
tags: [], tags: [],
assets: [], assets: [],
memoriesEnabled: true,
}), }),
}; };

View File

@ -954,6 +954,12 @@ export interface CreateUserDto {
* @memberof CreateUserDto * @memberof CreateUserDto
*/ */
'lastName': string; 'lastName': string;
/**
*
* @type {boolean}
* @memberof CreateUserDto
*/
'memoriesEnabled'?: boolean;
/** /**
* *
* @type {string} * @type {string}
@ -2995,6 +3001,12 @@ export interface UpdateUserDto {
* @memberof UpdateUserDto * @memberof UpdateUserDto
*/ */
'lastName'?: string; 'lastName'?: string;
/**
*
* @type {boolean}
* @memberof UpdateUserDto
*/
'memoriesEnabled'?: boolean;
/** /**
* *
* @type {string} * @type {string}
@ -3124,6 +3136,12 @@ export interface UserResponseDto {
* @memberof UserResponseDto * @memberof UserResponseDto
*/ */
'lastName': string; 'lastName': string;
/**
*
* @type {boolean}
* @memberof UserResponseDto
*/
'memoriesEnabled': boolean;
/** /**
* *
* @type {string} * @type {string}

View File

@ -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>

View File

@ -4,10 +4,11 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import SettingAccordion from '../admin-page/settings/setting-accordion.svelte'; import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
import ChangePasswordSettings from './change-password-settings.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 DeviceList from './device-list.svelte';
import MemoriesSettings from './memories-settings.svelte';
import OAuthSettings from './oauth-settings.svelte';
import PartnerSettings from './partner-settings.svelte'; import PartnerSettings from './partner-settings.svelte';
import UserAPIKeyList from './user-api-key-list.svelte';
import UserProfileSettings from './user-profile-settings.svelte'; import UserProfileSettings from './user-profile-settings.svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
@ -39,6 +40,10 @@
<DeviceList /> <DeviceList />
</SettingAccordion> </SettingAccordion>
<SettingAccordion title="Memories" subtitle="Manage what you see in your memories.">
<MemoriesSettings {user} />
</SettingAccordion>
{#if oauthEnabled} {#if oauthEnabled}
<SettingAccordion <SettingAccordion
title="OAuth" title="OAuth"

View File

@ -59,7 +59,9 @@
<svelte:fragment slot="content"> <svelte:fragment slot="content">
{#if assetCount} {#if assetCount}
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE}> <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE}>
<MemoryLane /> {#if data.user.memoriesEnabled}
<MemoryLane />
{/if}
</AssetGrid> </AssetGrid>
{:else} {:else}
<EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={() => openFileUploadDialog()} /> <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={() => openFileUploadDialog()} />

View File

@ -15,5 +15,6 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
createdAt: Sync.each(() => faker.date.past().toISOString()), createdAt: Sync.each(() => faker.date.past().toISOString()),
deletedAt: null, deletedAt: null,
updatedAt: Sync.each(() => faker.date.past().toISOString()), updatedAt: Sync.each(() => faker.date.past().toISOString()),
memoriesEnabled: true,
oauthId: '', oauthId: '',
}); });