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();
+ // });
}