diff --git a/lib/routes.dart b/lib/routes.dart index f982e11..4041deb 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -7,6 +7,7 @@ import 'screens/editor.dart'; import 'screens/follow_request_adjudication_screen.dart'; import 'screens/gallery_browsers_screen.dart'; import 'screens/gallery_screen.dart'; +import 'screens/group_add_users_screen.dart'; import 'screens/group_create_screen.dart'; import 'screens/group_editor_screen.dart'; import 'screens/group_management_screen.dart'; @@ -139,6 +140,11 @@ final appRouter = GoRouter( path: 'new', builder: (context, state) => GroupCreateScreen(), ), + GoRoute( + path: 'add_users/:id', + builder: (context, state) => + GroupAddUsersScreen(groupId: state.params['id']!), + ), ], ), GoRoute( diff --git a/lib/screens/group_add_users_screen.dart b/lib/screens/group_add_users_screen.dart new file mode 100644 index 0000000..6a73143 --- /dev/null +++ b/lib/screens/group_add_users_screen.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../controls/linear_status_indicator.dart'; +import '../controls/responsive_max_width.dart'; +import '../controls/status_and_refresh_button.dart'; +import '../globals.dart'; +import '../models/connection.dart'; +import '../models/exec_error.dart'; +import '../models/group_data.dart'; +import '../routes.dart'; +import '../services/auth_service.dart'; +import '../services/connections_manager.dart'; +import '../services/network_status_service.dart'; +import '../utils/active_profile_selector.dart'; +import '../utils/snackbar_builder.dart'; + +class GroupAddUsersScreen extends StatefulWidget { + final String groupId; + + const GroupAddUsersScreen({super.key, required this.groupId}); + + @override + State createState() => _GroupAddUsersScreenState(); +} + +class _GroupAddUsersScreenState extends State { + static final _logger = Logger('$GroupAddUsersScreen'); + var filterText = ''; + late GroupData groupData; + + @override + void initState() { + super.initState(); + final manager = + getIt>().activeEntry.value; + groupData = + manager.getMyGroups().where((g) => g.id == widget.groupId).first; + } + + Future addUserToGroup( + ConnectionsManager manager, + Connection connection, + ) async { + final messageBase = '${connection.name} from ${groupData.name}'; + final confirm = await showYesNoDialog(context, 'Add $messageBase?'); + if (context.mounted && confirm == true) { + final message = await manager + .addUserToGroup(groupData, connection) + .withResult((p0) => setState(() {})) + .fold( + onSuccess: (_) => 'Added $messageBase', + onError: (error) => 'Error adding $messageBase: $error', + ); + buildSnackbar(context, message); + } + } + + @override + Widget build(BuildContext context) { + _logger.finer('Build'); + final nss = getIt(); + final activeProfile = context.watch(); + final manager = context + .watch>() + .activeEntry + .value; + final groupMembers = manager + .getGroupMembers(groupData) + .withError((e) => logError(e, _logger)) + .getValueOrElse(() => []) + .toSet(); + final allContacts = manager.getMyContacts(); + final filterTextLC = filterText.toLowerCase(); + final contacts = allContacts + .where((c) => !groupMembers.contains(c)) + .where((c) => + filterText.isEmpty || + c.name.toLowerCase().contains(filterTextLC) || + c.handle.toLowerCase().contains(filterTextLC)) + .toList(); + contacts.sort((c1, c2) => c1.name.compareTo(c2.name)); + _logger.finer( + () => + '# in group: ${groupMembers.length} # Contacts: ${allContacts.length}, #filtered: ${contacts.length}', + ); + late Widget body; + if (contacts.isEmpty) { + body = const SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Center( + child: Text('No contacts'), + )); + } else { + body = ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) { + final contact = contacts[index]; + return ListTile( + onTap: () { + context.pushNamed(ScreenPaths.userProfile, + params: {'id': contact.id}); + }, + title: Text( + '${contact.name} (${contact.handle})', + softWrap: true, + ), + subtitle: Text( + 'Last Status: ${contact.lastStatus?.toIso8601String() ?? "Unknown"}', + softWrap: true, + ), + trailing: IconButton( + onPressed: () async => await addUserToGroup(manager, contact), + icon: const Icon(Icons.add)), + ); + }, + separatorBuilder: (context, index) => const Divider(), + itemCount: contacts.length); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Add Users'), + ), + body: SafeArea( + child: RefreshIndicator( + onRefresh: () async { + if (nss.connectionUpdateStatus.value) { + return; + } + manager.refreshGroupMemberships(groupData); + return; + }, + child: ResponsiveMaxWidth( + child: Column( + children: [ + Text( + 'Group: ${groupData.name}', + style: Theme.of(context).textTheme.bodyLarge, + softWrap: true, + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + onChanged: (value) { + setState(() { + filterText = value.toLowerCase(); + }); + }, + decoration: InputDecoration( + labelText: 'Filter By Name', + alignLabelWithHint: true, + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).highlightColor, + ), + borderRadius: BorderRadius.circular(5.0), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: StatusAndRefreshButton( + valueListenable: nss.connectionUpdateStatus, + refreshFunction: () async => + manager.refreshGroupMemberships(groupData), + ), + ) + ], + ), + StandardLinearProgressIndicator(nss.connectionUpdateStatus), + Expanded(child: body), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 6fc1115..63940ee 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -296,17 +296,15 @@ class ConnectionsManager extends ChangeNotifier { FutureResult addUserToGroup( GroupData group, Connection connection) async { _logger.finest('Adding ${connection.name} to group: ${group.name}'); - final result = await GroupsClient(getIt().currentProfile) - .addConnectionToGroup(group, connection); - result.match( - onSuccess: (_) => _refreshGroupListData(connection.id, true), - onError: (error) { - _logger - .severe('Error adding ${connection.name} to group: ${group.name}'); - }, - ); - - return result.execErrorCast(); + return await GroupsClient(getIt().currentProfile) + .addConnectionToGroup(group, connection) + .withResultAsync((_) async => refreshGroupMemberships(group)) + .withResult((_) => notifyListeners()) + .mapError((error) { + _logger + .severe('Error adding ${connection.name} from group: ${group.name}'); + return error; + }); } FutureResult removeUserFromGroup( @@ -315,7 +313,7 @@ class ConnectionsManager extends ChangeNotifier { return GroupsClient(getIt().currentProfile) .removeConnectionFromGroup(group, connection) .withResultAsync((_) async => refreshGroupMemberships(group)) - .withResultAsync((_) async => notifyListeners()) + .withResult((_) => notifyListeners()) .mapError( (error) { _logger.severe(