From 1d65ba2604214c5a96c303fc8c4fd4f99ab5719a Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 17 Nov 2023 21:47:44 -0500 Subject: [PATCH 1/4] Add more fine-grained privacy processing for direct messages and preserving settings on comments --- lib/friendica_client/friendica_client.dart | 2 +- .../timeline_entry_mastodon_extensions.dart | 30 +++++++++++++++++-- .../visibility_mastodon_extensions.dart | 22 +++++++++++--- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index e089c88..d627af0 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -843,7 +843,7 @@ class StatusesClient extends FriendicaClient { if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText, if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId, if (mediaIds.isNotEmpty) 'media_ids': mediaIds, - 'visibility': visibility.toCreateStatusValue(), + 'visibility': visibility.toCreateStatusValue(inReplyToId.isNotEmpty), 'friendica': { 'title': '', }, diff --git a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart index 29db1f3..50efe9a 100644 --- a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart +++ b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart @@ -71,9 +71,33 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { : 0; final youReshared = json['reblogged'] ?? false; - final visibility = ['public', 'unlisted'].contains(json['visibility']) - ? Visibility.public() - : Visibility.private(); + late final Visibility visibility; + final visibilityString = json['visibility']; + if (visibilityString == 'public') { + visibility = Visibility.public(); + } else if (visibilityString == 'private') { + final allowedUserIds = + json['friendica']?['visibility']?['allow_cid'] as List? ?? + []; + final excludedUserIds = + json['friendica']?['visibility']?['deny_cid'] as List? ?? []; + final allowedCircleIds = + json['friendica']?['visibility']?['allow_gid'] as List? ?? + []; + final excludedCircleIds = + json['friendica']?['visibility']?['deny_gid'] as List? ?? []; + visibility = Visibility( + type: VisibilityType.private, + allowedUserIds: allowedUserIds.map((e) => e.toString()).toList(), + excludedUserIds: excludedUserIds.map((e) => e.toString()).toList(), + allowedCircleIds: allowedCircleIds.map((e) => e.toString()).toList(), + excludedCircleIds: excludedCircleIds.map((e) => e.toString()).toList(), + ); + } else if (visibilityString == 'unlisted') { + visibility = Visibility.private(); + } else { + visibility = Visibility.private(); + } const title = ''; final body = json['content'] ?? ''; diff --git a/lib/serializers/mastodon/visibility_mastodon_extensions.dart b/lib/serializers/mastodon/visibility_mastodon_extensions.dart index afbd845..24d266b 100644 --- a/lib/serializers/mastodon/visibility_mastodon_extensions.dart +++ b/lib/serializers/mastodon/visibility_mastodon_extensions.dart @@ -3,20 +3,34 @@ import 'package:relatica/models/circle_data.dart'; import '../../models/visibility.dart'; extension VisibilityMastodonExtensions on Visibility { - String toCreateStatusValue() { + String toCreateStatusValue(bool onComment) { if (type == VisibilityType.public) { return 'public'; } - if (hasDetails) { - final circleId = allowedCircleIds.first; + if (!onComment && hasDetails) { + final circleId = + allowedCircleIds.firstOrNull ?? allowedUserIds.firstOrNull; if (circleId == CircleData.followersPseudoCircle.id) { return 'private'; } - return circleId; + return circleId ?? 'private'; + } + + if (onComment && !hasDetails && type == VisibilityType.private) { + return 'direct'; } return 'private'; } + + Map friendicaExtensionVisibilityJson() { + return { + "allow_cid": allowedUserIds, + "deny_cid": excludedUserIds, + "allow_gid": allowedCircleIds, + "deny_gid": excludedCircleIds, + }; + } } From 8b99f73238b18b24c769d37d3f540edf1400d8e9 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 17 Nov 2023 22:47:38 -0500 Subject: [PATCH 2/4] Add status visibility dialog box --- .../timeline/status_header_control.dart | 190 +++++++++++++++--- 1 file changed, 166 insertions(+), 24 deletions(-) diff --git a/lib/controls/timeline/status_header_control.dart b/lib/controls/timeline/status_header_control.dart index e512c28..168bd66 100644 --- a/lib/controls/timeline/status_header_control.dart +++ b/lib/controls/timeline/status_header_control.dart @@ -5,7 +5,7 @@ import 'package:logging/logging.dart'; import '../../globals.dart'; import '../../models/connection.dart'; import '../../models/timeline_entry.dart'; -import '../../models/visibility.dart'; +import '../../models/visibility.dart' as v; import '../../routes.dart'; import '../../services/auth_service.dart'; import '../../services/connections_manager.dart'; @@ -39,23 +39,23 @@ class StatusHeaderControl extends StatelessWidget { .getForProfile(activeProfile) .transform((s) => s.getForPost(entry.id)?.resharers.firstOrNull) .getValueOrElse(() => null); - - getIt>() + final manager = getIt>() .getForProfile(activeProfile) - .match( - onSuccess: (manager) { - author = - manager.getById(entry.authorId).getValueOrElse(() => Connection()); - reshareAuthor = reshareId == null - ? Connection() - : manager.getById(reshareId).getValueOrElse(() => Connection()); - }, - onError: (error) { - _logger.severe('Error getting connections manageR: $error'); - author = Connection(); - reshareAuthor = Connection(); - }, - ); + .fold( + onSuccess: (m) => m, + onError: (error) { + _logger.severe('Error getting connection manager: $error'); + return null; + }); + + author = manager!.getById(entry.authorId).getValueOrElse( + () => Connection(), + ); + reshareAuthor = reshareId == null + ? Connection() + : manager.getById(reshareId).getValueOrElse( + () => Connection(), + ); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -122,13 +122,17 @@ class StatusHeaderControl extends StatelessWidget { ElapsedDateUtils.epochSecondsToString(entry.backdatedTimestamp), style: Theme.of(context).textTheme.bodySmall, ), - const HorizontalPadding(), - Icon( - entry.visibility.type == VisibilityType.public - ? Icons.public - : Icons.lock, - color: Theme.of(context).hintColor, - size: Theme.of(context).textTheme.bodySmall?.fontSize, + IconButton( + onPressed: () async { + await _showVisibilityDialog(context, manager, entry.visibility); + }, + icon: Icon( + entry.visibility.type == v.VisibilityType.public + ? Icons.public + : Icons.lock, + color: Theme.of(context).hintColor, + size: Theme.of(context).textTheme.bodySmall?.fontSize, + ), ), ], ), @@ -136,3 +140,141 @@ class StatusHeaderControl extends StatelessWidget { ); } } + +Future _showVisibilityDialog( + BuildContext context, + ConnectionsManager cm, + v.Visibility visibility, +) async { + final circlesMap = {for (var item in cm.getMyCircles()) item.id: item}; + + final allowedCircles = visibility.allowedCircleIds.map((c) { + if (c == '~') { + return 'Followers'; + } + + return circlesMap[c]?.name ?? 'Circle #$c'; + }); + + final excludedCircles = visibility.excludedCircleIds.map((c) { + if (c == '~') { + return 'Followers'; + } + + return circlesMap[c]?.name ?? 'Circle #$c'; + }); + + final allowedUsers = visibility.allowedUserIds.map( + (u) => cm.getById(u).fold( + onSuccess: (connection) => connection.handle, + onError: (_) => 'User $u', + ), + ); + + final excludedUsers = visibility.excludedUserIds.map( + (u) => cm.getById(u).fold( + onSuccess: (connection) => connection.handle, + onError: (_) => 'User $u', + ), + ); + + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Visibility Details', + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(decoration: TextDecoration.underline), + ), + if (visibility.type == v.VisibilityType.public) ...[ + const Text('Public') + ], + if (visibility.type != v.VisibilityType.public) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Allowed Users: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Expanded( + child: Text( + allowedUsers.isEmpty + ? 'Empty' + : allowedUsers.join(', '), + softWrap: true, + ), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text( + 'Allowed Circles: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + allowedCircles.isEmpty + ? 'Empty' + : allowedCircles.join(','), + softWrap: true, + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text( + 'Excluded Users: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + excludedUsers.isEmpty ? 'Empty' : excludedUsers.join(','), + softWrap: true, + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text( + 'Excluded Circles: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + excludedCircles.isEmpty + ? 'Empty' + : excludedCircles.join(','), + softWrap: true, + ) + ], + ), + ], + ], + ), + ), + actions: [ + ElevatedButton( + child: const Text('Dismiss'), + onPressed: () { + Navigator.pop(context, true); // showDialog() returns true + }, + ), + ], + ); + }, + ); +} From 479fa2a54598f4f0b2239a1954f8e51379c8176a Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 17 Nov 2023 23:13:58 -0500 Subject: [PATCH 3/4] Add visibility lookup to editor and fix private Circles selector overflow --- .../timeline/status_header_control.dart | 141 +---------------- lib/controls/visibility_dialog.dart | 142 ++++++++++++++++++ lib/screens/editor.dart | 114 ++++++++------ 3 files changed, 210 insertions(+), 187 deletions(-) create mode 100644 lib/controls/visibility_dialog.dart diff --git a/lib/controls/timeline/status_header_control.dart b/lib/controls/timeline/status_header_control.dart index 168bd66..6e6c8b3 100644 --- a/lib/controls/timeline/status_header_control.dart +++ b/lib/controls/timeline/status_header_control.dart @@ -14,6 +14,7 @@ import '../../utils/active_profile_selector.dart'; import '../../utils/dateutils.dart'; import '../image_control.dart'; import '../padding.dart'; +import '../visibility_dialog.dart'; class StatusHeaderControl extends StatelessWidget { static final _logger = Logger('$StatusHeaderControl'); @@ -124,7 +125,7 @@ class StatusHeaderControl extends StatelessWidget { ), IconButton( onPressed: () async { - await _showVisibilityDialog(context, manager, entry.visibility); + await showVisibilityDialog(context, manager, entry.visibility); }, icon: Icon( entry.visibility.type == v.VisibilityType.public @@ -140,141 +141,3 @@ class StatusHeaderControl extends StatelessWidget { ); } } - -Future _showVisibilityDialog( - BuildContext context, - ConnectionsManager cm, - v.Visibility visibility, -) async { - final circlesMap = {for (var item in cm.getMyCircles()) item.id: item}; - - final allowedCircles = visibility.allowedCircleIds.map((c) { - if (c == '~') { - return 'Followers'; - } - - return circlesMap[c]?.name ?? 'Circle #$c'; - }); - - final excludedCircles = visibility.excludedCircleIds.map((c) { - if (c == '~') { - return 'Followers'; - } - - return circlesMap[c]?.name ?? 'Circle #$c'; - }); - - final allowedUsers = visibility.allowedUserIds.map( - (u) => cm.getById(u).fold( - onSuccess: (connection) => connection.handle, - onError: (_) => 'User $u', - ), - ); - - final excludedUsers = visibility.excludedUserIds.map( - (u) => cm.getById(u).fold( - onSuccess: (connection) => connection.handle, - onError: (_) => 'User $u', - ), - ); - - return showDialog( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return AlertDialog( - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Visibility Details', - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(decoration: TextDecoration.underline), - ), - if (visibility.type == v.VisibilityType.public) ...[ - const Text('Public') - ], - if (visibility.type != v.VisibilityType.public) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Allowed Users: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Expanded( - child: Text( - allowedUsers.isEmpty - ? 'Empty' - : allowedUsers.join(', '), - softWrap: true, - ), - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Text( - 'Allowed Circles: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text( - allowedCircles.isEmpty - ? 'Empty' - : allowedCircles.join(','), - softWrap: true, - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Text( - 'Excluded Users: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text( - excludedUsers.isEmpty ? 'Empty' : excludedUsers.join(','), - softWrap: true, - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Text( - 'Excluded Circles: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text( - excludedCircles.isEmpty - ? 'Empty' - : excludedCircles.join(','), - softWrap: true, - ) - ], - ), - ], - ], - ), - ), - actions: [ - ElevatedButton( - child: const Text('Dismiss'), - onPressed: () { - Navigator.pop(context, true); // showDialog() returns true - }, - ), - ], - ); - }, - ); -} diff --git a/lib/controls/visibility_dialog.dart b/lib/controls/visibility_dialog.dart new file mode 100644 index 0000000..f4cba5c --- /dev/null +++ b/lib/controls/visibility_dialog.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; + +import '../../models/visibility.dart' as v; +import '../services/connections_manager.dart'; + +Future showVisibilityDialog( + BuildContext context, + ConnectionsManager cm, + v.Visibility visibility, +) async { + final circlesMap = {for (var item in cm.getMyCircles()) item.id: item}; + + final allowedCircles = visibility.allowedCircleIds.map((c) { + if (c == '~') { + return 'Followers'; + } + + return circlesMap[c]?.name ?? 'Circle #$c'; + }); + + final excludedCircles = visibility.excludedCircleIds.map((c) { + if (c == '~') { + return 'Followers'; + } + + return circlesMap[c]?.name ?? 'Circle #$c'; + }); + + final allowedUsers = visibility.allowedUserIds.map( + (u) => cm.getById(u).fold( + onSuccess: (connection) => connection.handle, + onError: (_) => 'User $u', + ), + ); + + final excludedUsers = visibility.excludedUserIds.map( + (u) => cm.getById(u).fold( + onSuccess: (connection) => connection.handle, + onError: (_) => 'User $u', + ), + ); + + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Visibility Details', + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(decoration: TextDecoration.underline), + ), + if (visibility.type == v.VisibilityType.public) ...[ + const Text('Public') + ], + if (visibility.type != v.VisibilityType.public) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Allowed Users: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Expanded( + child: Text( + allowedUsers.isEmpty + ? 'Empty' + : allowedUsers.join(', '), + softWrap: true, + ), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text( + 'Allowed Circles: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + allowedCircles.isEmpty + ? 'Empty' + : allowedCircles.join(','), + softWrap: true, + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text( + 'Excluded Users: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + excludedUsers.isEmpty ? 'Empty' : excludedUsers.join(','), + softWrap: true, + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text( + 'Excluded Circles: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + excludedCircles.isEmpty + ? 'Empty' + : excludedCircles.join(','), + softWrap: true, + ) + ], + ), + ], + ], + ), + ), + actions: [ + ElevatedButton( + child: const Text('Dismiss'), + onPressed: () { + Navigator.pop(context, true); // showDialog() returns true + }, + ), + ], + ); + }, + ); +} diff --git a/lib/screens/editor.dart b/lib/screens/editor.dart index a5dce43..4450a2a 100644 --- a/lib/screens/editor.dart +++ b/lib/screens/editor.dart @@ -15,6 +15,7 @@ import '../controls/login_aware_cached_network_image.dart'; import '../controls/padding.dart'; import '../controls/standard_appbar.dart'; import '../controls/timeline/status_header_control.dart'; +import '../controls/visibility_dialog.dart'; import '../globals.dart'; import '../models/circle_data.dart'; import '../models/exec_error.dart'; @@ -24,6 +25,7 @@ import '../models/media_attachment_uploads/new_entry_media_items.dart'; import '../models/timeline_entry.dart'; import '../models/visibility.dart'; import '../serializers/friendica/link_preview_friendica_extensions.dart'; +import '../services/connections_manager.dart'; import '../services/feature_version_checker.dart'; import '../services/timeline_manager.dart'; import '../utils/active_profile_selector.dart'; @@ -566,13 +568,22 @@ class _EditorScreenState extends State { Widget buildVisibilitySelector(BuildContext context) { if (widget.forEditing || widget.parentId.isNotEmpty) { + final cm = context + .read>() + .activeEntry + .value; + return Row( children: [ const Text('Visibility:'), - const HorizontalPadding(), - visibility.type == VisibilityType.public - ? const Icon(Icons.public) - : const Icon(Icons.lock), + IconButton( + onPressed: () async { + await showVisibilityDialog(context, cm, visibility); + }, + icon: visibility.type == VisibilityType.public + ? const Icon(Icons.public) + : const Icon(Icons.lock), + ) ], ); } @@ -583,67 +594,74 @@ class _EditorScreenState extends State { .getValueOrElse(() => []); circles.sort((g1, g2) => g1.name.compareTo(g2.name)); - final circleMenuItems = >[]; - circleMenuItems.add(DropdownMenuEntry( + final circleMenuItems = >[]; + circleMenuItems.add(DropdownMenuItem( value: CircleData.followersPseudoCircle, - label: CircleData.followersPseudoCircle.name)); - circleMenuItems.add(DropdownMenuEntry( - value: CircleData('', ''), label: '-', enabled: false)); - circleMenuItems.addAll(circles.map((g) => DropdownMenuEntry( + child: Text(CircleData.followersPseudoCircle.name))); + circleMenuItems.add(DropdownMenuItem( + value: CircleData('', ''), child: const Divider(), enabled: false)); + circleMenuItems.addAll(circles.map((g) => DropdownMenuItem( value: g, - label: g.name, + child: Text(g.name), ))); - if (!circles.contains(currentCircle)) { + if (currentCircle != CircleData.followersPseudoCircle && + !circles.contains(currentCircle)) { currentCircle = null; } return Row( children: [ const Text('Visibility:'), const HorizontalPadding(), - DropdownMenu( - initialSelection: visibility.type, - enabled: !widget.forEditing, - onSelected: (value) { - setState(() { - if (value == VisibilityType.public) { - visibility = Visibility.public(); - return; - } + DropdownButton( + value: visibility.type, + onChanged: widget.forEditing + ? null + : (value) { + setState(() { + if (value == VisibilityType.public) { + visibility = Visibility.public(); + return; + } - if (value == VisibilityType.private && currentCircle == null) { - visibility = Visibility.private(); - return; - } + if (value == VisibilityType.private && + currentCircle == null) { + visibility = Visibility.private(); + return; + } - visibility = Visibility( - type: VisibilityType.private, - allowedCircleIds: [currentCircle!.id], - ); - }); - }, - dropdownMenuEntries: VisibilityType.values - .map((v) => DropdownMenuEntry( + visibility = Visibility( + type: VisibilityType.private, + allowedCircleIds: [currentCircle!.id], + ); + }); + }, + items: VisibilityType.values + .map((v) => DropdownMenuItem( value: v, - label: v.toLabel(), + child: Text(v.toLabel()), )) .toList(), ), const HorizontalPadding(), if (visibility.type == VisibilityType.private) - DropdownMenu( - enabled: !widget.forEditing, - initialSelection: currentCircle, - onSelected: (value) { - setState(() { - currentCircle = value; - visibility = Visibility( - type: VisibilityType.private, - allowedCircleIds: - currentCircle == null ? [] : [currentCircle!.id], - ); - }); - }, - dropdownMenuEntries: circleMenuItems, + Expanded( + child: DropdownButton( + value: currentCircle, + isExpanded: true, + onChanged: widget.forEditing + ? null + : (value) { + setState(() { + currentCircle = value; + visibility = Visibility( + type: VisibilityType.private, + allowedCircleIds: + currentCircle == null ? [] : [currentCircle!.id], + ); + }); + }, + items: circleMenuItems, + ), ), ], ); From 061a32c4fb348c83d3d3e0b7628ee8750b37881b Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 17 Nov 2023 23:16:48 -0500 Subject: [PATCH 4/4] Fix make list evaluation explicit in visibility dialog --- lib/controls/visibility_dialog.dart | 32 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/controls/visibility_dialog.dart b/lib/controls/visibility_dialog.dart index f4cba5c..96327d4 100644 --- a/lib/controls/visibility_dialog.dart +++ b/lib/controls/visibility_dialog.dart @@ -16,7 +16,7 @@ Future showVisibilityDialog( } return circlesMap[c]?.name ?? 'Circle #$c'; - }); + }).toList(); final excludedCircles = visibility.excludedCircleIds.map((c) { if (c == '~') { @@ -24,21 +24,25 @@ Future showVisibilityDialog( } return circlesMap[c]?.name ?? 'Circle #$c'; - }); + }).toList(); - final allowedUsers = visibility.allowedUserIds.map( - (u) => cm.getById(u).fold( - onSuccess: (connection) => connection.handle, - onError: (_) => 'User $u', - ), - ); + final allowedUsers = visibility.allowedUserIds + .map( + (u) => cm.getById(u).fold( + onSuccess: (connection) => connection.handle, + onError: (_) => 'User $u', + ), + ) + .toList(); - final excludedUsers = visibility.excludedUserIds.map( - (u) => cm.getById(u).fold( - onSuccess: (connection) => connection.handle, - onError: (_) => 'User $u', - ), - ); + final excludedUsers = visibility.excludedUserIds + .map( + (u) => cm.getById(u).fold( + onSuccess: (connection) => connection.handle, + onError: (_) => 'User $u', + ), + ) + .toList(); return showDialog( context: context,