mirror of https://github.com/immich-app/immich.git
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>pull/16855/head
parent
a65ce2ac55
commit
b778a86c99
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
UserApiRepository(this._api);
|
||||
const UserApiRepository(this._api);
|
||||
|
||||
@override
|
||||
Future<List<UserDto>> getAll() async {
|
||||
final dto = await checkNull(_api.searchUsers());
|
||||
return dto.map(UserConverter.fromSimpleUserDto).toList();
|
||||
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 profileImagePath})> createProfileImage({
|
||||
Future<String> createProfileImage({
|
||||
required String name,
|
||||
required Uint8List data,
|
||||
}) async {
|
||||
final response = await checkNull(
|
||||
final res = await checkNull(
|
||||
_api.createProfileImage(
|
||||
MultipartFile.fromBytes('file', data, filename: name),
|
||||
),
|
||||
);
|
||||
return (profileImagePath: response.profileImagePath);
|
||||
return res.profileImagePath;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<UserDto>> getAll() async {
|
||||
final dto = await checkNull(_api.searchUsers());
|
||||
return dto.map(UserConverter.fromSimpleUserDto).toList();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
Loading…
Reference in New Issue