diff --git a/.gitignore b/.gitignore index dd1ad7f..6c97826 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Custom report.xml /coverage/ +secrets/ # Miscellaneous *.class diff --git a/android/app/build.gradle b/android/app/build.gradle index ce95ef0..b60c32f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -57,7 +57,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "social.coagulate" + applicationId "social.coagulate.app" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. minSdkVersion Math.max(flutter.minSdkVersion, 24) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 9625e10..7c56964 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Podfile b/ios/Podfile index bd3431c..2cbcaa2 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -44,4 +44,4 @@ post_install do |installer| File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } end end -end \ No newline at end of file +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 125a3a8..7387257 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,13 +1,10 @@ PODS: - - camera_avfoundation (0.0.1): - - Flutter - Flutter (1.0.0) - - flutter_native_splash (0.0.1): + - flutter_contacts (0.0.1): + - Flutter + - geocoding_ios (1.0.5): - Flutter - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - - GoogleDataTransport (9.2.5): + - GoogleDataTransport (9.4.0): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) @@ -28,15 +25,21 @@ PODS: - GoogleToolboxForMac/Defines (= 2.3.2) - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)" - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)" - - GoogleUtilities/Environment (7.11.5): + - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Logger (7.13.0): - GoogleUtilities/Environment - - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.0) + - GoogleUtilities/UserDefaults (7.13.0): - GoogleUtilities/Logger + - GoogleUtilities/Privacy - GoogleUtilitiesComponents (1.1.0): - GoogleUtilities/Logger - GTMSessionFetcher/Core (2.3.0) + - location (0.0.1): + - Flutter - MLImage (1.0.0-beta4) - MLKitBarcodeScanning (3.0.0): - MLKitCommon (~> 9.0) @@ -55,55 +58,52 @@ PODS: - GTMSessionFetcher/Core (< 3.0, >= 1.1) - MLImage (= 1.0.0-beta4) - MLKitCommon (~> 9.0) - - mobile_scanner (3.2.0): + - mobile_scanner (3.5.6): - Flutter - GoogleMLKit/BarcodeScanning (~> 4.0.0) - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) - - pasteboard (0.0.1): - - Flutter + - nanopb (2.30909.1): + - nanopb/decode (= 2.30909.1) + - nanopb/encode (= 2.30909.1) + - nanopb/decode (2.30909.1) + - nanopb/encode (2.30909.1) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - PromisesObjC (2.3.1) + - PromisesObjC (2.4.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - smart_auth (0.0.1): - - Flutter - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) + - FlutterMacOS - system_info_plus (0.0.1): - Flutter - url_launcher_ios (0.0.1): - Flutter - veilid (0.0.1): - Flutter + - workmanager (0.0.1): + - Flutter DEPENDENCIES: - - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - Flutter (from `Flutter`) - - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`) + - geocoding_ios (from `.symlinks/plugins/geocoding_ios/ios`) + - location (from `.symlinks/plugins/location/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - smart_auth (from `.symlinks/plugins/smart_auth/ios`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - system_info_plus (from `.symlinks/plugins/system_info_plus/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - veilid (from `.symlinks/plugins/veilid/ios`) + - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: trunk: - - FMDB - GoogleDataTransport - GoogleMLKit - GoogleToolboxForMac @@ -118,61 +118,60 @@ SPEC REPOS: - PromisesObjC EXTERNAL SOURCES: - camera_avfoundation: - :path: ".symlinks/plugins/camera_avfoundation/ios" Flutter: :path: Flutter - flutter_native_splash: - :path: ".symlinks/plugins/flutter_native_splash/ios" + flutter_contacts: + :path: ".symlinks/plugins/flutter_contacts/ios" + geocoding_ios: + :path: ".symlinks/plugins/geocoding_ios/ios" + location: + :path: ".symlinks/plugins/location/ios" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/ios" - pasteboard: - :path: ".symlinks/plugins/pasteboard/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - smart_auth: - :path: ".symlinks/plugins/smart_auth/ios" sqflite: - :path: ".symlinks/plugins/sqflite/ios" + :path: ".symlinks/plugins/sqflite/darwin" system_info_plus: :path: ".symlinks/plugins/system_info_plus/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" veilid: :path: ".symlinks/plugins/veilid/ios" + workmanager: + :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: - camera_avfoundation: 3125e8cd1a4387f6f31c6c63abb8a55892a9eeeb - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_contacts: edb1c5ce76aa433e20e6cb14c615f4c0b66e0983 + geocoding_ios: d7460f56e80e118d57678efe5c2cdc888739ff18 + GoogleDataTransport: bed3a36c04c8552479fbb9b76326e0fc69bddcb2 GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 + location: d5cf8598915965547c3f36761ae9cc4f4e87d22e MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 47056db0c04027ea5f41a716385542da28574662 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 - pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + mobile_scanner: 38dcd8a49d7d485f632b7de65e4900010187aef2 + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 - veilid: 51243c25047dbc1ebbfd87d713560260d802b845 + url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 + veilid: 780f35f0d9b15f94b2d126599edb80c8a0ca5320 + workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 -PODFILE CHECKSUM: 7f4cf2154d55730d953b184299e6feee7a274740 +PODFILE CHECKSUM: 5d504085cd7c7a4d71ee600d7af087cb60ab75b2 -COCOAPODS: 1.14.2 +COCOAPODS: 1.15.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 09242d2..64b58a6 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - DEVELOPMENT_TEAM = 5V8UJLB8H3; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = G42ZSYUPFC; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Coagulate; @@ -369,8 +371,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = social.coagulate; + PRODUCT_BUNDLE_IDENTIFIER = social.coagulate.app; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -491,7 +494,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - DEVELOPMENT_TEAM = 5V8UJLB8H3; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = G42ZSYUPFC; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Coagulate; @@ -500,8 +505,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = social.coagulate; + PRODUCT_BUNDLE_IDENTIFIER = social.coagulate.app; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -516,7 +522,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - DEVELOPMENT_TEAM = 5V8UJLB8H3; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = G42ZSYUPFC; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Coagulate; @@ -525,8 +533,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = social.coagulate; + PRODUCT_BUNDLE_IDENTIFIER = social.coagulate.app; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/lib/data/repositories/contacts.dart b/lib/data/repositories/contacts.dart index 0175f65..27908fd 100644 --- a/lib/data/repositories/contacts.dart +++ b/lib/data/repositories/contacts.dart @@ -22,8 +22,6 @@ import '../providers/persistent_storage/base.dart'; import '../providers/system_contacts/base.dart'; import '../providers/system_contacts/system_contacts.dart'; -// TODO: Persist all changes to any contact by never accessing coagContacts directly, only via getter and setter - String contactDetailKey(int i, T detail) { if (detail is Organization) { return '$i|${detail.company}'; @@ -241,6 +239,7 @@ class ContactsRepository { _circleMemberships = memberships; _circlesStreamController.add(null); await persistentStorage.updateCircleMemberships(memberships); + // TODO: Update sharing profile and update dht record; only update where necessary } Future updateCircles(Map circles) async { @@ -253,7 +252,25 @@ class ContactsRepository { ProfileSharingSettings settings) async { _profileSharingSettings = settings; await persistentStorage.updateProfileSharingSettings(settings); - // TODO: Trigger update of all shareprofile + + final profileContact = getProfileContact(); + if (profileContact == null) { + return; + } + for (final contact in _contacts.values) { + if (contact.dhtSettingsForSharing?.psk == null) { + continue; + } + await updateContact(contact.copyWith( + sharedProfile: json.encode(removeNullOrEmptyValues( + filterAccordingToSharingProfile( + profile: profileContact, + settings: _profileSharingSettings, + activeCircles: + _circleMemberships[contact.coagContactId] ?? [], + shareBackSettings: contact.dhtSettingsForReceiving) + .toJson())))); + } } ProfileSharingSettings getProfileSharingSettings() => _profileSharingSettings; diff --git a/lib/ui/contact_details/page.dart b/lib/ui/contact_details/page.dart index fb003ec..e71aa5b 100644 --- a/lib/ui/contact_details/page.dart +++ b/lib/ui/contact_details/page.dart @@ -14,6 +14,7 @@ import '../../data/models/coag_contact.dart'; import '../../data/models/contact_location.dart'; import '../../data/repositories/contacts.dart'; import '../../ui/profile/cubit.dart'; +import '../../utils.dart'; import '../profile/page.dart'; import '../widgets/avatar.dart'; import '../widgets/circles/cubit.dart'; @@ -109,9 +110,8 @@ class ContactPage extends StatelessWidget { // TODO: Theme backgroundColor: const Color.fromARGB(255, 244, 244, 244), appBar: AppBar( - title: Text(state.contact.details?.displayName ?? - state.contact.systemContact?.displayName ?? - 'Contact Details'), + title: + Text(displayName(state.contact) ?? 'Contact Details'), ), body: _body(context, state.contact, state.circles)))); @@ -250,14 +250,14 @@ Widget receivingCard(BuildContext context, CoagContact contact) => Card( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Ask ${contact.details?.displayName ?? contact.systemContact!.displayName} to start sharing with you:', + 'Ask ${displayName(contact) ?? 'them'} to start sharing with you:', textScaler: const TextScaler.linear(1.2)), const SizedBox(height: 4), Center( child: _qrCodeButton(context, buttonText: 'QR code to request', alertTitle: - 'Request from ${contact.details?.displayName ?? contact.systemContact!.displayName}', + 'Request from ${displayName(contact) ?? 'them'}', qrCodeData: _receiveUrl( key: contact.dhtSettingsForReceiving!.key, psk: contact.dhtSettingsForReceiving!.psk!, @@ -305,15 +305,14 @@ Widget sharingCard(BuildContext context, CoagContact contact) => Card( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Start sharing your contact details with ${contact.details?.displayName ?? contact.systemContact!.displayName}:', + 'Start sharing your contact details with ${displayName(contact) ?? 'them'}:', textScaler: const TextScaler.linear(1.2)), const SizedBox(height: 4), // TODO: Only show share back button when receiving key and psk but not writer are set i.e. is receiving updates and has share back settings Center( child: _qrCodeButton(context, buttonText: 'QR code to share', - alertTitle: - 'Share with ${contact.details?.displayName ?? contact.systemContact!.displayName}', + alertTitle: 'Share with ${displayName(contact) ?? 'them'}', qrCodeData: _shareUrl( key: contact.dhtSettingsForSharing!.key, psk: contact.dhtSettingsForSharing!.psk!, @@ -361,15 +360,20 @@ Card circlesCard( margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: Padding( padding: - const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 8), + const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 12), child: Row(children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('circles'), - Text(circles.join(', '), - style: const TextStyle(fontSize: 19)), + const Text('circles', + style: TextStyle(fontSize: 16, color: Colors.black54)), + if (circles.isEmpty) + const Text('Add them to circles to start sharing.', + style: TextStyle(fontSize: 19)) + else + Text(circles.join(', '), + style: const TextStyle(fontSize: 19)), ])), IconButton( icon: const Icon(Icons.edit), diff --git a/lib/ui/contact_list/cubit.dart b/lib/ui/contact_list/cubit.dart index 19d1ce6..44320cc 100644 --- a/lib/ui/contact_list/cubit.dart +++ b/lib/ui/contact_list/cubit.dart @@ -10,6 +10,7 @@ import 'package:json_annotation/json_annotation.dart'; import '../../data/models/coag_contact.dart'; import '../../data/repositories/contacts.dart'; +import '../../utils.dart'; part 'cubit.g.dart'; part 'state.dart'; @@ -24,7 +25,8 @@ String extractAllValuesToString(dynamic value) { } } -Iterable filterAndSortContacts(Iterable contacts, {String filter = ''}) => +Iterable filterAndSortContacts(Iterable contacts, + {String filter = ''}) => ((filter.isEmpty) ? contacts : contacts.where((c) => @@ -37,9 +39,10 @@ Iterable filterAndSortContacts(Iterable contacts, {Str .toLowerCase() .contains(filter.toLowerCase())))) .toList() - ..sort((a, b) => compareNatural( - a.details?.displayName ?? a.systemContact?.displayName ?? 'A', - b.details?.displayName ?? b.systemContact?.displayName ?? 'A')); + ..sort((a, b) => + // Use + in case no display name could be determined to ensure the + // respective contacts end up before phone numbers with country codes + compareNatural(displayName(a) ?? '+', displayName(b) ?? '+')); // TODO: Figure out sorting of the contacts class ContactListCubit extends Cubit { diff --git a/lib/ui/contact_list/page.dart b/lib/ui/contact_list/page.dart index b88ba7b..9c209f8 100644 --- a/lib/ui/contact_list/page.dart +++ b/lib/ui/contact_list/page.dart @@ -8,6 +8,7 @@ import 'package:flutter_contacts/flutter_contacts.dart'; import '../../data/models/coag_contact.dart'; import '../../data/repositories/contacts.dart'; import '../../ui/profile/cubit.dart'; +import '../../utils.dart'; import '../contact_details/page.dart'; import '../receive_request/page.dart'; import '../widgets/avatar.dart'; @@ -91,9 +92,7 @@ class _ContactListPageState extends State { final contact = contacts[i]; return ListTile( leading: avatar(contact.systemContact, radius: 18), - title: Text(contact.details?.displayName ?? - contact.systemContact?.displayName ?? - 'unknown'), + title: Text(displayName(contact) ?? 'unknown'), trailing: Text(_contactSyncStatus(contact)), onTap: () => Navigator.of(context).push(ContactPage.route(contact))); diff --git a/lib/ui/locations/page.dart b/lib/ui/locations/page.dart index aed8afa..b6f82bd 100644 --- a/lib/ui/locations/page.dart +++ b/lib/ui/locations/page.dart @@ -335,17 +335,16 @@ Widget locationTile(ContactTemporaryLocation location, title: Text(location.name), tileColor: Colors.white, onTap: onTap, - subtitle: Row(children: [ - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('From: ${dateFormat.format(location.start)}'), - if (location.end == location.start) - Text('Till: ${dateFormat.format(location.end)}"}'), - Text('Lon: ${location.longitude.toStringAsFixed(4)}, ' - 'Lat: ${location.latitude.toStringAsFixed(4)}'), - Text( - 'Shared with ${numberContactsShared(circleMembersips.values, location.circles)} contacts'), - ]), - Text(location.details) + subtitle: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('From: ${dateFormat.format(location.start)}'), + if (location.end == location.start) + Text('Till: ${dateFormat.format(location.end)}"}'), + Text('Lon: ${location.longitude.toStringAsFixed(4)}, ' + 'Lat: ${location.latitude.toStringAsFixed(4)}'), + Text( + 'Shared with ${numberContactsShared(circleMembersips.values, location.circles)} contacts'), + Text(location.details), ]), trailing: // TODO: Better icon to indicate checked in @@ -415,11 +414,11 @@ class LocationsPage extends StatelessWidget { if (state.temporaryLocations .where((l) => !l.end.isBefore(DateTime.now())) .isEmpty) - const Padding( - padding: EdgeInsets.only(top: 16, bottom: 16), - child: Center( - child: Text( - 'Nothing coming up, check-in now or plan a future stay.'))), + Container( + padding: const EdgeInsets.all(20), + child: const Text( + 'Nothing coming up, check-in now or plan a future stay.', + style: TextStyle(fontSize: 16))), // Past locations if (state.temporaryLocations .where((l) => l.end.isBefore(DateTime.now())) diff --git a/lib/ui/profile/cubit.dart b/lib/ui/profile/cubit.dart index b1b65ca..f1dce1c 100644 --- a/lib/ui/profile/cubit.dart +++ b/lib/ui/profile/cubit.dart @@ -49,12 +49,18 @@ class ProfileCubit extends Cubit { late final StreamSubscription _contactsSubscription; late final StreamSubscription _permissionsSubscription; - void promptCreate() { - emit(state.copyWith(status: ProfileStatus.create)); + Future promptCreate() async { + if (await FlutterContacts.requestPermission()) { + emit(state.copyWith(status: ProfileStatus.create)); + await setContact((await FlutterContacts.openExternalInsert())?.id); + } } - void promptPick() { - emit(state.copyWith(status: ProfileStatus.pick)); + Future promptPick() async { + if (await FlutterContacts.requestPermission()) { + emit(state.copyWith(status: ProfileStatus.pick)); + await setContact((await FlutterContacts.openExternalPick())?.id); + } } Future setContact(String? systemContactId) async { diff --git a/lib/ui/profile/page.dart b/lib/ui/profile/page.dart index 5ecfdd8..5c9bf92 100644 --- a/lib/ui/profile/page.dart +++ b/lib/ui/profile/page.dart @@ -66,7 +66,7 @@ Card _card(List children) => Card( child: SizedBox( child: Padding( padding: - const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 12), + const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: children)))); @@ -206,6 +206,7 @@ Widget addressesWithForms(BuildContext context, List
addresses, .map((i, e) => MapEntry( i, Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 8), Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: Column( @@ -347,115 +348,125 @@ Widget buildProfileScrollView( if (contact.phones.isNotEmpty) phones( contact.phones, - (i, label) async => showPickCirclesBottomSheet( - context: context, - label: label, - coagContactId: coagContactId, - circles: circles - .map((cId, cLabel) => MapEntry(cId, ( - cId, - cLabel, - profileSharingSettings - .phones['$i|$label'] - ?.contains(cId) ?? - false - ))) - .values - .toList(), - callback: (selectedCircles) => context - .read() - .updatePhoneSharingCircles( - i, label, selectedCircles))), + (circles.isEmpty) + ? null + : (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + coagContactId: coagContactId, + circles: circles + .map((cId, cLabel) => MapEntry(cId, ( + cId, + cLabel, + profileSharingSettings + .phones['$i|$label'] + ?.contains(cId) ?? + false + ))) + .values + .toList(), + callback: (selectedCircles) => context + .read() + .updatePhoneSharingCircles( + i, label, selectedCircles))), if (contact.emails.isNotEmpty) emails( contact.emails, - (i, label) async => showPickCirclesBottomSheet( - context: context, - label: label, - coagContactId: coagContactId, - circles: circles - .map((cId, cLabel) => MapEntry(cId, ( - cId, - cLabel, - profileSharingSettings - .emails['$i|$label'] - ?.contains(cId) ?? - false - ))) - .values - .toList(), - callback: (selectedCircles) => context - .read() - .updateEmailSharingCircles( - i, label, selectedCircles))), + (circles.isEmpty) + ? null + : (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + coagContactId: coagContactId, + circles: circles + .map((cId, cLabel) => MapEntry(cId, ( + cId, + cLabel, + profileSharingSettings + .emails['$i|$label'] + ?.contains(cId) ?? + false + ))) + .values + .toList(), + callback: (selectedCircles) => context + .read() + .updateEmailSharingCircles( + i, label, selectedCircles))), if (contact.addresses.isNotEmpty) addressesWithForms( context, contact.addresses, addressLocations, - (i, label) async => showPickCirclesBottomSheet( - context: context, - label: label, - coagContactId: coagContactId, - circles: circles - .map((cId, cLabel) => MapEntry(cId, ( - cId, - cLabel, - profileSharingSettings - .addresses['$i|$label'] - ?.contains(cId) ?? - false - ))) - .values - .toList(), - callback: (selectedCircles) => context - .read() - .updateAddressSharingCircles( - i, label, selectedCircles))), + (circles.isEmpty) + ? null + : (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + coagContactId: coagContactId, + circles: circles + .map((cId, cLabel) => MapEntry(cId, ( + cId, + cLabel, + profileSharingSettings + .addresses['$i|$label'] + ?.contains(cId) ?? + false + ))) + .values + .toList(), + callback: (selectedCircles) => context + .read() + .updateAddressSharingCircles( + i, label, selectedCircles))), if (contact.websites.isNotEmpty) websites( contact.websites, - (i, label) async => showPickCirclesBottomSheet( - context: context, - label: label, - coagContactId: coagContactId, - circles: circles - .map((cId, cLabel) => MapEntry(cId, ( - cId, - cLabel, - profileSharingSettings - .websites['$i|$label'] - ?.contains(cId) ?? - false - ))) - .values - .toList(), - callback: (selectedCircles) => context - .read() - .updateWebsiteSharingCircles( - i, label, selectedCircles))), + (circles.isEmpty) + ? null + : (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + coagContactId: coagContactId, + circles: circles + .map((cId, cLabel) => MapEntry(cId, ( + cId, + cLabel, + profileSharingSettings + .websites['$i|$label'] + ?.contains(cId) ?? + false + ))) + .values + .toList(), + callback: (selectedCircles) => context + .read() + .updateWebsiteSharingCircles( + i, label, selectedCircles))), if (contact.socialMedias.isNotEmpty) socialMedias( contact.socialMedias, - (i, label) async => showPickCirclesBottomSheet( - context: context, - label: label, - coagContactId: coagContactId, - circles: circles - .map((cId, cLabel) => MapEntry(cId, ( - cId, - cLabel, - profileSharingSettings - .socialMedias['$i|$label'] - ?.contains(cId) ?? - false - ))) - .values - .toList(), - callback: (selectedCircles) => context - .read() - .updateSocialMediaSharingCircles( - i, label, selectedCircles))), + (circles.isEmpty) + ? null + : (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + coagContactId: coagContactId, + circles: circles + .map((cId, cLabel) => MapEntry(cId, ( + cId, + cLabel, + profileSharingSettings + .socialMedias['$i|$label'] + ?.contains(cId) ?? + false + ))) + .values + .toList(), + callback: (selectedCircles) => context + .read() + .updateSocialMediaSharingCircles( + i, label, selectedCircles))), ])) ])); @@ -475,13 +486,14 @@ class ProfileViewState extends State { textScaler: TextScaler.linear(1.2))), // TODO: Only display them when permissions granted, unless creation is possible in coagulate only if (state.permissionsGranted) ...[ - TextButton( - onPressed: context.read().promptCreate, - child: const Text('Create Profile', - textScaler: TextScaler.linear(1.2))), - Container( - padding: const EdgeInsets.all(8), - child: const Text('or', textScaler: TextScaler.linear(1.2))), + // Re-enable when fixed: https://github.com/QuisApp/flutter_contacts/issues/100 + // TextButton( + // onPressed: context.read().promptCreate, + // child: const Text('Create Profile', + // textScaler: TextScaler.linear(1.2))), + // Container( + // padding: const EdgeInsets.all(8), + // child: const Text('or', textScaler: TextScaler.linear(1.2))), TextButton( onPressed: context.read().promptPick, child: const Text('Pick Contact as Profile', @@ -524,27 +536,7 @@ class ProfileViewState extends State { BuildContext context, ) => BlocConsumer( - listener: (context, state) async { - if (state.status.isPick) { - if (await FlutterContacts.requestPermission()) { - await context - .read() - .setContact((await FlutterContacts.openExternalPick())?.id); - } else { - // TODO: Trigger hint about missing permission - return; - } - } else if (state.status.isCreate) { - if (await FlutterContacts.requestPermission()) { - // TODO: This doesn't seem to return the contact after creation, leaving the profile page with the spinner - await context.read().setContact( - (await FlutterContacts.openExternalInsert())?.id); - } else { - // TODO: Trigger hint about missing permission - return; - } - } - }, + listener: (context, state) {}, builder: (context, state) => Scaffold( // TODO: Theme backgroundColor: const Color.fromARGB(255, 244, 244, 244), diff --git a/lib/ui/settings/page.dart b/lib/ui/settings/page.dart index 97cf52c..4627233 100644 --- a/lib/ui/settings/page.dart +++ b/lib/ui/settings/page.dart @@ -38,10 +38,19 @@ class SettingsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row(children: [ - const Text('Network Status:'), - const SizedBox(width: 10), + Text('Network Status:'), + SizedBox(width: 10), SignalStrengthMeterWidget() ]), + Row(children: [ + const Expanded( + child: Text('Automatic address resolution')), + Switch( + value: true, + activeColor: Colors.green, + // TODO: Add state handling + onChanged: (bool value) {}) + ]), // TODO: Move async things to cubit // if (Platform.isIOS) _backgroundPermissionStatus(), // TODO: Add dark mode switch diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..0c05d3f --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,25 @@ +import 'data/models/coag_contact.dart'; + +/// Find a name to display; mostly required because on iOS the default +/// displayName seems to be empty when only an email address is present +String? displayName(CoagContact contact) { + if (contact.details?.displayName.isNotEmpty ?? false) { + return contact.details!.displayName; + } + if (contact.systemContact?.displayName.isNotEmpty ?? false) { + return contact.systemContact!.displayName; + } + if (contact.details?.emails.isNotEmpty ?? false) { + return contact.details!.emails.first.address; + } + if (contact.systemContact?.emails.isNotEmpty ?? false) { + return contact.systemContact!.emails.first.address; + } + if (contact.details?.phones.isNotEmpty ?? false) { + return contact.details!.phones.first.number; + } + if (contact.systemContact?.phones.isNotEmpty ?? false) { + return contact.systemContact!.phones.first.number; + } + return null; +} diff --git a/test/ui/profile_test.dart b/test/ui/profile_test.dart index fd1e186..8b55056 100644 --- a/test/ui/profile_test.dart +++ b/test/ui/profile_test.dart @@ -73,4 +73,31 @@ void main() { contactsRepository.timerDhtRefresh?.cancel(); contactsRepository.timerPersistentStorageRefresh?.cancel(); }); + + // testWidgets('Choose system contact as profile', (tester) async { + // final contactsRepository = ContactsRepository( + // DummyPersistentStorage({}), + // DummyDistributedStorage(), + // DummySystemContacts([ + // Contact( + // displayName: 'Sys Contact', + // name: Name(first: 'Sys', last: 'Contact')) + // ])); + // final page = await createProfilePage(contactsRepository); + // await tester.pumpWidget(page); + + // await tester.tap(find.byKey(const Key('profilePickContactAsProfile'))); + + // await tester.pump(); + + // // start with no profile contact + // // push choose contact button + // // have predefined contact returned from provider + // // check that its displayed + + // expect(find.text('Sys Contact'), findsOneWidget); + + // contactsRepository.timerDhtRefresh?.cancel(); + // contactsRepository.timerPersistentStorageRefresh?.cancel(); + // }); }