From d3f3508c7b3c2ea9e5b739d7aeb1d40452ceeceb Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 3 Mar 2025 17:38:32 -0600 Subject: [PATCH 1/3] feat(mobile): new sync --- mobile/lib/interfaces/sync_api.interface.dart | 3 + mobile/lib/models/sync/sync_event.model.dart | 66 ++++++++++ mobile/lib/models/sync/sync_user.model.dart | 77 +++++++++++ .../lib/repositories/sync_api.repository.dart | 123 ++++++++++++++++++ mobile/lib/services/sync_stream.service.dart | 16 +++ mobile/lib/widgets/common/immich_app_bar.dart | 7 + 6 files changed, 292 insertions(+) create mode 100644 mobile/lib/interfaces/sync_api.interface.dart create mode 100644 mobile/lib/models/sync/sync_event.model.dart create mode 100644 mobile/lib/models/sync/sync_user.model.dart create mode 100644 mobile/lib/repositories/sync_api.repository.dart create mode 100644 mobile/lib/services/sync_stream.service.dart diff --git a/mobile/lib/interfaces/sync_api.interface.dart b/mobile/lib/interfaces/sync_api.interface.dart new file mode 100644 index 0000000000000..180635fa6731f --- /dev/null +++ b/mobile/lib/interfaces/sync_api.interface.dart @@ -0,0 +1,3 @@ +abstract interface class ISyncApiRepository { + Future getUsers(); +} diff --git a/mobile/lib/models/sync/sync_event.model.dart b/mobile/lib/models/sync/sync_event.model.dart new file mode 100644 index 0000000000000..f6feee3100a31 --- /dev/null +++ b/mobile/lib/models/sync/sync_event.model.dart @@ -0,0 +1,66 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +enum SyncTypeEnum { user, userDelete } + +class SyncEvent { + // enum + final SyncTypeEnum syncType; + + // dynamic + final dynamic data; + + final String ack; + + SyncEvent({ + required this.syncType, + required this.data, + required this.ack, + }); + + SyncEvent copyWith({ + SyncTypeEnum? syncType, + dynamic data, + String? ack, + }) { + return SyncEvent( + syncType: syncType ?? this.syncType, + data: data ?? this.data, + ack: ack ?? this.ack, + ); + } + + Map toMap() { + return { + 'syncType': syncType.index, + 'data': data, + 'ack': ack, + }; + } + + factory SyncEvent.fromMap(Map map) { + return SyncEvent( + syncType: SyncTypeEnum.values[map['syncType'] as int], + data: map['data'] as dynamic, + ack: map['ack'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory SyncEvent.fromJson(String source) => + SyncEvent.fromMap(json.decode(source) as Map); + + @override + String toString() => 'SyncEvent(syncType: $syncType, data: $data, ack: $ack)'; + + @override + bool operator ==(covariant SyncEvent other) { + if (identical(this, other)) return true; + + return other.syncType == syncType && other.data == data && other.ack == ack; + } + + @override + int get hashCode => syncType.hashCode ^ data.hashCode ^ ack.hashCode; +} diff --git a/mobile/lib/models/sync/sync_user.model.dart b/mobile/lib/models/sync/sync_user.model.dart new file mode 100644 index 0000000000000..df48c748fe2d6 --- /dev/null +++ b/mobile/lib/models/sync/sync_user.model.dart @@ -0,0 +1,77 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +class SyncUserResponse { + final String id; + + final String name; + + final String email; + + final DateTime? deletedAt; + + SyncUserResponse({ + required this.id, + required this.name, + required this.email, + required this.deletedAt, + }); + + SyncUserResponse copyWith({ + String? id, + String? name, + String? email, + DateTime? deletedAt, + }) { + return SyncUserResponse( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + deletedAt: deletedAt ?? this.deletedAt, + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'email': email, + 'deletedAt': deletedAt, + }; + } + + factory SyncUserResponse.fromMap(Map map) { + return SyncUserResponse( + id: map['id'] as String, + name: map['name'] as String, + email: map['email'] as String, + deletedAt: + map['deletedAt'] != null ? DateTime.parse(map['deletedAt']) : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory SyncUserResponse.fromJson(String source) => + SyncUserResponse.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'SyncUserResponse(id: $id, name: $name, email: $email, deletedAt: $deletedAt)'; + } + + @override + bool operator ==(covariant SyncUserResponse other) { + if (identical(this, other)) return true; + + return other.id == id && + other.name == name && + other.email == email && + other.deletedAt == deletedAt; + } + + @override + int get hashCode { + return id.hashCode ^ name.hashCode ^ email.hashCode ^ deletedAt.hashCode; + } +} diff --git a/mobile/lib/repositories/sync_api.repository.dart b/mobile/lib/repositories/sync_api.repository.dart new file mode 100644 index 0000000000000..9afd9edac47ef --- /dev/null +++ b/mobile/lib/repositories/sync_api.repository.dart @@ -0,0 +1,123 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/interfaces/sync_api.interface.dart'; +import 'package:immich_mobile/models/sync/sync_event.model.dart'; +import 'package:immich_mobile/models/sync/sync_user.model.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/database.repository.dart'; +import 'package:http/http.dart' as http; +import 'package:openapi/api.dart'; + +final syncApiRepositoryProvider = Provider( + (ref) => SyncApiRepository( + ref.watch(dbProvider), + ), +); + +class SyncApiRepository extends DatabaseRepository + implements ISyncApiRepository { + SyncApiRepository(super.db); + + @override + Future getUsers() async { + final timer = Stopwatch()..start(); + final batchSize = 100; + String previousChunk = ''; + List lines = []; + + final client = http.Client(); + final request = + _getRequest([SyncRequestType.usersV1, SyncRequestType.usersV1]); + if (request == null) { + return; + } + + try { + final response = await client.send(request); + + await for (var chunk in response.stream.transform(utf8.decoder)) { + previousChunk += chunk; + final parts = previousChunk.split('\n'); + previousChunk = parts.removeLast(); + lines.addAll(parts); + + if (lines.length < batchSize) { + continue; + } + + await compute(_parseSyncReponse, lines); + lines.clear(); + } + } finally { + await compute(_parseSyncReponse, lines); + client.close(); + + timer.stop(); + debugPrint( + "[SyncApiRepository.getUsers] Elapsed time: ${timer.elapsedMilliseconds}ms", + ); + } + } + + Request? _getRequest(List types) { + final serverUrl = Store.tryGet(StoreKey.serverUrl); + final accessToken = Store.tryGet(StoreKey.accessToken); + if (serverUrl == null || accessToken == null) { + return null; + } + + final url = Uri.parse('$serverUrl/sync/stream'); + final request = http.Request('POST', url); + final headers = { + 'Content-Type': 'application/json', + 'x-immich-user-token': accessToken, + }; + + request.headers.addAll(headers); + request.body = json.encode({ + "types": [...types], + }); + + return request; + } +} + +// Need to be outside of the class to be able to use compute +List _parseSyncReponse( + List lines, +) { + final List data = []; + + for (var line in lines) { + try { + final jsonData = jsonDecode(line); + final type = SyncEntityType.fromJson(jsonData['type'])!; + final dataJson = jsonData['data']; + final ack = jsonData['ack']; + + switch (type) { + case SyncEntityType.userV1: + data.add( + SyncEvent( + syncType: SyncTypeEnum.user, + data: SyncUserResponse.fromMap(dataJson), + ack: ack, + ), + ); + break; + + default: + debugPrint("[_parseSyncReponse] Unknown type $type"); + } + } catch (error, stack) { + debugPrint("[_parseSyncReponse] Error parsing json $error $stack"); + } + } + + return data; +} diff --git a/mobile/lib/services/sync_stream.service.dart b/mobile/lib/services/sync_stream.service.dart new file mode 100644 index 0000000000000..668fb24aa0529 --- /dev/null +++ b/mobile/lib/services/sync_stream.service.dart @@ -0,0 +1,16 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/sync_api.interface.dart'; +import 'package:immich_mobile/repositories/sync_api.repository.dart'; + +final syncStreamServiceProvider = + Provider((ref) => SyncStreamService(ref.watch(syncApiRepositoryProvider))); + +class SyncStreamService { + final ISyncApiRepository _syncApiRepository; + + SyncStreamService(this._syncApiRepository); + + void getUsers() { + _syncApiRepository.getUsers(); + } +} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 7a42606797db2..9d00cac2bffb3 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/immich_logo_provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/sync_stream.service.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -185,6 +186,12 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { }, ), actions: [ + IconButton( + onPressed: () { + ref.read(syncStreamServiceProvider).getUsers(); + }, + icon: const Icon(Icons.sync), + ), if (actions != null) ...actions!.map( (action) => Padding( From e7326cef4400aa23771f6c0fe26e5eb61c5231da Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Mar 2025 09:18:31 -0600 Subject: [PATCH 2/3] refactor --- mobile/lib/interfaces/sync_api.interface.dart | 6 ++- .../lib/repositories/sync_api.repository.dart | 39 ++++++++++++++----- mobile/lib/services/sync_stream.service.dart | 6 ++- mobile/lib/widgets/common/immich_app_bar.dart | 2 +- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/mobile/lib/interfaces/sync_api.interface.dart b/mobile/lib/interfaces/sync_api.interface.dart index 180635fa6731f..d42ca103e2fb4 100644 --- a/mobile/lib/interfaces/sync_api.interface.dart +++ b/mobile/lib/interfaces/sync_api.interface.dart @@ -1,3 +1,7 @@ +import 'package:immich_mobile/models/sync/sync_event.model.dart'; + abstract interface class ISyncApiRepository { - Future getUsers(); + Future ack(String data); + + Stream> watchUserSyncEvent(); } diff --git a/mobile/lib/repositories/sync_api.repository.dart b/mobile/lib/repositories/sync_api.repository.dart index 9afd9edac47ef..3cd07c0baa607 100644 --- a/mobile/lib/repositories/sync_api.repository.dart +++ b/mobile/lib/repositories/sync_api.repository.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/models/sync/sync_event.model.dart'; import 'package:immich_mobile/models/sync/sync_user.model.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; import 'package:http/http.dart' as http; @@ -16,23 +17,34 @@ import 'package:openapi/api.dart'; final syncApiRepositoryProvider = Provider( (ref) => SyncApiRepository( ref.watch(dbProvider), + ref.watch(apiServiceProvider).syncApi, ), ); class SyncApiRepository extends DatabaseRepository implements ISyncApiRepository { - SyncApiRepository(super.db); + final SyncApi _syncApi; + SyncApiRepository(super.db, this._syncApi); @override - Future getUsers() async { + Stream> watchUserSyncEvent() { + return _streamSync( + types: [SyncRequestType.usersV1], + methodName: 'watchUserSyncEvent', + ); + } + + Stream> _streamSync({ + required List types, + required String methodName, + int batchSize = 5000, + }) async* { final timer = Stopwatch()..start(); - final batchSize = 100; String previousChunk = ''; List lines = []; final client = http.Client(); - final request = - _getRequest([SyncRequestType.usersV1, SyncRequestType.usersV1]); + final request = _getRequest(types); if (request == null) { return; } @@ -50,16 +62,20 @@ class SyncApiRepository extends DatabaseRepository continue; } - await compute(_parseSyncReponse, lines); + final events = await compute(_parseSyncResponse, lines); + yield events; lines.clear(); } } finally { - await compute(_parseSyncReponse, lines); + if (lines.isNotEmpty) { + final events = await compute(_parseSyncResponse, lines); + yield events; + } client.close(); timer.stop(); debugPrint( - "[SyncApiRepository.getUsers] Elapsed time: ${timer.elapsedMilliseconds}ms", + "[SyncApiRepository.$methodName] Elapsed time: ${timer.elapsedMilliseconds}ms", ); } } @@ -85,10 +101,15 @@ class SyncApiRepository extends DatabaseRepository return request; } + + @override + Future ack(String data) { + return _syncApi.sendSyncAck(SyncAckSetDto(acks: [data])); + } } // Need to be outside of the class to be able to use compute -List _parseSyncReponse( +List _parseSyncResponse( List lines, ) { final List data = []; diff --git a/mobile/lib/services/sync_stream.service.dart b/mobile/lib/services/sync_stream.service.dart index 668fb24aa0529..43d1b4dacee40 100644 --- a/mobile/lib/services/sync_stream.service.dart +++ b/mobile/lib/services/sync_stream.service.dart @@ -10,7 +10,9 @@ class SyncStreamService { SyncStreamService(this._syncApiRepository); - void getUsers() { - _syncApiRepository.getUsers(); + void syncUsers() { + _syncApiRepository.watchUserSyncEvent().listen((event) { + print(event); + }); } } diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 9d00cac2bffb3..faf7d0060956b 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -188,7 +188,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { actions: [ IconButton( onPressed: () { - ref.read(syncStreamServiceProvider).getUsers(); + ref.read(syncStreamServiceProvider).syncUsers(); }, icon: const Icon(Icons.sync), ), From 279c660bcb391be8a17f2e8f770da45c7634f268 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Mar 2025 10:29:09 -0600 Subject: [PATCH 3/3] refactor --- .../interfaces/sync_api.interface.dart | 2 +- .../models/sync/sync_event.model.dart | 16 ++---- .../models/sync/sync_user_delete.model.dart | 49 ++++++++++++++++++ .../models/sync/sync_user_update.model.dart} | 20 ++++---- .../domain/services/sync_stream.service.dart | 50 +++++++++++++++++++ .../repositories/sync_api.repository.dart | 19 ++++--- mobile/lib/services/sync_stream.service.dart | 18 ------- mobile/lib/widgets/common/immich_app_bar.dart | 2 +- 8 files changed, 128 insertions(+), 48 deletions(-) rename mobile/lib/{ => domain}/interfaces/sync_api.interface.dart (64%) rename mobile/lib/{ => domain}/models/sync/sync_event.model.dart (65%) create mode 100644 mobile/lib/domain/models/sync/sync_user_delete.model.dart rename mobile/lib/{models/sync/sync_user.model.dart => domain/models/sync/sync_user_update.model.dart} (75%) create mode 100644 mobile/lib/domain/services/sync_stream.service.dart rename mobile/lib/{ => infrastructure}/repositories/sync_api.repository.dart (86%) delete mode 100644 mobile/lib/services/sync_stream.service.dart diff --git a/mobile/lib/interfaces/sync_api.interface.dart b/mobile/lib/domain/interfaces/sync_api.interface.dart similarity index 64% rename from mobile/lib/interfaces/sync_api.interface.dart rename to mobile/lib/domain/interfaces/sync_api.interface.dart index d42ca103e2fb4..fb8f1aa46e07e 100644 --- a/mobile/lib/interfaces/sync_api.interface.dart +++ b/mobile/lib/domain/interfaces/sync_api.interface.dart @@ -1,4 +1,4 @@ -import 'package:immich_mobile/models/sync/sync_event.model.dart'; +import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; abstract interface class ISyncApiRepository { Future ack(String data); diff --git a/mobile/lib/models/sync/sync_event.model.dart b/mobile/lib/domain/models/sync/sync_event.model.dart similarity index 65% rename from mobile/lib/models/sync/sync_event.model.dart rename to mobile/lib/domain/models/sync/sync_event.model.dart index f6feee3100a31..133d22256a24c 100644 --- a/mobile/lib/models/sync/sync_event.model.dart +++ b/mobile/lib/domain/models/sync/sync_event.model.dart @@ -1,30 +1,22 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -enum SyncTypeEnum { user, userDelete } - class SyncEvent { - // enum - final SyncTypeEnum syncType; - // dynamic final dynamic data; final String ack; SyncEvent({ - required this.syncType, required this.data, required this.ack, }); SyncEvent copyWith({ - SyncTypeEnum? syncType, dynamic data, String? ack, }) { return SyncEvent( - syncType: syncType ?? this.syncType, data: data ?? this.data, ack: ack ?? this.ack, ); @@ -32,7 +24,6 @@ class SyncEvent { Map toMap() { return { - 'syncType': syncType.index, 'data': data, 'ack': ack, }; @@ -40,7 +31,6 @@ class SyncEvent { factory SyncEvent.fromMap(Map map) { return SyncEvent( - syncType: SyncTypeEnum.values[map['syncType'] as int], data: map['data'] as dynamic, ack: map['ack'] as String, ); @@ -52,15 +42,15 @@ class SyncEvent { SyncEvent.fromMap(json.decode(source) as Map); @override - String toString() => 'SyncEvent(syncType: $syncType, data: $data, ack: $ack)'; + String toString() => 'SyncEvent(data: $data, ack: $ack)'; @override bool operator ==(covariant SyncEvent other) { if (identical(this, other)) return true; - return other.syncType == syncType && other.data == data && other.ack == ack; + return other.data == data && other.ack == ack; } @override - int get hashCode => syncType.hashCode ^ data.hashCode ^ ack.hashCode; + int get hashCode => data.hashCode ^ ack.hashCode; } diff --git a/mobile/lib/domain/models/sync/sync_user_delete.model.dart b/mobile/lib/domain/models/sync/sync_user_delete.model.dart new file mode 100644 index 0000000000000..268de647ccc29 --- /dev/null +++ b/mobile/lib/domain/models/sync/sync_user_delete.model.dart @@ -0,0 +1,49 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +class SyncUserDeleteResponse { + final String userId; + SyncUserDeleteResponse({ + required this.userId, + }); + + SyncUserDeleteResponse copyWith({ + String? userId, + }) { + return SyncUserDeleteResponse( + userId: userId ?? this.userId, + ); + } + + Map toMap() { + return { + 'userId': userId, + }; + } + + factory SyncUserDeleteResponse.fromMap(Map map) { + return SyncUserDeleteResponse( + userId: map['userId'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory SyncUserDeleteResponse.fromJson(String source) => + SyncUserDeleteResponse.fromMap( + json.decode(source) as Map, + ); + + @override + String toString() => 'SyncUserDeleteResponse(userId: $userId)'; + + @override + bool operator ==(covariant SyncUserDeleteResponse other) { + if (identical(this, other)) return true; + + return other.userId == userId; + } + + @override + int get hashCode => userId.hashCode; +} diff --git a/mobile/lib/models/sync/sync_user.model.dart b/mobile/lib/domain/models/sync/sync_user_update.model.dart similarity index 75% rename from mobile/lib/models/sync/sync_user.model.dart rename to mobile/lib/domain/models/sync/sync_user_update.model.dart index df48c748fe2d6..3f6ed893a5aba 100644 --- a/mobile/lib/models/sync/sync_user.model.dart +++ b/mobile/lib/domain/models/sync/sync_user_update.model.dart @@ -1,7 +1,7 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -class SyncUserResponse { +class SyncUserUpdateResponse { final String id; final String name; @@ -10,20 +10,20 @@ class SyncUserResponse { final DateTime? deletedAt; - SyncUserResponse({ + SyncUserUpdateResponse({ required this.id, required this.name, required this.email, required this.deletedAt, }); - SyncUserResponse copyWith({ + SyncUserUpdateResponse copyWith({ String? id, String? name, String? email, DateTime? deletedAt, }) { - return SyncUserResponse( + return SyncUserUpdateResponse( id: id ?? this.id, name: name ?? this.name, email: email ?? this.email, @@ -40,8 +40,8 @@ class SyncUserResponse { }; } - factory SyncUserResponse.fromMap(Map map) { - return SyncUserResponse( + factory SyncUserUpdateResponse.fromMap(Map map) { + return SyncUserUpdateResponse( id: map['id'] as String, name: map['name'] as String, email: map['email'] as String, @@ -52,8 +52,10 @@ class SyncUserResponse { String toJson() => json.encode(toMap()); - factory SyncUserResponse.fromJson(String source) => - SyncUserResponse.fromMap(json.decode(source) as Map); + factory SyncUserUpdateResponse.fromJson(String source) => + SyncUserUpdateResponse.fromMap( + json.decode(source) as Map, + ); @override String toString() { @@ -61,7 +63,7 @@ class SyncUserResponse { } @override - bool operator ==(covariant SyncUserResponse other) { + bool operator ==(covariant SyncUserUpdateResponse other) { if (identical(this, other)) return true; return other.id == id && diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart new file mode 100644 index 0000000000000..3e4216d17e394 --- /dev/null +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -0,0 +1,50 @@ +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/domain/models/sync/sync_user_delete.model.dart'; +import 'package:immich_mobile/domain/models/sync/sync_user_update.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; + +final syncStreamServiceProvider = Provider( + (ref) => SyncStreamService( + ref.watch(syncApiRepositoryProvider), + ref.watch(userRepositoryProvider), + ), +); + +class SyncStreamService { + final ISyncApiRepository _syncApiRepository; + final IUserRepository _userRepository; + + SyncStreamService(this._syncApiRepository, this._userRepository); + + void syncUsers() { + _syncApiRepository.watchUserSyncEvent().listen((events) async { + for (final event in events) { + if (event.data is SyncUserUpdateResponse) { + final data = event.data as SyncUserUpdateResponse; + final user = await _userRepository.get(data.id); + + if (user == null) { + continue; + } + + user.name = data.name; + user.email = data.email; + user.updatedAt = DateTime.now(); + + await _userRepository.update(user); + await _syncApiRepository.ack(event.ack); + } + + if (event.data is SyncUserDeleteResponse) { + final data = event.data as SyncUserDeleteResponse; + + debugPrint("User delete: $data"); + } + } + }); + } +} diff --git a/mobile/lib/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart similarity index 86% rename from mobile/lib/repositories/sync_api.repository.dart rename to mobile/lib/infrastructure/repositories/sync_api.repository.dart index 3cd07c0baa607..94a28666c15e3 100644 --- a/mobile/lib/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -5,9 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/interfaces/sync_api.interface.dart'; -import 'package:immich_mobile/models/sync/sync_event.model.dart'; -import 'package:immich_mobile/models/sync/sync_user.model.dart'; +import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; +import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; +import 'package:immich_mobile/domain/models/sync/sync_user_delete.model.dart'; +import 'package:immich_mobile/domain/models/sync/sync_user_update.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; @@ -125,13 +126,19 @@ List _parseSyncResponse( case SyncEntityType.userV1: data.add( SyncEvent( - syncType: SyncTypeEnum.user, - data: SyncUserResponse.fromMap(dataJson), + data: SyncUserUpdateResponse.fromMap(dataJson), + ack: ack, + ), + ); + break; + case SyncEntityType.userDeleteV1: + data.add( + SyncEvent( + data: SyncUserDeleteResponse.fromMap(dataJson), ack: ack, ), ); break; - default: debugPrint("[_parseSyncReponse] Unknown type $type"); } diff --git a/mobile/lib/services/sync_stream.service.dart b/mobile/lib/services/sync_stream.service.dart deleted file mode 100644 index 43d1b4dacee40..0000000000000 --- a/mobile/lib/services/sync_stream.service.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/interfaces/sync_api.interface.dart'; -import 'package:immich_mobile/repositories/sync_api.repository.dart'; - -final syncStreamServiceProvider = - Provider((ref) => SyncStreamService(ref.watch(syncApiRepositoryProvider))); - -class SyncStreamService { - final ISyncApiRepository _syncApiRepository; - - SyncStreamService(this._syncApiRepository); - - void syncUsers() { - _syncApiRepository.watchUserSyncEvent().listen((event) { - print(event); - }); - } -} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index faf7d0060956b..c43dae3e5adc3 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/immich_logo_provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/sync_stream.service.dart'; +import 'package:immich_mobile/domain/services/sync_stream.service.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';