From 017796621f245ec1a72e94e9ded44d43c7600ecb Mon Sep 17 00:00:00 2001 From: Lukas Grossberger Date: Sat, 11 May 2024 11:41:59 +0200 Subject: [PATCH] add share with circles ui for all profile details but name #2, #12, #13 --- .../sharing_profile_details.md | 3 + lib/data/models/profile_sharing_settings.dart | 146 +++++ lib/data/repositories/contacts.dart | 6 + lib/ui/contact_details/page.dart | 2 +- lib/ui/profile/cubit.dart | 74 +++ lib/ui/profile/page.dart | 523 +++++++++++++----- 6 files changed, 614 insertions(+), 140 deletions(-) create mode 100644 architectural_decision_records/sharing_profile_details.md create mode 100644 lib/data/models/profile_sharing_settings.dart diff --git a/architectural_decision_records/sharing_profile_details.md b/architectural_decision_records/sharing_profile_details.md new file mode 100644 index 0000000..e061b81 --- /dev/null +++ b/architectural_decision_records/sharing_profile_details.md @@ -0,0 +1,3 @@ +# Sharing Profile Details + +In the context of enabling fine grained control over which contact sees which part of one's profile, facing the concern that with strong system contact schema integration, only with consistent index and label value a contact detail can be tracked for changes and otherwise needs to be reassigned sharing preferences from scratch we decided for only allowing the assignment between contact details and circles to achieve an easy maintainability in case of profile changes / additions, accepting that the shared profile can not be managed fine grained for any contact individually. diff --git a/lib/data/models/profile_sharing_settings.dart b/lib/data/models/profile_sharing_settings.dart new file mode 100644 index 0000000..eab01ba --- /dev/null +++ b/lib/data/models/profile_sharing_settings.dart @@ -0,0 +1,146 @@ +// Copyright 2024 The Coagulate Authors. All rights reserved. +// SPDX-License-Identifier: MPL-2.0 + +import 'package:equatable/equatable.dart'; +// import 'package:json_annotation/json_annotation.dart'; +// part 'profile_sharing_settings.g.dart'; + +/// Lists of circle IDs that have access to the corresponding name fields +// @JsonSerializable() +class NameSharingSettings extends Equatable { + const NameSharingSettings({ + this.first = const [], + this.last = const [], + this.middle = const [], + this.prefix = const [], + this.suffix = const [], + this.nickname = const [], + this.firstPhonetic = const [], + this.lastPhonetic = const [], + this.middlePhonetic = const [], + }); + + final List first; + final List last; + final List middle; + final List prefix; + final List suffix; + final List nickname; + final List firstPhonetic; + final List lastPhonetic; + final List middlePhonetic; + + NameSharingSettings copyWith( + List? first, + List? last, + List? middle, + List? prefix, + List? suffix, + List? nickname, + List? firstPhonetic, + List? lastPhonetic, + List? middlePhonetic, + ) => + NameSharingSettings( + first: first ?? this.first, + last: last ?? this.last, + middle: middle ?? this.middle, + prefix: prefix ?? this.prefix, + suffix: suffix ?? this.suffix, + nickname: nickname ?? this.nickname, + firstPhonetic: firstPhonetic ?? this.firstPhonetic, + lastPhonetic: lastPhonetic ?? this.lastPhonetic, + middlePhonetic: middlePhonetic ?? this.middlePhonetic, + ); + + @override + List get props => [ + first, + last, + middle, + prefix, + suffix, + nickname, + firstPhonetic, + lastPhonetic, + middlePhonetic, + ]; +} + +// @JsonSerializable() +class ProfileSharingSettings extends Equatable { + const ProfileSharingSettings({ + this.displayName = const [], + this.name = const NameSharingSettings(), + this.phones = const {}, + this.emails = const {}, + this.addresses = const {}, + this.organizations = const {}, + this.websites = const {}, + this.socialMedias = const {}, + this.events = const {}, + }); + + /// List of circle IDs that have access to the displayName + final List displayName; + + /// Settings for which of the name fields are shared with which circle + final NameSharingSettings name; + + /// Map of index|label to circle IDs that have access to phones + final Map> phones; + + /// Map of index|label to circle IDs that have access to emails + final Map> emails; + + /// Map of index|label to circle IDs that have access to addresses + final Map> addresses; + + /// Map of index|label to circle IDs that have access to organizations + final Map> organizations; + + /// Map of index|label to circle IDs that have access to websites + final Map> websites; + + /// Map of index|label to circle IDs that have access to socialMedias + final Map> socialMedias; + + /// Map of index|label to circle IDs that have access to events + final Map> events; + + ProfileSharingSettings copyWith({ + List? displayName, + NameSharingSettings? name, + Map>? phones, + Map>? emails, + Map>? addresses, + Map>? organizations, + Map>? websites, + Map>? socialMedias, + Map>? events, + }) => + ProfileSharingSettings( + displayName: displayName ?? this.displayName, + name: name ?? this.name, + phones: phones ?? this.phones, + emails: emails ?? this.emails, + addresses: addresses ?? this.addresses, + organizations: organizations ?? this.organizations, + websites: websites ?? this.websites, + socialMedias: socialMedias ?? this.socialMedias, + events: events ?? this.events, + ); + + @override + List get props => [ + displayName, + name, + phones, + emails, + addresses, + organizations, + websites, + socialMedias, + events, + ]; +} diff --git a/lib/data/repositories/contacts.dart b/lib/data/repositories/contacts.dart index 516ff57..c3a653a 100644 --- a/lib/data/repositories/contacts.dart +++ b/lib/data/repositories/contacts.dart @@ -15,6 +15,7 @@ import 'package:veilid/veilid.dart'; import '../../veilid_processor/repository/processor_repository.dart'; import '../models/coag_contact.dart'; import '../models/contact_update.dart'; +import '../models/profile_sharing_settings.dart'; import '../providers/distributed_storage/base.dart'; import '../providers/persistent_storage/base.dart'; import '../providers/system_contacts/base.dart'; @@ -70,6 +71,11 @@ class ContactsRepository { late final Timer? timerDhtRefresh; String? profileContactId; + // TODO: Persistent storage and initialization + Map circles = {'C1': 'Friends', 'C2': 'Neighbors'}; + ProfileSharingSettings profileSharingSettings = + const ProfileSharingSettings(); + Map _contacts = {}; final _contactsStreamController = BehaviorSubject(); diff --git a/lib/ui/contact_details/page.dart b/lib/ui/contact_details/page.dart index 4408176..bd90894 100644 --- a/lib/ui/contact_details/page.dart +++ b/lib/ui/contact_details/page.dart @@ -66,7 +66,7 @@ Widget _qrCodeButton(BuildContext context, ]), onPressed: () async { print(qrCodeData); - showDialog( + await showDialog( context: context, builder: (_) => AlertDialog( title: Text(alertTitle), diff --git a/lib/ui/profile/cubit.dart b/lib/ui/profile/cubit.dart index 748f987..b049cd7 100644 --- a/lib/ui/profile/cubit.dart +++ b/lib/ui/profile/cubit.dart @@ -11,6 +11,7 @@ import 'package:json_annotation/json_annotation.dart'; import '../../data/models/coag_contact.dart'; import '../../data/models/contact_location.dart'; +import '../../data/models/profile_sharing_settings.dart'; import '../../data/repositories/contacts.dart'; part 'cubit.g.dart'; @@ -111,6 +112,79 @@ class ProfileCubit extends Cubit { emit(state.copyWith(profileContact: updatedContact)); } + void updatePhoneSharingCircles( + int index, String label, List circles) { + // TODO: Trigger a cleanup of the available label/index combinations somewhere, doesn't need to be here + final phones = Map>.from( + contactsRepository.profileSharingSettings.phones); + phones['$index|$label'] = circles; + contactsRepository.profileSharingSettings = + contactsRepository.profileSharingSettings.copyWith(phones: phones); + } + + void updateEmailSharingCircles( + int index, String label, List circles) { + // TODO: Trigger a cleanup of the available label/index combinations somewhere, doesn't need to be here + final emails = Map>.from( + contactsRepository.profileSharingSettings.emails); + emails['$index|$label'] = circles; + contactsRepository.profileSharingSettings = + contactsRepository.profileSharingSettings.copyWith(emails: emails); + } + + void updateAddressSharingCircles( + int index, String label, List circles) { + // TODO: Trigger a cleanup of the available label/index combinations somewhere, doesn't need to be here + final addresses = Map>.from( + contactsRepository.profileSharingSettings.addresses); + addresses['$index|$label'] = circles; + contactsRepository.profileSharingSettings = contactsRepository + .profileSharingSettings + .copyWith(addresses: addresses); + } + + void updateOrganizationSharingCircles( + int index, String label, List circles) { + // TODO: Trigger a cleanup of the available label/index combinations somewhere, doesn't need to be here + final organizations = Map>.from( + contactsRepository.profileSharingSettings.organizations); + organizations['$index|$label'] = circles; + contactsRepository.profileSharingSettings = contactsRepository + .profileSharingSettings + .copyWith(organizations: organizations); + } + + void updateWebsiteSharingCircles( + int index, String label, List circles) { + // TODO: Trigger a cleanup of the available label/index combinations somewhere, doesn't need to be here + final websites = Map>.from( + contactsRepository.profileSharingSettings.websites); + websites['$index|$label'] = circles; + contactsRepository.profileSharingSettings = + contactsRepository.profileSharingSettings.copyWith(websites: websites); + } + + void updateSocialMediaSharingCircles( + int index, String label, List circles) { + // TODO: Trigger a cleanup of the available label/index combinations somewhere, doesn't need to be here + final socialMedias = Map>.from( + contactsRepository.profileSharingSettings.socialMedias); + socialMedias['$index|$label'] = circles; + contactsRepository.profileSharingSettings = contactsRepository + .profileSharingSettings + .copyWith(socialMedias: socialMedias); + } + + void updateEventSharingCircles( + int index, String label, List circles) { + // TODO: Trigger a cleanup of the available label/index combinations somewhere, doesn't need to be here + final events = Map>.from( + contactsRepository.profileSharingSettings.events); + events['$index|$label'] = circles; + contactsRepository.profileSharingSettings = + contactsRepository.profileSharingSettings.copyWith(events: events); + } + @override Future close() { _contactsSubscription.cancel(); diff --git a/lib/ui/profile/page.dart b/lib/ui/profile/page.dart index 5e3b888..8f4d5e1 100644 --- a/lib/ui/profile/page.dart +++ b/lib/ui/profile/page.dart @@ -8,6 +8,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:multi_select_flutter/multi_select_flutter.dart'; import '../../data/models/contact_location.dart'; import '../../data/repositories/contacts.dart'; @@ -15,97 +16,225 @@ import '../widgets/address_coordinates_form.dart'; import '../widgets/avatar.dart'; import 'cubit.dart'; -Widget emails(List emails) => Card( - color: Colors.white, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: SizedBox( - child: Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 16), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ...emails.map((e) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 8), - child: _label(e.label.name, e.customLabel)), - Padding( - padding: const EdgeInsets.only(top: 0), - child: Text(e.address, - style: const TextStyle(fontSize: 19))) - ])), - ])))); +Future showPickCirclesBottomSheet( + {required BuildContext context, + required String label, + required Map circles, + required List selectedCircles, + required void Function(List selectedCircles) callback}) async => + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (modalContext) => MultiSelectBottomSheet( + title: Padding( + padding: const EdgeInsets.only(left: 16, top: 12), + child: Text('Share "$label" with', + textScaler: const TextScaler.linear(1.4))), + searchable: circles.length > 10, + items: circles + .map((id, label) => MapEntry(id, MultiSelectItem(id, label))) + .values + .asList(), + initialValue: selectedCircles, + onConfirm: (values) => callback(values), + maxChildSize: 0.8, + )); -Widget phones(List phones) => Card( - color: Colors.white, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: SizedBox( - child: Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 16), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ...phones.map((e) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 8), - child: _label(e.label.name, e.customLabel)), - Padding( - padding: const EdgeInsets.only(top: 0), - child: Text(e.number, - style: const TextStyle(fontSize: 19))) - ])) - ])))); +Widget emails(List emails, + [void Function(int index, String label)? onTap]) => + Card( + color: Colors.white, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: SizedBox( + child: Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, top: 4, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: emails + .asMap() + .map((i, e) => MapEntry( + i, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.only(top: 8), + child: _label( + e.label.name, e.customLabel)), + Padding( + padding: + const EdgeInsets.only(top: 0), + child: Text(e.address, + style: const TextStyle( + fontSize: 19))) + ])), + IconButton( + onPressed: () => (onTap == null) + ? null + : onTap( + i, + _label(e.label.name, + e.customLabel) + .data!), + icon: const Icon(Icons.add_task)) + ]))) + .values + .asList())))); -Widget websites(List websites) => Card( - color: Colors.white, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: SizedBox( - child: Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 16), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ...websites.map((e) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 8), - child: _label(e.label.name, e.customLabel)), - Padding( - padding: const EdgeInsets.only(top: 0), - child: Text(e.url, - style: const TextStyle(fontSize: 19))) - ])) - ])))); +Widget phones(List phones, + [void Function(int index, String label)? onTap]) => + Card( + color: Colors.white, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: SizedBox( + child: Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, top: 4, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: phones + .asMap() + .map((i, e) => MapEntry( + i, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.only(top: 8), + child: _label( + e.label.name, e.customLabel)), + Padding( + padding: + const EdgeInsets.only(top: 0), + child: Text(e.number, + style: const TextStyle( + fontSize: 19))) + ])), + IconButton( + onPressed: () => (onTap == null) + ? null + : onTap( + i, + _label(e.label.name, + e.customLabel) + .data!), + icon: const Icon(Icons.add_task)) + ]))) + .values + .asList())))); -Widget socialMedias(List websites) => Card( - color: Colors.white, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: SizedBox( - child: Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 16), - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ...websites.map((e) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 8), - child: _label(e.label.name, e.customLabel)), - Padding( - padding: const EdgeInsets.only(top: 0), - child: Text(e.userName, - style: const TextStyle(fontSize: 19))) - ])) - ])))); +Widget websites(List websites, + [void Function(int index, String label)? onTap]) => + Card( + color: Colors.white, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: SizedBox( + child: Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, top: 4, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: websites + .asMap() + .map((i, e) => MapEntry( + i, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.only(top: 8), + child: _label( + e.label.name, e.customLabel)), + Padding( + padding: + const EdgeInsets.only(top: 0), + child: Text(e.url, + style: const TextStyle( + fontSize: 19))) + ])), + IconButton( + onPressed: () => (onTap == null) + ? null + : onTap( + i, + _label(e.label.name, + e.customLabel) + .data!), + icon: const Icon(Icons.add_task)) + ]))) + .values + .asList())))); + +Widget socialMedias(List websites, + [void Function(int index, String label)? onTap]) => + Card( + color: Colors.white, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: SizedBox( + child: Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, top: 4, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: websites + .asMap() + .map((i, e) => MapEntry( + i, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.only(top: 8), + child: _label( + e.label.name, e.customLabel)), + Padding( + padding: + const EdgeInsets.only(top: 0), + child: Text(e.userName, + style: const TextStyle( + fontSize: 19))) + ])), + IconButton( + onPressed: () => (onTap == null) + ? null + : onTap( + i, + _label(e.label.name, + e.customLabel) + .data!), + icon: const Icon(Icons.add_task)) + ]))) + .values + .asList())))); String _commaToNewline(String s) => s.replaceAll(', ', ',').replaceAll(',', '\n'); @@ -123,7 +252,8 @@ bool labelDoesMatch(String name, Address address) { } Widget addressesWithForms(BuildContext context, List
addresses, - List locations) => + List locations, + [void Function(int index, String label)? onTap]) => Card( color: Colors.white, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), @@ -133,51 +263,72 @@ Widget addressesWithForms(BuildContext context, List
addresses, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...addresses - .asMap() - .map((int i, Address e) => MapEntry( - i, - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _label(e.label.name, e.customLabel), - Text(_commaToNewline(e.address), - style: const TextStyle(fontSize: 19)), - const SizedBox(height: 8), - // TODO: This is not updated when fetch coordinates emits new state - AddressCoordinatesForm( - lng: locations - .where((l) => - labelDoesMatch(l.name, e)) - .firstOrNull - ?.longitude, - lat: locations - .where((l) => - labelDoesMatch(l.name, e)) - .firstOrNull - ?.latitude, - callback: (lng, lat) => context - .read() - .updateCoordinates(i, lng, lat)), - // TODO: Add small map previewing the location when coordinates are available - TextButton( - child: const Text( - 'Auto Fetch Coordinates'), - // TODO: Switch to address index instead of label? Can there be duplicates? - onPressed: () async => showDialog( - context: context, - // barrierDismissible: false, - builder: (dialogContext) => - _confirmPrivacyLeakDialog( - dialogContext, - e.address, - () => unawaited(context - .read() - .fetchCoordinates(i))))) - ]))) - .values - ])))); + children: addresses + .asMap() + .map((i, e) => MapEntry( + i, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + _label( + e.label.name, e.customLabel), + Text(_commaToNewline(e.address), + style: const TextStyle( + fontSize: 19)), + ])), + IconButton( + onPressed: () => (onTap == null) + ? null + : onTap( + i, + _label(e.label.name, + e.customLabel) + .data!), + icon: const Icon(Icons.add_task)), + ]), + const SizedBox(height: 8), + // TODO: This is not updated when fetch coordinates emits new state + AddressCoordinatesForm( + lng: locations + .where( + (l) => labelDoesMatch(l.name, e)) + .firstOrNull + ?.longitude, + lat: locations + .where( + (l) => labelDoesMatch(l.name, e)) + .firstOrNull + ?.latitude, + callback: (lng, lat) => context + .read() + .updateCoordinates(i, lng, lat)), + // TODO: Add small map previewing the location when coordinates are available + TextButton( + child: + const Text('Auto Fetch Coordinates'), + // TODO: Switch to address index instead of label? Can there be duplicates? + onPressed: () async => showDialog( + context: context, + // barrierDismissible: false, + builder: (dialogContext) => + _confirmPrivacyLeakDialog( + dialogContext, + e.address, + () => unawaited(context + .read() + .fetchCoordinates(i))))) + ]))) + .values + .asList())))); Widget addresses(List
addresses) => Card( color: Colors.white, @@ -278,14 +429,108 @@ Widget buildProfileScrollView(BuildContext context, Contact contact, children: [ const SizedBox(height: 8), header(contact), - if (contact.phones.isNotEmpty) phones(contact.phones), - if (contact.emails.isNotEmpty) emails(contact.emails), + if (contact.phones.isNotEmpty) + phones( + contact.phones, + (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + circles: context + .read() + .contactsRepository + .circles, + selectedCircles: context + .read() + .contactsRepository + .profileSharingSettings + .phones['$i|$label'] ?? + [], + callback: (selectedCircles) => context + .read() + .updatePhoneSharingCircles( + i, label, selectedCircles))), + if (contact.emails.isNotEmpty) + emails( + contact.emails, + (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + circles: context + .read() + .contactsRepository + .circles, + selectedCircles: context + .read() + .contactsRepository + .profileSharingSettings + .emails['$i|$label'] ?? + [], + callback: (selectedCircles) => context + .read() + .updateEmailSharingCircles( + i, label, selectedCircles))), if (contact.addresses.isNotEmpty) addressesWithForms( - context, contact.addresses, addressLocations), - if (contact.websites.isNotEmpty) websites(contact.websites), + context, + contact.addresses, + addressLocations, + (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + circles: context + .read() + .contactsRepository + .circles, + selectedCircles: context + .read() + .contactsRepository + .profileSharingSettings + .addresses['$i|$label'] ?? + [], + callback: (selectedCircles) => context + .read() + .updateAddressSharingCircles( + i, label, selectedCircles))), + if (contact.websites.isNotEmpty) + websites( + contact.websites, + (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + circles: context + .read() + .contactsRepository + .circles, + selectedCircles: context + .read() + .contactsRepository + .profileSharingSettings + .websites['$i|$label'] ?? + [], + callback: (selectedCircles) => context + .read() + .updateWebsiteSharingCircles( + i, label, selectedCircles))), if (contact.socialMedias.isNotEmpty) - socialMedias(contact.socialMedias), + socialMedias( + contact.socialMedias, + (i, label) async => showPickCirclesBottomSheet( + context: context, + label: label, + circles: context + .read() + .contactsRepository + .circles, + selectedCircles: context + .read() + .contactsRepository + .profileSharingSettings + .socialMedias['$i|$label'] ?? + [], + callback: (selectedCircles) => context + .read() + .updateSocialMediaSharingCircles( + i, label, selectedCircles))), ])) ]));