mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
remove and add members #277
This commit is contained in:
parent
d616e08dec
commit
4bc7db75e9
15 changed files with 463 additions and 176 deletions
|
|
@ -46,8 +46,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
return _insertGroup(group);
|
||||
}
|
||||
|
||||
Future<void> insertGroupMember(GroupMembersCompanion members) async {
|
||||
await into(groupMembers).insert(members);
|
||||
Future<void> insertOrUpdateGroupMember(GroupMembersCompanion members) async {
|
||||
await into(groupMembers).insertOnConflictUpdate(members);
|
||||
}
|
||||
|
||||
Future<void> insertGroupAction(GroupHistoriesCompanion action) async {
|
||||
|
|
@ -159,8 +159,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<Group>> watchGroups() {
|
||||
return select(groups).watch();
|
||||
Stream<List<Group>> watchGroupsForShareImage() {
|
||||
return (select(groups)..where((g) => g.leftGroup.equals(false))).watch();
|
||||
}
|
||||
|
||||
Stream<Group?> watchGroup(String groupId) {
|
||||
|
|
|
|||
|
|
@ -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?"
|
||||
}
|
||||
|
|
@ -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?"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ Future<void> handleGroupCreate(
|
|||
|
||||
final myGroupKey = generateIdentityKeyPair();
|
||||
|
||||
var group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||
if (group == null) {
|
||||
// Group state is joinedGroup -> As the current state has not yet been downloaded.
|
||||
final group = await twonlyDB.groupsDao.createNewGroup(
|
||||
group = await twonlyDB.groupsDao.createNewGroup(
|
||||
GroupsCompanion(
|
||||
groupId: Value(groupId),
|
||||
stateVersionId: const Value(0),
|
||||
|
|
@ -44,6 +46,20 @@ Future<void> handleGroupCreate(
|
|||
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<void> handleGroupCreate(
|
|||
),
|
||||
);
|
||||
|
||||
await twonlyDB.groupsDao.insertGroupMember(
|
||||
await twonlyDB.groupsDao.insertOrUpdateGroupMember(
|
||||
GroupMembersCompanion(
|
||||
groupId: Value(groupId),
|
||||
contactId: Value(fromUserId),
|
||||
|
|
@ -120,6 +136,10 @@ Future<void> 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(
|
||||
|
|
|
|||
|
|
@ -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<bool> createNewGroup(String groupName, List<Contact> 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<void> 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<bool> addNewHiddenContact(int contactId) async {
|
|||
return true;
|
||||
}
|
||||
|
||||
Future<bool> updateGroupState(
|
||||
Future<bool> _updateGroupState(
|
||||
Group group,
|
||||
EncryptedGroupState state, {
|
||||
Uint8List? addAdmin,
|
||||
|
|
@ -394,9 +411,7 @@ Future<bool> updateGroupState(
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update database to the newest state
|
||||
return (await fetchGroupState(group)) != null;
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> manageAdminState(
|
||||
|
|
@ -439,7 +454,7 @@ Future<bool> manageAdminState(
|
|||
}
|
||||
|
||||
// send new state to the server
|
||||
if (!await updateGroupState(
|
||||
if (!await _updateGroupState(
|
||||
group,
|
||||
state,
|
||||
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 {
|
||||
|
|
@ -481,7 +497,7 @@ Future<bool> 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<bool> updateGroupeName(Group group, String groupName) async {
|
|||
),
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
|
||||
allGroupSub = twonlyDB.groupsDao.watchGroups().listen((allGroups) async {
|
||||
allGroupSub =
|
||||
twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async {
|
||||
setState(() {
|
||||
contacts = allGroups;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -255,7 +255,9 @@ class _UserListItem extends State<GroupListItem> {
|
|||
},
|
||||
child: AvatarIcon(group: widget.group),
|
||||
),
|
||||
trailing: IconButton(
|
||||
trailing: (widget.group.leftGroup)
|
||||
? null
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -452,6 +452,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (!group.leftGroup)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 30,
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
|||
|
||||
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<ChatGroupAction> {
|
|||
? '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<ChatGroupAction> {
|
|||
case GroupActionType.demoteToMember:
|
||||
icon = FontAwesomeIcons.key;
|
||||
text = (contact == null)
|
||||
? 'You revoked $affected admin rights.'
|
||||
? 'You revoked $affectedR admin rights.'
|
||||
: '$maker revoked $affectedR admin rights.';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -133,7 +149,7 @@ class _GroupViewState extends State<GroupView> {
|
|||
BetterListTile(
|
||||
icon: FontAwesomeIcons.plus,
|
||||
text: context.lang.addMember,
|
||||
onTap: () => {},
|
||||
onTap: _addNewGroupMembers,
|
||||
),
|
||||
BetterListTile(
|
||||
padding: const EdgeInsets.only(left: 13),
|
||||
|
|
|
|||
|
|
@ -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<GroupCreateSelectMembersView> createState() => _StartNewChatView();
|
||||
}
|
||||
|
|
@ -24,6 +25,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|||
late StreamSubscription<List<Contact>> contactSub;
|
||||
|
||||
final HashSet<int> selectedUsers = HashSet();
|
||||
final HashSet<int> alreadyInGroup = HashSet();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -40,6 +42,18 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|||
});
|
||||
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
|
||||
|
|
@ -68,6 +82,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|||
}
|
||||
|
||||
void toggleSelectedUser(int userId) {
|
||||
if (alreadyInGroup.contains(userId)) return;
|
||||
if (!selectedUsers.contains(userId)) {
|
||||
selectedUsers.add(userId);
|
||||
} else {
|
||||
|
|
@ -76,18 +91,12 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.selectMembers),
|
||||
),
|
||||
floatingActionButton: FilledButton.icon(
|
||||
onPressed: selectedUsers.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
Future<void> submitChanges() async {
|
||||
if (widget.group != null) {
|
||||
Navigator.pop(context, selectedUsers.toList());
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -98,8 +107,25 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
|||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
label: Text(context.lang.next),
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.group == null
|
||||
? context.lang.selectMembers
|
||||
: context.lang.addMember,
|
||||
),
|
||||
),
|
||||
floatingActionButton: FilledButton.icon(
|
||||
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<GroupCreateSelectMembersView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
return GestureDetector(
|
||||
onTap: () => onTap(contact.userId),
|
||||
child: Chip(
|
||||
key: GlobalKey(),
|
||||
avatar: AvatarIcon(
|
||||
contact: contact,
|
||||
fontSize: 10,
|
||||
),
|
||||
label: GestureDetector(
|
||||
onTap: () => onTap(contact.userId),
|
||||
child: Row(
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,67 @@ class GroupMemberContextMenu extends StatelessWidget {
|
|||
final Group group;
|
||||
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
|
||||
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,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue