remove and add members #277

This commit is contained in:
otsmr 2025-11-02 15:48:50 +01:00
parent d616e08dec
commit 4bc7db75e9
15 changed files with 463 additions and 176 deletions

View file

@ -46,8 +46,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
return _insertGroup(group); return _insertGroup(group);
} }
Future<void> insertGroupMember(GroupMembersCompanion members) async { Future<void> insertOrUpdateGroupMember(GroupMembersCompanion members) async {
await into(groupMembers).insert(members); await into(groupMembers).insertOnConflictUpdate(members);
} }
Future<void> insertGroupAction(GroupHistoriesCompanion action) async { Future<void> insertGroupAction(GroupHistoriesCompanion action) async {
@ -159,8 +159,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.watch(); .watch();
} }
Stream<List<Group>> watchGroups() { Stream<List<Group>> watchGroupsForShareImage() {
return select(groups).watch(); return (select(groups)..where((g) => g.leftGroup.equals(false))).watch();
} }
Stream<Group?> watchGroup(String groupId) { Stream<Group?> watchGroup(String groupId) {

View file

@ -377,5 +377,8 @@
"revokeAdminRightsOkBtn": "Als Admin entfernen", "revokeAdminRightsOkBtn": "Als Admin entfernen",
"makeAdminRightsTitle": "{username} zum Admin machen?", "makeAdminRightsTitle": "{username} zum Admin machen?",
"makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.", "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?"
} }

View file

@ -533,5 +533,8 @@
"revokeAdminRightsOkBtn": "Remove as admin", "revokeAdminRightsOkBtn": "Remove as admin",
"makeAdminRightsTitle": "Make {username} an admin?", "makeAdminRightsTitle": "Make {username} an admin?",
"makeAdminRightsBody": "{username} will be able to edit this group and its members.", "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?"
} }

View file

@ -2305,6 +2305,24 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Make admin'** /// **'Make admin'**
String get makeAdminRightsOkBtn; 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 class _AppLocalizationsDelegate

View file

@ -1225,4 +1225,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get makeAdminRightsOkBtn => 'Zum Admin machen'; 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?';
}
} }

View file

@ -1218,4 +1218,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get makeAdminRightsOkBtn => 'Make admin'; 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?';
}
} }

View file

