From 6b98c696ea7d606f0e081ff377e25972f9ddf544 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 24 Mar 2023 21:10:42 +0100 Subject: [PATCH] Finishied room member screen The screen allows every room member, to view a list of other room members, and their role (Owner, Admin or Member). If the user is allowed (Owner, Admin, changeAdmin/manageMembers), to either kick or promote members to the admin role, the list tile will have a tap event. NOTE: This is why some members do not have a hover animation. For example the owner cannot be kicked. --- lib/backend/room.dart | 8 +- lib/main.dart | 8 + lib/screens/room/members.dart | 346 ++++++++++++++++++++++++++++++++++ lib/tools/fetch_wrapper.dart | 28 +-- 4 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 lib/screens/room/members.dart diff --git a/lib/backend/room.dart b/lib/backend/room.dart index 5c1687f..67f52c7 100644 --- a/lib/backend/room.dart +++ b/lib/backend/room.dart @@ -320,7 +320,13 @@ class RoomMember { factory RoomMember.fromJSON(dynamic json) { return RoomMember( - id: json['name'], serverTag: json['server'], isAdmin: json['admin']); + id: json['name'], + serverTag: json['server'], + isAdmin: json['admin'] == 1); + } + + String get humanReadableName { + return '$id@$serverTag'; } } diff --git a/lib/main.dart b/lib/main.dart index ba187b3..80457b7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/screens/room/edit.dart'; import 'package:outbag_app/screens/room/join.dart'; +import 'package:outbag_app/screens/room/members.dart'; import 'package:outbag_app/screens/room/new.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; @@ -38,6 +39,13 @@ final routesLoggedIn = RouteMap(routes: { return MaterialPage(child: EditRoomPage(server, tag)); }, + '/r/:server/:tag/members': (info) { + final server = info.pathParameters['server'] ?? ""; + final tag = info.pathParameters['tag'] ?? ""; + + return MaterialPage(child: ManageRoomMembersPage(server, tag)); + }, + }, onUnknownRoute: (_) => const Redirect('/')); void main() { diff --git a/lib/screens/room/members.dart b/lib/screens/room/members.dart new file mode 100644 index 0000000..82bc2f6 --- /dev/null +++ b/lib/screens/room/members.dart @@ -0,0 +1,346 @@ +import 'package:flutter/material.dart'; +import 'package:outbag_app/backend/permissions.dart'; +import 'package:outbag_app/backend/request.dart'; +import 'package:outbag_app/backend/room.dart'; +import 'package:outbag_app/tools/fetch_wrapper.dart'; +import 'package:routemaster/routemaster.dart'; + +class ManageRoomMembersPage extends StatefulWidget { + final String server; + final String tag; + + const ManageRoomMembersPage(this.server, this.tag, {super.key}); + + @override + State createState() => _ManageRoomMembersPageState(); +} + +class _ManageRoomMembersPageState extends State { + List list = []; + RoomInfo? info; + + void fetchUserInfo() { + final rmaster = Routemaster.of(context); + final sm = ScaffoldMessenger.of(context); + + doNetworkRequest( + sm, + req: (user) => postWithCreadentials( + path: 'getRoomInfo', + credentials: user!, + target: (user.server), + body: {'room': widget.tag, 'server': widget.server}), + onAnyErr: () { + // user should not be here + // close screen + rmaster.replace('/'); + return false; + }, + onOK: (body) async { + final info = RoomInfo.fromJSON(body['data']); + setState(() { + this.info = info; + }); + return true; + }, + ); + } + + void fetchMembers() { + final rmaster = Routemaster.of(context); + final sm = ScaffoldMessenger.of(context); + + doNetworkRequest(sm, + req: (user) => postWithCreadentials( + credentials: user!, + target: user.server, + path: 'getRoomMembers', + body: {'room': widget.tag, 'server': widget.server}), + onAnyErr: () { + // user should not be here + // close screen + rmaster.replace('/'); + return false; + }, + onOK: (body) { + final List list = body['data'].map((json) { + return RoomMember.fromJSON(json); + }).toList(); + + setState(() { + this.list = list; + }); + }); + } + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + fetchUserInfo(); + fetchMembers(); + }); + } + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context) + .textTheme + .apply(displayColor: Theme.of(context).colorScheme.onSurface); + + return Scaffold( + appBar: AppBar( + title: Text('Room Members (${list.length})'), + leading: IconButton( + onPressed: () { + // go back + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.arrow_back), + tooltip: "Go back", + ), + //actions: [ + // // NOTE: Maybe add a search icon + // // and general search functionality here + //], + ), + body: ListView.builder( + itemBuilder: (BuildContext context, int index) { + final item = list[index]; + + String role = "Member"; + if (info != null && + (info?.owner)! == item.id && + widget.server == item.serverTag) { + role = "Owner"; + } else if (item.isAdmin) { + role = "Admin"; + } + + bool enable = true; + // perform permission check + if (info == null || + !((info?.isAdmin)! || + (info?.isOwner)! || + ((info?.permissions)! & oB("1100000") != 0))) { + // NOTE: do not show error message + // user should assume, + // that it wasn't even possible + // to click on ListTile + enable = false; + } else if (info != null && + item.id == info?.owner && + widget.server == item.serverTag) { + // cannot kick admin + enable = false; + } + + return ListTile( + title: Text(item.humanReadableName), + subtitle: Text(role), + leading: const Icon(Icons.person), + onTap: !enable?null:() { + showModalBottomSheet( + context: context, + builder: (context) => BottomSheet( + onClosing: () {}, + builder: (context) => Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Text(item.humanReadableName, + style: textTheme.displaySmall)), + Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + ...((info?.isAdmin)! || + (info?.isOwner)! || + ((info?.permissions)! & + RoomPermission.changeAdmin != + 0)) + ? [ + ListTile( + leading: const Icon( + Icons.supervisor_account), + title: Text(item.isAdmin + ? 'Remove admin privileges' + : 'Make Admin'), + subtitle: Text(item.isAdmin + ? 'Revokes admin privileges from the user' + : 'Grants the user the permission to do everything'), + onTap: () { + // make user admin + showDialog( + context: context, + builder: + (context) => AlertDialog( + icon: const Icon(Icons + .supervisor_account), + title: Text(item + .isAdmin + ? 'Remove admin privileges' + : 'Make Admin'), + content: Text(item + .isAdmin + ? "Do you really want to remove ${item.humanReadableName}'s admin privileges" + : 'Do you really want to make ${item.humanReadableName} admin?'), + actions: [ + TextButton( + onPressed: () { + // close popup + // NOTE: cancel only closes the dialog + // whilst OK closes both + Navigator.of( + context) + .pop(); + }, + child: const Text( + 'Cancel'), + ), + FilledButton( + onPressed: + () async { + // send request + final scaffMgr = + ScaffoldMessenger.of( + context); + final nav = + Navigator.of( + context); + + doNetworkRequest( + scaffMgr, + req: (user) => + postWithCreadentials( + path: 'setAdminStatus', + credentials: user!, + target: user.server, + body: { + 'room': widget.tag, + 'roomServer': widget.server, + 'server': item.serverTag, + 'name': item.id, + 'admin': !item.isAdmin + }), + onOK: (_) { + fetchMembers(); + }, + after: () { + // close popup + nav.pop(); + nav.pop(); + }); + }, + child: + const Text( + 'OK'), + ) + ], + )); + }, + ) + ] + : [], + ...((info?.isAdmin)! || + (info?.isOwner)! || + ((info?.permissions)! & + RoomPermission + .manageMembers != + 0)) + ? [ + ListTile( + leading: + const Icon(Icons.person_remove), + title: const Text('Kick User'), + subtitle: const Text( + "Temporarrily remove user from server (they'll be able to join the room again)"), + onTap: () { + // remove user from room + showDialog( + context: context, + builder: + (context) => AlertDialog( + icon: const Icon(Icons + .person_remove), + title: const Text( + 'Kick User'), + content: Text( + 'Do you really want to kick ${item.humanReadableName}?'), + actions: [ + TextButton( + onPressed: () { + // close popup + // NOTE: cancel only closes the dialog + // whilst OK closes both + + Navigator.of( + context) + .pop(); + }, + child: const Text( + 'Cancel'), + ), + FilledButton( + onPressed: + () async { + // send request + final scaffMgr = + ScaffoldMessenger.of( + context); + final nav = + Navigator.of( + context); + + doNetworkRequest( + scaffMgr, + req: (user) => + postWithCreadentials( + path: 'kickMember', + credentials: user!, + target: user.server, + body: { + 'room': widget.tag, + 'roomServer': widget.server, + 'name': item.id, + 'server': item.serverTag + }), + onOK: (_) { + fetchMembers(); + }, + after: () { + // close popup + nav.pop(); + nav.pop(); + }); + }, + child: const Text( + 'Kick User'), + ) + ], + )); + }, + ) + ] + : [], + ], + ), + ), + FilledButton( + child: const Text('Close'), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + ), + )); + }, + ); + }, + itemCount: list.length, + ), + ); + } +} diff --git a/lib/tools/fetch_wrapper.dart b/lib/tools/fetch_wrapper.dart index 69bc666..5b9859d 100644 --- a/lib/tools/fetch_wrapper.dart +++ b/lib/tools/fetch_wrapper.dart @@ -5,11 +5,12 @@ import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/snackbar.dart'; void doNetworkRequest(ScaffoldMessengerState? sm, - {required Future Function(User?) req, + {required Future Function(User?) req, Function(Map)? onOK, bool Function()? onNetworkErr, bool Function()? onUnknownErr, bool Function()? onUserErr, + Function()? onAnyErr, Function()? after, bool Function(Map)? onServerErr, bool needUser = true}) async { @@ -28,13 +29,16 @@ void doNetworkRequest(ScaffoldMessengerState? sm, if (showBar && sm != null) { showSimpleSnackbar(sm, text: 'No user found', action: 'Authorize', - onTap: () { - try { - // try to remove broken user - User.removeDisk(); - } catch (_) {} + onTap: () { + try { + // try to remove broken user + User.removeDisk(); + } catch (_) {} }); } + if (onAnyErr != null) { + onAnyErr(); + } if (after != null) { after(); } @@ -56,7 +60,9 @@ void doNetworkRequest(ScaffoldMessengerState? sm, if (showBar && sm != null) { showSimpleSnackbar(sm, text: 'Network Error', action: 'Dismiss'); } - + if (onAnyErr != null) { + onAnyErr(); + } if (after != null) { after(); } @@ -75,16 +81,16 @@ void doNetworkRequest(ScaffoldMessengerState? sm, if (onServerErr != null) { showBar = onServerErr(res.body); } - if (showBar && sm != null) { showSimpleSnackbar(sm, text: errorAsString(res.body), action: 'OK'); } + if (onAnyErr != null) { + onAnyErr(); + } } - } catch (e) { + } catch (_) { bool showBar = true; - print(e); - if (onUnknownErr != null) { showBar = onUnknownErr(); }