mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:58:40 +00:00
make and remove admin rights
This commit is contained in:
parent
30086c2475
commit
8cf57224ce
9 changed files with 260 additions and 28 deletions
|
|
@ -372,5 +372,10 @@
|
|||
"makeAdmin": "Zum Admin machen",
|
||||
"removeAdmin": "Als Admin entfernen",
|
||||
"removeFromGroup": "Aus Gruppe entfernen",
|
||||
"admin": "Admin"
|
||||
"admin": "Admin",
|
||||
"revokeAdminRightsTitle": "Adminrechte von {username} entfernen?",
|
||||
"revokeAdminRightsOkBtn": "Als Admin entfernen",
|
||||
"makeAdminRightsTitle": "{username} zum Admin machen?",
|
||||
"makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.",
|
||||
"makeAdminRightsOkBtn": "Zum Admin machen"
|
||||
}
|
||||
|
|
@ -528,5 +528,10 @@
|
|||
"makeAdmin": "Make admin",
|
||||
"removeAdmin": "Remove as admin",
|
||||
"removeFromGroup": "Remove from group",
|
||||
"admin": "Admin"
|
||||
"admin": "Admin",
|
||||
"revokeAdminRightsTitle": "Revoke {username}'s admin rights?",
|
||||
"revokeAdminRightsOkBtn": "Remove as admin",
|
||||
"makeAdminRightsTitle": "Make {username} an admin?",
|
||||
"makeAdminRightsBody": "{username} will be able to edit this group and its members.",
|
||||
"makeAdminRightsOkBtn": "Make admin"
|
||||
}
|
||||
|
|
@ -2275,6 +2275,36 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Admin'**
|
||||
String get admin;
|
||||
|
||||
/// No description provided for @revokeAdminRightsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Revoke {username}\'s admin rights?'**
|
||||
String revokeAdminRightsTitle(Object username);
|
||||
|
||||
/// No description provided for @revokeAdminRightsOkBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove as admin'**
|
||||
String get revokeAdminRightsOkBtn;
|
||||
|
||||
/// No description provided for @makeAdminRightsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Make {username} an admin?'**
|
||||
String makeAdminRightsTitle(Object username);
|
||||
|
||||
/// No description provided for @makeAdminRightsBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{username} will be able to edit this group and its members.'**
|
||||
String makeAdminRightsBody(Object username);
|
||||
|
||||
/// No description provided for @makeAdminRightsOkBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Make admin'**
|
||||
String get makeAdminRightsOkBtn;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -1204,4 +1204,25 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get admin => 'Admin';
|
||||
|
||||
@override
|
||||
String revokeAdminRightsTitle(Object username) {
|
||||
return 'Adminrechte von $username entfernen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get revokeAdminRightsOkBtn => 'Als Admin entfernen';
|
||||
|
||||
@override
|
||||
String makeAdminRightsTitle(Object username) {
|
||||
return '$username zum Admin machen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String makeAdminRightsBody(Object username) {
|
||||
return '$username wird diese Gruppe und ihre Mitglieder bearbeiten können.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get makeAdminRightsOkBtn => 'Zum Admin machen';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1197,4 +1197,25 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get admin => 'Admin';
|
||||
|
||||
@override
|
||||
String revokeAdminRightsTitle(Object username) {
|
||||
return 'Revoke $username\'s admin rights?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get revokeAdminRightsOkBtn => 'Remove as admin';
|
||||
|
||||
@override
|
||||
String makeAdminRightsTitle(Object username) {
|
||||
return 'Make $username an admin?';
|
||||
}
|
||||
|
||||
@override
|
||||
String makeAdminRightsBody(Object username) {
|
||||
return '$username will be able to edit this group and its members.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get makeAdminRightsOkBtn => 'Make admin';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +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';
|
||||
|
|
@ -317,7 +318,12 @@ Future<bool> addNewHiddenContact(int contactId) async {
|
|||
return true;
|
||||
}
|
||||
|
||||
Future<bool> updateGroupState(Group group, EncryptedGroupState state) async {
|
||||
Future<bool> updateGroupState(
|
||||
Group group,
|
||||
EncryptedGroupState state, {
|
||||
Uint8List? addAdmin,
|
||||
Uint8List? removeAdmin,
|
||||
}) async {
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
final encryptionNonce = chacha20.newNonce();
|
||||
|
||||
|
|
@ -358,6 +364,8 @@ Future<bool> updateGroupState(Group group, EncryptedGroupState state) async {
|
|||
encryptedGroupState: encryptedGroupState.writeToBuffer(),
|
||||
publicKey: keyPair.getPublicKey().serialize(),
|
||||
nonce: responseNonce.bodyBytes,
|
||||
addAdmin: addAdmin,
|
||||
removeAdmin: removeAdmin,
|
||||
);
|
||||
|
||||
final random = getRandomUint8List(32);
|
||||
|
|
@ -391,6 +399,79 @@ Future<bool> updateGroupState(Group group, EncryptedGroupState state) async {
|
|||
return (await fetchGroupState(group)) != null;
|
||||
}
|
||||
|
||||
Future<bool> manageAdminState(
|
||||
Group group,
|
||||
GroupMember member,
|
||||
int contactId,
|
||||
bool remove,
|
||||
) async {
|
||||
// ensure the latest state is used
|
||||
final currentState = await fetchGroupState(group);
|
||||
if (currentState == null) return false;
|
||||
final (versionId, state) = currentState;
|
||||
|
||||
final userId = Int64(contactId);
|
||||
|
||||
Uint8List? addAdmin;
|
||||
Uint8List? removeAdmin;
|
||||
|
||||
if (remove) {
|
||||
if (state.adminIds.contains(userId)) {
|
||||
state.adminIds.remove(userId);
|
||||
removeAdmin = member.groupPublicKey;
|
||||
} else {
|
||||
Log.info('User was already removed as admin.');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (!state.adminIds.contains(userId)) {
|
||||
state.adminIds.add(userId);
|
||||
addAdmin = member.groupPublicKey;
|
||||
} else {
|
||||
Log.info('User is already admin.');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (addAdmin == null && removeAdmin == null) {
|
||||
Log.info('User does not have a group public key.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// send new state to the server
|
||||
if (!await updateGroupState(
|
||||
group,
|
||||
state,
|
||||
addAdmin: addAdmin,
|
||||
removeAdmin: removeAdmin,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final groupActionType =
|
||||
remove ? GroupActionType.demoteToMember : GroupActionType.promoteToAdmin;
|
||||
|
||||
await sendCipherTextToGroup(
|
||||
group.groupId,
|
||||
EncryptedContent(
|
||||
groupUpdate: EncryptedContent_GroupUpdate(
|
||||
groupActionType: groupActionType.name,
|
||||
affectedContactId: Int64(contactId),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await twonlyDB.groupsDao.insertGroupAction(
|
||||
GroupHistoriesCompanion(
|
||||
groupId: Value(group.groupId),
|
||||
type: Value(groupActionType),
|
||||
affectedContactId: Value(contactId),
|
||||
),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> updateGroupeName(Group group, String groupName) async {
|
||||
// ensure the latest state is used
|
||||
final currentState = await fetchGroupState(group);
|
||||
|
|
|
|||
|
|
@ -28,13 +28,15 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
if (widget.action.contactId == null) return;
|
||||
contact =
|
||||
await twonlyDB.contactsDao.getContactById(widget.action.contactId!);
|
||||
if (widget.action.contactId != null) {
|
||||
contact =
|
||||
await twonlyDB.contactsDao.getContactById(widget.action.contactId!);
|
||||
}
|
||||
|
||||
if (widget.action.affectedContactId == null) return;
|
||||
affectedContact = await twonlyDB.contactsDao
|
||||
.getContactById(widget.action.affectedContactId!);
|
||||
if (widget.action.affectedContactId != null) {
|
||||
affectedContact = await twonlyDB.contactsDao
|
||||
.getContactById(widget.action.affectedContactId!);
|
||||
}
|
||||
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
|
@ -43,14 +45,30 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
|||
Widget build(BuildContext context) {
|
||||
var text = '';
|
||||
|
||||
if (widget.action.type == GroupActionType.updatedGroupName) {
|
||||
if (contact == null) {
|
||||
text =
|
||||
'You have changed the group name to "${widget.action.newGroupName}".';
|
||||
} else {
|
||||
text =
|
||||
'${getContactDisplayName(contact!)} has changed the group name to "${widget.action.newGroupName}".';
|
||||
}
|
||||
final affected = (affectedContact == null)
|
||||
? 'you'
|
||||
: "${getContactDisplayName(affectedContact!)}'s";
|
||||
final affectedR = (affectedContact == null) ? 'your' : affected;
|
||||
final maker = (contact == null) ? '' : getContactDisplayName(contact!);
|
||||
|
||||
switch (widget.action.type) {
|
||||
case GroupActionType.updatedGroupName:
|
||||
text = (contact == null)
|
||||
? 'You have changed the group name to "${widget.action.newGroupName}".'
|
||||
: '$maker has changed the group name to "${widget.action.newGroupName}".';
|
||||
case GroupActionType.createdGroup:
|
||||
case GroupActionType.removedMember:
|
||||
case GroupActionType.addMember:
|
||||
case GroupActionType.leftGroup:
|
||||
break;
|
||||
case GroupActionType.promoteToAdmin:
|
||||
text = (contact == null)
|
||||
? 'You made $affected an admin.'
|
||||
: '$maker made $affected an admin.';
|
||||
case GroupActionType.demoteToMember:
|
||||
text = (contact == null)
|
||||
? 'You revoked $affected admin rights.'
|
||||
: '$maker revoked $affectedR admin rights.';
|
||||
}
|
||||
|
||||
if (text == '') return Container();
|
||||
|
|
|
|||
|
|
@ -75,12 +75,7 @@ class _GroupViewState extends State<GroupView> {
|
|||
newGroupName != group.groupName) {
|
||||
if (!await updateGroupeName(group, newGroupName)) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Network issue. Try again later.'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
showNetworkIssue(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,6 +138,7 @@ class _GroupViewState extends State<GroupView> {
|
|||
BetterListTile(
|
||||
padding: const EdgeInsets.only(left: 13),
|
||||
leading: AvatarIcon(
|
||||
key: GlobalKey(),
|
||||
userData: gUser,
|
||||
fontSize: 16,
|
||||
),
|
||||
|
|
@ -159,12 +155,13 @@ class _GroupViewState extends State<GroupView> {
|
|||
),
|
||||
...members.map((member) {
|
||||
return GroupMemberContextMenu(
|
||||
group: widget.group,
|
||||
group: group,
|
||||
contact: member.$1,
|
||||
member: member.$2,
|
||||
child: BetterListTile(
|
||||
padding: const EdgeInsets.only(left: 13),
|
||||
leading: AvatarIcon(
|
||||
key: GlobalKey(),
|
||||
contact: member.$1,
|
||||
fontSize: 16,
|
||||
),
|
||||
|
|
@ -232,3 +229,12 @@ Future<String?> showGroupNameChangeDialog(
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showNetworkIssue(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Network issue. Try again later.'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/components/context_menu.component.dart';
|
||||
import 'package:twonly/src/views/groups/group.view.dart';
|
||||
|
||||
class GroupMemberContextMenu extends StatelessWidget {
|
||||
const GroupMemberContextMenu({
|
||||
|
|
@ -52,19 +56,60 @@ class GroupMemberContextMenu extends StatelessWidget {
|
|||
},
|
||||
icon: FontAwesomeIcons.userPlus,
|
||||
),
|
||||
if (group.isGroupAdmin && member.memberState == MemberState.normal)
|
||||
if (member.groupPublicKey != null &&
|
||||
group.isGroupAdmin &&
|
||||
member.memberState == MemberState.normal)
|
||||
ContextMenuItem(
|
||||
title: context.lang.makeAdmin,
|
||||
onTap: () async {
|
||||
// onResponseTriggered();
|
||||
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,
|
||||
),
|
||||
if (group.isGroupAdmin && member.memberState == MemberState.admin)
|
||||
if (member.groupPublicKey != null &&
|
||||
group.isGroupAdmin &&
|
||||
member.memberState == MemberState.admin)
|
||||
ContextMenuItem(
|
||||
title: context.lang.removeAdmin,
|
||||
onTap: () async {
|
||||
// onResponseTriggered();
|
||||
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,
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue