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.
This commit is contained in:
Jakob Meier 2023-03-24 21:10:42 +01:00
parent ecd87cf2cd
commit 6b98c696ea
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
4 changed files with 378 additions and 12 deletions

View file

@ -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';
}
}

View file

@ -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() {

View file

@ -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<StatefulWidget> createState() => _ManageRoomMembersPageState();
}
class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
List<RoomMember> 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<RoomMember> list = body['data'].map<RoomMember>((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,
),
);
}
}

View file

@ -10,6 +10,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
bool Function()? onNetworkErr,
bool Function()? onUnknownErr,
bool Function()? onUserErr,
Function()? onAnyErr,
Function()? after,
bool Function(Map<String, dynamic>)? onServerErr,
bool needUser = true}) async {
@ -35,6 +36,9 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
} 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();
}