diff --git a/mobile/drift_schemas/main/drift_schema_v5.json b/mobile/drift_schemas/main/drift_schema_v5.json new file mode 100644 index 000000000..ce29eaabd Binary files /dev/null and b/mobile/drift_schemas/main/drift_schema_v5.json differ diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart index 1e83fa498..b0a66f7d7 100644 --- a/mobile/lib/domain/models/user.model.dart +++ b/mobile/lib/domain/models/user.model.dart @@ -11,7 +11,6 @@ class UserDto { final bool isAdmin; final DateTime updatedAt; - final String? profileImagePath; final AvatarColor avatarColor; final bool memoryEnabled; @@ -25,18 +24,22 @@ class UserDto { bool get hasQuota => quotaSizeInBytes > 0; + final bool hasProfileImage; + final DateTime profileChangedAt; + const UserDto({ required this.id, required this.email, required this.name, required this.isAdmin, required this.updatedAt, - this.profileImagePath, + required this.profileChangedAt, this.avatarColor = AvatarColor.primary, this.memoryEnabled = true, this.inTimeline = false, this.isPartnerSharedBy = false, this.isPartnerSharedWith = false, + this.hasProfileImage = false, this.quotaUsageInBytes = 0, this.quotaSizeInBytes = 0, }); @@ -49,14 +52,13 @@ email: $email, name: $name, isAdmin: $isAdmin, updatedAt: $updatedAt, -profileImagePath: ${profileImagePath ?? ''}, avatarColor: $avatarColor, memoryEnabled: $memoryEnabled, inTimeline: $inTimeline, isPartnerSharedBy: $isPartnerSharedBy, isPartnerSharedWith: $isPartnerSharedWith, -quotaUsageInBytes: $quotaUsageInBytes, -quotaSizeInBytes: $quotaSizeInBytes, +hasProfileImage: $hasProfileImage +profileChangedAt: $profileChangedAt }'''; } @@ -66,28 +68,26 @@ quotaSizeInBytes: $quotaSizeInBytes, String? name, bool? isAdmin, DateTime? updatedAt, - String? profileImagePath, AvatarColor? avatarColor, bool? memoryEnabled, bool? inTimeline, bool? isPartnerSharedBy, bool? isPartnerSharedWith, - int? quotaUsageInBytes, - int? quotaSizeInBytes, + bool? hasProfileImage, + DateTime? profileChangedAt, }) => UserDto( id: id ?? this.id, email: email ?? this.email, name: name ?? this.name, isAdmin: isAdmin ?? this.isAdmin, updatedAt: updatedAt ?? this.updatedAt, - profileImagePath: profileImagePath ?? this.profileImagePath, avatarColor: avatarColor ?? this.avatarColor, memoryEnabled: memoryEnabled ?? this.memoryEnabled, inTimeline: inTimeline ?? this.inTimeline, isPartnerSharedBy: isPartnerSharedBy ?? this.isPartnerSharedBy, isPartnerSharedWith: isPartnerSharedWith ?? this.isPartnerSharedWith, - quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, - quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, ); @override @@ -101,12 +101,11 @@ quotaSizeInBytes: $quotaSizeInBytes, other.name == name && other.isPartnerSharedBy == isPartnerSharedBy && other.isPartnerSharedWith == isPartnerSharedWith && - other.profileImagePath == profileImagePath && other.isAdmin == isAdmin && other.memoryEnabled == memoryEnabled && other.inTimeline == inTimeline && - other.quotaUsageInBytes == quotaUsageInBytes && - other.quotaSizeInBytes == quotaSizeInBytes; + other.hasProfileImage == hasProfileImage && + other.profileChangedAt.isAtSameMomentAs(profileChangedAt); } @override @@ -116,14 +115,13 @@ quotaSizeInBytes: $quotaSizeInBytes, email.hashCode ^ updatedAt.hashCode ^ isAdmin.hashCode ^ - profileImagePath.hashCode ^ avatarColor.hashCode ^ memoryEnabled.hashCode ^ inTimeline.hashCode ^ isPartnerSharedBy.hashCode ^ isPartnerSharedWith.hashCode ^ - quotaUsageInBytes.hashCode ^ - quotaSizeInBytes.hashCode; + hasProfileImage.hashCode ^ + profileChangedAt.hashCode; } class PartnerUserDto { diff --git a/mobile/lib/domain/services/user.service.dart b/mobile/lib/domain/services/user.service.dart index 3e948fe0f..d347d8aa4 100644 --- a/mobile/lib/domain/services/user.service.dart +++ b/mobile/lib/domain/services/user.service.dart @@ -45,7 +45,7 @@ class UserService { Future createProfileImage(String name, Uint8List image) async { try { final path = await _userApiRepository.createProfileImage(name: name, data: image); - final updatedUser = getMyUser().copyWith(profileImagePath: path); + final updatedUser = getMyUser(); await _storeService.put(StoreKey.currentUser, updatedUser); await _isarUserRepository.update(updatedUser); return path; diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index ab5b9a562..78fc76b45 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -50,12 +50,10 @@ class User { isAdmin: dto.isAdmin, isPartnerSharedBy: dto.isPartnerSharedBy, isPartnerSharedWith: dto.isPartnerSharedWith, - profileImagePath: dto.profileImagePath ?? "", + profileImagePath: dto.hasProfileImage ? "HAS_PROFILE_IMAGE" : "", avatarColor: dto.avatarColor, memoryEnabled: dto.memoryEnabled, inTimeline: dto.inTimeline, - quotaUsageInBytes: dto.quotaUsageInBytes, - quotaSizeInBytes: dto.quotaSizeInBytes, ); UserDto toDto() => UserDto( @@ -64,12 +62,13 @@ class User { name: name, isAdmin: isAdmin, updatedAt: updatedAt, - profileImagePath: profileImagePath.isEmpty ? null : profileImagePath, avatarColor: avatarColor, memoryEnabled: memoryEnabled, inTimeline: inTimeline, isPartnerSharedBy: isPartnerSharedBy, isPartnerSharedWith: isPartnerSharedWith, + hasProfileImage: profileImagePath.isNotEmpty, + profileChangedAt: updatedAt, quotaUsageInBytes: quotaUsageInBytes, quotaSizeInBytes: quotaSizeInBytes, ); @@ -82,11 +81,11 @@ class UserEntity extends Table with DriftDefaultsMixin { TextColumn get name => text()(); BoolColumn get isAdmin => boolean().withDefault(const Constant(false))(); TextColumn get email => text()(); - TextColumn get profileImagePath => text().nullable()(); + + BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))(); + DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); - // Quota - IntColumn get quotaSizeInBytes => integer().nullable()(); - IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/user.entity.drift.dart b/mobile/lib/infrastructure/entities/user.entity.drift.dart index 2c3c8a1f9..dbfddab4a 100644 Binary files a/mobile/lib/infrastructure/entities/user.entity.drift.dart and b/mobile/lib/infrastructure/entities/user.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 6e574afa8..353cabf31 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -66,7 +66,7 @@ class Drift extends $Drift implements IDatabaseRepository { : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); @override - int get schemaVersion => 4; + int get schemaVersion => 5; @override MigrationStrategy get migration => MigrationStrategy( @@ -94,6 +94,15 @@ class Drift extends $Drift implements IDatabaseRepository { // asset_face_entity is added await m.create(v4.assetFaceEntity); }, + from4To5: (m, v5) async { + await m.alterTable( + TableMigration( + v5.userEntity, + newColumns: [v5.userEntity.hasProfileImage, v5.userEntity.profileChangedAt], + columnTransformer: {v5.userEntity.profileChangedAt: currentDateAndTime}, + ), + ); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 5bf20780f..8129bba00 100644 Binary files a/mobile/lib/infrastructure/repositories/db.repository.steps.dart and b/mobile/lib/infrastructure/repositories/db.repository.steps.dart differ diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index 79f3d78fd..6bc6a7066 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -173,15 +173,14 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { id: user.id, email: user.email, name: user.name, - profileImagePath: user.profileImagePath?.isEmpty == true ? null : user.profileImagePath, isAdmin: user.isAdmin, updatedAt: user.updatedAt, - quotaSizeInBytes: user.quotaSizeInBytes ?? 0, - quotaUsageInBytes: user.quotaUsageInBytes, memoryEnabled: true, inTimeline: false, isPartnerSharedBy: false, isPartnerSharedWith: false, + profileChangedAt: user.profileChangedAt, + hasProfileImage: user.hasProfileImage, ), ) .get(); diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 64b0661e1..2eefa298f 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -42,7 +42,12 @@ class SyncStreamRepository extends DriftDatabaseRepository { try { await _db.batch((batch) { for (final user in data) { - final companion = UserEntityCompanion(name: Value(user.name), email: Value(user.email)); + final companion = UserEntityCompanion( + name: Value(user.name), + email: Value(user.email), + hasProfileImage: Value(user.hasProfileImage), + profileChangedAt: Value(user.profileChangedAt), + ); batch.insert(_db.userEntity, companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion)); } diff --git a/mobile/lib/infrastructure/utils/user.converter.dart b/mobile/lib/infrastructure/utils/user.converter.dart index 19958beab..dc107e6fb 100644 --- a/mobile/lib/infrastructure/utils/user.converter.dart +++ b/mobile/lib/infrastructure/utils/user.converter.dart @@ -11,7 +11,8 @@ abstract final class UserConverter { name: dto.name, isAdmin: false, updatedAt: DateTime.now(), - profileImagePath: dto.profileImagePath, + hasProfileImage: dto.profileImagePath.isNotEmpty, + profileChangedAt: dto.profileChangedAt, avatarColor: dto.avatarColor.toAvatarColor(), ); @@ -21,14 +22,13 @@ abstract final class UserConverter { name: adminDto.name, isAdmin: adminDto.isAdmin, updatedAt: adminDto.updatedAt, - profileImagePath: adminDto.profileImagePath, avatarColor: adminDto.avatarColor.toAvatarColor(), memoryEnabled: preferenceDto?.memories.enabled ?? true, inTimeline: false, isPartnerSharedBy: false, isPartnerSharedWith: false, - quotaUsageInBytes: adminDto.quotaUsageInBytes ?? 0, - quotaSizeInBytes: adminDto.quotaSizeInBytes ?? 0, + profileChangedAt: adminDto.profileChangedAt, + hasProfileImage: adminDto.profileImagePath.isNotEmpty, ); static UserDto fromPartnerDto(PartnerResponseDto dto) => UserDto( @@ -37,14 +37,13 @@ abstract final class UserConverter { name: dto.name, isAdmin: false, updatedAt: DateTime.now(), - profileImagePath: dto.profileImagePath, avatarColor: dto.avatarColor.toAvatarColor(), memoryEnabled: false, inTimeline: dto.inTimeline ?? false, isPartnerSharedBy: false, isPartnerSharedWith: false, - quotaUsageInBytes: 0, - quotaSizeInBytes: 0, + profileChangedAt: dto.profileChangedAt, + hasProfileImage: dto.profileImagePath.isNotEmpty, ); } diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index e8835e714..5bd32aaf8 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -27,15 +27,14 @@ final driftUsersProvider = FutureProvider.autoDispose>((ref) async name: entity.name, email: entity.email, isAdmin: entity.isAdmin, - profileImagePath: entity.profileImagePath, updatedAt: entity.updatedAt, - quotaSizeInBytes: entity.quotaSizeInBytes ?? 0, - quotaUsageInBytes: entity.quotaUsageInBytes, isPartnerSharedBy: false, isPartnerSharedWith: false, avatarColor: AvatarColor.primary, memoryEnabled: true, inTimeline: true, + profileChangedAt: entity.profileChangedAt, + hasProfileImage: entity.hasProfileImage, ), ) .toList(); diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index fc3e08472..02f7920d6 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -167,7 +167,6 @@ class AuthNotifier extends StateNotifier { isAuthenticated: true, name: user.name, isAdmin: user.isAdmin, - profileImagePath: user.profileImagePath, ); return true; diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index efbcf1c13..3e4539019 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -40,6 +40,11 @@ dynamic upgradeDto(dynamic value, String targetType) { addDefault(value, 'isOnboarded', false); } break; + case 'SyncUserV1': + if (value is Map) { + addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); + addDefault(value, 'hasProfileImage', false); + } } } diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index 8be71e9b2..1fbfce6c5 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -32,6 +32,7 @@ class UserCircleAvatar extends ConsumerWidget { ), child: Text(user.name[0].toUpperCase()), ); + return Tooltip( message: user.name, child: Container( @@ -42,13 +43,12 @@ class UserCircleAvatar extends ConsumerWidget { child: CircleAvatar( backgroundColor: userAvatarColor, radius: radius, - child: user.profileImagePath == null - ? textIcon - : ClipRRect( + child: user.hasProfileImage + ? ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(50)), child: CachedNetworkImage( fit: BoxFit.cover, - cacheKey: user.profileImagePath, + cacheKey: user.profileChangedAt.toIso8601String(), width: size, height: size, placeholder: (_, __) => Image.memory(kTransparentImage), @@ -57,7 +57,8 @@ class UserCircleAvatar extends ConsumerWidget { fadeInDuration: const Duration(milliseconds: 300), errorWidget: (context, error, stackTrace) => textIcon, ), - ), + ) + : textIcon, ), ), ); diff --git a/mobile/openapi/lib/model/sync_user_v1.dart b/mobile/openapi/lib/model/sync_user_v1.dart index c01ddcc9f..b9fad5ae8 100644 Binary files a/mobile/openapi/lib/model/sync_user_v1.dart and b/mobile/openapi/lib/model/sync_user_v1.dart differ diff --git a/mobile/test/domain/services/user_service_test.dart b/mobile/test/domain/services/user_service_test.dart index b3d967154..395f38a20 100644 --- a/mobile/test/domain/services/user_service_test.dart +++ b/mobile/test/domain/services/user_service_test.dart @@ -98,7 +98,7 @@ void main() { group('createProfileImage', () { test('should return profile image path', () async { const profileImagePath = 'profile.jpg'; - final updatedUser = UserStub.admin.copyWith(profileImagePath: profileImagePath); + final updatedUser = UserStub.admin; when( () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), @@ -115,7 +115,7 @@ void main() { test('should return null if profile image creation fails', () async { const profileImagePath = 'profile.jpg'; - final updatedUser = UserStub.admin.copyWith(profileImagePath: profileImagePath); + final updatedUser = UserStub.admin; when( () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index 22131b11b..c42542afb 100644 Binary files a/mobile/test/drift/main/generated/schema.dart and b/mobile/test/drift/main/generated/schema.dart differ diff --git a/mobile/test/drift/main/generated/schema_v5.dart b/mobile/test/drift/main/generated/schema_v5.dart new file mode 100644 index 000000000..5c94ff26c Binary files /dev/null and b/mobile/test/drift/main/generated/schema_v5.dart differ diff --git a/mobile/test/fixtures/sync_stream.stub.dart b/mobile/test/fixtures/sync_stream.stub.dart index a89be5ad2..23c750d6d 100644 --- a/mobile/test/fixtures/sync_stream.stub.dart +++ b/mobile/test/fixtures/sync_stream.stub.dart @@ -4,12 +4,28 @@ import 'package:openapi/api.dart'; abstract final class SyncStreamStub { static final userV1Admin = SyncEvent( type: SyncEntityType.userV1, - data: SyncUserV1(deletedAt: DateTime(2020), email: "admin@admin", id: "1", name: "Admin", avatarColor: null), + data: SyncUserV1( + deletedAt: DateTime(2020), + email: "admin@admin", + id: "1", + name: "Admin", + avatarColor: null, + hasProfileImage: false, + profileChangedAt: DateTime(2025), + ), ack: "1", ); static final userV1User = SyncEvent( type: SyncEntityType.userV1, - data: SyncUserV1(deletedAt: DateTime(2021), email: "user@user", id: "5", name: "User", avatarColor: null), + data: SyncUserV1( + deletedAt: DateTime(2021), + email: "user@user", + id: "5", + name: "User", + avatarColor: null, + hasProfileImage: false, + profileChangedAt: DateTime(2025), + ), ack: "5", ); static final userDeleteV1 = SyncEvent( diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart index 764342520..369e62440 100644 --- a/mobile/test/fixtures/user.stub.dart +++ b/mobile/test/fixtures/user.stub.dart @@ -10,7 +10,7 @@ abstract final class UserStub { name: "admin", isAdmin: true, updatedAt: DateTime(2021), - profileImagePath: null, + profileChangedAt: DateTime(2021), avatarColor: AvatarColor.green, ); @@ -20,7 +20,7 @@ abstract final class UserStub { name: "user1", isAdmin: false, updatedAt: DateTime(2022), - profileImagePath: null, + profileChangedAt: DateTime(2022), avatarColor: AvatarColor.red, ); @@ -30,7 +30,7 @@ abstract final class UserStub { name: "user2", isAdmin: false, updatedAt: DateTime(2023), - profileImagePath: null, + profileChangedAt: DateTime(2023), avatarColor: AvatarColor.primary, ); } diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index b51a4d67f..22fd3cacf 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -66,7 +66,14 @@ void main() { final MockPartnerRepository partnerRepository = MockPartnerRepository(); final MockUserService userService = MockUserService(); - final owner = UserDto(id: "1", updatedAt: DateTime.now(), email: "a@b.c", name: "first last", isAdmin: false); + final owner = UserDto( + id: "1", + updatedAt: DateTime.now(), + email: "a@b.c", + name: "first last", + isAdmin: false, + profileChangedAt: DateTime(2021), + ); late SyncService s; setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 1e9fddf79..8c491ca47 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -15124,19 +15124,28 @@ "email": { "type": "string" }, + "hasProfileImage": { + "type": "boolean" + }, "id": { "type": "string" }, "name": { "type": "string" + }, + "profileChangedAt": { + "format": "date-time", + "type": "string" } }, "required": [ "avatarColor", "deletedAt", "email", + "hasProfileImage", "id", - "name" + "name", + "profileChangedAt" ], "type": "object" }, diff --git a/server/src/database.ts b/server/src/database.ts index 44bdefa08..052955636 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -357,7 +357,7 @@ export const columns = { ], syncAlbumUser: ['album_user.albumsId as albumId', 'album_user.usersId as userId', 'album_user.role'], syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'], - syncUser: ['id', 'name', 'email', 'avatarColor', 'deletedAt', 'updateId'], + syncUser: ['id', 'name', 'email', 'avatarColor', 'deletedAt', 'updateId', 'profileImagePath', 'profileChangedAt'], stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'], syncAssetExif: [ 'asset_exif.assetId', diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 92aea8f5e..66061e7bb 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -62,6 +62,8 @@ export class SyncUserV1 { @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', nullable: true }) avatarColor!: UserAvatarColor | null; deletedAt!: Date | null; + hasProfileImage!: boolean; + profileChangedAt!: Date; } @ExtraModel() @@ -74,8 +76,6 @@ export class SyncAuthUserV1 extends SyncUserV1 { quotaSizeInBytes!: number | null; @ApiProperty({ type: 'integer' }) quotaUsageInBytes!: number; - hasProfileImage!: boolean; - profileChangedAt!: Date; } @ExtraModel() diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index 7c7774a02..5c8046015 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -452,14 +452,14 @@ select "avatarColor", "deletedAt", "updateId", + "profileImagePath", + "profileChangedAt", "isAdmin", "pinCode", "oauthId", "storageLabel", "quotaSizeInBytes", - "quotaUsageInBytes", - "profileImagePath", - "profileChangedAt" + "quotaUsageInBytes" from "user" where @@ -896,7 +896,9 @@ select "email", "avatarColor", "deletedAt", - "updateId" + "updateId", + "profileImagePath", + "profileChangedAt" from "user" where diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 486984118..d72ddcfc4 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -375,16 +375,7 @@ class AuthUserSync extends BaseSync { return this.db .selectFrom('user') .select(columns.syncUser) - .select([ - 'isAdmin', - 'pinCode', - 'oauthId', - 'storageLabel', - 'quotaSizeInBytes', - 'quotaUsageInBytes', - 'profileImagePath', - 'profileChangedAt', - ]) + .select(['isAdmin', 'pinCode', 'oauthId', 'storageLabel', 'quotaSizeInBytes', 'quotaUsageInBytes']) .$call(this.upsertTableFilters(ack)) .stream(); } diff --git a/server/src/schema/migrations/1753800911775-ProfileImageCheckpointRemoval.ts b/server/src/schema/migrations/1753800911775-ProfileImageCheckpointRemoval.ts new file mode 100644 index 000000000..4f741f211 --- /dev/null +++ b/server/src/schema/migrations/1753800911775-ProfileImageCheckpointRemoval.ts @@ -0,0 +1,25 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`DELETE FROM session_sync_checkpoint + WHERE type IN ( + 'UserV1', + 'AssetV1', + 'PartnerAssetV1', + 'PartnerAssetBackfillV1', + 'AlbumAssetV1', + 'AlbumAssetBackfillV1' + )`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DELETE FROM session_sync_checkpoint + WHERE type IN ( + 'UserV1', + 'AssetV1', + 'PartnerAssetV1', + 'PartnerAssetBackfillV1', + 'AlbumAssetV1', + 'AlbumAssetBackfillV1' + )`.execute(db); +} diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 57b953f12..fee77f35b 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -188,8 +188,8 @@ export class SyncService extends BaseService { const upsertType = SyncEntityType.UserV1; const upserts = this.syncRepository.user.getUpserts(checkpointMap[upsertType]); - for await (const { updateId, ...data } of upserts) { - send(response, { type: upsertType, ids: [updateId], data }); + for await (const { updateId, profileImagePath, ...data } of upserts) { + send(response, { type: upsertType, ids: [updateId], data: { ...data, hasProfileImage: !!profileImagePath } }); } } diff --git a/server/test/medium/specs/sync/sync-user.spec.ts b/server/test/medium/specs/sync/sync-user.spec.ts index 72661e119..c5d572d7d 100644 --- a/server/test/medium/specs/sync/sync-user.spec.ts +++ b/server/test/medium/specs/sync/sync-user.spec.ts @@ -35,9 +35,11 @@ describe(SyncEntityType.UserV1, () => { data: { deletedAt: user.deletedAt, email: user.email, + hasProfileImage: user.profileImagePath !== '', id: user.id, name: user.name, avatarColor: user.avatarColor, + profileChangedAt: user.profileChangedAt.toISOString(), }, type: 'UserV1', },