@ -33,17 +33,33 @@ Future<void> handleGroupCreate(
final myGroupKey = generateIdentityKeyPair(); final myGroupKey = generateIdentityKeyPair();
// Group state is joinedGroup -> As the current state has not yet been downloaded. var group = await twonlyDB.groupsDao.getGroup(groupId);
final group = await twonlyDB.groupsDao.createNewGroup( if (group == null) {
GroupsCompanion( // Group state is joinedGroup -> As the current state has not yet been downloaded.
groupId: Value(groupId), group = await twonlyDB.groupsDao.createNewGroup(
stateVersionId: const Value(0), GroupsCompanion(
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), groupId: Value(groupId),
myGroupPrivateKey: Value(myGroupKey.serialize()), stateVersionId: const Value(0),
groupName: const Value(''), stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
joinedGroup: const Value(false), 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) { if (group == null) {
Log.error( Log.error(
@ -61,7 +77,7 @@ Future<void> handleGroupCreate(
), ),
); );
await twonlyDB.groupsDao.insertGroupMember( await twonlyDB.groupsDao.insertOrUpdateGroupMember(
GroupMembersCompanion( GroupMembersCompanion(
groupId: Value(groupId), groupId: Value(groupId),
contactId: Value(fromUserId), contactId: Value(fromUserId),
@ -120,6 +136,10 @@ Future<void> handleGroupUpdate(
if (affectedContactId == gUser.userId) { if (affectedContactId == gUser.userId) {
affectedContactId = null; 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( await twonlyDB.groupsDao.insertGroupAction(

View file

@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
@ -111,7 +110,7 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
Log.info('Created new group: ${group.groupId}'); Log.info('Created new group: ${group.groupId}');
for (final member in members) { for (final member in members) {
await twonlyDB.groupsDao.insertGroupMember( await twonlyDB.groupsDao.insertOrUpdateGroupMember(
GroupMembersCompanion( GroupMembersCompanion(
groupId: Value(group.groupId), groupId: Value(group.groupId),
contactId: Value(member.userId), contactId: Value(member.userId),
@ -151,6 +150,12 @@ Future<void> fetchGroupStatesForUnjoinedGroups() async {
} }
Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) 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 { try {
var isSuccess = true; var isSuccess = true;
@ -191,6 +196,18 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
Log.info( Log.info(
'Group ${group.groupId} has already newest group state from the server!', '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); return (groupStateServer.versionId.toInt(), encryptedGroupState);
} }
@ -236,7 +253,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
} }
if (inContacts) { if (inContacts) {
// User is already a contact, so just add him to the group members list // User is already a contact, so just add him to the group members list
await twonlyDB.groupsDao.insertGroupMember( await twonlyDB.groupsDao.insertOrUpdateGroupMember(
GroupMembersCompanion( GroupMembersCompanion(
groupId: Value(group.groupId), groupId: Value(group.groupId),
contactId: Value(memberId.toInt()), contactId: Value(memberId.toInt()),
@ -318,7 +335,7 @@ Future<bool> addNewHiddenContact(int contactId) async {
return true; return true;
} }
Future<bool> updateGroupState( Future<bool> _updateGroupState(
Group group, Group group,
EncryptedGroupState state, { EncryptedGroupState state, {
Uint8List? addAdmin, Uint8List? addAdmin,
@ -394,9 +411,7 @@ Future<bool> updateGroupState(
return false; return false;
} }
} }
return true;
// Update database to the newest state
return (await fetchGroupState(group)) != null;
} }
Future<bool> manageAdminState( Future<bool> manageAdminState(
@ -439,7 +454,7 @@ Future<bool> manageAdminState(
} }
// send new state to the server // send new state to the server
if (!await updateGroupState( if (!await _updateGroupState(
group, group,
state, state,
addAdmin: addAdmin, addAdmin: addAdmin,
@ -469,7 +484,8 @@ Future<bool> manageAdminState(
), ),
); );
return true; // Updates the memberState :)
return (await fetchGroupState(group)) != null;
} }
Future<bool> updateGroupeName(Group group, String groupName) async { Future<bool> updateGroupeName(Group group, String groupName) async {
@ -481,7 +497,7 @@ Future<bool> updateGroupeName(Group group, String groupName) async {
state.groupName = groupName; state.groupName = groupName;
// send new state to the server // send new state to the server
if (!await updateGroupState(group, state)) { if (!await _updateGroupState(group, state)) {
return false; return false;
} }
@ -504,5 +520,138 @@ Future<bool> updateGroupeName(Group group, String groupName) async {
), ),
); );
return true; // Updates the groupName :)
return (await fetchGroupState(group)) != null;
}
Future<bool> addNewGroupMembers(
Group group,
List<int> 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<int>.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<bool> 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<int>.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;
} }

View file

@ -50,7 +50,8 @@ class _ShareImageView extends State<ShareImageView> {
void initState() { void initState() {
super.initState(); super.initState();
allGroupSub = twonlyDB.groupsDao.watchGroups().listen((allGroups) async { allGroupSub =
twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async {
setState(() { setState(() {
contacts = allGroups; contacts = allGroups;
}); });

View file

@ -255,28 +255,30 @@ class _UserListItem extends State<GroupListItem> {
}, },
child: AvatarIcon(group: widget.group), child: AvatarIcon(group: widget.group),
), ),
trailing: IconButton( trailing: (widget.group.leftGroup)
onPressed: () { ? null
Navigator.push( : IconButton(
context, onPressed: () {
MaterialPageRoute( Navigator.push(
builder: (context) { context,
if (_hasNonOpenedMediaFile) { MaterialPageRoute(
return ChatMessagesView(widget.group); builder: (context) {
} else { if (_hasNonOpenedMediaFile) {
return CameraSendToView(widget.group); 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, onTap: onTap,
), ),
); );

View file

@ -452,58 +452,59 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
], ],
), ),
), ),
Padding( if (!group.leftGroup)
padding: const EdgeInsets.only( Padding(
bottom: 30, padding: const EdgeInsets.only(
left: 20, bottom: 30,
right: 20, left: 20,
top: 10, right: 20,
), top: 10,
child: Row( ),
children: [ child: Row(
Expanded( children: [
child: TextField( Expanded(
controller: newMessageController, child: TextField(
focusNode: textFieldFocus, controller: newMessageController,
keyboardType: TextInputType.multiline, focusNode: textFieldFocus,
maxLines: 4, keyboardType: TextInputType.multiline,
minLines: 1, maxLines: 4,
onChanged: (value) { minLines: 1,
currentInputText = value; onChanged: (value) {
setState(() {}); currentInputText = value;
}, setState(() {});
onSubmitted: (_) { },
_sendMessage(); onSubmitted: (_) {
}, _sendMessage();
decoration: inputTextMessageDeco(context), },
), decoration: inputTextMessageDeco(context),
),
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);
},
),
);
},
), ),
], 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);
},
),
);
},
),
],
),
), ),
),
], ],
), ),
), ),

View file

@ -48,8 +48,8 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
final affected = (affectedContact == null) final affected = (affectedContact == null)
? 'you' ? 'you'
: "${getContactDisplayName(affectedContact!)}'s"; : getContactDisplayName(affectedContact!);
final affectedR = (affectedContact == null) ? 'your' : affected; final affectedR = (affectedContact == null) ? 'your' : "$affected'";
final maker = (contact == null) ? '' : getContactDisplayName(contact!); final maker = (contact == null) ? '' : getContactDisplayName(contact!);
switch (widget.action.type) { switch (widget.action.type) {
@ -64,6 +64,10 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
? 'You have created the group.' ? 'You have created the group.'
: '$maker has created the group.'; : '$maker has created the group.';
case GroupActionType.removedMember: 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: case GroupActionType.addMember:
icon = FontAwesomeIcons.userPlus; icon = FontAwesomeIcons.userPlus;
text = (contact == null) text = (contact == null)
@ -79,7 +83,7 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
case GroupActionType.demoteToMember: case GroupActionType.demoteToMember:
icon = FontAwesomeIcons.key; icon = FontAwesomeIcons.key;
text = (contact == null) text = (contact == null)
? 'You revoked $affected admin rights.' ? 'You revoked $affectedR admin rights.'
: '$maker revoked $affectedR admin rights.'; : '$maker revoked $affectedR admin rights.';
} }

View file

@ -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/better_list_title.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact.view.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/groups/group_member.context.dart';
import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart';
@ -81,6 +82,21 @@ class _GroupViewState extends State<GroupView> {
} }
} }
Future<void> _addNewGroupMembers() async {
final selectedUserIds = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GroupCreateSelectMembersView(group: group),
),
) as List<int>?;
if (selectedUserIds == null) return;
if (!await addNewGroupMembers(group, selectedUserIds)) {
if (mounted) {
showNetworkIssue(context);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -133,7 +149,7 @@ class _GroupViewState extends State<GroupView> {
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.plus, icon: FontAwesomeIcons.plus,
text: context.lang.addMember, text: context.lang.addMember,
onTap: () => {}, onTap: _addNewGroupMembers,
), ),
BetterListTile( BetterListTile(
padding: const EdgeInsets.only(left: 13), padding: const EdgeInsets.only(left: 13),

View file

@ -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'; import 'package:twonly/src/views/groups/group_create_select_group_name.view.dart';
class GroupCreateSelectMembersView extends StatefulWidget { class GroupCreateSelectMembersView extends StatefulWidget {
const GroupCreateSelectMembersView({super.key}); const GroupCreateSelectMembersView({this.group, super.key});
final Group? group;
@override @override
State<GroupCreateSelectMembersView> createState() => _StartNewChatView(); State<GroupCreateSelectMembersView> createState() => _StartNewChatView();
} }
@ -24,6 +25,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
late StreamSubscription<List<Contact>> contactSub; late StreamSubscription<List<Contact>> contactSub;
final HashSet<int> selectedUsers = HashSet(); final HashSet<int> selectedUsers = HashSet();
final HashSet<int> alreadyInGroup = HashSet();
@override @override
void initState() { void initState() {
@ -40,6 +42,18 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
}); });
await filterUsers(); await filterUsers();
}); });
initAsync();
}
Future<void> 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 @override
@ -68,6 +82,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
} }
void toggleSelectedUser(int userId) { void toggleSelectedUser(int userId) {
if (alreadyInGroup.contains(userId)) return;
if (!selectedUsers.contains(userId)) { if (!selectedUsers.contains(userId)) {
selectedUsers.add(userId); selectedUsers.add(userId);
} else { } else {
@ -76,30 +91,41 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
setState(() {}); setState(() {});
} }
Future<void> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.selectMembers), title: Text(
widget.group == null
? context.lang.selectMembers
: context.lang.addMember,
),
), ),
floatingActionButton: FilledButton.icon( floatingActionButton: FilledButton.icon(
onPressed: selectedUsers.isEmpty onPressed: selectedUsers.isEmpty ? null : submitChanges,
? null label: Text(
: () async { widget.group == null ? context.lang.next : context.lang.updateGroup,
await Navigator.push( ),
context,
MaterialPageRoute(
builder: (context) => GroupCreateSelectGroupNameView(
selectedUsers: allContacts
.where((t) => selectedUsers.contains(t.userId))
.toList(),
),
),
);
},
label: Text(context.lang.next),
icon: const FaIcon(FontAwesomeIcons.penToSquare), icon: const FaIcon(FontAwesomeIcons.penToSquare),
), ),
body: SafeArea( body: SafeArea(
@ -174,12 +200,16 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
), ),
], ],
), ),
subtitle: (alreadyInGroup.contains(user.userId))
? Text(context.lang.alreadyInGroup)
: null,
leading: AvatarIcon( leading: AvatarIcon(
contact: user, contact: user,
fontSize: 13, fontSize: 13,
), ),
trailing: Checkbox( trailing: Checkbox(
value: selectedUsers.contains(user.userId), value: selectedUsers.contains(user.userId) |
alreadyInGroup.contains(user.userId),
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
(states) { (states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
@ -221,15 +251,15 @@ class _Chip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Chip( return GestureDetector(
key: GlobalKey(), onTap: () => onTap(contact.userId),
avatar: AvatarIcon( child: Chip(
contact: contact, key: GlobalKey(),
fontSize: 10, avatar: AvatarIcon(
), contact: contact,
label: GestureDetector( fontSize: 10,
onTap: () => onTap(contact.userId), ),
child: Row( label: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(

View file

@ -24,6 +24,67 @@ class GroupMemberContextMenu extends StatelessWidget {
final Group group; final Group group;
final Widget child; final Widget child;
Future<void> _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<void> _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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ContextMenu( return ContextMenu(
@ -61,28 +122,7 @@ class GroupMemberContextMenu extends StatelessWidget {
member.memberState == MemberState.normal) member.memberState == MemberState.normal)
ContextMenuItem( ContextMenuItem(
title: context.lang.makeAdmin, title: context.lang.makeAdmin,
onTap: () async { onTap: () => _makeContactAdmin(context),
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);
}
}
}
},
icon: FontAwesomeIcons.key, icon: FontAwesomeIcons.key,
), ),
if (member.groupPublicKey != null && if (member.groupPublicKey != null &&
@ -90,35 +130,13 @@ class GroupMemberContextMenu extends StatelessWidget {
member.memberState == MemberState.admin) member.memberState == MemberState.admin)
ContextMenuItem( ContextMenuItem(
title: context.lang.removeAdmin, title: context.lang.removeAdmin,
onTap: () async { onTap: () => _removeContactAsAdmin(context),
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);
}
}
}
},
icon: FontAwesomeIcons.key, icon: FontAwesomeIcons.key,
), ),
if (group.isGroupAdmin) if (group.isGroupAdmin)
ContextMenuItem( ContextMenuItem(
title: context.lang.removeFromGroup, title: context.lang.removeFromGroup,
onTap: () async { onTap: () => _removeContactFromGroup(context),
// onResponseTriggered();
},
icon: FontAwesomeIcons.rightFromBracket, icon: FontAwesomeIcons.rightFromBracket,
), ),
], ],