diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 6ca2225180..0148f2e1e9 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -94,6 +94,7 @@ export const signupResponseDto = { quotaSizeInBytes: null, status: 'active', license: null, + profileChangedAt: expect.any(String), }, }; diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index c9ae3ea651..86624ed06b 100644 --- a/mobile/openapi/lib/model/create_profile_image_response_dto.dart +++ b/mobile/openapi/lib/model/create_profile_image_response_dto.dart @@ -13,30 +13,36 @@ part of openapi.api; class CreateProfileImageResponseDto { /// Returns a new [CreateProfileImageResponseDto] instance. CreateProfileImageResponseDto({ + required this.profileChangedAt, required this.profileImagePath, required this.userId, }); + DateTime profileChangedAt; + String profileImagePath; String userId; @override bool operator ==(Object other) => identical(this, other) || other is CreateProfileImageResponseDto && + other.profileChangedAt == profileChangedAt && other.profileImagePath == profileImagePath && other.userId == userId; @override int get hashCode => // ignore: unnecessary_parenthesis + (profileChangedAt.hashCode) + (profileImagePath.hashCode) + (userId.hashCode); @override - String toString() => 'CreateProfileImageResponseDto[profileImagePath=$profileImagePath, userId=$userId]'; + String toString() => 'CreateProfileImageResponseDto[profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, userId=$userId]'; Map<String, dynamic> toJson() { final json = <String, dynamic>{}; + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; json[r'userId'] = this.userId; return json; @@ -50,6 +56,7 @@ class CreateProfileImageResponseDto { final json = value.cast<String, dynamic>(); return CreateProfileImageResponseDto( + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, userId: mapValueOfType<String>(json, r'userId')!, ); @@ -99,6 +106,7 @@ class CreateProfileImageResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = <String>{ + 'profileChangedAt', 'profileImagePath', 'userId', }; diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 7c3cf03bd9..375303c94a 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -18,6 +18,7 @@ class PartnerResponseDto { required this.id, this.inTimeline, required this.name, + required this.profileChangedAt, required this.profileImagePath, }); @@ -37,6 +38,8 @@ class PartnerResponseDto { String name; + DateTime profileChangedAt; + String profileImagePath; @override @@ -46,6 +49,7 @@ class PartnerResponseDto { other.id == id && other.inTimeline == inTimeline && other.name == name && + other.profileChangedAt == profileChangedAt && other.profileImagePath == profileImagePath; @override @@ -56,10 +60,11 @@ class PartnerResponseDto { (id.hashCode) + (inTimeline == null ? 0 : inTimeline!.hashCode) + (name.hashCode) + + (profileChangedAt.hashCode) + (profileImagePath.hashCode); @override - String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, email=$email, id=$id, inTimeline=$inTimeline, name=$name, profileImagePath=$profileImagePath]'; + String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, email=$email, id=$id, inTimeline=$inTimeline, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath]'; Map<String, dynamic> toJson() { final json = <String, dynamic>{}; @@ -72,6 +77,7 @@ class PartnerResponseDto { // json[r'inTimeline'] = null; } json[r'name'] = this.name; + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; return json; } @@ -89,6 +95,7 @@ class PartnerResponseDto { id: mapValueOfType<String>(json, r'id')!, inTimeline: mapValueOfType<bool>(json, r'inTimeline'), name: mapValueOfType<String>(json, r'name')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, ); } @@ -141,6 +148,7 @@ class PartnerResponseDto { 'email', 'id', 'name', + 'profileChangedAt', 'profileImagePath', }; } diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index af1ad3ad1c..461596b7bf 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -22,6 +22,7 @@ class UserAdminResponseDto { required this.license, required this.name, required this.oauthId, + required this.profileChangedAt, required this.profileImagePath, required this.quotaSizeInBytes, required this.quotaUsageInBytes, @@ -49,6 +50,8 @@ class UserAdminResponseDto { String oauthId; + DateTime profileChangedAt; + String profileImagePath; int? quotaSizeInBytes; @@ -74,6 +77,7 @@ class UserAdminResponseDto { other.license == license && other.name == name && other.oauthId == oauthId && + other.profileChangedAt == profileChangedAt && other.profileImagePath == profileImagePath && other.quotaSizeInBytes == quotaSizeInBytes && other.quotaUsageInBytes == quotaUsageInBytes && @@ -94,6 +98,7 @@ class UserAdminResponseDto { (license == null ? 0 : license!.hashCode) + (name.hashCode) + (oauthId.hashCode) + + (profileChangedAt.hashCode) + (profileImagePath.hashCode) + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + (quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) + @@ -103,7 +108,7 @@ class UserAdminResponseDto { (updatedAt.hashCode); @override - String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, license=$license, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, license=$license, name=$name, oauthId=$oauthId, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]'; Map<String, dynamic> toJson() { final json = <String, dynamic>{}; @@ -124,6 +129,7 @@ class UserAdminResponseDto { } json[r'name'] = this.name; json[r'oauthId'] = this.oauthId; + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; if (this.quotaSizeInBytes != null) { json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; @@ -163,6 +169,7 @@ class UserAdminResponseDto { license: UserLicense.fromJson(json[r'license']), name: mapValueOfType<String>(json, r'name')!, oauthId: mapValueOfType<String>(json, r'oauthId')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'), quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'), @@ -226,6 +233,7 @@ class UserAdminResponseDto { 'license', 'name', 'oauthId', + 'profileChangedAt', 'profileImagePath', 'quotaSizeInBytes', 'quotaUsageInBytes', diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 41c1899848..282a5a40dc 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -17,6 +17,7 @@ class UserResponseDto { required this.email, required this.id, required this.name, + required this.profileChangedAt, required this.profileImagePath, }); @@ -28,6 +29,8 @@ class UserResponseDto { String name; + DateTime profileChangedAt; + String profileImagePath; @override @@ -36,6 +39,7 @@ class UserResponseDto { other.email == email && other.id == id && other.name == name && + other.profileChangedAt == profileChangedAt && other.profileImagePath == profileImagePath; @override @@ -45,10 +49,11 @@ class UserResponseDto { (email.hashCode) + (id.hashCode) + (name.hashCode) + + (profileChangedAt.hashCode) + (profileImagePath.hashCode); @override - String toString() => 'UserResponseDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]'; + String toString() => 'UserResponseDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath]'; Map<String, dynamic> toJson() { final json = <String, dynamic>{}; @@ -56,6 +61,7 @@ class UserResponseDto { json[r'email'] = this.email; json[r'id'] = this.id; json[r'name'] = this.name; + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; return json; } @@ -72,6 +78,7 @@ class UserResponseDto { email: mapValueOfType<String>(json, r'email')!, id: mapValueOfType<String>(json, r'id')!, name: mapValueOfType<String>(json, r'name')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, ); } @@ -124,6 +131,7 @@ class UserResponseDto { 'email', 'id', 'name', + 'profileChangedAt', 'profileImagePath', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index af79815563..fc813ae244 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8779,6 +8779,10 @@ }, "CreateProfileImageResponseDto": { "properties": { + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, "profileImagePath": { "type": "string" }, @@ -8787,6 +8791,7 @@ } }, "required": [ + "profileChangedAt", "profileImagePath", "userId" ], @@ -10015,6 +10020,10 @@ "name": { "type": "string" }, + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, "profileImagePath": { "type": "string" } @@ -10024,6 +10033,7 @@ "email", "id", "name", + "profileChangedAt", "profileImagePath" ], "type": "object" @@ -12454,6 +12464,10 @@ "oauthId": { "type": "string" }, + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, "profileImagePath": { "type": "string" }, @@ -12492,6 +12506,7 @@ "license", "name", "oauthId", + "profileChangedAt", "profileImagePath", "quotaSizeInBytes", "quotaUsageInBytes", @@ -12653,6 +12668,10 @@ "name": { "type": "string" }, + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, "profileImagePath": { "type": "string" } @@ -12662,6 +12681,7 @@ "email", "id", "name", + "profileChangedAt", "profileImagePath" ], "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index da57313692..dec8ad1e3d 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -19,6 +19,7 @@ export type UserResponseDto = { email: string; id: string; name: string; + profileChangedAt: string; profileImagePath: string; }; export type ActivityResponseDto = { @@ -53,6 +54,7 @@ export type UserAdminResponseDto = { license: (UserLicense) | null; name: string; oauthId: string; + profileChangedAt: string; profileImagePath: string; quotaSizeInBytes: number | null; quotaUsageInBytes: number | null; @@ -669,6 +671,7 @@ export type PartnerResponseDto = { id: string; inTimeline?: boolean; name: string; + profileChangedAt: string; profileImagePath: string; }; export type UpdatePartnerDto = { @@ -1252,6 +1255,7 @@ export type CreateProfileImageDto = { file: Blob; }; export type CreateProfileImageResponseDto = { + profileChangedAt: string; profileImagePath: string; userId: string; }; diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index 9659fa3965..16eea373e3 100644 --- a/server/src/dtos/user-profile.dto.ts +++ b/server/src/dtos/user-profile.dto.ts @@ -8,12 +8,6 @@ export class CreateProfileImageDto { export class CreateProfileImageResponseDto { userId!: string; + profileChangedAt!: Date; profileImagePath!: string; } - -export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { - return { - userId, - profileImagePath, - }; -} diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index f7cd70ee74..36f0b6386f 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -32,6 +32,7 @@ export class UserResponseDto { profileImagePath!: string; @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) avatarColor!: UserAvatarColor; + profileChangedAt!: Date; } export class UserLicense { @@ -47,6 +48,7 @@ export const mapUser = (entity: UserEntity): UserResponseDto => { name: entity.name, profileImagePath: entity.profileImagePath, avatarColor: getPreferences(entity).avatar.color, + profileChangedAt: entity.profileChangedAt, }; }; diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 9cacad315b..ea446be390 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -67,4 +67,7 @@ export class UserEntity { @OneToMany(() => UserMetadataEntity, (metadata) => metadata.user) metadata!: UserMetadataEntity[]; + + @Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + profileChangedAt!: Date; } diff --git a/server/src/migrations/1726491047923-AddprofileChangedAt.ts b/server/src/migrations/1726491047923-AddprofileChangedAt.ts new file mode 100644 index 0000000000..bcf568426a --- /dev/null +++ b/server/src/migrations/1726491047923-AddprofileChangedAt.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddprofileChangedAt1726491047923 implements MigrationInterface { + name = 'AddprofileChangedAt1726491047923' + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`ALTER TABLE "users" ADD "profileChangedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "profileChangedAt"`); + } + +} diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 3f3e04140c..44042c0e6d 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -23,7 +23,8 @@ SELECT "ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status", "ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt", "ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes", - "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes" + "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes", + "ActivityEntity__ActivityEntity_user"."profileChangedAt" AS "ActivityEntity__ActivityEntity_user_profileChangedAt" FROM "activity" "ActivityEntity" LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId" diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 729f7c7f20..b9531a43dc 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -30,6 +30,7 @@ FROM "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", @@ -47,6 +48,7 @@ FROM "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -106,6 +108,7 @@ SELECT "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", @@ -122,7 +125,8 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes" + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" @@ -164,6 +168,7 @@ SELECT "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", @@ -180,7 +185,8 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes" + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" @@ -299,6 +305,7 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -324,7 +331,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -372,6 +380,7 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -397,7 +406,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -495,7 +505,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -553,7 +564,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" diff --git a/server/src/queries/api.key.repository.sql b/server/src/queries/api.key.repository.sql index e5f389ac4d..f4989d355e 100644 --- a/server/src/queries/api.key.repository.sql +++ b/server/src/queries/api.key.repository.sql @@ -24,6 +24,7 @@ FROM "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt", "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes", "APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes", + "APIKeyEntity__APIKeyEntity_user"."profileChangedAt" AS "APIKeyEntity__APIKeyEntity_user_profileChangedAt", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_userId", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."key" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_key", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."value" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_value" diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index 5dd32ce365..a5d6ba05db 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -28,7 +28,8 @@ FROM "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -68,7 +69,8 @@ SELECT "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -104,7 +106,8 @@ SELECT "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 17fff94f42..2f0613b4d0 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -39,6 +39,7 @@ FROM "SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt", "SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes", "SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes", + "SessionEntity__SessionEntity_user"."profileChangedAt" AS "SessionEntity__SessionEntity_user_profileChangedAt", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_userId", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."key" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_key", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."value" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_value" diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 10af8d17db..f3b3c3140d 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -156,7 +156,8 @@ FROM "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes" + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" @@ -257,7 +258,8 @@ SELECT "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes" + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" @@ -309,7 +311,8 @@ FROM "SharedLinkEntity__SharedLinkEntity_user"."status" AS "SharedLinkEntity__SharedLinkEntity_user_status", "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt", "SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes", - "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes" + "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes", + "SharedLinkEntity__SharedLinkEntity_user"."profileChangedAt" AS "SharedLinkEntity__SharedLinkEntity_user_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId" diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 2c75786f97..ab0a6cc534 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -15,7 +15,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE @@ -60,7 +61,8 @@ SELECT "user"."status" AS "user_status", "user"."updatedAt" AS "user_updatedAt", "user"."quotaSizeInBytes" AS "user_quotaSizeInBytes", - "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes" + "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes", + "user"."profileChangedAt" AS "user_profileChangedAt" FROM "users" "user" WHERE @@ -82,7 +84,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE @@ -106,7 +109,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 92404a6958..cf918198ab 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -7,7 +7,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; -import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; +import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; @@ -93,13 +93,23 @@ export class UserService { return mapUser(user); } - async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> { + async createProfileImage(auth: AuthDto, file: Express.Multer.File): Promise<CreateProfileImageResponseDto> { const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false }); - const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path }); + + const user = await this.userRepository.update(auth.user.id, { + profileImagePath: file.path, + profileChangedAt: new Date(), + }); + if (oldpath !== '') { await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } }); } - return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath); + + return { + userId: user.id, + profileImagePath: user.profileImagePath, + profileChangedAt: user.profileChangedAt, + }; } async deleteProfileImage(auth: AuthDto): Promise<void> { @@ -107,7 +117,7 @@ export class UserService { if (user.profileImagePath === '') { throw new BadRequestException("Can't delete a missing profile Image"); } - await this.userRepository.update(auth.user.id, { profileImagePath: '' }); + await this.userRepository.update(auth.user.id, { profileImagePath: '', profileChangedAt: new Date() }); await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } }); } diff --git a/web/src/lib/components/shared-components/profile-image-cropper.svelte b/web/src/lib/components/shared-components/profile-image-cropper.svelte index 0c3f79895e..3dabd86d4f 100644 --- a/web/src/lib/components/shared-components/profile-image-cropper.svelte +++ b/web/src/lib/components/shared-components/profile-image-cropper.svelte @@ -56,13 +56,14 @@ return; } const file = new File([blob], 'profile-picture.png', { type: 'image/png' }); - const { profileImagePath } = await createProfileImage({ createProfileImageDto: { file } }); + const { profileImagePath, profileChangedAt } = await createProfileImage({ createProfileImageDto: { file } }); notificationController.show({ type: NotificationType.Info, message: $t('profile_picture_set'), timeout: 3000, }); $user.profileImagePath = profileImagePath; + $user.profileChangedAt = profileChangedAt; } catch (error) { handleError(error, $t('errors.unable_to_set_profile_picture')); } diff --git a/web/src/lib/components/shared-components/user-avatar.svelte b/web/src/lib/components/shared-components/user-avatar.svelte index bb59af9aab..74750953b5 100644 --- a/web/src/lib/components/shared-components/user-avatar.svelte +++ b/web/src/lib/components/shared-components/user-avatar.svelte @@ -13,6 +13,7 @@ email: string; profileImagePath: string; avatarColor: UserAvatarColor; + profileChangedAt: string; } export let user: User; @@ -79,7 +80,7 @@ {#if showProfileImage && user.profileImagePath} <img bind:this={img} - src={getProfileImageUrl(user.id)} + src={getProfileImageUrl(user)} alt={$t('profile_image_of_user', { values: { user: title } })} class="h-full w-full object-cover" class:hidden={showFallback} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 395d9796f4..29c7552d0c 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -19,6 +19,7 @@ import { type AssetResponseDto, type PersonResponseDto, type SharedLinkResponseDto, + type UserResponseDto, } from '@immich/sdk'; import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js'; import { sortBy } from 'lodash-es'; @@ -204,7 +205,8 @@ export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: s return createUrl(getAssetPlaybackPath(id), { key: getKey(), c: checksum }); }; -export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileImagePath(userId)); +export const getProfileImageUrl = (user: UserResponseDto) => + createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt }); export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) => createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt }); diff --git a/web/src/test-data/factories/user-factory.ts b/web/src/test-data/factories/user-factory.ts index c89dccb2bb..92d1510d40 100644 --- a/web/src/test-data/factories/user-factory.ts +++ b/web/src/test-data/factories/user-factory.ts @@ -8,6 +8,7 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({ name: Sync.each(() => faker.person.fullName()), profileImagePath: '', avatarColor: UserAvatarColor.Primary, + profileChangedAt: Sync.each(() => faker.date.recent().toISOString()), }); export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({ @@ -31,4 +32,5 @@ export const userAdminFactory = Sync.makeFactory<UserAdminResponseDto>({ activationKey: 'activation-key', activatedAt: new Date().toISOString(), }, + profileChangedAt: Sync.each(() => faker.date.recent().toISOString()), });