0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-18 02:31:28 -05:00

refactor(mobile): move user service to domain (#16782)

* refactor: user entity

* chore: rebase fixes

* refactor(mobile): move user service to domain

* fix: timeline not visible on album selection page

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2025-03-14 08:50:26 +05:30 committed by GitHub
parent a65ce2ac55
commit b778a86c99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 408 additions and 183 deletions

View file

@ -3,8 +3,12 @@ import 'dart:typed_data';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserApiRepository {
Future<UserDto?> getMyUser();
Future<List<UserDto>> getAll();
Future<({String profileImagePath})> createProfileImage({
/// Saves the [data] in the server and uses it as the current users profile image
Future<String> createProfileImage({
required String name,
required Uint8List data,
});

View file

@ -0,0 +1,64 @@
import 'dart:async';
import 'dart:typed_data';
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:logging/logging.dart';
class UserService {
final Logger _log = Logger("UserService");
final IUserRepository _userRepository;
final IUserApiRepository _userApiRepository;
final StoreService _storeService;
UserService({
required IUserRepository userRepository,
required IUserApiRepository userApiRepository,
required StoreService storeService,
}) : _userRepository = userRepository,
_userApiRepository = userApiRepository,
_storeService = storeService;
UserDto getMyUser() {
return _storeService.get(StoreKey.currentUser);
}
UserDto? tryGetMyUser() {
return _storeService.tryGet(StoreKey.currentUser);
}
Stream<UserDto?> watchMyUser() {
return _storeService.watch(StoreKey.currentUser);
}
Future<UserDto?> refreshMyUser() async {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
await _storeService.put(StoreKey.currentUser, user);
await _userRepository.update(user);
return user;
}
Future<String?> createProfileImage(String name, Uint8List image) async {
try {
return await _userApiRepository.createProfileImage(
name: name,
data: image,
);
} catch (e) {
_log.warning("Failed to upload profile image", e);
return null;
}
}
Future<List<UserDto>> getAll() async {
return await _userRepository.getAll();
}
Future<void> deleteAll() {
return _userRepository.deleteAll();
}
}

View file

@ -0,0 +1,11 @@
import 'package:immich_mobile/constants/errors.dart';
class ApiRepository {
const ApiRepository();
Future<T> checkNull<T>(Future<T?> future) async {
final response = await future;
if (response == null) throw NoResponseDtoError();
return response;
}
}

View file

@ -1,41 +1,41 @@
import 'dart:typed_data';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/interfaces/user_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
final userApiRepositoryProvider = Provider(
(ref) => UserApiRepository(
ref.watch(apiServiceProvider).usersApi,
),
);
class UserApiRepository extends ApiRepository implements IUserApiRepository {
final UsersApi _api;
const UserApiRepository(this._api);
UserApiRepository(this._api);
@override
Future<UserDto?> getMyUser() async {
final (adminDto, preferenceDto) =
await (_api.getMyUser(), _api.getMyPreferences()).wait;
if (adminDto == null) return null;
return UserConverter.fromAdminDto(adminDto, preferenceDto);
}
@override
Future<String> createProfileImage({
required String name,
required Uint8List data,
}) async {
final res = await checkNull(
_api.createProfileImage(
MultipartFile.fromBytes('file', data, filename: name),
),
);
return res.profileImagePath;
}
@override
Future<List<UserDto>> getAll() async {
final dto = await checkNull(_api.searchUsers());
return dto.map(UserConverter.fromSimpleUserDto).toList();
}
@override
Future<({String profileImagePath})> createProfileImage({
required String name,
required Uint8List data,
}) async {
final response = await checkNull(
_api.createProfileImage(
MultipartFile.fromBytes('file', data, filename: name),
),
);
return (profileImagePath: response.profileImagePath);
}
}

View file

@ -1,7 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/user.service.dart';
final otherUsersProvider =
FutureProvider.autoDispose<List<UserDto>>((ref) async {

View file

@ -1,15 +1,16 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/services/etag.service.dart';
import 'package:immich_mobile/services/exif.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:logging/logging.dart';
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
@ -59,7 +60,7 @@ class AssetNotifier extends StateNotifier<bool> {
await clearAllAssets();
log.info("Manual refresh requested, cleared assets and albums from db");
}
final users = await _userService.getUsersFromServer();
final users = await _syncService.getUsersFromServer();
bool changedUsers = false;
if (users != null) {
changedUsers = await _syncService.syncUsersFromServer(users);
@ -86,7 +87,7 @@ class AssetNotifier extends StateNotifier<bool> {
_assetService.clearTable(),
_exifService.clearTable(),
_albumService.clearTable(),
_userService.clearTable(),
_userService.deleteAll(),
_etagService.clearTable(),
]);
}

View file

@ -1,7 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.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/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user.provider.g.dart';
@ -9,3 +14,14 @@ part 'user.provider.g.dart';
@Riverpod(keepAlive: true)
IUserRepository userRepository(Ref ref) =>
IsarUserRepository(ref.watch(isarProvider));
@Riverpod(keepAlive: true)
IUserApiRepository userApiRepository(Ref ref) =>
UserApiRepository(ref.watch(apiServiceProvider).usersApi);
@Riverpod(keepAlive: true)
UserService userService(Ref ref) => UserService(
userRepository: ref.watch(userRepositoryProvider),
userApiRepository: ref.watch(userApiRepositoryProvider),
storeService: ref.watch(storeServiceProvider),
);

View file

@ -23,5 +23,38 @@ final userRepositoryProvider = Provider<IUserRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserRepositoryRef = ProviderRef<IUserRepository>;
String _$userApiRepositoryHash() => r'6b19f2c99fb83162a5ceb91adb8589eaae01bc92';
/// See also [userApiRepository].
@ProviderFor(userApiRepository)
final userApiRepositoryProvider = Provider<IUserApiRepository>.internal(
userApiRepository,
name: r'userApiRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$userApiRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserApiRepositoryRef = ProviderRef<IUserApiRepository>;
String _$userServiceHash() => r'4a0873357b7115b4d6bfa8e89b847c0b74ce0d93';
/// See also [userService].
@ProviderFor(userService)
final userServiceProvider = Provider<UserService>.internal(
userService,
name: r'userServiceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$userServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserServiceRef = ProviderRef<UserService>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -3,8 +3,8 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
enum UploadProfileStatus {
idle,
@ -72,7 +72,7 @@ class UploadProfileImageState {
class UploadProfileImageNotifier
extends StateNotifier<UploadProfileImageState> {
UploadProfileImageNotifier(this._userSErvice)
UploadProfileImageNotifier(this._userService)
: super(
UploadProfileImageState(
profileImagePath: '',
@ -80,18 +80,21 @@ class UploadProfileImageNotifier
),
);
final UserService _userSErvice;
final UserService _userService;
Future<bool> upload(XFile file) async {
state = state.copyWith(status: UploadProfileStatus.loading);
var res = await _userSErvice.uploadProfileImage(file);
var profileImagePath = await _userService.createProfileImage(
file.name,
await file.readAsBytes(),
);
if (res != null) {
if (profileImagePath != null) {
debugPrint("Successfully upload profile image");
state = state.copyWith(
status: UploadProfileStatus.success,
profileImagePath: res.profileImagePath,
profileImagePath: profileImagePath,
);
return true;
}

View file

@ -28,12 +28,10 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:logging/logging.dart';
final albumServiceProvider = Provider(
(ref) => AlbumService(
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(entityServiceProvider),
ref.watch(albumRepositoryProvider),
@ -45,7 +43,6 @@ final albumServiceProvider = Provider(
);
class AlbumService {
final UserService _userService;
final SyncService _syncService;
final EntityService _entityService;
final IAlbumRepository _albumRepository;
@ -58,7 +55,6 @@ class AlbumService {
Completer<bool> _remoteCompleter = Completer()..complete(false);
AlbumService(
this._userService,
this._syncService,
this._entityService,
this._albumRepository,
@ -171,7 +167,7 @@ class AlbumService {
final Stopwatch sw = Stopwatch()..start();
bool changes = false;
try {
final users = await _userService.getUsersFromServer();
final users = await _syncService.getUsersFromServer();
if (users != null) {
await _syncService.syncUsersFromServer(users);
}

View file

@ -20,7 +20,8 @@ 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';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart'
hide userServiceProvider;
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';
@ -30,7 +31,6 @@ import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart';
@ -45,7 +45,6 @@ final assetServiceProvider = Provider(
ref.watch(backupAlbumRepositoryProvider),
ref.watch(apiServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(userServiceProvider),
ref.watch(backupServiceProvider),
ref.watch(albumServiceProvider),
ref.watch(storeServiceProvider),
@ -62,7 +61,6 @@ class AssetService {
final IBackupAlbumRepository _backupRepository;
final ApiService _apiService;
final SyncService _syncService;
final UserService _userService;
final BackupService _backupService;
final AlbumService _albumService;
final StoreService _storeService;
@ -78,7 +76,6 @@ class AssetService {
this._backupRepository,
this._apiService,
this._syncService,
this._userService,
this._backupService,
this._albumService,
this._storeService,
@ -97,7 +94,6 @@ class AssetService {
users: syncedUsers,
getChangedAssets: _getRemoteAssetChanges,
loadAssets: _getRemoteAssets,
refreshUsers: _userService.getUsersFromServer,
);
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
return changes;

View file

@ -13,12 +13,14 @@ import 'package:flutter/widgets.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/interfaces/user_api.repository.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
@ -39,7 +41,6 @@ import 'package:immich_mobile/repositories/network.repository.dart';
import 'package:immich_mobile/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:immich_mobile/repositories/user_api.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
@ -50,7 +51,6 @@ import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/services/network.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/diff.dart';
@ -390,7 +390,7 @@ class BackgroundService {
FileMediaRepository fileMediaRepository = FileMediaRepository();
AssetMediaRepository assetMediaRepository = AssetMediaRepository();
IUserRepository userRepository = IsarUserRepository(db);
UserApiRepository userApiRepository =
IUserApiRepository userApiRepository =
UserApiRepository(apiService.usersApi);
AlbumApiRepository albumApiRepository =
AlbumApiRepository(apiService.albumsApi);
@ -413,14 +413,10 @@ class BackgroundService {
userRepository,
StoreService.I,
eTagRepository,
);
UserService userService = UserService(
partnerApiRepository,
userApiRepository,
userRepository,
);
AlbumService albumService = AlbumService(
userService,
syncSerive,
entityService,
albumRepository,

View file

@ -4,6 +4,7 @@ import 'package:collection/collection.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/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';
@ -17,6 +18,7 @@ import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
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';
@ -26,6 +28,7 @@ import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
@ -46,6 +49,8 @@ final syncServiceProvider = Provider(
ref.watch(userRepositoryProvider),
ref.watch(storeServiceProvider),
ref.watch(etagRepositoryProvider),
ref.watch(partnerApiRepositoryProvider),
ref.watch(userApiRepositoryProvider),
),
);
@ -61,6 +66,8 @@ class SyncService {
final IPartnerRepository _partnerRepository;
final StoreService _storeService;
final IETagRepository _eTagRepository;
final IPartnerApiRepository _partnerApiRepository;
final IUserApiRepository _userApiRepository;
final AsyncMutex _lock = AsyncMutex();
final Logger _log = Logger('SyncService');
@ -76,6 +83,8 @@ class SyncService {
this._userRepository,
this._storeService,
this._eTagRepository,
this._partnerApiRepository,
this._userApiRepository,
);
// public methods:
@ -95,12 +104,11 @@ class SyncService {
) getChangedAssets,
required FutureOr<List<Asset>?> Function(UserDto user, DateTime until)
loadAssets,
required FutureOr<List<UserDto>?> Function() refreshUsers,
}) =>
_lock.run(
() async =>
await _syncRemoteAssetChanges(users, getChangedAssets) ??
await _syncRemoteAssetsFull(refreshUsers, loadAssets),
await _syncRemoteAssetsFull(getUsersFromServer, loadAssets),
);
/// Syncs remote albums to the database
@ -853,6 +861,61 @@ class SyncService {
return false;
}
}
Future<List<UserDto>?> getUsersFromServer() async {
List<UserDto>? users;
try {
users = await _userApiRepository.getAll();
} catch (e) {
_log.warning("Failed to fetch users", e);
users = null;
}
final List<UserDto> sharedBy =
await _partnerApiRepository.getAll(Direction.sharedByMe);
final List<UserDto> sharedWith =
await _partnerApiRepository.getAll(Direction.sharedWithMe);
if (users == null) {
_log.warning("Failed to refresh users");
return null;
}
users.sortBy((u) => u.uid);
sharedBy.sortBy((u) => u.uid);
sharedWith.sortBy((u) => u.uid);
final updatedSharedBy = <UserDto>[];
diffSortedListsSync(
users,
sharedBy,
compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid),
both: (UserDto a, UserDto b) {
updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true));
return true;
},
onlyFirst: (UserDto a) => updatedSharedBy.add(a),
onlySecond: (UserDto b) => updatedSharedBy.add(b),
);
final updatedSharedWith = <UserDto>[];
diffSortedListsSync(
updatedSharedBy,
sharedWith,
compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid),
both: (UserDto a, UserDto b) {
updatedSharedWith.add(
a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true),
);
return true;
},
onlyFirst: (UserDto a) => updatedSharedWith.add(a),
onlySecond: (UserDto b) => updatedSharedWith.add(b),
);
return updatedSharedWith;
}
}
/// Returns a triple(toAdd, toUpdate, toRemove)

View file

@ -1,108 +0,0 @@
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/interfaces/user_api.interface.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/user_api.repository.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart';
final userServiceProvider = Provider(
(ref) => UserService(
ref.watch(partnerApiRepositoryProvider),
ref.watch(userApiRepositoryProvider),
ref.watch(userRepositoryProvider),
),
);
class UserService {
final IPartnerApiRepository _partnerApiRepository;
final IUserApiRepository _userApiRepository;
final IUserRepository _userRepository;
final Logger _log = Logger("UserService");
UserService(
this._partnerApiRepository,
this._userApiRepository,
this._userRepository,
);
Future<({String profileImagePath})?> uploadProfileImage(XFile image) async {
try {
return await _userApiRepository.createProfileImage(
name: image.name,
data: await image.readAsBytes(),
);
} catch (e) {
_log.warning("Failed to upload profile image", e);
return null;
}
}
Future<List<UserDto>> getAll() async {
return await _userRepository.getAll();
}
Future<List<UserDto>?> getUsersFromServer() async {
List<UserDto>? users;
try {
users = await _userApiRepository.getAll();
} catch (e) {
_log.warning("Failed to fetch users", e);
users = null;
}
final List<UserDto> sharedBy =
await _partnerApiRepository.getAll(Direction.sharedByMe);
final List<UserDto> sharedWith =
await _partnerApiRepository.getAll(Direction.sharedWithMe);
if (users == null) {
_log.warning("Failed to refresh users");
return null;
}
users.sortBy((u) => u.uid);
sharedBy.sortBy((u) => u.uid);
sharedWith.sortBy((u) => u.uid);
final updatedSharedBy = <UserDto>[];
diffSortedListsSync(
users,
sharedBy,
compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid),
both: (UserDto a, UserDto b) {
updatedSharedBy.add(a.copyWith(isPartnerSharedBy: true));
return true;
},
onlyFirst: (UserDto a) => updatedSharedBy.add(a),
onlySecond: (UserDto b) => updatedSharedBy.add(b),
);
final updatedSharedWith = <UserDto>[];
diffSortedListsSync(
updatedSharedBy,
sharedWith,
compare: (UserDto a, UserDto b) => a.uid.compareTo(b.uid),
both: (UserDto a, UserDto b) {
updatedSharedWith.add(
a.copyWith(inTimeline: b.inTimeline, isPartnerSharedWith: true),
);
return true;
},
onlyFirst: (UserDto a) => updatedSharedWith.add(a),
onlySecond: (UserDto b) => updatedSharedWith.add(b),
);
return updatedSharedWith;
}
Future<void> clearTable() {
return _userRepository.deleteAll();
}
}

View file

@ -1,11 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/activity_api.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/person_api.repository.dart';
import 'package:immich_mobile/repositories/user_api.repository.dart';
import 'package:immich_mobile/repositories/timeline.repository.dart';
void invalidateAllApiRepositoryProviders(WidgetRef ref) {
ref.invalidate(userApiRepositoryProvider);
@ -14,5 +14,5 @@ void invalidateAllApiRepositoryProviders(WidgetRef ref) {
ref.invalidate(albumApiRepositoryProvider);
ref.invalidate(personApiRepositoryProvider);
ref.invalidate(assetApiRepositoryProvider);
ref.invalidate(timelineUsersIdsProvider);
ref.invalidate(timelineRepositoryProvider);
}

View file

@ -0,0 +1,7 @@
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:mocktail/mocktail.dart';
class MockStoreService extends Mock implements StoreService {}
class MockUserService extends Mock implements UserService {}

View file

@ -0,0 +1,133 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.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/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:mocktail/mocktail.dart';
import '../../fixtures/user.stub.dart';
import '../../infrastructure/repository.mock.dart';
import '../service.mock.dart';
void main() {
late UserService sut;
late IUserRepository mockUserRepo;
late IUserApiRepository mockUserApiRepo;
late StoreService mockStoreService;
setUp(() {
mockUserRepo = MockUserRepository();
mockUserApiRepo = MockUserApiRepository();
mockStoreService = MockStoreService();
sut = UserService(
userRepository: mockUserRepo,
userApiRepository: mockUserApiRepo,
storeService: mockStoreService,
);
});
group('getMyUser', () {
test('should return user from store', () {
when(() => mockStoreService.get(StoreKey.currentUser))
.thenReturn(UserStub.admin);
final result = sut.getMyUser();
expect(result, UserStub.admin);
});
test('should handle user not found scenario', () {
when(() => mockStoreService.get(StoreKey.currentUser))
.thenThrow(Exception('User not found'));
expect(() => sut.getMyUser(), throwsA(isA<Exception>()));
});
});
group('tryGetMyUser', () {
test('should return user from store', () {
when(() => mockStoreService.tryGet(StoreKey.currentUser))
.thenReturn(UserStub.admin);
final result = sut.tryGetMyUser();
expect(result, UserStub.admin);
});
test('should return null if user not found', () {
when(() => mockStoreService.tryGet(StoreKey.currentUser))
.thenReturn(null);
final result = sut.tryGetMyUser();
expect(result, isNull);
});
});
group('watchMyUser', () {
test('should return user stream from store', () {
when(() => mockStoreService.watch(StoreKey.currentUser))
.thenAnswer((_) => Stream.value(UserStub.admin));
final result = sut.watchMyUser();
expect(result, emits(UserStub.admin));
});
test('should return an empty stream if user not found', () {
when(() => mockStoreService.watch(StoreKey.currentUser))
.thenAnswer((_) => const Stream.empty());
final result = sut.watchMyUser();
expect(result, emitsInOrder([]));
});
});
group('refreshMyUser', () {
test('should return user from api and store it', () async {
when(() => mockUserApiRepo.getMyUser())
.thenAnswer((_) async => UserStub.admin);
when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin))
.thenAnswer((_) async => true);
when(() => mockUserRepo.update(UserStub.admin))
.thenAnswer((_) async => UserStub.admin);
final result = await sut.refreshMyUser();
verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin))
.called(1);
verify(() => mockUserRepo.update(UserStub.admin)).called(1);
expect(result, UserStub.admin);
});
test('should return null if user not found', () async {
when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => null);
final result = await sut.refreshMyUser();
verifyNever(
() => mockStoreService.put(StoreKey.currentUser, UserStub.admin),
);
verifyNever(() => mockUserRepo.update(UserStub.admin));
expect(result, isNull);
});
});
group('createProfileImage', () {
test('should return profile image path', () async {
when(
() => mockUserApiRepo.createProfileImage(
name: 'profile.jpg',
data: Uint8List(0),
),
).thenAnswer((_) async => 'profile.jpg');
final result = await sut.createProfileImage('profile.jpg', Uint8List(0));
expect(result, 'profile.jpg');
});
test('should return null if profile image creation fails', () async {
when(
() => mockUserApiRepo.createProfileImage(
name: 'profile.jpg',
data: Uint8List(0),
),
).thenThrow(Exception('Failed to create profile image'));
final result = await sut.createProfileImage('profile.jpg', Uint8List(0));
expect(result, isNull);
});
});
}

View file

@ -1,7 +1,14 @@
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
import 'package:mocktail/mocktail.dart';
class MockStoreRepository extends Mock implements IStoreRepository {}
class MockLogRepository extends Mock implements ILogRepository {}
class MockUserRepository extends Mock implements IUserRepository {}
// API Repos
class MockUserApiRepository extends Mock implements IUserApiRepository {}

View file

@ -11,9 +11,11 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:mocktail/mocktail.dart';
import '../../infrastructure/repository.mock.dart';
import '../../repository.mocks.dart';
import '../../service.mocks.dart';
import '../../test_utils.dart';
@ -56,6 +58,9 @@ void main() {
final MockAlbumMediaRepository albumMediaRepository =
MockAlbumMediaRepository();
final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
final MockPartnerApiRepository partnerApiRepository =
MockPartnerApiRepository();
final MockUserApiRepository userApiRepository = MockUserApiRepository();
final MockPartnerRepository partnerRepository = MockPartnerRepository();
final owner = UserDto(
@ -98,6 +103,8 @@ void main() {
userRepository,
StoreService.I,
eTagRepository,
partnerApiRepository,
userApiRepository,
);
when(() => eTagRepository.get(owner.id))
.thenAnswer((_) async => ETag(id: owner.uid, time: DateTime.now()));
@ -125,6 +132,10 @@ void main() {
when(() => assetRepository.transaction<Null>(any())).thenAnswer(
(call) => (call.positionalArguments.first as Function).call(),
);
when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]);
registerFallbackValue(Direction.sharedByMe);
when(() => partnerApiRepository.getAll(any()))
.thenAnswer((_) async => []);
});
test('test inserting existing assets', () async {
final List<Asset> remoteAssets = [
@ -136,7 +147,6 @@ void main() {
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
refreshUsers: () => [owner],
);
expect(c1, isFalse);
verifyNever(() => assetRepository.updateAll(any()));
@ -155,7 +165,6 @@ void main() {
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
refreshUsers: () => [owner],
);
expect(c1, isTrue);
final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]);
@ -178,7 +187,6 @@ void main() {
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
refreshUsers: () => [owner],
);
expect(c1, isTrue);
when(
@ -191,7 +199,6 @@ void main() {
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
refreshUsers: () => [owner],
);
expect(c2, isFalse);
final currentState = [...remoteAssets];
@ -206,7 +213,6 @@ void main() {
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
refreshUsers: () => [owner],
);
expect(c3, isTrue);
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
@ -215,7 +221,6 @@ void main() {
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
refreshUsers: () => [owner],
);
expect(c4, isTrue);
});
@ -246,7 +251,6 @@ void main() {
users: [owner],
getChangedAssets: (user, since) async => (toUpsert, toDelete),
loadAssets: (user, date) => throw Exception(),
refreshUsers: () => throw Exception(),
);
expect(c, isTrue);
verify(() => assetRepository.updateAll(expected));

View file

@ -1,5 +1,4 @@
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
@ -10,6 +9,7 @@ import 'package:immich_mobile/interfaces/auth_api.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:mocktail/mocktail.dart';
@ -17,8 +17,6 @@ class MockAlbumRepository extends Mock implements IAlbumRepository {}
class MockAssetRepository extends Mock implements IAssetRepository {}
class MockUserRepository extends Mock implements IUserRepository {}
class MockBackupRepository extends Mock implements IBackupAlbumRepository {}
class MockExifInfoRepository extends Mock implements IExifInfoRepository {}
@ -37,4 +35,6 @@ class MockAuthApiRepository extends Mock implements IAuthApiRepository {}
class MockAuthRepository extends Mock implements IAuthRepository {}
class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {}
class MockPartnerRepository extends Mock implements IPartnerRepository {}

View file

@ -3,14 +3,11 @@ import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/services/network.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:mocktail/mocktail.dart';
import 'package:openapi/api.dart';
class MockApiService extends Mock implements ApiService {}
class MockUserService extends Mock implements UserService {}
class MockSyncService extends Mock implements SyncService {}
class MockHashService extends Mock implements HashService {}

View file

@ -3,6 +3,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:mocktail/mocktail.dart';
import '../domain/service.mock.dart';
import '../fixtures/album.stub.dart';
import '../fixtures/asset.stub.dart';
import '../fixtures/user.stub.dart';
@ -38,7 +39,6 @@ void main() {
);
sut = AlbumService(
userService,
syncService,
entityService,
albumRepository,
@ -84,7 +84,7 @@ void main() {
group('refreshRemoteAlbums', () {
test('is working', () async {
when(() => userService.getUsersFromServer()).thenAnswer((_) async => []);
when(() => syncService.getUsersFromServer()).thenAnswer((_) async => []);
when(() => syncService.syncUsersFromServer(any()))
.thenAnswer((_) async => true);
when(() => albumApiRepository.getAll(shared: true))
@ -102,7 +102,7 @@ void main() {
).thenAnswer((_) async => true);
final result = await sut.refreshRemoteAlbums();
expect(result, true);
verify(() => userService.getUsersFromServer()).called(1);
verify(() => syncService.getUsersFromServer()).called(1);
verify(() => syncService.syncUsersFromServer([])).called(1);
verify(() => albumApiRepository.getAll(shared: true)).called(1);
verify(() => albumApiRepository.getAll(shared: null)).called(1);

View file

@ -6,6 +6,7 @@ import 'package:mocktail/mocktail.dart';
import '../fixtures/asset.stub.dart';
import '../fixtures/user.stub.dart';
import '../infrastructure/repository.mock.dart';
import '../repository.mocks.dart';
void main() {