From dd263b010c4a799a1cf94137805a6988010da94e Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 18 Mar 2025 19:02:33 +0530 Subject: [PATCH] refactor(mobile): use user service methods (#16783) * refactor: user entity * chore: rebase fixes * refactor(mobile): refactor to use user service methods * fix: late init error * fix: lint --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- mobile/lib/domain/services/user.service.dart | 6 +- mobile/lib/providers/auth.provider.dart | 55 ++++++++----------- mobile/lib/providers/user.provider.dart | 28 +++------- .../lib/routing/tab_navigation_observer.dart | 19 +------ mobile/lib/services/album.service.dart | 9 ++- mobile/lib/services/api.service.dart | 3 + mobile/lib/services/asset.service.dart | 19 +++---- .../services/backup_verification.service.dart | 9 ++- mobile/lib/services/sync.service.dart | 18 +++--- mobile/lib/services/timeline.service.dart | 23 ++++---- mobile/lib/services/trash.service.dart | 17 +++--- .../widgets/album/album_thumbnail_card.dart | 10 ++-- .../app_bar_dialog/app_bar_profile_info.dart | 7 +-- mobile/lib/widgets/common/immich_app_bar.dart | 5 +- .../domain/services/user_service_test.dart | 44 +++++++++++---- .../modules/shared/sync_service_test.dart | 5 +- mobile/test/services/album.service_test.dart | 3 + 17 files changed, 137 insertions(+), 143 deletions(-) diff --git a/mobile/lib/domain/services/user.service.dart b/mobile/lib/domain/services/user.service.dart index 48a6f5777d..a7eac71291 100644 --- a/mobile/lib/domain/services/user.service.dart +++ b/mobile/lib/domain/services/user.service.dart @@ -44,10 +44,14 @@ class UserService { Future createProfileImage(String name, Uint8List image) async { try { - return await _userApiRepository.createProfileImage( + final path = await _userApiRepository.createProfileImage( name: name, data: image, ); + final updatedUser = getMyUser().copyWith(profileImagePath: path); + await _storeService.put(StoreKey.currentUser, updatedUser); + await _userRepository.update(updatedUser); + return path; } catch (e) { _log.warning("Failed to upload profile image", e); return null; diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 2a140911b0..9187808984 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -3,11 +3,12 @@ import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/utils/hash.dart'; @@ -18,20 +19,20 @@ final authProvider = StateNotifierProvider((ref) { return AuthNotifier( ref.watch(authServiceProvider), ref.watch(apiServiceProvider), + ref.watch(userServiceProvider), ); }); class AuthNotifier extends StateNotifier { final AuthService _authService; final ApiService _apiService; + final UserService _userService; final _log = Logger("AuthenticationNotifier"); static const Duration _timeoutDuration = Duration(seconds: 7); - AuthNotifier( - this._authService, - this._apiService, - ) : super( + AuthNotifier(this._authService, this._apiService, this._userService) + : super( AuthState( deviceId: "", userId: "", @@ -106,17 +107,21 @@ class AuthNotifier extends StateNotifier { String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; - UserDto? user = Store.tryGet(StoreKey.currentUser); + UserDto? user = _userService.tryGetMyUser(); - UserAdminResponseDto? userResponse; - UserPreferencesResponseDto? userPreferences; try { - final responses = await Future.wait([ - _apiService.usersApi.getMyUser().timeout(_timeoutDuration), - _apiService.usersApi.getMyPreferences().timeout(_timeoutDuration), - ]); - userResponse = responses[0] as UserAdminResponseDto; - userPreferences = responses[1] as UserPreferencesResponseDto; + final serverUser = + await _userService.refreshMyUser().timeout(_timeoutDuration); + if (serverUser == null) { + _log.severe("Unable to get user information from the server."); + } else { + // If the user information is successfully retrieved, update the store + // Due to the flow of the code, this will always happen on first login + user = serverUser; + await Store.put(StoreKey.deviceId, deviceId); + await Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); + await Store.put(StoreKey.accessToken, accessToken); + } } on ApiException catch (error, stackTrace) { if (error.code == 401) { _log.severe("Unauthorized access, token likely expired. Logging out."); @@ -140,22 +145,6 @@ class AuthNotifier extends StateNotifier { } } - // If the user information is successfully retrieved, update the store - // Due to the flow of the code, this will always happen on first login - if (userResponse == null) { - _log.severe("Unable to get user information from the server."); - } else { - await Store.put(StoreKey.deviceId, deviceId); - await Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); - await Store.put( - StoreKey.currentUser, - UserConverter.fromAdminDto(userResponse, userPreferences), - ); - await Store.put(StoreKey.accessToken, accessToken); - - user = UserConverter.fromAdminDto(userResponse, userPreferences); - } - // If the user is null, the login was not successful // and we don't have a local copy of the user from a prior successful login if (user == null) { @@ -163,13 +152,13 @@ class AuthNotifier extends StateNotifier { } state = state.copyWith( - isAuthenticated: true, + deviceId: deviceId, userId: user.uid, userEmail: user.email, + isAuthenticated: true, name: user.name, - profileImagePath: user.profileImagePath, isAdmin: user.isAdmin, - deviceId: deviceId, + profileImagePath: user.profileImagePath, ); return true; diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index fb574fa99a..c3623106e8 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -1,34 +1,24 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/services/timeline.service.dart'; class CurrentUserProvider extends StateNotifier { - CurrentUserProvider(this._apiService) : super(null) { - state = Store.tryGet(StoreKey.currentUser); + CurrentUserProvider(this._userService) : super(null) { + state = _userService.tryGetMyUser(); streamSub = - Store.watch(StoreKey.currentUser).listen((user) => state = user); + _userService.watchMyUser().listen((user) => state = user ?? state); } - final ApiService _apiService; + final UserService _userService; late final StreamSubscription streamSub; refresh() async { try { - final user = await _apiService.usersApi.getMyUser(); - final userPreferences = await _apiService.usersApi.getMyPreferences(); - if (user != null) { - await Store.put( - StoreKey.currentUser, - UserConverter.fromAdminDto(user, userPreferences), - ); - } + await _userService.refreshMyUser(); } catch (_) {} } @@ -41,9 +31,7 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { - return CurrentUserProvider( - ref.watch(apiServiceProvider), - ); + return CurrentUserProvider(ref.watch(userServiceProvider)); }); class TimelineUserIdsProvider extends StateNotifier> { diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index edbfe6da4c..d95820885e 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -1,11 +1,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/utils/user.converter.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -28,19 +25,7 @@ class TabNavigationObserver extends AutoRouterObserver { // Update user info try { - final userResponseDto = - await ref.read(apiServiceProvider).usersApi.getMyUser(); - final userPreferences = - await ref.read(apiServiceProvider).usersApi.getMyPreferences(); - - if (userResponseDto == null) { - return; - } - - await Store.put( - StoreKey.currentUser, - UserConverter.fromAdminDto(userResponseDto, userPreferences), - ); + ref.read(userServiceProvider).refreshMyUser(); ref.read(serverInfoProvider.notifier).getServerVersion(); } catch (e) { debugPrint("Error refreshing user info $e"); diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index d3fe7674d5..1251ef51fe 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -6,12 +6,11 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity; import 'package:immich_mobile/interfaces/album.interface.dart'; @@ -21,6 +20,7 @@ import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/album.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart'; @@ -33,6 +33,7 @@ import 'package:logging/logging.dart'; final albumServiceProvider = Provider( (ref) => AlbumService( ref.watch(syncServiceProvider), + ref.watch(userServiceProvider), ref.watch(entityServiceProvider), ref.watch(albumRepositoryProvider), ref.watch(assetRepositoryProvider), @@ -44,6 +45,7 @@ final albumServiceProvider = Provider( class AlbumService { final SyncService _syncService; + final UserService _userService; final EntityService _entityService; final IAlbumRepository _albumRepository; final IAssetRepository _assetRepository; @@ -56,6 +58,7 @@ class AlbumService { AlbumService( this._syncService, + this._userService, this._entityService, this._albumRepository, this._assetRepository, @@ -292,7 +295,7 @@ class AlbumService { Future deleteAlbum(Album album) async { try { - final userId = Store.get(StoreKey.currentUser).id; + final userId = _userService.getMyUser().id; if (album.owner.value?.isarId == userId) { await _albumApiRepository.delete(album.remoteId!); } diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 0ef68e1c41..1d17b71211 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -35,6 +35,9 @@ class ApiService implements Authentication { late MemoriesApi memoriesApi; ApiService() { + // The below line ensures that the api clients are initialized when the service is instantiated + // This is required to avoid late initialization errors when the clients are access before the endpoint is resolved + setEndpoint(''); final endpoint = Store.tryGet(StoreKey.serverEndpoint); if (endpoint != null && endpoint.isNotEmpty) { setEndpoint(endpoint); diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index ff3e908ac3..6eff80ae02 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -6,9 +6,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; @@ -19,9 +18,7 @@ import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user.provider.dart' - hide userServiceProvider; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; @@ -47,7 +44,7 @@ final assetServiceProvider = Provider( ref.watch(syncServiceProvider), ref.watch(backupServiceProvider), ref.watch(albumServiceProvider), - ref.watch(storeServiceProvider), + ref.watch(userServiceProvider), ref.watch(assetMediaRepositoryProvider), ), ); @@ -63,7 +60,7 @@ class AssetService { final SyncService _syncService; final BackupService _backupService; final AlbumService _albumService; - final StoreService _storeService; + final UserService _userService; final IAssetMediaRepository _assetMediaRepository; final log = Logger('AssetService'); @@ -78,7 +75,7 @@ class AssetService { this._syncService, this._backupService, this._albumService, - this._storeService, + this._userService, this._assetMediaRepository, ); @@ -316,7 +313,7 @@ class AssetService { ); await refreshRemoteAssets(); - final owner = _storeService.get(StoreKey.currentUser); + final owner = _userService.getMyUser(); final remoteAssets = await _assetRepository.getAll( ownerId: owner.id, state: AssetState.merged, @@ -522,12 +519,12 @@ class AssetService { } Future> getRecentlyAddedAssets() { - final me = _storeService.get(StoreKey.currentUser); + final me = _userService.getMyUser(); return _assetRepository.getRecentlyAddedAssets(me.id); } Future> getMotionAssets() { - final me = _storeService.get(StoreKey.currentUser); + final me = _userService.getMyUser(); return _assetRepository.getMotionAssets(me.id); } } diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart index e4d5ab4afd..9aa021a324 100644 --- a/mobile/lib/services/backup_verification.service.dart +++ b/mobile/lib/services/backup_verification.service.dart @@ -8,12 +8,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -22,11 +24,13 @@ import 'package:immich_mobile/utils/diff.dart'; /// Finds duplicates originating from missing EXIF information class BackupVerificationService { + final UserService _userService; final IFileMediaRepository _fileMediaRepository; final IAssetRepository _assetRepository; final IExifInfoRepository _exifInfoRepository; - BackupVerificationService( + const BackupVerificationService( + this._userService, this._fileMediaRepository, this._assetRepository, this._exifInfoRepository, @@ -34,7 +38,7 @@ class BackupVerificationService { /// Returns at most [limit] assets that were backed up without exif Future> findWronglyBackedUpAssets({int limit = 100}) async { - final owner = Store.get(StoreKey.currentUser).id; + final owner = _userService.getMyUser().id; final List onlyLocal = await _assetRepository.getAll( ownerId: owner, state: AssetState.local, @@ -214,6 +218,7 @@ class BackupVerificationService { final backupVerificationServiceProvider = Provider( (ref) => BackupVerificationService( + ref.watch(userServiceProvider), ref.watch(fileMediaRepositoryProvider), ref.watch(assetRepositoryProvider), ref.watch(exifRepositoryProvider), diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index a598941f81..24f2fd00d6 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -5,9 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/interfaces/exif.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/interfaces/user_api.repository.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; @@ -20,7 +19,6 @@ import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/interfaces/partner.interface.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:immich_mobile/providers/infrastructure/exif.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/album.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart'; @@ -47,7 +45,7 @@ final syncServiceProvider = Provider( ref.watch(exifRepositoryProvider), ref.watch(partnerRepositoryProvider), ref.watch(userRepositoryProvider), - ref.watch(storeServiceProvider), + ref.watch(userServiceProvider), ref.watch(etagRepositoryProvider), ref.watch(partnerApiRepositoryProvider), ref.watch(userApiRepositoryProvider), @@ -63,8 +61,8 @@ class SyncService { final IAssetRepository _assetRepository; final IExifInfoRepository _exifInfoRepository; final IUserRepository _userRepository; + final UserService _userService; final IPartnerRepository _partnerRepository; - final StoreService _storeService; final IETagRepository _eTagRepository; final IPartnerApiRepository _partnerApiRepository; final IUserApiRepository _userApiRepository; @@ -81,7 +79,7 @@ class SyncService { this._exifInfoRepository, this._partnerRepository, this._userRepository, - this._storeService, + this._userService, this._eTagRepository, this._partnerApiRepository, this._userApiRepository, @@ -210,7 +208,7 @@ class SyncService { DateTime since, ) getChangedAssets, ) async { - final currentUser = _storeService.get(StoreKey.currentUser); + final currentUser = _userService.getMyUser(); final DateTime? since = (await _eTagRepository.get(currentUser.id))?.time?.toUtc(); if (since == null) return null; @@ -261,7 +259,7 @@ class SyncService { Future> _getAllAccessibleUsers() async { final sharedWith = (await _partnerRepository.getSharedWith()).toSet(); - sharedWith.add(_storeService.get(StoreKey.currentUser)); + sharedWith.add(_userService.getMyUser()); return sharedWith.toList(); } @@ -455,7 +453,7 @@ class SyncService { } if (album.shared || dto.shared) { - final userId = (_storeService.get(StoreKey.currentUser)).id; + final userId = (_userService.getMyUser()).id; final foreign = await _assetRepository.getByAlbum(album, notOwnedBy: [userId]); existing.addAll(foreign); @@ -591,7 +589,7 @@ class SyncService { // general case, e.g. some assets have been deleted or there are excluded albums on iOS final inDb = await _assetRepository.getByAlbum( dbAlbum, - ownerId: (_storeService.get(StoreKey.currentUser)).id, + ownerId: (_userService.getMyUser()).id, sortBy: AssetSort.checksum, ); diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart index 03042e266b..dd2252602d 100644 --- a/mobile/lib/services/timeline.service.dart +++ b/mobile/lib/services/timeline.service.dart @@ -1,11 +1,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/interfaces/timeline.interface.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/timeline.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; @@ -14,28 +13,28 @@ final timelineServiceProvider = Provider((ref) { return TimelineService( ref.watch(timelineRepositoryProvider), ref.watch(appSettingsServiceProvider), - ref.watch(storeServiceProvider), + ref.watch(userServiceProvider), ); }); class TimelineService { final ITimelineRepository _timelineRepository; final AppSettingsService _appSettingsService; - final StoreService _storeService; + final UserService _userService; const TimelineService( this._timelineRepository, this._appSettingsService, - this._storeService, + this._userService, ); Future> getTimelineUserIds() async { - final me = _storeService.get(StoreKey.currentUser); + final me = _userService.getMyUser(); return _timelineRepository.getTimelineUserIds(me.id); } Stream> watchTimelineUserIds() async* { - final me = _storeService.get(StoreKey.currentUser); + final me = _userService.getMyUser(); yield* _timelineRepository.watchTimelineUsers(me.id); } @@ -51,13 +50,13 @@ class TimelineService { } Stream watchArchiveTimeline() async* { - final user = _storeService.get(StoreKey.currentUser); + final user = _userService.getMyUser(); yield* _timelineRepository.watchArchiveTimeline(user.id); } Stream watchFavoriteTimeline() async* { - final user = _storeService.get(StoreKey.currentUser); + final user = _userService.getMyUser(); yield* _timelineRepository.watchFavoriteTimeline(user.id); } @@ -70,7 +69,7 @@ class TimelineService { } Stream watchTrashTimeline() async* { - final user = _storeService.get(StoreKey.currentUser); + final user = _userService.getMyUser(); yield* _timelineRepository.watchTrashTimeline(user.id); } @@ -97,7 +96,7 @@ class TimelineService { } Stream watchAssetSelectionTimeline() async* { - final user = _storeService.get(StoreKey.currentUser); + final user = _userService.getMyUser(); yield* _timelineRepository.watchAssetSelectionTimeline(user.id); } diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart index 338f063fd3..478ad65db0 100644 --- a/mobile/lib/services/trash.service.dart +++ b/mobile/lib/services/trash.service.dart @@ -1,10 +1,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:openapi/api.dart'; @@ -13,19 +12,19 @@ final trashServiceProvider = Provider((ref) { return TrashService( ref.watch(apiServiceProvider), ref.watch(assetRepositoryProvider), - ref.watch(storeServiceProvider), + ref.watch(userServiceProvider), ); }); class TrashService { final ApiService _apiService; final IAssetRepository _assetRepository; - final StoreService _storeService; + final UserService _userService; - TrashService( + const TrashService( this._apiService, this._assetRepository, - this._storeService, + this._userService, ); Future restoreAssets(Iterable assetList) async { @@ -43,7 +42,7 @@ class TrashService { } Future emptyTrash() async { - final user = _storeService.get(StoreKey.currentUser); + final user = _userService.getMyUser(); await _apiService.trashApi.emptyTrash(); @@ -74,7 +73,7 @@ class TrashService { } Future restoreTrash() async { - final user = _storeService.get(StoreKey.currentUser); + final user = _userService.getMyUser(); await _apiService.trashApi.restoreTrash(); diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index ec984d1017..089544a9f1 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -1,13 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; -class AlbumThumbnailCard extends StatelessWidget { +class AlbumThumbnailCard extends ConsumerWidget { final Function()? onTap; /// Whether or not to show the owner of the album (or "Owned") @@ -26,7 +26,7 @@ class AlbumThumbnailCard extends StatelessWidget { final Album album; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return LayoutBuilder( builder: (context, constraints) { var cardSize = constraints.maxWidth; @@ -58,7 +58,7 @@ class AlbumThumbnailCard extends StatelessWidget { // Add the owner name to the subtitle String? owner; if (showOwner) { - if (album.ownerId == Store.get(StoreKey.currentUser).uid) { + if (album.ownerId == ref.read(currentUserProvider)?.uid) { owner = 'album_thumbnail_owned'.tr(); } else if (album.ownerName != null) { owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]); diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index 4d4376b71d..93cdcd0e6f 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -21,7 +19,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { final authState = ref.watch(authProvider); final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status; - final user = Store.tryGet(StoreKey.currentUser); + final user = ref.watch(currentUserProvider); buildUserProfileImage() { if (user == null) { @@ -67,9 +65,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget { profileImagePath, ); if (user != null) { - final updatedUser = - user.copyWith(profileImagePath: profileImagePath); - await Store.put(StoreKey.currentUser, updatedUser); ref.read(currentUserProvider.notifier).refresh(); } } diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 7c1e0c1c4e..c776f7aa53 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -3,14 +3,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; 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/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -30,7 +29,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { backupState.backgroundBackup || backupState.autoBackup; final ServerInfo serverInfoState = ref.watch(serverInfoProvider); final immichLogo = ref.watch(immichLogoProvider); - final user = Store.tryGet(StoreKey.currentUser); + final user = ref.watch(currentUserProvider); final isDarkTheme = context.isDarkTheme; const widgetSize = 30.0; diff --git a/mobile/test/domain/services/user_service_test.dart b/mobile/test/domain/services/user_service_test.dart index 512f5cb4a2..52344cc9a7 100644 --- a/mobile/test/domain/services/user_service_test.dart +++ b/mobile/test/domain/services/user_service_test.dart @@ -27,12 +27,16 @@ void main() { userApiRepository: mockUserApiRepo, storeService: mockStoreService, ); + + registerFallbackValue(UserStub.admin); + when(() => mockStoreService.get(StoreKey.currentUser)) + .thenReturn(UserStub.admin); + when(() => mockStoreService.tryGet(StoreKey.currentUser)) + .thenReturn(UserStub.admin); }); group('getMyUser', () { test('should return user from store', () { - when(() => mockStoreService.get(StoreKey.currentUser)) - .thenReturn(UserStub.admin); final result = sut.getMyUser(); expect(result, UserStub.admin); }); @@ -47,8 +51,6 @@ void main() { group('tryGetMyUser', () { test('should return user from store', () { - when(() => mockStoreService.tryGet(StoreKey.currentUser)) - .thenReturn(UserStub.admin); final result = sut.tryGetMyUser(); expect(result, UserStub.admin); }); @@ -107,26 +109,48 @@ void main() { group('createProfileImage', () { test('should return profile image path', () async { + const profileImagePath = 'profile.jpg'; + final updatedUser = + UserStub.admin.copyWith(profileImagePath: profileImagePath); + when( () => mockUserApiRepo.createProfileImage( - name: 'profile.jpg', + name: profileImagePath, data: Uint8List(0), ), - ).thenAnswer((_) async => 'profile.jpg'); + ).thenAnswer((_) async => profileImagePath); + when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)) + .thenAnswer((_) async => true); + when(() => mockUserRepo.update(updatedUser)) + .thenAnswer((_) async => UserStub.admin); - final result = await sut.createProfileImage('profile.jpg', Uint8List(0)); - expect(result, 'profile.jpg'); + final result = + await sut.createProfileImage(profileImagePath, Uint8List(0)); + + verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)) + .called(1); + verify(() => mockUserRepo.update(updatedUser)).called(1); + expect(result, profileImagePath); }); test('should return null if profile image creation fails', () async { + const profileImagePath = 'profile.jpg'; + final updatedUser = + UserStub.admin.copyWith(profileImagePath: profileImagePath); + when( () => mockUserApiRepo.createProfileImage( - name: 'profile.jpg', + name: profileImagePath, data: Uint8List(0), ), ).thenThrow(Exception('Failed to create profile image')); - final result = await sut.createProfileImage('profile.jpg', Uint8List(0)); + final result = + await sut.createProfileImage(profileImagePath, Uint8List(0)); + verifyNever( + () => mockStoreService.put(StoreKey.currentUser, updatedUser), + ); + verifyNever(() => mockUserRepo.update(updatedUser)); expect(result, isNull); }); }); diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index f74c815997..7594c35fff 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -15,6 +15,7 @@ import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; +import '../../domain/service.mock.dart'; import '../../infrastructure/repository.mock.dart'; import '../../repository.mocks.dart'; import '../../service.mocks.dart'; @@ -62,6 +63,7 @@ void main() { MockPartnerApiRepository(); final MockUserApiRepository userApiRepository = MockUserApiRepository(); final MockPartnerRepository partnerRepository = MockPartnerRepository(); + final MockUserService userService = MockUserService(); final owner = UserDto( uid: "1", @@ -101,11 +103,12 @@ void main() { exifInfoRepository, partnerRepository, userRepository, - StoreService.I, + userService, eTagRepository, partnerApiRepository, userApiRepository, ); + when(() => userService.getMyUser()).thenReturn(owner); when(() => eTagRepository.get(owner.id)) .thenAnswer((_) async => ETag(id: owner.uid, time: DateTime.now())); when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {}); diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart index 993456ad99..0efd695560 100644 --- a/mobile/test/services/album.service_test.dart +++ b/mobile/test/services/album.service_test.dart @@ -31,6 +31,8 @@ void main() { albumMediaRepository = MockAlbumMediaRepository(); albumApiRepository = MockAlbumApiRepository(); + when(() => userService.getMyUser()).thenReturn(UserStub.user1); + when(() => albumRepository.transaction(any())).thenAnswer( (call) => (call.positionalArguments.first as Function).call(), ); @@ -40,6 +42,7 @@ void main() { sut = AlbumService( syncService, + userService, entityService, albumRepository, assetRepository,