refactor: user avatar color (#17753)

This commit is contained in:
Jason Rasmussen 2025-04-28 09:54:51 -04:00 committed by GitHub
parent 460d594791
commit ad272333db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 200 additions and 220 deletions

View File

@ -215,6 +215,19 @@ describe('/admin/users', () => {
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) }); const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
expect(user).toMatchObject({ email: nonAdmin.userEmail }); expect(user).toMatchObject({ email: nonAdmin.userEmail });
}); });
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ avatarColor: 'orange' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'orange' });
const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'orange' });
});
}); });
describe('PUT /admin/users/:id/preferences', () => { describe('PUT /admin/users/:id/preferences', () => {
@ -240,19 +253,6 @@ describe('/admin/users', () => {
expect(after).toMatchObject({ memories: { enabled: false } }); expect(after).toMatchObject({ memories: { enabled: false } });
}); });
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ avatar: { color: 'orange' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'orange' } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'orange' } });
});
it('should update download archive size', async () => { it('should update download archive size', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`) .put(`/admin/users/${admin.userId}/preferences`)

View File

@ -139,6 +139,19 @@ describe('/users', () => {
profileChangedAt: expect.anything(), profileChangedAt: expect.anything(),
}); });
}); });
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ avatarColor: 'blue' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'blue' });
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'blue' });
});
}); });
describe('PUT /users/me/preferences', () => { describe('PUT /users/me/preferences', () => {
@ -158,19 +171,6 @@ describe('/users', () => {
expect(after).toMatchObject({ memories: { enabled: false } }); expect(after).toMatchObject({ memories: { enabled: false } });
}); });
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ avatar: { color: 'blue' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'blue' } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'blue' } });
});
it('should require an integer for download archive size', async () => { it('should require an integer for download archive size', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me/preferences`) .put(`/users/me/preferences`)

View File

@ -300,7 +300,6 @@ Class | Method | HTTP request | Description
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
- [AssetTypeEnum](doc//AssetTypeEnum.md) - [AssetTypeEnum](doc//AssetTypeEnum.md)
- [AudioCodec](doc//AudioCodec.md) - [AudioCodec](doc//AudioCodec.md)
- [AvatarResponse](doc//AvatarResponse.md)
- [AvatarUpdate](doc//AvatarUpdate.md) - [AvatarUpdate](doc//AvatarUpdate.md)
- [BulkIdResponseDto](doc//BulkIdResponseDto.md) - [BulkIdResponseDto](doc//BulkIdResponseDto.md)
- [BulkIdsDto](doc//BulkIdsDto.md) - [BulkIdsDto](doc//BulkIdsDto.md)

View File

@ -107,7 +107,6 @@ part 'model/asset_stack_response_dto.dart';
part 'model/asset_stats_response_dto.dart'; part 'model/asset_stats_response_dto.dart';
part 'model/asset_type_enum.dart'; part 'model/asset_type_enum.dart';
part 'model/audio_codec.dart'; part 'model/audio_codec.dart';
part 'model/avatar_response.dart';
part 'model/avatar_update.dart'; part 'model/avatar_update.dart';
part 'model/bulk_id_response_dto.dart'; part 'model/bulk_id_response_dto.dart';
part 'model/bulk_ids_dto.dart'; part 'model/bulk_ids_dto.dart';

View File

@ -270,8 +270,6 @@ class ApiClient {
return AssetTypeEnumTypeTransformer().decode(value); return AssetTypeEnumTypeTransformer().decode(value);
case 'AudioCodec': case 'AudioCodec':
return AudioCodecTypeTransformer().decode(value); return AudioCodecTypeTransformer().decode(value);
case 'AvatarResponse':
return AvatarResponse.fromJson(value);
case 'AvatarUpdate': case 'AvatarUpdate':
return AvatarUpdate.fromJson(value); return AvatarUpdate.fromJson(value);
case 'BulkIdResponseDto': case 'BulkIdResponseDto':

View File

@ -1,99 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class AvatarResponse {
/// Returns a new [AvatarResponse] instance.
AvatarResponse({
required this.color,
});
UserAvatarColor color;
@override
bool operator ==(Object other) => identical(this, other) || other is AvatarResponse &&
other.color == color;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(color.hashCode);
@override
String toString() => 'AvatarResponse[color=$color]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'color'] = this.color;
return json;
}
/// Returns a new [AvatarResponse] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AvatarResponse? fromJson(dynamic value) {
upgradeDto(value, "AvatarResponse");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AvatarResponse(
color: UserAvatarColor.fromJson(json[r'color'])!,
);
}
return null;
}
static List<AvatarResponse> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AvatarResponse>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AvatarResponse.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AvatarResponse> mapFromJson(dynamic json) {
final map = <String, AvatarResponse>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AvatarResponse.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AvatarResponse-objects as value to a dart map
static Map<String, List<AvatarResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AvatarResponse>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AvatarResponse.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'color',
};
}

View File

@ -13,6 +13,7 @@ part of openapi.api;
class UserAdminCreateDto { class UserAdminCreateDto {
/// Returns a new [UserAdminCreateDto] instance. /// Returns a new [UserAdminCreateDto] instance.
UserAdminCreateDto({ UserAdminCreateDto({
this.avatarColor,
required this.email, required this.email,
required this.name, required this.name,
this.notify, this.notify,
@ -22,6 +23,8 @@ class UserAdminCreateDto {
this.storageLabel, this.storageLabel,
}); });
UserAvatarColor? avatarColor;
String email; String email;
String name; String name;
@ -51,6 +54,7 @@ class UserAdminCreateDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto && bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto &&
other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.name == name && other.name == name &&
other.notify == notify && other.notify == notify &&
@ -62,6 +66,7 @@ class UserAdminCreateDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email.hashCode) + (email.hashCode) +
(name.hashCode) + (name.hashCode) +
(notify == null ? 0 : notify!.hashCode) + (notify == null ? 0 : notify!.hashCode) +
@ -71,10 +76,15 @@ class UserAdminCreateDto {
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @override
String toString() => 'UserAdminCreateDto[email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
json[r'email'] = this.email; json[r'email'] = this.email;
json[r'name'] = this.name; json[r'name'] = this.name;
if (this.notify != null) { if (this.notify != null) {
@ -110,6 +120,7 @@ class UserAdminCreateDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserAdminCreateDto( return UserAdminCreateDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
notify: mapValueOfType<bool>(json, r'notify'), notify: mapValueOfType<bool>(json, r'notify'),

View File

@ -13,6 +13,7 @@ part of openapi.api;
class UserAdminUpdateDto { class UserAdminUpdateDto {
/// Returns a new [UserAdminUpdateDto] instance. /// Returns a new [UserAdminUpdateDto] instance.
UserAdminUpdateDto({ UserAdminUpdateDto({
this.avatarColor,
this.email, this.email,
this.name, this.name,
this.password, this.password,
@ -21,6 +22,8 @@ class UserAdminUpdateDto {
this.storageLabel, this.storageLabel,
}); });
UserAvatarColor? avatarColor;
/// ///
/// 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
@ -60,6 +63,7 @@ class UserAdminUpdateDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto && bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto &&
other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.name == name && other.name == name &&
other.password == password && other.password == password &&
@ -70,6 +74,7 @@ class UserAdminUpdateDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email == null ? 0 : email!.hashCode) + (email == null ? 0 : email!.hashCode) +
(name == null ? 0 : name!.hashCode) + (name == null ? 0 : name!.hashCode) +
(password == null ? 0 : password!.hashCode) + (password == null ? 0 : password!.hashCode) +
@ -78,10 +83,15 @@ class UserAdminUpdateDto {
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @override
String toString() => 'UserAdminUpdateDto[email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
if (this.email != null) { if (this.email != null) {
json[r'email'] = this.email; json[r'email'] = this.email;
} else { } else {
@ -124,6 +134,7 @@ class UserAdminUpdateDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserAdminUpdateDto( return UserAdminUpdateDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'), email: mapValueOfType<String>(json, r'email'),
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
password: mapValueOfType<String>(json, r'password'), password: mapValueOfType<String>(json, r'password'),

View File

@ -13,7 +13,6 @@ part of openapi.api;
class UserPreferencesResponseDto { class UserPreferencesResponseDto {
/// Returns a new [UserPreferencesResponseDto] instance. /// Returns a new [UserPreferencesResponseDto] instance.
UserPreferencesResponseDto({ UserPreferencesResponseDto({
required this.avatar,
required this.download, required this.download,
required this.emailNotifications, required this.emailNotifications,
required this.folders, required this.folders,
@ -25,8 +24,6 @@ class UserPreferencesResponseDto {
required this.tags, required this.tags,
}); });
AvatarResponse avatar;
DownloadResponse download; DownloadResponse download;
EmailNotificationsResponse emailNotifications; EmailNotificationsResponse emailNotifications;
@ -47,7 +44,6 @@ class UserPreferencesResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
other.avatar == avatar &&
other.download == download && other.download == download &&
other.emailNotifications == emailNotifications && other.emailNotifications == emailNotifications &&
other.folders == folders && other.folders == folders &&
@ -61,7 +57,6 @@ class UserPreferencesResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatar.hashCode) +
(download.hashCode) + (download.hashCode) +
(emailNotifications.hashCode) + (emailNotifications.hashCode) +
(folders.hashCode) + (folders.hashCode) +
@ -73,11 +68,10 @@ class UserPreferencesResponseDto {
(tags.hashCode); (tags.hashCode);
@override @override
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'avatar'] = this.avatar;
json[r'download'] = this.download; json[r'download'] = this.download;
json[r'emailNotifications'] = this.emailNotifications; json[r'emailNotifications'] = this.emailNotifications;
json[r'folders'] = this.folders; json[r'folders'] = this.folders;
@ -99,7 +93,6 @@ class UserPreferencesResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserPreferencesResponseDto( return UserPreferencesResponseDto(
avatar: AvatarResponse.fromJson(json[r'avatar'])!,
download: DownloadResponse.fromJson(json[r'download'])!, download: DownloadResponse.fromJson(json[r'download'])!,
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
folders: FoldersResponse.fromJson(json[r'folders'])!, folders: FoldersResponse.fromJson(json[r'folders'])!,
@ -156,7 +149,6 @@ class UserPreferencesResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'avatar',
'download', 'download',
'emailNotifications', 'emailNotifications',
'folders', 'folders',

View File

@ -13,11 +13,14 @@ part of openapi.api;
class UserUpdateMeDto { class UserUpdateMeDto {
/// Returns a new [UserUpdateMeDto] instance. /// Returns a new [UserUpdateMeDto] instance.
UserUpdateMeDto({ UserUpdateMeDto({
this.avatarColor,
this.email, this.email,
this.name, this.name,
this.password, this.password,
}); });
UserAvatarColor? avatarColor;
/// ///
/// 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
@ -44,6 +47,7 @@ class UserUpdateMeDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto && bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto &&
other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.name == name && other.name == name &&
other.password == password; other.password == password;
@ -51,15 +55,21 @@ class UserUpdateMeDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email == null ? 0 : email!.hashCode) + (email == null ? 0 : email!.hashCode) +
(name == null ? 0 : name!.hashCode) + (name == null ? 0 : name!.hashCode) +
(password == null ? 0 : password!.hashCode); (password == null ? 0 : password!.hashCode);
@override @override
String toString() => 'UserUpdateMeDto[email=$email, name=$name, password=$password]'; String toString() => 'UserUpdateMeDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
if (this.email != null) { if (this.email != null) {
json[r'email'] = this.email; json[r'email'] = this.email;
} else { } else {
@ -87,6 +97,7 @@ class UserUpdateMeDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserUpdateMeDto( return UserUpdateMeDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'), email: mapValueOfType<String>(json, r'email'),
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
password: mapValueOfType<String>(json, r'password'), password: mapValueOfType<String>(json, r'password'),

View File

@ -8884,21 +8884,6 @@
], ],
"type": "string" "type": "string"
}, },
"AvatarResponse": {
"properties": {
"color": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
}
},
"required": [
"color"
],
"type": "object"
},
"AvatarUpdate": { "AvatarUpdate": {
"properties": { "properties": {
"color": { "color": {
@ -13621,6 +13606,14 @@
}, },
"UserAdminCreateDto": { "UserAdminCreateDto": {
"properties": { "properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"email": { "email": {
"format": "email", "format": "email",
"type": "string" "type": "string"
@ -13763,6 +13756,14 @@
}, },
"UserAdminUpdateDto": { "UserAdminUpdateDto": {
"properties": { "properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"email": { "email": {
"format": "email", "format": "email",
"type": "string" "type": "string"
@ -13826,9 +13827,6 @@
}, },
"UserPreferencesResponseDto": { "UserPreferencesResponseDto": {
"properties": { "properties": {
"avatar": {
"$ref": "#/components/schemas/AvatarResponse"
},
"download": { "download": {
"$ref": "#/components/schemas/DownloadResponse" "$ref": "#/components/schemas/DownloadResponse"
}, },
@ -13858,7 +13856,6 @@
} }
}, },
"required": [ "required": [
"avatar",
"download", "download",
"emailNotifications", "emailNotifications",
"folders", "folders",
@ -13952,6 +13949,14 @@
}, },
"UserUpdateMeDto": { "UserUpdateMeDto": {
"properties": { "properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"email": { "email": {
"format": "email", "format": "email",
"type": "string" "type": "string"

View File

@ -64,6 +64,7 @@ export type UserAdminResponseDto = {
updatedAt: string; updatedAt: string;
}; };
export type UserAdminCreateDto = { export type UserAdminCreateDto = {
avatarColor?: (UserAvatarColor) | null;
email: string; email: string;
name: string; name: string;
notify?: boolean; notify?: boolean;
@ -76,6 +77,7 @@ export type UserAdminDeleteDto = {
force?: boolean; force?: boolean;
}; };
export type UserAdminUpdateDto = { export type UserAdminUpdateDto = {
avatarColor?: (UserAvatarColor) | null;
email?: string; email?: string;
name?: string; name?: string;
password?: string; password?: string;
@ -83,9 +85,6 @@ export type UserAdminUpdateDto = {
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
storageLabel?: string | null; storageLabel?: string | null;
}; };
export type AvatarResponse = {
color: UserAvatarColor;
};
export type DownloadResponse = { export type DownloadResponse = {
archiveSize: number; archiveSize: number;
includeEmbeddedVideos: boolean; includeEmbeddedVideos: boolean;
@ -122,7 +121,6 @@ export type TagsResponse = {
sidebarWeb: boolean; sidebarWeb: boolean;
}; };
export type UserPreferencesResponseDto = { export type UserPreferencesResponseDto = {
avatar: AvatarResponse;
download: DownloadResponse; download: DownloadResponse;
emailNotifications: EmailNotificationsResponse; emailNotifications: EmailNotificationsResponse;
folders: FoldersResponse; folders: FoldersResponse;
@ -1388,6 +1386,7 @@ export type TrashResponseDto = {
count: number; count: number;
}; };
export type UserUpdateMeDto = { export type UserUpdateMeDto = {
avatarColor?: (UserAvatarColor) | null;
email?: string; email?: string;
name?: string; name?: string;
password?: string; password?: string;

View File

@ -9,6 +9,7 @@ import {
Permission, Permission,
SharedLinkType, SharedLinkType,
SourceType, SourceType,
UserAvatarColor,
UserStatus, UserStatus,
} from 'src/enum'; } from 'src/enum';
import { OnThisDayData, UserMetadataItem } from 'src/types'; import { OnThisDayData, UserMetadataItem } from 'src/types';
@ -122,6 +123,7 @@ export type User = {
id: string; id: string;
name: string; name: string;
email: string; email: string;
avatarColor: UserAvatarColor | null;
profileImagePath: string; profileImagePath: string;
profileChangedAt: Date; profileChangedAt: Date;
}; };
@ -264,7 +266,15 @@ export type AssetFace = {
person?: Person | null; person?: Person | null;
}; };
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const; const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const;
const userWithPrefixColumns = [
'users.id',
'users.name',
'users.email',
'users.avatarColor',
'users.profileImagePath',
'users.profileChangedAt',
] as const;
export const columns = { export const columns = {
asset: [ asset: [
@ -306,7 +316,7 @@ export const columns = {
'shared_links.password', 'shared_links.password',
], ],
user: userColumns, user: userColumns,
userWithPrefix: ['users.id', 'users.name', 'users.email', 'users.profileImagePath', 'users.profileChangedAt'], userWithPrefix: userWithPrefixColumns,
userAdmin: [ userAdmin: [
...userColumns, ...userColumns,
'createdAt', 'createdAt',

View File

@ -137,11 +137,6 @@ export class UserPreferencesUpdateDto {
purchase?: PurchaseUpdate; purchase?: PurchaseUpdate;
} }
class AvatarResponse {
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
color!: UserAvatarColor;
}
class RatingsResponse { class RatingsResponse {
enabled: boolean = false; enabled: boolean = false;
} }
@ -195,7 +190,6 @@ export class UserPreferencesResponseDto implements UserPreferences {
ratings!: RatingsResponse; ratings!: RatingsResponse;
sharedLinks!: SharedLinksResponse; sharedLinks!: SharedLinksResponse;
tags!: TagsResponse; tags!: TagsResponse;
avatar!: AvatarResponse;
emailNotifications!: EmailNotificationsResponse; emailNotifications!: EmailNotificationsResponse;
download!: DownloadResponse; download!: DownloadResponse;
purchase!: PurchaseResponse; purchase!: PurchaseResponse;

View File

@ -1,10 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
import { User, UserAdmin } from 'src/database'; import { User, UserAdmin } from 'src/database';
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
import { UserMetadataItem } from 'src/types'; import { UserMetadataItem } from 'src/types';
import { getPreferences } from 'src/utils/preferences';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
export class UserUpdateMeDto { export class UserUpdateMeDto {
@ -23,6 +22,11 @@ export class UserUpdateMeDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
name?: string; name?: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor?: UserAvatarColor | null;
} }
export class UserResponseDto { export class UserResponseDto {
@ -41,13 +45,21 @@ export class UserLicense {
activatedAt!: Date; activatedAt!: Date;
} }
const emailToAvatarColor = (email: string): UserAvatarColor => {
const values = Object.values(UserAvatarColor);
const randomIndex = Math.floor(
[...email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
);
return values[randomIndex];
};
export const mapUser = (entity: User | UserAdmin): UserResponseDto => { export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
return { return {
id: entity.id, id: entity.id,
email: entity.email, email: entity.email,
name: entity.name, name: entity.name,
profileImagePath: entity.profileImagePath, profileImagePath: entity.profileImagePath,
avatarColor: getPreferences(entity.email, (entity as UserAdmin).metadata || []).avatar.color, avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
profileChangedAt: entity.profileChangedAt, profileChangedAt: entity.profileChangedAt,
}; };
}; };
@ -69,6 +81,11 @@ export class UserAdminCreateDto {
@IsString() @IsString()
name!: string; name!: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor?: UserAvatarColor | null;
@Optional({ nullable: true }) @Optional({ nullable: true })
@IsString() @IsString()
@Transform(toSanitized) @Transform(toSanitized)
@ -104,6 +121,11 @@ export class UserAdminUpdateDto {
@IsNotEmpty() @IsNotEmpty()
name?: string; name?: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor?: UserAvatarColor | null;
@Optional({ nullable: true }) @Optional({ nullable: true })
@IsString() @IsString()
@Transform(toSanitized) @Transform(toSanitized)

View File

@ -13,6 +13,7 @@ from
"users"."id", "users"."id",
"users"."name", "users"."name",
"users"."email", "users"."email",
"users"."avatarColor",
"users"."profileImagePath", "users"."profileImagePath",
"users"."profileChangedAt" "users"."profileChangedAt"
from from
@ -44,6 +45,7 @@ returning
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from

View File

@ -12,6 +12,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -36,6 +37,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -100,6 +102,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -124,6 +127,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -191,6 +195,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -215,6 +220,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -269,6 +275,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -292,6 +299,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -353,6 +361,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from

View File

@ -12,6 +12,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -29,6 +30,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -61,6 +63,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -78,6 +81,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -112,6 +116,7 @@ returning
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -129,6 +134,7 @@ returning
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -156,6 +162,7 @@ returning
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@ -173,6 +180,7 @@ returning
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from

View File

@ -5,6 +5,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt", "profileChangedAt",
"createdAt", "createdAt",
@ -43,6 +44,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt", "profileChangedAt",
"createdAt", "createdAt",
@ -90,6 +92,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt", "profileChangedAt",
"createdAt", "createdAt",
@ -128,6 +131,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt", "profileChangedAt",
"createdAt", "createdAt",
@ -152,6 +156,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt", "profileChangedAt",
"createdAt", "createdAt",
@ -198,6 +203,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt", "profileChangedAt",
"createdAt", "createdAt",
@ -235,6 +241,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt", "profileChangedAt",
"createdAt", "createdAt",

View File

@ -0,0 +1,14 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "users" ADD "avatarColor" character varying;`.execute(db);
await sql`
UPDATE "users"
SET "avatarColor" = "user_metadata"."value"->'avatar'->>'color'
FROM "user_metadata"
WHERE "users"."id" = "user_metadata"."userId" AND "user_metadata"."key" = 'preferences';`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "users" DROP COLUMN "avatarColor";`.execute(db);
}

View File

@ -1,6 +1,6 @@
import { ColumnType } from 'kysely'; import { ColumnType } from 'kysely';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { UserStatus } from 'src/enum'; import { UserAvatarColor, UserStatus } from 'src/enum';
import { users_delete_audit } from 'src/schema/functions'; import { users_delete_audit } from 'src/schema/functions';
import { import {
AfterDeleteTrigger, AfterDeleteTrigger,
@ -49,6 +49,9 @@ export class UserTable {
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
shouldChangePassword!: Generated<boolean>; shouldChangePassword!: Generated<boolean>;
@Column({ default: null })
avatarColor!: UserAvatarColor | null;
@DeleteDateColumn() @DeleteDateColumn()
deletedAt!: Timestamp | null; deletedAt!: Timestamp | null;

View File

@ -33,7 +33,7 @@ export class DownloadService extends BaseService {
const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4; const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
const metadata = await this.userRepository.getMetadata(auth.user.id); const metadata = await this.userRepository.getMetadata(auth.user.id);
const preferences = getPreferences(auth.user.email, metadata); const preferences = getPreferences(metadata);
const motionIds = new Set<string>(); const motionIds = new Set<string>();
const archives: DownloadArchiveInfo[] = []; const archives: DownloadArchiveInfo[] = [];
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] }; let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };

View File

@ -271,7 +271,7 @@ export class NotificationService extends BaseService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
const { emailNotifications } = getPreferences(recipient.email, recipient.metadata); const { emailNotifications } = getPreferences(recipient.metadata);
if (!emailNotifications.enabled || !emailNotifications.albumInvite) { if (!emailNotifications.enabled || !emailNotifications.albumInvite) {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
@ -333,7 +333,7 @@ export class NotificationService extends BaseService {
continue; continue;
} }
const { emailNotifications } = getPreferences(user.email, user.metadata); const { emailNotifications } = getPreferences(user.metadata);
if (!emailNotifications.enabled || !emailNotifications.albumUpdate) { if (!emailNotifications.enabled || !emailNotifications.albumUpdate) {
continue; continue;

View File

@ -106,21 +106,19 @@ export class UserAdminService extends BaseService {
} }
async getPreferences(auth: AuthDto, id: string): Promise<UserPreferencesResponseDto> { async getPreferences(auth: AuthDto, id: string): Promise<UserPreferencesResponseDto> {
const { email } = await this.findOrFail(id, { withDeleted: true }); await this.findOrFail(id, { withDeleted: true });
const metadata = await this.userRepository.getMetadata(id); const metadata = await this.userRepository.getMetadata(id);
const preferences = getPreferences(email, metadata); return mapPreferences(getPreferences(metadata));
return mapPreferences(preferences);
} }
async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) { async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) {
const { email } = await this.findOrFail(id, { withDeleted: false }); await this.findOrFail(id, { withDeleted: false });
const metadata = await this.userRepository.getMetadata(id); const metadata = await this.userRepository.getMetadata(id);
const preferences = getPreferences(email, metadata); const newPreferences = mergePreferences(getPreferences(metadata), dto);
const newPreferences = mergePreferences(preferences, dto);
await this.userRepository.upsertMetadata(id, { await this.userRepository.upsertMetadata(id, {
key: UserMetadataKey.PREFERENCES, key: UserMetadataKey.PREFERENCES,
value: getPreferencesPartial({ email }, newPreferences), value: getPreferencesPartial(newPreferences),
}); });
return mapPreferences(newPreferences); return mapPreferences(newPreferences);

View File

@ -53,6 +53,7 @@ export class UserService extends BaseService {
const update: Updateable<UserTable> = { const update: Updateable<UserTable> = {
email: dto.email, email: dto.email,
name: dto.name, name: dto.name,
avatarColor: dto.avatarColor,
}; };
if (dto.password) { if (dto.password) {
@ -68,18 +69,16 @@ export class UserService extends BaseService {
async getMyPreferences(auth: AuthDto): Promise<UserPreferencesResponseDto> { async getMyPreferences(auth: AuthDto): Promise<UserPreferencesResponseDto> {
const metadata = await this.userRepository.getMetadata(auth.user.id); const metadata = await this.userRepository.getMetadata(auth.user.id);
const preferences = getPreferences(auth.user.email, metadata); return mapPreferences(getPreferences(metadata));
return mapPreferences(preferences);
} }
async updateMyPreferences(auth: AuthDto, dto: UserPreferencesUpdateDto) { async updateMyPreferences(auth: AuthDto, dto: UserPreferencesUpdateDto) {
const metadata = await this.userRepository.getMetadata(auth.user.id); const metadata = await this.userRepository.getMetadata(auth.user.id);
const current = getPreferences(auth.user.email, metadata); const updated = mergePreferences(getPreferences(metadata), dto);
const updated = mergePreferences(current, dto);
await this.userRepository.upsertMetadata(auth.user.id, { await this.userRepository.upsertMetadata(auth.user.id, {
key: UserMetadataKey.PREFERENCES, key: UserMetadataKey.PREFERENCES,
value: getPreferencesPartial(auth.user, updated), value: getPreferencesPartial(updated),
}); });
return mapPreferences(updated); return mapPreferences(updated);

View File

@ -11,7 +11,6 @@ import {
SyncEntityType, SyncEntityType,
SystemMetadataKey, SystemMetadataKey,
TranscodeTarget, TranscodeTarget,
UserAvatarColor,
UserMetadataKey, UserMetadataKey,
VideoCodec, VideoCodec,
} from 'src/enum'; } from 'src/enum';
@ -486,9 +485,6 @@ export interface UserPreferences {
enabled: boolean; enabled: boolean;
sidebarWeb: boolean; sidebarWeb: boolean;
}; };
avatar: {
color: UserAvatarColor;
};
emailNotifications: { emailNotifications: {
enabled: boolean; enabled: boolean;
albumInvite: boolean; albumInvite: boolean;

View File

@ -1,16 +1,11 @@
import _ from 'lodash'; import _ from 'lodash';
import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
import { UserAvatarColor, UserMetadataKey } from 'src/enum'; import { UserMetadataKey } from 'src/enum';
import { DeepPartial, UserMetadataItem, UserPreferences } from 'src/types'; import { DeepPartial, UserMetadataItem, UserPreferences } from 'src/types';
import { HumanReadableSize } from 'src/utils/bytes'; import { HumanReadableSize } from 'src/utils/bytes';
import { getKeysDeep } from 'src/utils/misc'; import { getKeysDeep } from 'src/utils/misc';
const getDefaultPreferences = (user: { email: string }): UserPreferences => { const getDefaultPreferences = (): UserPreferences => {
const values = Object.values(UserAvatarColor);
const randomIndex = Math.floor(
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
);
return { return {
folders: { folders: {
enabled: false, enabled: false,
@ -34,9 +29,6 @@ const getDefaultPreferences = (user: { email: string }): UserPreferences => {
enabled: false, enabled: false,
sidebarWeb: false, sidebarWeb: false,
}, },
avatar: {
color: values[randomIndex],
},
emailNotifications: { emailNotifications: {
enabled: true, enabled: true,
albumInvite: true, albumInvite: true,
@ -53,8 +45,8 @@ const getDefaultPreferences = (user: { email: string }): UserPreferences => {
}; };
}; };
export const getPreferences = (email: string, metadata: UserMetadataItem[]): UserPreferences => { export const getPreferences = (metadata: UserMetadataItem[]): UserPreferences => {
const preferences = getDefaultPreferences({ email }); const preferences = getDefaultPreferences();
const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES); const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES);
const partial = item?.value || {}; const partial = item?.value || {};
for (const property of getKeysDeep(partial)) { for (const property of getKeysDeep(partial)) {
@ -64,8 +56,8 @@ export const getPreferences = (email: string, metadata: UserMetadataItem[]): Use
return preferences; return preferences;
}; };
export const getPreferencesPartial = (user: { email: string }, newPreferences: UserPreferences) => { export const getPreferencesPartial = (newPreferences: UserPreferences) => {
const defaultPreferences = getDefaultPreferences(user); const defaultPreferences = getDefaultPreferences();
const partial: DeepPartial<UserPreferences> = {}; const partial: DeepPartial<UserPreferences> = {};
for (const property of getKeysDeep(defaultPreferences)) { for (const property of getKeysDeep(defaultPreferences)) {
const newValue = _.get(newPreferences, property); const newValue = _.get(newPreferences, property);

View File

@ -1,5 +1,5 @@
import { UserAdmin } from 'src/database'; import { UserAdmin } from 'src/database';
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { UserStatus } from 'src/enum';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
export const userStub = { export const userStub = {
@ -12,6 +12,7 @@ export const userStub = {
storageLabel: 'admin', storageLabel: 'admin',
oauthId: '', oauthId: '',
shouldChangePassword: false, shouldChangePassword: false,
avatarColor: null,
profileImagePath: '', profileImagePath: '',
createdAt: new Date('2021-01-01'), createdAt: new Date('2021-01-01'),
deletedAt: null, deletedAt: null,
@ -28,16 +29,12 @@ export const userStub = {
storageLabel: null, storageLabel: null,
oauthId: '', oauthId: '',
shouldChangePassword: false, shouldChangePassword: false,
avatarColor: null,
profileImagePath: '', profileImagePath: '',
createdAt: new Date('2021-01-01'), createdAt: new Date('2021-01-01'),
deletedAt: null, deletedAt: null,
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
metadata: [ metadata: [],
{
key: UserMetadataKey.PREFERENCES,
value: { avatar: { color: UserAvatarColor.PRIMARY } },
},
],
quotaSizeInBytes: null, quotaSizeInBytes: null,
quotaUsageInBytes: 0, quotaUsageInBytes: 0,
}, },
@ -50,6 +47,7 @@ export const userStub = {
storageLabel: null, storageLabel: null,
oauthId: '', oauthId: '',
shouldChangePassword: false, shouldChangePassword: false,
avatarColor: null,
profileImagePath: '', profileImagePath: '',
createdAt: new Date('2021-01-01'), createdAt: new Date('2021-01-01'),
deletedAt: null, deletedAt: null,

View File

@ -140,6 +140,7 @@ const userFactory = (user: Partial<User> = {}) => ({
id: newUuid(), id: newUuid(),
name: 'Test User', name: 'Test User',
email: 'test@immich.cloud', email: 'test@immich.cloud',
avatarColor: null,
profileImagePath: '', profileImagePath: '',
profileChangedAt: newDate(), profileChangedAt: newDate(),
...user, ...user,
@ -155,6 +156,7 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
storageLabel = null, storageLabel = null,
shouldChangePassword = false, shouldChangePassword = false,
isAdmin = false, isAdmin = false,
avatarColor = null,
createdAt = newDate(), createdAt = newDate(),
updatedAt = newDate(), updatedAt = newDate(),
deletedAt = null, deletedAt = null,
@ -173,6 +175,7 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
storageLabel, storageLabel,
shouldChangePassword, shouldChangePassword,
isAdmin, isAdmin,
avatarColor,
createdAt, createdAt,
updatedAt, updatedAt,
deletedAt, deletedAt,

View File

@ -5,9 +5,9 @@
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { preferences, user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { deleteProfileImage, updateMyPreferences, type UserAvatarColor } from '@immich/sdk'; import { deleteProfileImage, updateMyUser, type UserAvatarColor } from '@immich/sdk';
import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js'; import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@ -30,8 +30,7 @@
await deleteProfileImage(); await deleteProfileImage();
} }
$preferences = await updateMyPreferences({ userPreferencesUpdateDto: { avatar: { color } } }); $user = await updateMyUser({ userUpdateMeDto: { avatarColor: color } });
$user = { ...$user, profileImagePath: '', avatarColor: $preferences.avatar.color };
isShowSelectAvatar = false; isShowSelectAvatar = false;
notificationController.show({ notificationController.show({