refactor/partner-page
shenlong-tanwen 2026-06-04 04:06:57 +05:30
parent 4e27647233
commit f746993a6d
15 changed files with 171 additions and 171 deletions

View File

@ -332,6 +332,16 @@ class Partner extends User {
this.inTimeline = false,
});
Partner.fromUser(User user, {this.inTimeline = false})
: super(
id: user.id,
name: user.name,
email: user.email,
profileChangedAt: user.profileChangedAt,
hasProfileImage: user.hasProfileImage,
avatarColor: user.avatarColor,
);
@override
String toString() {
return 'Partner(user: ${super.toString()}, inTimeline: $inTimeline)';

View File

@ -25,18 +25,18 @@ class PartnerService {
Stream<Iterable<Partner>> search(String userId, PartnerDirection direction) =>
_partnerRepository.search(userId, direction);
Future<void> update(String partnerId, String userId, {required bool inTimeline}) async {
await _partnerApiRepository.update(partnerId, inTimeline: inTimeline);
await _partnerRepository.update(partnerId, userId, inTimeline: inTimeline);
Future<void> update({required String sharedById, required String sharedWithId, required bool inTimeline}) async {
await _partnerApiRepository.update(sharedById, inTimeline: inTimeline);
await _partnerRepository.update(sharedById: sharedById, sharedWithId: sharedWithId, inTimeline: inTimeline);
}
Future<void> create(String partnerId, String userId) async {
await _partnerApiRepository.create(partnerId);
await _partnerRepository.create(partnerId, userId);
Future<void> create({required String sharedById, required String sharedWithId, bool inTimeline = false}) async {
await _partnerApiRepository.create(sharedWithId);
await _partnerRepository.create(sharedById: sharedById, sharedWithId: sharedWithId, inTimeline: inTimeline);
}
Future<void> delete(String partnerId, String userId) async {
await _partnerApiRepository.delete(partnerId);
await _partnerRepository.delete(partnerId, userId);
Future<void> delete({required String sharedById, required String sharedWithId}) async {
await _partnerApiRepository.delete(sharedWithId);
await _partnerRepository.delete(sharedById: sharedById, sharedWithId: sharedWithId);
}
}

View File

@ -1,8 +1,5 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)')
@ -17,14 +14,4 @@ class PartnerEntity extends Table with DriftDefaultsMixin {
@override
Set<Column> get primaryKey => {sharedById, sharedWithId};
static Partner rowToPartner(UserEntityData user, PartnerEntityData partner) => Partner(
id: user.id,
email: user.email,
name: user.name,
profileChangedAt: user.profileChangedAt,
hasProfileImage: user.hasProfileImage,
avatarColor: user.avatarColor,
inTimeline: partner.inTimeline,
);
}

View File

@ -1,6 +1,5 @@
import 'package:drift/drift.dart' hide Index;
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class UserEntity extends Table with DriftDefaultsMixin {
@ -17,13 +16,4 @@ class UserEntity extends Table with DriftDefaultsMixin {
@override
Set<Column> get primaryKey => {id};
static User rowToUser(UserEntityData row) => User(
id: row.id,
name: row.name,
email: row.email,
profileChangedAt: row.profileChangedAt,
hasProfileImage: row.hasProfileImage,
avatarColor: row.avatarColor,
);
}

View File

@ -0,0 +1,15 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
User mapToUser(UserEntityData data) => User(
id: data.id,
name: data.name,
email: data.email,
hasProfileImage: data.hasProfileImage,
profileChangedAt: data.profileChangedAt,
avatarColor: data.avatarColor,
);
Partner mapToPartner(UserEntityData user, PartnerEntityData partner) =>
Partner.fromUser(mapToUser(user), inTimeline: partner.inTimeline);

View File

@ -1,24 +1,20 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
import 'package:immich_mobile/infrastructure/mapper.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class PartnerRepository {
final Drift _db;
const PartnerRepository(this._db);
Partner _resultToPartner(TypedResult result) {
final user = result.readTable(_db.userEntity);
final partner = result.readTable(_db.partnerEntity);
return PartnerEntity.rowToPartner(user, partner);
}
Future<Partner> get(String partnerId, String userId) =>
Future<Partner> get({required String sharedById, required String sharedWithId}) =>
(_db.select(_db.partnerEntity).join([
innerJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.partnerEntity.sharedById)),
])..where(_db.partnerEntity.sharedById.equals(partnerId) & _db.partnerEntity.sharedWithId.equals(userId)))
])..where(
_db.partnerEntity.sharedById.equals(sharedById) & _db.partnerEntity.sharedWithId.equals(sharedWithId),
))
.map(_resultToPartner)
.getSingle();
@ -41,15 +37,26 @@ class PartnerRepository {
.map(_resultToPartner)
.watch();
Future<void> create(String partnerId, String userId) => _db.partnerEntity.insertOnConflictUpdate(
PartnerEntityCompanion(sharedById: Value(userId), sharedWithId: Value(partnerId), inTimeline: const Value(false)),
);
Future<void> update(String partnerId, String userId, {required bool inTimeline}) =>
(_db.partnerEntity.update()..where((t) => t.sharedById.equals(partnerId) & t.sharedWithId.equals(userId))).write(
PartnerEntityCompanion(inTimeline: Value(inTimeline)),
Future<void> create({required String sharedById, required String sharedWithId, bool inTimeline = false}) =>
_db.partnerEntity.insertOnConflictUpdate(
PartnerEntityCompanion(
sharedById: Value(sharedById),
sharedWithId: Value(sharedWithId),
inTimeline: Value(inTimeline),
),
);
Future<void> delete(String partnerId, String userId) =>
(_db.partnerEntity.delete()..where((t) => t.sharedById.equals(userId) & t.sharedWithId.equals(partnerId))).go();
Future<void> update({required String sharedById, required String sharedWithId, required bool inTimeline}) =>
(_db.partnerEntity.update()..where((t) => t.sharedById.equals(sharedById) & t.sharedWithId.equals(sharedWithId)))
.write(PartnerEntityCompanion(inTimeline: Value(inTimeline)));
Future<void> delete({required String sharedById, required String sharedWithId}) =>
(_db.partnerEntity.delete()..where((t) => t.sharedById.equals(sharedById) & t.sharedWithId.equals(sharedWithId)))
.go();
Partner _resultToPartner(TypedResult result) {
final user = result.readTable(_db.userEntity);
final partner = result.readTable(_db.partnerEntity);
return mapToPartner(user, partner);
}
}

View File

@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/mapper.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
@ -10,7 +10,7 @@ class UserRepository {
final Drift _db;
const UserRepository(this._db);
Stream<Iterable<User>> getAll() => _db.select(_db.userEntity).map(UserEntity.rowToUser).watch();
Stream<Iterable<User>> getAll() => _db.select(_db.userEntity).map(mapToUser).watch();
}
class DriftAuthUserRepository extends DriftDatabaseRepository {

View File

@ -10,7 +10,7 @@ import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
@visibleForTesting
final candidatesProvider = StreamProvider.autoDispose<Iterable<User>>((ref) {
final candidatesStateProvider = StreamProvider.autoDispose<Iterable<User>>((ref) {
final currentUser = ref.watch(currentUserProvider);
// TODO: Refactor with a route guard to avoid this check in every provider
if (currentUser == null) {
@ -20,7 +20,7 @@ final candidatesProvider = StreamProvider.autoDispose<Iterable<User>>((ref) {
});
@visibleForTesting
final partnersProvider = StreamProvider.autoDispose<Iterable<Partner>>((ref) {
final partnersStateProvider = StreamProvider.autoDispose<Iterable<Partner>>((ref) {
final currentUser = ref.watch(currentUserProvider);
// TODO: Refactor with a route guard to avoid this check in every provider
if (currentUser == null) {
@ -30,35 +30,35 @@ final partnersProvider = StreamProvider.autoDispose<Iterable<Partner>>((ref) {
return ref.watch(partnerServiceProvider).search(currentUser.id, .sharedBy);
});
@RoutePage()
class DriftPartnerPage extends ConsumerWidget {
const DriftPartnerPage({super.key});
Future<void> _addPartner(BuildContext context, WidgetRef ref) async {
final selected = await showDialog<User>(context: context, builder: (_) => const PartnerSelectionDialog());
final currentUser = ref.read(currentUserProvider);
if (selected != null && currentUser != null) {
await ref.read(partnerServiceProvider).create(selected.id, currentUser.id);
}
Future<void> _addPartner(BuildContext context, WidgetRef ref) async {
final selected = await showDialog<User>(context: context, builder: (_) => const PartnerSelectionDialog());
final currentUser = ref.read(currentUserProvider);
if (selected != null && currentUser != null) {
await ref.read(partnerServiceProvider).create(sharedById: currentUser.id, sharedWithId: selected.id);
}
}
Future<void> _removePartner(BuildContext context, WidgetRef ref, Partner partner) => showDialog(
context: context,
builder: (_) => ConfirmDialog(
title: "stop_photo_sharing",
content: context.t.partner_page_stop_sharing_content(partner: partner.name),
onOk: () {
final currentUser = ref.read(currentUserProvider);
if (currentUser != null) {
ref.read(partnerServiceProvider).delete(partner.id, currentUser.id);
}
},
),
);
Future<void> _removePartner(BuildContext context, WidgetRef ref, Partner partner) => showDialog(
context: context,
builder: (_) => ConfirmDialog(
title: "stop_photo_sharing",
content: context.t.partner_page_stop_sharing_content(partner: partner.name),
onOk: () {
final currentUser = ref.read(currentUserProvider);
if (currentUser != null) {
ref.read(partnerServiceProvider).delete(sharedById: currentUser.id, sharedWithId: partner.id);
}
},
),
);
@RoutePage()
class PartnerPage extends ConsumerWidget {
const PartnerPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedByAsync = ref.watch(partnersProvider);
final sharedByAsync = ref.watch(partnersStateProvider);
return Scaffold(
appBar: AppBar(
@ -76,8 +76,8 @@ class DriftPartnerPage extends ConsumerWidget {
body: sharedByAsync.when(
data: (partners) => PartnerSharedByList(
partners: partners.toList(growable: false),
onAddPartner: () => _addPartner(context, ref),
onRemovePartner: (partner) => _removePartner(context, ref, partner),
onAdd: () => _addPartner(context, ref),
onRemove: (partner) => _removePartner(context, ref, partner),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text(context.t.error_loading_partners(error: error))),
@ -86,42 +86,48 @@ class DriftPartnerPage extends ConsumerWidget {
}
}
class _EmptyPartners extends StatelessWidget {
const _EmptyPartners({required this.onAdd});
final VoidCallback onAdd;
@override
Widget build(BuildContext context) {
return Padding(
padding: const .symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: .start,
children: [
Padding(
padding: const .symmetric(vertical: 8),
child: Text(context.t.partner_page_empty_message, style: const TextStyle(fontSize: 14)),
),
Align(
alignment: .center,
child: ElevatedButton.icon(
onPressed: onAdd,
icon: const Icon(Icons.person_add),
label: Text(context.t.add_partner),
),
),
],
),
);
}
}
@visibleForTesting
class PartnerSharedByList extends StatelessWidget {
const PartnerSharedByList({
super.key,
required this.partners,
required this.onAddPartner,
required this.onRemovePartner,
});
const PartnerSharedByList({super.key, required this.partners, required this.onAdd, required this.onRemove});
final List<Partner> partners;
final VoidCallback onAddPartner;
final ValueChanged<Partner> onRemovePartner;
final VoidCallback onAdd;
final ValueChanged<Partner> onRemove;
@override
Widget build(BuildContext context) {
if (partners.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(context.t.partner_page_empty_message, style: const TextStyle(fontSize: 14)),
),
Align(
alignment: Alignment.center,
child: ElevatedButton.icon(
onPressed: onAddPartner,
icon: const Icon(Icons.person_add),
label: Text(context.t.add_partner),
),
),
],
),
);
return _EmptyPartners(onAdd: onAdd);
}
return ListView.builder(
@ -132,7 +138,7 @@ class PartnerSharedByList extends StatelessWidget {
leading: PartnerUserAvatar(userId: partner.id, name: partner.name),
title: Text(partner.name),
subtitle: Text(partner.email),
trailing: IconButton(icon: const Icon(Icons.person_remove), onPressed: () => onRemovePartner(partner)),
trailing: IconButton(icon: const Icon(Icons.person_remove), onPressed: () => onRemove(partner)),
);
},
);
@ -145,7 +151,7 @@ class PartnerSelectionDialog extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final candidatesAsync = ref.watch(candidatesProvider);
final candidatesAsync = ref.watch(candidatesStateProvider);
return SimpleDialog(
title: const Text("partner_page_select_partner").tr(),
@ -155,7 +161,7 @@ class PartnerSelectionDialog extends ConsumerWidget {
if (users.isEmpty) {
return [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding: const .symmetric(horizontal: 24, vertical: 8),
child: const Text("partner_page_no_more_users").tr(),
),
];
@ -167,7 +173,7 @@ class PartnerSelectionDialog extends ConsumerWidget {
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
padding: const .only(right: 8),
child: PartnerUserAvatar(userId: candidate.id, name: candidate.name),
),
Text(candidate.name),
@ -178,14 +184,14 @@ class PartnerSelectionDialog extends ConsumerWidget {
},
loading: () => const [
Padding(
padding: EdgeInsets.all(24),
padding: .all(24),
child: Center(child: CircularProgressIndicator()),
),
],
error: (error, _) => [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Text("error_loading_partners".tr(args: [error.toString()])),
padding: const .symmetric(horizontal: 24, vertical: 8),
child: Text(context.t.error_loading_partners(error: error)),
),
],
),

View File

@ -329,11 +329,11 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
}
@visibleForTesting
final driftSharedWithPartnerProvider = StreamProvider.autoDispose<Iterable<Partner>>((ref) {
final sharedWithPartnerProvider = StreamProvider.autoDispose<Iterable<Partner>>((ref) {
final currentUser = ref.watch(currentUserProvider);
if (currentUser == null) {
// TODO: Refactor with a route guard to avoid this check in every provider
return const Stream.empty();
return const .empty();
}
return ref.watch(partnerServiceProvider).search(currentUser.id, .sharedWith);
@ -344,7 +344,7 @@ class _QuickAccessButtonList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final partnerSharedWithAsync = ref.watch(driftSharedWithPartnerProvider);
final partnerSharedWithAsync = ref.watch(sharedWithPartnerProvider);
final partners = partnerSharedWithAsync.valueOrNull ?? [];
return SliverPadding(
@ -399,7 +399,7 @@ class _QuickAccessButtonList extends ConsumerWidget {
'partners'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
),
onTap: () => context.pushRoute(const DriftPartnerRoute()),
onTap: () => context.pushRoute(const PartnerRoute()),
),
_PartnerList(partners: partners.toList()),
],

View File

@ -63,7 +63,9 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
}
try {
await ref.read(partnerServiceProvider).update(widget.partner.id, user.id, inTimeline: !_inTimeline);
await ref
.read(partnerServiceProvider)
.update(sharedById: widget.partner.id, sharedWithId: user.id, inTimeline: !_inTimeline);
setState(() {
_inTimeline = !_inTimeline;

View File

@ -27,7 +27,7 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart';
import 'package:immich_mobile/pages/common/tab_shell.page.dart';
import 'package:immich_mobile/pages/library/folder/folder.page.dart';
import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart';
import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart';
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
import 'package:immich_mobile/pages/login/change_password.page.dart';
@ -176,7 +176,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftPlaceRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPlaceDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftUserSelectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),

View File

@ -869,22 +869,6 @@ class DriftPartnerDetailRouteArgs {
int get hashCode => key.hashCode ^ partner.hashCode;
}
/// generated route for
/// [DriftPartnerPage]
class DriftPartnerRoute extends PageRouteInfo<void> {
const DriftPartnerRoute({List<PageRouteInfo>? children})
: super(DriftPartnerRoute.name, initialChildren: children);
static const String name = 'DriftPartnerRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftPartnerPage();
},
);
}
/// generated route for
/// [DriftPeopleCollectionPage]
class DriftPeopleCollectionRoute extends PageRouteInfo<void> {
@ -1456,6 +1440,22 @@ class MapLocationPickerRouteArgs {
int get hashCode => key.hashCode ^ initialLatLng.hashCode;
}
/// generated route for
/// [PartnerPage]
class PartnerRoute extends PageRouteInfo<void> {
const PartnerRoute({List<PageRouteInfo>? children})
: super(PartnerRoute.name, initialChildren: children);
static const String name = 'PartnerRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const PartnerPage();
},
);
}
/// generated route for
/// [PinAuthPage]
class PinAuthRoute extends PageRouteInfo<PinAuthRouteArgs> {

View File

@ -1,5 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
import '../repository_context.dart';
@ -25,7 +24,7 @@ void main() {
await ctx.newPartner(sharedById: me.id, sharedWithId: recipient.id);
await ctx.newPartner(sharedById: sharer.id, sharedWithId: me.id);
final result = await sut.search(me.id, PartnerDirection.sharedBy).first;
final result = await sut.search(me.id, .sharedBy).first;
expect(result.map((partner) => partner.id), unorderedEquals([recipient.id]));
});
@ -37,7 +36,7 @@ void main() {
await ctx.newPartner(sharedById: me.id, sharedWithId: recipient.id);
await ctx.newPartner(sharedById: sharer.id, sharedWithId: me.id);
final result = await sut.search(me.id, PartnerDirection.sharedWith).first;
final result = await sut.search(me.id, .sharedWith).first;
expect(result.map((partner) => partner.id), unorderedEquals([sharer.id]));
});
@ -46,7 +45,7 @@ void main() {
final me = await ctx.newUser();
final recipient = await ctx.newUser();
final ids = sut.search(me.id, PartnerDirection.sharedBy).map((partners) => partners.map((p) => p.id).toList());
final ids = sut.search(me.id, .sharedBy).map((partners) => partners.map((p) => p.id).toList());
final expectation = expectLater(
ids,
emitsInOrder([
@ -65,7 +64,7 @@ void main() {
final me = await ctx.newUser();
final partner = await ctx.newUser();
await sut.create(partner.id, me.id);
await sut.create(sharedById: me.id, sharedWithId: partner.id);
final result = (await sut.search(me.id, .sharedBy).first).first;
expect(result.id, partner.id);
@ -79,9 +78,9 @@ void main() {
final sharer = await ctx.newUser();
await ctx.newPartner(sharedById: sharer.id, sharedWithId: me.id, inTimeline: false);
await sut.update(sharer.id, me.id, inTimeline: true);
await sut.update(sharedById: sharer.id, sharedWithId: me.id, inTimeline: true);
final result = await sut.get(sharer.id, me.id);
final result = await sut.get(sharedById: sharer.id, sharedWithId: me.id);
expect(result.inTimeline, isTrue);
});
});
@ -92,7 +91,7 @@ void main() {
final recipient = await ctx.newUser();
await ctx.newPartner(sharedById: me.id, sharedWithId: recipient.id);
await sut.delete(recipient.id, me.id);
await sut.delete(sharedById: me.id, sharedWithId: recipient.id);
final rows = await ctx.db.select(ctx.db.partnerEntity).get();
expect(rows, isEmpty);

View File

@ -72,7 +72,7 @@ void main() {
final me = await ctx.newUser();
final partner = await ctx.newUser();
await sut.create(partner.id, me.id);
await sut.create(sharedById: me.id, sharedWithId: partner.id);
verify(() => ctx.partnerApi.create(partner.id)).called(1);
final shared = await sut.search(me.id, .sharedBy).first;
@ -86,7 +86,7 @@ void main() {
final recipient = await ctx.newUser();
await ctx.newPartner(sharedById: me.id, sharedWithId: recipient.id);
await sut.delete(recipient.id, me.id);
await sut.delete(sharedById: me.id, sharedWithId: recipient.id);
verify(() => ctx.partnerApi.delete(recipient.id)).called(1);
final shared = await sut.search(me.id, .sharedBy).first;
@ -100,10 +100,10 @@ void main() {
final sharer = await ctx.newUser();
await ctx.newPartner(sharedById: sharer.id, sharedWithId: me.id, inTimeline: false);
await sut.update(sharer.id, me.id, inTimeline: true);
await sut.update(sharedById: sharer.id, sharedWithId: me.id, inTimeline: true);
verify(() => ctx.partnerApi.update(sharer.id, inTimeline: true)).called(1);
final partner = await ctx.partnerRepository.get(sharer.id, me.id);
final partner = await ctx.partnerRepository.get(sharedById: sharer.id, sharedWithId: me.id);
expect(partner.inTimeline, isTrue);
});
});

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart';
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
import '../factories/partner_user_factory.dart';
import '../factories/user_factory.dart';
@ -16,32 +16,16 @@ void main() {
group('PartnerSharedByList', () {
testWidgets('shows the empty-state add button when there are no partners', (tester) async {
await tester.pumpTestWidget(
PartnerSharedByList(partners: const [], onAddPartner: () {}, onRemovePartner: (_) {}),
);
await tester.pumpTestWidget(PartnerSharedByList(partners: const [], onAdd: () {}, onRemove: (_) {}));
expect(find.byType(ListView), findsNothing);
expect(find.widgetWithIcon(ElevatedButton, Icons.person_add), findsOneWidget);
});
testWidgets('invokes onAddPartner when the empty-state button is tapped', (tester) async {
var addCalls = 0;
await tester.pumpTestWidget(
PartnerSharedByList(partners: const [], onAddPartner: () => addCalls++, onRemovePartner: (_) {}),
);
await tester.tap(find.widgetWithIcon(ElevatedButton, Icons.person_add));
await tester.pump();
expect(addCalls, 1);
});
testWidgets('renders a tile per partner with name and email', (tester) async {
final partner1 = PartnerFactory.create();
final partner2 = PartnerFactory.create();
await tester.pumpTestWidget(
PartnerSharedByList(partners: [partner1, partner2], onAddPartner: () {}, onRemovePartner: (_) {}),
);
await tester.pumpTestWidget(PartnerSharedByList(partners: [partner1, partner2], onAdd: () {}, onRemove: (_) {}));
expect(find.byType(ListTile), findsNWidgets(2));
expect(find.text(partner1.name), findsOneWidget);
@ -55,7 +39,7 @@ void main() {
final partner2 = PartnerFactory.create();
Partner? removed;
await tester.pumpTestWidget(
PartnerSharedByList(partners: [partner1, partner2], onAddPartner: () {}, onRemovePartner: (p) => removed = p),
PartnerSharedByList(partners: [partner1, partner2], onAdd: () {}, onRemove: (p) => removed = p),
);
await tester.tap(find.byIcon(Icons.person_remove).first);
@ -81,7 +65,7 @@ void main() {
}
List<Override> withCandidates(List<User> candidates) => [
candidatesProvider.overrideWith((ref) => Stream<Iterable<User>>.value(candidates)),
candidatesStateProvider.overrideWith((ref) => Stream<Iterable<User>>.value(candidates)),
];
testWidgets('renders an option per candidate fetched from the provider', (tester) async {