From 4bc7db75e9c1ef01d45e0a967a335a08d45754d9 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 2 Nov 2025 15:48:50 +0100 Subject: [PATCH] remove and add members #277 --- lib/src/database/daos/groups.dao.dart | 8 +- lib/src/localization/app_de.arb | 5 +- lib/src/localization/app_en.arb | 5 +- .../generated/app_localizations.dart | 18 ++ .../generated/app_localizations_de.dart | 11 ++ .../generated/app_localizations_en.dart | 11 ++ .../api/client2client/groups.c2c.dart | 44 +++-- lib/src/services/group.services.dart | 171 ++++++++++++++++-- lib/src/views/camera/share_image_view.dart | 3 +- .../chat_list_components/group_list_item.dart | 42 +++-- lib/src/views/chats/chat_messages.view.dart | 99 +++++----- .../chat_group_action.dart | 10 +- lib/src/views/groups/group.view.dart | 18 +- .../group_create_select_members.view.dart | 84 ++++++--- .../views/groups/group_member.context.dart | 110 ++++++----- 15 files changed, 463 insertions(+), 176 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index b1da33f..96ed572 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -46,8 +46,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return _insertGroup(group); } - Future insertGroupMember(GroupMembersCompanion members) async { - await into(groupMembers).insert(members); + Future insertOrUpdateGroupMember(GroupMembersCompanion members) async { + await into(groupMembers).insertOnConflictUpdate(members); } Future insertGroupAction(GroupHistoriesCompanion action) async { @@ -159,8 +159,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .watch(); } - Stream> watchGroups() { - return select(groups).watch(); + Stream> watchGroupsForShareImage() { + return (select(groups)..where((g) => g.leftGroup.equals(false))).watch(); } Stream watchGroup(String groupId) { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 16565cd..e667679 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -377,5 +377,8 @@ "revokeAdminRightsOkBtn": "Als Admin entfernen", "makeAdminRightsTitle": "{username} zum Admin machen?", "makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.", - "makeAdminRightsOkBtn": "Zum Admin machen" + "makeAdminRightsOkBtn": "Zum Admin machen", + "updateGroup": "Gruppe aktualisieren", + "alreadyInGroup": "Bereits Mitglied", + "removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index e0e3870..c663ba8 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -533,5 +533,8 @@ "revokeAdminRightsOkBtn": "Remove as admin", "makeAdminRightsTitle": "Make {username} an admin?", "makeAdminRightsBody": "{username} will be able to edit this group and its members.", - "makeAdminRightsOkBtn": "Make admin" + "makeAdminRightsOkBtn": "Make admin", + "updateGroup": "Update group", + "alreadyInGroup": "Already in Group", + "removeContactFromGroupTitle": "Remove {username} from this group?" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 7bac4bb..ec7eb68 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2305,6 +2305,24 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Make admin'** String get makeAdminRightsOkBtn; + + /// No description provided for @updateGroup. + /// + /// In en, this message translates to: + /// **'Update group'** + String get updateGroup; + + /// No description provided for @alreadyInGroup. + /// + /// In en, this message translates to: + /// **'Already in Group'** + String get alreadyInGroup; + + /// No description provided for @removeContactFromGroupTitle. + /// + /// In en, this message translates to: + /// **'Remove {username} from this group?'** + String removeContactFromGroupTitle(Object username); } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index cb7374a..04e5cd3 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1225,4 +1225,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get makeAdminRightsOkBtn => 'Zum Admin machen'; + + @override + String get updateGroup => 'Gruppe aktualisieren'; + + @override + String get alreadyInGroup => 'Bereits Mitglied'; + + @override + String removeContactFromGroupTitle(Object username) { + return '$username aus dieser Gruppe entfernen?'; + } } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 99ccfc3..169b063 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1218,4 +1218,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get makeAdminRightsOkBtn => 'Make admin'; + + @override + String get updateGroup => 'Update group'; + + @override + String get alreadyInGroup => 'Already in Group'; + + @override + String removeContactFromGroupTitle(Object username) { + return 'Remove $username from this group?'; + } } diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index 2bba307..5eb5399 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -33,17 +33,33 @@ Future handleGroupCreate( final myGroupKey = generateIdentityKeyPair(); - // Group state is joinedGroup -> As the current state has not yet been downloaded. - final group = await twonlyDB.groupsDao.createNewGroup( - GroupsCompanion( - groupId: Value(groupId), - stateVersionId: const Value(0), - stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), - myGroupPrivateKey: Value(myGroupKey.serialize()), - groupName: const Value(''), - joinedGroup: const Value(false), - ), - ); + var group = await twonlyDB.groupsDao.getGroup(groupId); + if (group == null) { + // Group state is joinedGroup -> As the current state has not yet been downloaded. + group = await twonlyDB.groupsDao.createNewGroup( + GroupsCompanion( + groupId: Value(groupId), + stateVersionId: const Value(0), + stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), + myGroupPrivateKey: Value(myGroupKey.serialize()), + groupName: const Value(''), + joinedGroup: const Value(false), + ), + ); + } else { + // User was already in the group, so update leftGroup back to false + await twonlyDB.groupsDao.updateGroup( + groupId, + GroupsCompanion( + stateVersionId: const Value(0), + stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), + myGroupPrivateKey: Value(myGroupKey.serialize()), + groupName: const Value(''), + joinedGroup: const Value(false), + leftGroup: const Value(false), + ), + ); + } if (group == null) { Log.error( @@ -61,7 +77,7 @@ Future handleGroupCreate( ), ); - await twonlyDB.groupsDao.insertGroupMember( + await twonlyDB.groupsDao.insertOrUpdateGroupMember( GroupMembersCompanion( groupId: Value(groupId), contactId: Value(fromUserId), @@ -120,6 +136,10 @@ Future handleGroupUpdate( if (affectedContactId == gUser.userId) { affectedContactId = null; + if (actionType == GroupActionType.removedMember) { + // Oh no, I just got removed from the group... + // This state is handle this case in the fetchGroupState.... + } } await twonlyDB.groupsDao.insertGroupAction( diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 9f7d25a..9d24c39 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; - import 'package:collection/collection.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; @@ -111,7 +110,7 @@ Future createNewGroup(String groupName, List members) async { Log.info('Created new group: ${group.groupId}'); for (final member in members) { - await twonlyDB.groupsDao.insertGroupMember( + await twonlyDB.groupsDao.insertOrUpdateGroupMember( GroupMembersCompanion( groupId: Value(group.groupId), contactId: Value(member.userId), @@ -151,6 +150,12 @@ Future fetchGroupStatesForUnjoinedGroups() async { } Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { + if (group.leftGroup) { + Log.error( + 'Could not refresh group state, as user is no longer part of the group', + ); + return null; + } try { var isSuccess = true; @@ -191,6 +196,18 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { Log.info( 'Group ${group.groupId} has already newest group state from the server!', ); + // return (groupStateServer.versionId.toInt(), encryptedGroupState); + } + + if (!encryptedGroupState.memberIds.contains(Int64(gUser.userId))) { + // OH no, I am no longer a member of this group... + // -> + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + leftGroup: Value(true), + ), + ); return (groupStateServer.versionId.toInt(), encryptedGroupState); } @@ -236,7 +253,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { } if (inContacts) { // User is already a contact, so just add him to the group members list - await twonlyDB.groupsDao.insertGroupMember( + await twonlyDB.groupsDao.insertOrUpdateGroupMember( GroupMembersCompanion( groupId: Value(group.groupId), contactId: Value(memberId.toInt()), @@ -318,7 +335,7 @@ Future addNewHiddenContact(int contactId) async { return true; } -Future updateGroupState( +Future _updateGroupState( Group group, EncryptedGroupState state, { Uint8List? addAdmin, @@ -394,9 +411,7 @@ Future updateGroupState( return false; } } - - // Update database to the newest state - return (await fetchGroupState(group)) != null; + return true; } Future manageAdminState( @@ -439,7 +454,7 @@ Future manageAdminState( } // send new state to the server - if (!await updateGroupState( + if (!await _updateGroupState( group, state, addAdmin: addAdmin, @@ -469,7 +484,8 @@ Future manageAdminState( ), ); - return true; + // Updates the memberState :) + return (await fetchGroupState(group)) != null; } Future updateGroupeName(Group group, String groupName) async { @@ -481,7 +497,7 @@ Future updateGroupeName(Group group, String groupName) async { state.groupName = groupName; // send new state to the server - if (!await updateGroupState(group, state)) { + if (!await _updateGroupState(group, state)) { return false; } @@ -504,5 +520,138 @@ Future updateGroupeName(Group group, String groupName) async { ), ); - return true; + // Updates the groupName :) + return (await fetchGroupState(group)) != null; +} + +Future addNewGroupMembers( + Group group, + List newGroupMemberIds, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + var memberIds = state.memberIds + newGroupMemberIds.map(Int64.new).toList(); + memberIds = memberIds.toSet().toList(); + + final newState = EncryptedGroupState( + groupName: state.groupName, + deleteMessagesAfterMilliseconds: state.deleteMessagesAfterMilliseconds, + memberIds: memberIds, + adminIds: state.adminIds, + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + + // send new state to the server + if (!await _updateGroupState(group, newState)) { + return false; + } + + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + for (final newMember in newGroupMemberIds) { + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.addMember.name, + affectedContactId: Int64(newMember), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.addMember), + affectedContactId: Value(newMember), + ), + ); + + await sendCipherText( + newMember, + EncryptedContent( + groupId: group.groupId, + groupCreate: EncryptedContent_GroupCreate( + stateKey: group.stateEncryptionKey, + groupPublicKey: keyPair.getPublicKey().serialize(), + ), + ), + ); + } + + // Updates the groupMembers table :) + return (await fetchGroupState(group)) != null; +} + +Future removeMemberFromGroup( + Group group, + GroupMember member, + int removeContactId, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + final contactId = Int64(removeContactId); + + final membersIdSet = state.memberIds.toSet(); + final adminIdSet = state.adminIds.toSet(); + Uint8List? removeAdmin; + if (!membersIdSet.contains(contactId)) { + Log.info('User was already removed from the group!'); + return true; + } + if (adminIdSet.contains(contactId)) { + if (member.groupPublicKey == null) { + // If the admin public key is not removed, that the user could potentially still update the group state. So only + // allow the user removal, if this key is known. It is better the users can not remove the other user, then + // the he can but the other user, could still update the group state. + Log.error( + 'Could not remove user. User is admin, but groupPublicKey is unknown.', + ); + return false; + } + removeAdmin = member.groupPublicKey; + } + + membersIdSet.remove(contactId); + adminIdSet.remove(contactId); + + final newState = EncryptedGroupState( + groupName: state.groupName, + deleteMessagesAfterMilliseconds: state.deleteMessagesAfterMilliseconds, + memberIds: membersIdSet.toList(), + adminIds: adminIdSet.toList(), + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + + // send new state to the server + if (!await _updateGroupState(group, newState, removeAdmin: removeAdmin)) { + return false; + } + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.removedMember.name, + affectedContactId: Int64(removeContactId), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.removedMember), + affectedContactId: Value(removeContactId), + ), + ); + + // Updates the groupMembers table :) + return (await fetchGroupState(group)) != null; } diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 7acf01e..38fa15f 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -50,7 +50,8 @@ class _ShareImageView extends State { void initState() { super.initState(); - allGroupSub = twonlyDB.groupsDao.watchGroups().listen((allGroups) async { + allGroupSub = + twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async { setState(() { contacts = allGroups; }); diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index c903069..4b1ce6c 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -255,28 +255,30 @@ class _UserListItem extends State { }, child: AvatarIcon(group: widget.group), ), - trailing: IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - if (_hasNonOpenedMediaFile) { - return ChatMessagesView(widget.group); - } else { - return CameraSendToView(widget.group); - } + trailing: (widget.group.leftGroup) + ? null + : IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + if (_hasNonOpenedMediaFile) { + return ChatMessagesView(widget.group); + } else { + return CameraSendToView(widget.group); + } + }, + ), + ); }, + icon: FaIcon( + _hasNonOpenedMediaFile + ? FontAwesomeIcons.solidComments + : FontAwesomeIcons.camera, + color: context.color.outline.withAlpha(150), + ), ), - ); - }, - icon: FaIcon( - _hasNonOpenedMediaFile - ? FontAwesomeIcons.solidComments - : FontAwesomeIcons.camera, - color: context.color.outline.withAlpha(150), - ), - ), onTap: onTap, ), ); diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 240ac51..b6f2d06 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -452,58 +452,59 @@ class _ChatMessagesViewState extends State { ], ), ), - Padding( - padding: const EdgeInsets.only( - bottom: 30, - left: 20, - right: 20, - top: 10, - ), - child: Row( - children: [ - Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: inputTextMessageDeco(context), - ), - ), - if (currentInputText != '') - IconButton( - padding: const EdgeInsets.all(15), - icon: const FaIcon( - FontAwesomeIcons.solidPaperPlane, + if (!group.leftGroup) + Padding( + padding: const EdgeInsets.only( + bottom: 30, + left: 20, + right: 20, + top: 10, + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: newMessageController, + focusNode: textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + currentInputText = value; + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + decoration: inputTextMessageDeco(context), ), - onPressed: _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - padding: const EdgeInsets.all(15), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, ), - ], + if (currentInputText != '') + IconButton( + padding: const EdgeInsets.all(15), + icon: const FaIcon( + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + padding: const EdgeInsets.all(15), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + ], + ), ), - ), ], ), ), diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart index 0576654..3e3add2 100644 --- a/lib/src/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -48,8 +48,8 @@ class _ChatGroupActionState extends State { final affected = (affectedContact == null) ? 'you' - : "${getContactDisplayName(affectedContact!)}'s"; - final affectedR = (affectedContact == null) ? 'your' : affected; + : getContactDisplayName(affectedContact!); + final affectedR = (affectedContact == null) ? 'your' : "$affected'"; final maker = (contact == null) ? '' : getContactDisplayName(contact!); switch (widget.action.type) { @@ -64,6 +64,10 @@ class _ChatGroupActionState extends State { ? 'You have created the group.' : '$maker has created the group.'; case GroupActionType.removedMember: + icon = FontAwesomeIcons.userMinus; + text = (contact == null) + ? 'You have removed $affected from the group.' + : '$maker has removed $affected from the group.'; case GroupActionType.addMember: icon = FontAwesomeIcons.userPlus; text = (contact == null) @@ -79,7 +83,7 @@ class _ChatGroupActionState extends State { case GroupActionType.demoteToMember: icon = FontAwesomeIcons.key; text = (contact == null) - ? 'You revoked $affected admin rights.' + ? 'You revoked $affectedR admin rights.' : '$maker revoked $affectedR admin rights.'; } diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index 87bb0b4..3631f46 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/views/groups/group_member.context.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart'; @@ -81,6 +82,21 @@ class _GroupViewState extends State { } } + Future _addNewGroupMembers() async { + final selectedUserIds = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupCreateSelectMembersView(group: group), + ), + ) as List?; + if (selectedUserIds == null) return; + if (!await addNewGroupMembers(group, selectedUserIds)) { + if (mounted) { + showNetworkIssue(context); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -133,7 +149,7 @@ class _GroupViewState extends State { BetterListTile( icon: FontAwesomeIcons.plus, text: context.lang.addMember, - onTap: () => {}, + onTap: _addNewGroupMembers, ), BetterListTile( padding: const EdgeInsets.only(left: 13), diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart index 103bdde..f4ffd37 100644 --- a/lib/src/views/groups/group_create_select_members.view.dart +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -12,7 +12,8 @@ import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/groups/group_create_select_group_name.view.dart'; class GroupCreateSelectMembersView extends StatefulWidget { - const GroupCreateSelectMembersView({super.key}); + const GroupCreateSelectMembersView({this.group, super.key}); + final Group? group; @override State createState() => _StartNewChatView(); } @@ -24,6 +25,7 @@ class _StartNewChatView extends State { late StreamSubscription> contactSub; final HashSet selectedUsers = HashSet(); + final HashSet alreadyInGroup = HashSet(); @override void initState() { @@ -40,6 +42,18 @@ class _StartNewChatView extends State { }); await filterUsers(); }); + initAsync(); + } + + Future initAsync() async { + if (widget.group != null) { + final members = + await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId); + for (final member in members) { + alreadyInGroup.add(member.userId); + } + if (mounted) setState(() {}); + } } @override @@ -68,6 +82,7 @@ class _StartNewChatView extends State { } void toggleSelectedUser(int userId) { + if (alreadyInGroup.contains(userId)) return; if (!selectedUsers.contains(userId)) { selectedUsers.add(userId); } else { @@ -76,30 +91,41 @@ class _StartNewChatView extends State { setState(() {}); } + Future submitChanges() async { + if (widget.group != null) { + Navigator.pop(context, selectedUsers.toList()); + return; + } + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupCreateSelectGroupNameView( + selectedUsers: allContacts + .where((t) => selectedUsers.contains(t.userId)) + .toList(), + ), + ), + ); + } + @override Widget build(BuildContext context) { return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( appBar: AppBar( - title: Text(context.lang.selectMembers), + title: Text( + widget.group == null + ? context.lang.selectMembers + : context.lang.addMember, + ), ), floatingActionButton: FilledButton.icon( - onPressed: selectedUsers.isEmpty - ? null - : () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GroupCreateSelectGroupNameView( - selectedUsers: allContacts - .where((t) => selectedUsers.contains(t.userId)) - .toList(), - ), - ), - ); - }, - label: Text(context.lang.next), + onPressed: selectedUsers.isEmpty ? null : submitChanges, + label: Text( + widget.group == null ? context.lang.next : context.lang.updateGroup, + ), icon: const FaIcon(FontAwesomeIcons.penToSquare), ), body: SafeArea( @@ -174,12 +200,16 @@ class _StartNewChatView extends State { ), ], ), + subtitle: (alreadyInGroup.contains(user.userId)) + ? Text(context.lang.alreadyInGroup) + : null, leading: AvatarIcon( contact: user, fontSize: 13, ), trailing: Checkbox( - value: selectedUsers.contains(user.userId), + value: selectedUsers.contains(user.userId) | + alreadyInGroup.contains(user.userId), side: WidgetStateBorderSide.resolveWith( (states) { if (states.contains(WidgetState.selected)) { @@ -221,15 +251,15 @@ class _Chip extends StatelessWidget { @override Widget build(BuildContext context) { - return Chip( - key: GlobalKey(), - avatar: AvatarIcon( - contact: contact, - fontSize: 10, - ), - label: GestureDetector( - onTap: () => onTap(contact.userId), - child: Row( + return GestureDetector( + onTap: () => onTap(contact.userId), + child: Chip( + key: GlobalKey(), + avatar: AvatarIcon( + contact: contact, + fontSize: 10, + ), + label: Row( mainAxisSize: MainAxisSize.min, children: [ Text( diff --git a/lib/src/views/groups/group_member.context.dart b/lib/src/views/groups/group_member.context.dart index 1b39a69..d3b7b42 100644 --- a/lib/src/views/groups/group_member.context.dart +++ b/lib/src/views/groups/group_member.context.dart @@ -24,6 +24,67 @@ class GroupMemberContextMenu extends StatelessWidget { final Group group; final Widget child; + Future _makeContactAdmin(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.makeAdminRightsTitle(getContactDisplayName(contact)), + context.lang.makeAdminRightsBody(getContactDisplayName(contact)), + customOk: context.lang.makeAdminRightsOkBtn, + ); + if (ok) { + if (!await manageAdminState( + group, + member, + contact.userId, + false, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + + Future _removeContactAsAdmin(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.revokeAdminRightsTitle(getContactDisplayName(contact)), + '', + customOk: context.lang.revokeAdminRightsOkBtn, + ); + if (ok) { + if (!await manageAdminState( + group, + member, + contact.userId, + true, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + + Future _removeContactFromGroup(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.removeContactFromGroupTitle(getContactDisplayName(contact)), + '', + ); + if (ok) { + if (!await removeMemberFromGroup( + group, + member, + contact.userId, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + @override Widget build(BuildContext context) { return ContextMenu( @@ -61,28 +122,7 @@ class GroupMemberContextMenu extends StatelessWidget { member.memberState == MemberState.normal) ContextMenuItem( title: context.lang.makeAdmin, - onTap: () async { - final ok = await showAlertDialog( - context, - context.lang - .makeAdminRightsTitle(getContactDisplayName(contact)), - context.lang - .makeAdminRightsBody(getContactDisplayName(contact)), - customOk: context.lang.makeAdminRightsOkBtn, - ); - if (ok) { - if (!await manageAdminState( - group, - member, - contact.userId, - false, - )) { - if (context.mounted) { - showNetworkIssue(context); - } - } - } - }, + onTap: () => _makeContactAdmin(context), icon: FontAwesomeIcons.key, ), if (member.groupPublicKey != null && @@ -90,35 +130,13 @@ class GroupMemberContextMenu extends StatelessWidget { member.memberState == MemberState.admin) ContextMenuItem( title: context.lang.removeAdmin, - onTap: () async { - final ok = await showAlertDialog( - context, - context.lang - .revokeAdminRightsTitle(getContactDisplayName(contact)), - '', - customOk: context.lang.revokeAdminRightsOkBtn, - ); - if (ok) { - if (!await manageAdminState( - group, - member, - contact.userId, - true, - )) { - if (context.mounted) { - showNetworkIssue(context); - } - } - } - }, + onTap: () => _removeContactAsAdmin(context), icon: FontAwesomeIcons.key, ), if (group.isGroupAdmin) ContextMenuItem( title: context.lang.removeFromGroup, - onTap: () async { - // onResponseTriggered(); - }, + onTap: () => _removeContactFromGroup(context), icon: FontAwesomeIcons.rightFromBracket, ), ],