From 32124851bc0a91c5445943b5d54b672e3ed0b1f6 Mon Sep 17 00:00:00 2001 From: samolego <34912839+samolego@users.noreply.github.com> Date: Fri, 31 Jan 2025 22:51:09 +0100 Subject: [PATCH 1/4] Allow contact picking for the People in mobile app --- .../android/app/src/main/AndroidManifest.xml | 3 +- mobile/ios/Runner/Info.plist | 2 + .../widgets/search/person_name_edit_form.dart | 135 ++++++++++++++++-- mobile/pubspec.lock | 56 ++++---- mobile/pubspec.yaml | 2 + 5 files changed, 164 insertions(+), 34 deletions(-) diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index ac57884eef34e..bbe6dafdff637 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ android:maxSdkVersion="32" /> + @@ -124,4 +125,4 @@ - \ No newline at end of file + diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index a2688775dc042..7fe96afb84d56 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -127,6 +127,8 @@ We need to manage backup your photos album NSPhotoLibraryUsageDescription We need to manage backup your photos album + NSContactsUsageDescription + We need contact access to help you assign names to people in your photos from your contacts' names. NSUserActivityTypes INSendMessageIntent diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart index 6038966ca152c..6ac79bc93d3f0 100644 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ b/mobile/lib/widgets/search/person_name_edit_form.dart @@ -1,9 +1,13 @@ +import 'dart:typed_data'; + import 'package:easy_localization/easy_localization.dart'; +import 'package:fast_contacts/fast_contacts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:permission_handler/permission_handler.dart'; class PersonNameEditFormResult { final bool success; @@ -26,21 +30,134 @@ class PersonNameEditForm extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final controller = useTextEditingController(text: personName); final isError = useState(false); + final contacts = useState>([]); + final filteredContacts = useState>([]); + final isLoading = useState(false); + final isEditing = useState(true); + + useEffect( + () { + loadContacts() async { + try { + final status = await Permission.contacts.request(); + if (status.isGranted) { + isLoading.value = true; + contacts.value = await FastContacts.getAllContacts(); + isLoading.value = false; + } + } catch (e) { + debugPrint('Failed to load contacts: $e'); + } + } + + loadContacts(); + return null; + }, + [], + ); + + useEffect( + () { + // Filter contacts based on the input name + void filterContacts() { + if (controller.text.isEmpty || !isEditing.value) { + filteredContacts.value = []; + return; + } + filteredContacts.value = contacts.value + .where( + (contact) => + contact.displayName + .toLowerCase() + .contains(controller.text.toLowerCase()) && + contact.displayName != + controller + .text, // If contact 100% matches the input, also hide it + ) + .toList(); + } + + controller.addListener(filterContacts); + return () => controller.removeListener(filterContacts); + }, + [contacts.value], + ); return AlertDialog( title: const Text( "search_page_person_add_name_dialog_title", style: TextStyle(fontWeight: FontWeight.bold), ).tr(), - content: SingleChildScrollView( - child: TextFormField( - controller: controller, - autofocus: true, - decoration: InputDecoration( - hintText: 'search_page_person_add_name_dialog_hint'.tr(), - border: const OutlineInputBorder(), - errorText: isError.value ? 'Error occured' : null, - ), + content: SizedBox( + width: double.maxFinite, + height: filteredContacts.value.isEmpty + ? 80 + : MediaQuery.of(context).size.height * 0.4, + child: Column( + children: [ + TextFormField( + controller: controller, + autofocus: true, + onTap: () { + isEditing.value = true; + }, + decoration: InputDecoration( + hintText: 'search_page_person_add_name_dialog_hint'.tr(), + border: const OutlineInputBorder(), + errorText: isError.value ? 'Error occured' : null, + suffixIcon: isLoading.value + ? const SizedBox( + width: 20, + height: 20, + child: Padding( + padding: EdgeInsets.all(10.0), + child: CircularProgressIndicator(), + ), + ) + : null, + ), + ), + const SizedBox(height: 8), + Expanded( + child: filteredContacts.value.isNotEmpty + ? ListView.builder( + itemCount: filteredContacts.value.length, + itemBuilder: (context, index) { + final contact = filteredContacts.value[index]; + return ListTile( + leading: FutureBuilder( + future: FastContacts.getContactImage(contact.id), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data != null) { + return CircleAvatar( + radius: 16, + backgroundImage: MemoryImage(snapshot.data!), + ); + } + return const CircleAvatar( + radius: 16, + child: Icon(Icons.person, size: 20), + ); + }, + ), + dense: true, + title: Text( + contact.displayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + controller.text = contact.displayName; + filteredContacts.value = []; + isEditing.value = false; + FocusScope.of(context).unfocus(); + }, + ); + }, + ) + : const SizedBox.shrink(), + ), + ], ), ), actions: [ diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5a15bf5f5e3ef..4da94b4936c53 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: "direct overridden" description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" analyzer_plugin: dependency: "direct overridden" description: @@ -250,10 +250,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" connectivity_plus: dependency: "direct main" description: @@ -414,6 +414,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fast_contacts: + dependency: "direct main" + description: + name: fast_contacts + sha256: "69b7c2208f9da3666c1577191b3d8f6193c90567eb0a9dfead8e59607caebe87" + url: "https://pub.dev" + source: hosted + version: "4.0.0" ffi: dependency: transitive description: @@ -900,18 +908,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -940,10 +948,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" maplibre_gl: dependency: "direct main" description: @@ -1452,7 +1460,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socket_io_client: dependency: "direct main" description: @@ -1513,10 +1521,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" state_notifier: dependency: transitive description: @@ -1545,10 +1553,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" sync_http: dependency: transitive description: @@ -1577,10 +1585,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" thumbhash: dependency: "direct main" description: @@ -1737,10 +1745,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" wakelock_plus: dependency: "direct main" description: @@ -1793,10 +1801,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" win32: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 162ad08571642..157abfade0273 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -70,6 +70,8 @@ dependencies: #image editing packages crop_image: ^1.0.13 + fast_contacts: 4.0.0 + openapi: path: openapi From 299ae593d21287efeb2b15d22a0689b954ec9dc6 Mon Sep 17 00:00:00 2001 From: samolego <34912839+samolego@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:33:57 +0100 Subject: [PATCH 2/4] Fix keyboard disappearing if contacts permission is denied. --- .../widgets/search/person_name_edit_form.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart index 6ac79bc93d3f0..5247e7f580c75 100644 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ b/mobile/lib/widgets/search/person_name_edit_form.dart @@ -33,13 +33,17 @@ class PersonNameEditForm extends HookConsumerWidget { final contacts = useState>([]); final filteredContacts = useState>([]); final isLoading = useState(false); - final isEditing = useState(true); useEffect( () { - loadContacts() async { + Future loadContacts() async { try { - final status = await Permission.contacts.request(); + var status = await Permission.contacts.status; + // Check status and re-request permission if not needed + if (!status.isGranted && !status.isPermanentlyDenied) { + status = await Permission.contacts.request(); + } + if (status.isGranted) { isLoading.value = true; contacts.value = await FastContacts.getAllContacts(); @@ -60,7 +64,7 @@ class PersonNameEditForm extends HookConsumerWidget { () { // Filter contacts based on the input name void filterContacts() { - if (controller.text.isEmpty || !isEditing.value) { + if (controller.text.isEmpty) { filteredContacts.value = []; return; } @@ -89,7 +93,7 @@ class PersonNameEditForm extends HookConsumerWidget { style: TextStyle(fontWeight: FontWeight.bold), ).tr(), content: SizedBox( - width: double.maxFinite, + width: MediaQuery.of(context).size.width * 0.6, height: filteredContacts.value.isEmpty ? 80 : MediaQuery.of(context).size.height * 0.4, @@ -98,9 +102,6 @@ class PersonNameEditForm extends HookConsumerWidget { TextFormField( controller: controller, autofocus: true, - onTap: () { - isEditing.value = true; - }, decoration: InputDecoration( hintText: 'search_page_person_add_name_dialog_hint'.tr(), border: const OutlineInputBorder(), @@ -149,7 +150,6 @@ class PersonNameEditForm extends HookConsumerWidget { onTap: () { controller.text = contact.displayName; filteredContacts.value = []; - isEditing.value = false; FocusScope.of(context).unfocus(); }, ); From bfc8f026ebc2863075ba2b87ab551f5a335a07cd Mon Sep 17 00:00:00 2001 From: samolego <34912839+samolego@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:46:09 +0100 Subject: [PATCH 3/4] Suggest contacts on dialog open too --- mobile/lib/widgets/search/person_name_edit_form.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart index 5247e7f580c75..b4c646d46110e 100644 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ b/mobile/lib/widgets/search/person_name_edit_form.dart @@ -39,8 +39,8 @@ class PersonNameEditForm extends HookConsumerWidget { Future loadContacts() async { try { var status = await Permission.contacts.status; - // Check status and re-request permission if not needed - if (!status.isGranted && !status.isPermanentlyDenied) { + // Check status and re-request permission if needed + if (!status.isPermanentlyDenied) { status = await Permission.contacts.request(); } @@ -81,6 +81,9 @@ class PersonNameEditForm extends HookConsumerWidget { .toList(); } + // Filter contacts on first load too + filterContacts(); + controller.addListener(filterContacts); return () => controller.removeListener(filterContacts); }, From b03a4c104f6170ae89a60bea476b2fbe26356a74 Mon Sep 17 00:00:00 2001 From: Samo Hribar <34912839+samolego@users.noreply.github.com> Date: Sat, 8 Feb 2025 20:59:02 +0100 Subject: [PATCH 4/4] Update mobile/lib/widgets/search/person_name_edit_form.dart Co-authored-by: Nicholas Flamy <30300649+NicholasFlamy@users.noreply.github.com> --- mobile/lib/widgets/search/person_name_edit_form.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart index b4c646d46110e..6f6f25e1f4aeb 100644 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ b/mobile/lib/widgets/search/person_name_edit_form.dart @@ -98,7 +98,7 @@ class PersonNameEditForm extends HookConsumerWidget { content: SizedBox( width: MediaQuery.of(context).size.width * 0.6, height: filteredContacts.value.isEmpty - ? 80 + ? 64 : MediaQuery.of(context).size.height * 0.4, child: Column( children: [