mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:08:41 +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",
|
"makeAdmin": "Zum Admin machen",
|
||||||
"removeAdmin": "Als Admin entfernen",
|
"removeAdmin": "Als Admin entfernen",
|
||||||
"removeFromGroup": "Aus Gruppe 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",
|
"makeAdmin": "Make admin",
|
||||||
"removeAdmin": "Remove as admin",
|
"removeAdmin": "Remove as admin",
|
||||||
"removeFromGroup": "Remove from group",
|
"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:
|
/// In en, this message translates to:
|
||||||
/// **'Admin'**
|
/// **'Admin'**
|
||||||
String get 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
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -1204,4 +1204,25 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get admin => 'Admin';
|
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
|
@override
|
||||||
String get admin => 'Admin';
|
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:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
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';
|
||||||
|
|
@ -317,7 +318,12 @@ Future<bool> addNewHiddenContact(int contactId) async {
|
||||||
return true;
|
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 chacha20 = FlutterChacha20.poly1305Aead();
|
||||||
final encryptionNonce = chacha20.newNonce();
|
final encryptionNonce = chacha20.newNonce();
|
||||||
|
|
||||||
|
|
@ -358,6 +364,8 @@ Future<bool> updateGroupState(Group group, EncryptedGroupState state) async {
|
||||||
encryptedGroupState: encryptedGroupState.writeToBuffer(),
|
encryptedGroupState: encryptedGroupState.writeToBuffer(),
|
||||||
publicKey: keyPair.getPublicKey().serialize(),
|
publicKey: keyPair.getPublicKey().serialize(),
|
||||||
nonce: responseNonce.bodyBytes,
|
nonce: responseNonce.bodyBytes,
|
||||||
|
addAdmin: addAdmin,
|
||||||
|
removeAdmin: removeAdmin,
|
||||||
);
|
);
|
||||||
|
|
||||||
final random = getRandomUint8List(32);
|
final random = getRandomUint8List(32);
|
||||||
|
|
@ -391,6 +399,79 @@ Future<bool> updateGroupState(Group group, EncryptedGroupState state) async {
|
||||||
return (await fetchGroupState(group)) != null;
|
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 {
|
Future<bool> updateGroupeName(Group group, String groupName) async {
|
||||||
// ensure the latest state is used
|
// ensure the latest state is used
|
||||||
final currentState = await fetchGroupState(group);
|
final currentState = await fetchGroupState(group);
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,15 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
if (widget.action.contactId == null) return;
|
if (widget.action.contactId != null) {
|
||||||
contact =
|
contact =
|
||||||
await twonlyDB.contactsDao.getContactById(widget.action.contactId!);
|
await twonlyDB.contactsDao.getContactById(widget.action.contactId!);
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.action.affectedContactId == null) return;
|
if (widget.action.affectedContactId != null) {
|
||||||
affectedContact = await twonlyDB.contactsDao
|
affectedContact = await twonlyDB.contactsDao
|
||||||
.getContactById(widget.action.affectedContactId!);
|
.getContactById(widget.action.affectedContactId!);
|
||||||
|
}
|
||||||
|
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
|
|
@ -43,14 +45,30 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var text = '';
|
var text = '';
|
||||||
|
|
||||||
if (widget.action.type == GroupActionType.updatedGroupName) {
|
final affected = (affectedContact == null)
|
||||||
if (contact == null) {
|
? 'you'
|
||||||
text =
|
: "${getContactDisplayName(affectedContact!)}'s";
|
||||||
'You have changed the group name to "${widget.action.newGroupName}".';
|
final affectedR = (affectedContact == null) ? 'your' : affected;
|
||||||
} else {
|
final maker = (contact == null) ? '' : getContactDisplayName(contact!);
|
||||||
text =
|
|
||||||
'${getContactDisplayName(contact!)} has changed the group name to "${widget.action.newGroupName}".';
|
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();
|
if (text == '') return Container();
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,7 @@ class _GroupViewState extends State<GroupView> {
|
||||||
newGroupName != group.groupName) {
|
newGroupName != group.groupName) {
|
||||||
if (!await updateGroupeName(group, newGroupName)) {
|
if (!await updateGroupeName(group, newGroupName)) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showNetworkIssue(context);
|
||||||
const SnackBar(
|
|
||||||
content: Text('Network issue. Try again later.'),
|
|
||||||
duration: Duration(seconds: 3),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +138,7 @@ class _GroupViewState extends State<GroupView> {
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
padding: const EdgeInsets.only(left: 13),
|
padding: const EdgeInsets.only(left: 13),
|
||||||
leading: AvatarIcon(
|
leading: AvatarIcon(
|
||||||
|
key: GlobalKey(),
|
||||||
userData: gUser,
|
userData: gUser,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
|
|
@ -159,12 +155,13 @@ class _GroupViewState extends State<GroupView> {
|
||||||
),
|
),
|
||||||
...members.map((member) {
|
...members.map((member) {
|
||||||
return GroupMemberContextMenu(
|
return GroupMemberContextMenu(
|
||||||
group: widget.group,
|
group: group,
|
||||||
contact: member.$1,
|
contact: member.$1,
|
||||||
member: member.$2,
|
member: member.$2,
|
||||||
child: BetterListTile(
|
child: BetterListTile(
|
||||||
padding: const EdgeInsets.only(left: 13),
|
padding: const EdgeInsets.only(left: 13),
|
||||||
leading: AvatarIcon(
|
leading: AvatarIcon(
|
||||||
|
key: GlobalKey(),
|
||||||
contact: member.$1,
|
contact: member.$1,
|
||||||
fontSize: 16,
|
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:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/globals.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/tables/groups.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.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/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_messages.view.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/components/context_menu.component.dart';
|
||||||
|
import 'package:twonly/src/views/groups/group.view.dart';
|
||||||
|
|
||||||
class GroupMemberContextMenu extends StatelessWidget {
|
class GroupMemberContextMenu extends StatelessWidget {
|
||||||
const GroupMemberContextMenu({
|
const GroupMemberContextMenu({
|
||||||
|
|
@ -52,19 +56,60 @@ class GroupMemberContextMenu extends StatelessWidget {
|
||||||
},
|
},
|
||||||
icon: FontAwesomeIcons.userPlus,
|
icon: FontAwesomeIcons.userPlus,
|
||||||
),
|
),
|
||||||
if (group.isGroupAdmin && member.memberState == MemberState.normal)
|
if (member.groupPublicKey != null &&
|
||||||
|
group.isGroupAdmin &&
|
||||||
|
member.memberState == MemberState.normal)
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
title: context.lang.makeAdmin,
|
title: context.lang.makeAdmin,
|
||||||
onTap: () async {
|
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,
|
icon: FontAwesomeIcons.key,
|
||||||
),
|
),
|
||||||
if (group.isGroupAdmin && member.memberState == MemberState.admin)
|
if (member.groupPublicKey != null &&
|
||||||
|
group.isGroupAdmin &&
|
||||||
|
member.memberState == MemberState.admin)
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
title: context.lang.removeAdmin,
|
title: context.lang.removeAdmin,
|
||||||
onTap: () async {
|
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,
|
icon: FontAwesomeIcons.key,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue