feat: allow admins to delete rooms
This commit is contained in:
parent
8bac7b8c51
commit
fc9c175117
6 changed files with 437 additions and 3 deletions
|
|
@ -4936,5 +4936,7 @@
|
|||
"permissions": "Permissions",
|
||||
"spaceChildPermission": "Who can add new chats and subspaces to this space",
|
||||
"addEnvironmentOverride": "Add environment override",
|
||||
"defaultOption": "Default"
|
||||
"defaultOption": "Default",
|
||||
"deleteChatDesc": "Are you sure you want to delete this chat? It will be deleted for all participants and all messages within the chat will no longer be available for practice or learning analytics.",
|
||||
"deleteSpaceDesc": "The space and any selected chats and/or subspaces will be deleted for all participants and all messages within the chat will no longer be available for practice or learning analytics. This action cannot be undone."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
|||
import 'package:fluffychat/pangea/chat_list/utils/app_version_util.dart';
|
||||
import 'package:fluffychat/pangea/chat_list/utils/chat_list_handle_space_tap.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
|
|
@ -885,7 +887,10 @@ class ChatListController extends State<ChatList>
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete_outlined,
|
||||
// #Pangea
|
||||
// Icons.delete_outlined,
|
||||
Icons.logout_outlined,
|
||||
// Pangea#
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
|
@ -900,6 +905,28 @@ class ChatListController extends State<ChatList>
|
|||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
if (room.isRoomAdmin)
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.delete,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete_outlined,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
L10n.of(context).delete,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -1021,6 +1048,37 @@ class ChatListController extends State<ChatList>
|
|||
},
|
||||
);
|
||||
return;
|
||||
case ChatContextAction.delete:
|
||||
if (room.isSpace) {
|
||||
final resp = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (_) => DeleteSpaceDialog(space: room),
|
||||
);
|
||||
if (resp == true && mounted) {
|
||||
context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
} else {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).delete,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
isDestructive: true,
|
||||
message: room.isSpace
|
||||
? L10n.of(context).deleteSpaceDesc
|
||||
: L10n.of(context).deleteChatDesc,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.delete,
|
||||
);
|
||||
if (resp.isError) return;
|
||||
if (mounted) context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
return;
|
||||
// Pangea#
|
||||
case ChatContextAction.block:
|
||||
final userId =
|
||||
|
|
@ -1295,5 +1353,6 @@ enum ChatContextAction {
|
|||
block,
|
||||
// #Pangea
|
||||
removeFromSpace,
|
||||
delete,
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/class_name_header.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/download_space_analytics_button.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/room_capacity_button.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/visibility_toggle.dart';
|
||||
|
|
@ -426,6 +428,58 @@ class PangeaChatDetailsView extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
Divider(color: theme.dividerColor, height: 1),
|
||||
if (room.isRoomAdmin)
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).delete,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor: iconColor,
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
if (room.isSpace) {
|
||||
final resp = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
DeleteSpaceDialog(space: room),
|
||||
);
|
||||
|
||||
if (resp == true) {
|
||||
context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
} else {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).delete,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
isDestructive: true,
|
||||
message: room.isSpace
|
||||
? L10n.of(context).deleteSpaceDesc
|
||||
: L10n.of(context).deleteChatDesc,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.delete,
|
||||
);
|
||||
if (resp.isError) return;
|
||||
context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
},
|
||||
),
|
||||
Divider(color: theme.dividerColor, height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context).countParticipants(
|
||||
|
|
|
|||
59
lib/pangea/chat_settings/utils/delete_room.dart
Normal file
59
lib/pangea/chat_settings/utils/delete_room.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/matrix_api_lite/generated/api.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
|
||||
|
||||
extension on Api {
|
||||
// Send a POST request to /_synapse/client/pangea/v1/delete_room with JSON body {room_id: string}.
|
||||
// Response 200 OK format: { message: "Deleted" }.
|
||||
// Requester must be member of the room and have the highest power level of the room to perform this request.
|
||||
Future<void> delete(String roomId) async {
|
||||
final requestUri = Uri(
|
||||
path: '_synapse/client/pangea/v1/delete_room',
|
||||
);
|
||||
final request = Request('POST', baseUri!.resolveUri(requestUri));
|
||||
request.headers['content-type'] = 'application/json';
|
||||
request.headers['authorization'] = 'Bearer ${bearerToken!}';
|
||||
request.bodyBytes = utf8.encode(
|
||||
jsonEncode({
|
||||
'room_id': roomId,
|
||||
}),
|
||||
);
|
||||
final response = await httpClient.send(request);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('http error response');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DeleteRoom on Room {
|
||||
Future<void> delete() async {
|
||||
await client.delete(id);
|
||||
}
|
||||
|
||||
Future<List<SpaceRoomsChunk>> getSpaceChildrenToDelete() async {
|
||||
final List<SpaceRoomsChunk> rooms = [];
|
||||
String? nextBatch;
|
||||
int calls = 0;
|
||||
|
||||
while ((nextBatch != null || calls == 0) && calls < 10) {
|
||||
final resp = await client.getSpaceHierarchy(
|
||||
id,
|
||||
from: nextBatch,
|
||||
limit: 100,
|
||||
);
|
||||
rooms.addAll(resp.rooms);
|
||||
nextBatch = resp.nextBatch;
|
||||
calls++;
|
||||
}
|
||||
|
||||
return rooms
|
||||
.where(
|
||||
(r) => r.roomType != PangeaRoomTypes.analytics && r.roomId != id,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
260
lib/pangea/chat_settings/widgets/delete_space_dialog.dart
Normal file
260
lib/pangea/chat_settings/widgets/delete_space_dialog.dart
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
|
||||
class DeleteSpaceDialog extends StatefulWidget {
|
||||
final Room space;
|
||||
const DeleteSpaceDialog({
|
||||
super.key,
|
||||
required this.space,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DeleteSpaceDialog> createState() => DeleteSpaceDialogState();
|
||||
}
|
||||
|
||||
class DeleteSpaceDialogState extends State<DeleteSpaceDialog> {
|
||||
List<SpaceRoomsChunk> _rooms = [];
|
||||
final List<SpaceRoomsChunk> _roomsToDelete = [];
|
||||
|
||||
bool _loadingRooms = true;
|
||||
String? _roomLoadError;
|
||||
|
||||
bool _deleting = false;
|
||||
String? _deleteError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getSpaceChildrenToDelete();
|
||||
}
|
||||
|
||||
Future<void> _getSpaceChildrenToDelete() async {
|
||||
setState(() {
|
||||
_loadingRooms = true;
|
||||
_roomLoadError = null;
|
||||
});
|
||||
|
||||
try {
|
||||
_rooms = await widget.space.getSpaceChildrenToDelete();
|
||||
} catch (e, s) {
|
||||
_roomLoadError = L10n.of(context).oopsSomethingWentWrong;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": widget.space.id,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
_loadingRooms = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onRoomSelected(
|
||||
bool? selected,
|
||||
SpaceRoomsChunk room,
|
||||
) {
|
||||
if (selected == null ||
|
||||
(selected && _roomsToDelete.contains(room)) ||
|
||||
(!selected && !_roomsToDelete.contains(room))) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
selected ? _roomsToDelete.add(room) : _roomsToDelete.remove(room);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteSpace() async {
|
||||
setState(() {
|
||||
_deleting = true;
|
||||
_deleteError = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final List<Future<void>> deleteFutures = [];
|
||||
for (final room in _roomsToDelete) {
|
||||
final roomInstance = widget.space.client.getRoomById(room.roomId);
|
||||
if (roomInstance != null) {
|
||||
deleteFutures.add(roomInstance.delete());
|
||||
}
|
||||
}
|
||||
await Future.wait(deleteFutures);
|
||||
await widget.space.delete();
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (e, s) {
|
||||
_deleteError = L10n.of(context).oopsSomethingWentWrong;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": widget.space.id,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_deleting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 400,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).areYouSure,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
L10n.of(context).deleteSpaceDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 300,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (_loadingRooms) {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_roomLoadError != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _rooms.length,
|
||||
itemBuilder: (context, index) {
|
||||
final chunk = _rooms[index];
|
||||
|
||||
final room =
|
||||
widget.space.client.getRoomById(chunk.roomId);
|
||||
final isMember = room != null &&
|
||||
room.membership == Membership.join &&
|
||||
room.isRoomAdmin;
|
||||
|
||||
final displayname = chunk.name ??
|
||||
chunk.canonicalAlias ??
|
||||
L10n.of(context).emptyChat;
|
||||
|
||||
return AnimatedOpacity(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
opacity: isMember ? 1 : 0.5,
|
||||
child: CheckboxListTile(
|
||||
value: _roomsToDelete.contains(chunk),
|
||||
onChanged: isMember
|
||||
? (value) => _onRoomSelected(value, chunk)
|
||||
: null,
|
||||
title: Text(displayname),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 16.0, 8.0, 8.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: OutlinedButton(
|
||||
onPressed: _deleting ? null : _deleteSpace,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
side: BorderSide(
|
||||
color: _deleting
|
||||
? Theme.of(context).disabledColor
|
||||
: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
child: _deleting
|
||||
? const SizedBox(
|
||||
height: 10,
|
||||
width: 100,
|
||||
child: LinearProgressIndicator(),
|
||||
)
|
||||
: Text(L10n.of(context).delete),
|
||||
),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: Text(L10n.of(context).cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: _deleteError != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ class PracticeSelectionRepo {
|
|||
}
|
||||
|
||||
static void clean() {
|
||||
final Iterable<String> keys = _storage.getKeys();
|
||||
final keys = _storage.getKeys();
|
||||
if (keys.length > 300) {
|
||||
final entries = keys
|
||||
.map((key) => _parsePracticeSelection(key))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue