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: [