From 9c6928d214c07f2a5d090de32162ff86ddc96793 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 18 May 2025 18:03:22 +0800 Subject: [PATCH] :sparkles: Chat room member manage --- assets/i18n/en-US.json | 7 +- lib/screens/chat/room.dart | 7 +- lib/screens/chat/room_detail.dart | 171 +++++++++++++++++++++++++++++- 3 files changed, 180 insertions(+), 5 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 75b24c4..af1fc12 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -237,5 +237,10 @@ "levelingProgress": "Leveling Progress", "levelingProgressExperience": "{} EXP", "levelingProgressLevel": "Level {}", - "fileUploadingProgress": "Uploading file #{}: {}%" + "fileUploadingProgress": "Uploading file #{}: {}%", + "removeChatMember": "Remove Chat Room Member", + "removeChatMemberHint": "Are you sure to remove this member from the room?", + "memberRole": "Member Role", + "memberRoleHint": "Greater number has higher permission.", + "memberRoleEdit": "Edit role for @{}" } diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index f3b4d0f..897db28 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -417,10 +417,11 @@ class ChatRoomScreen extends HookConsumerWidget { return Scaffold( appBar: AppBar( + toolbarHeight: 64, title: chatRoom.when( data: - (room) => Row( - spacing: 8, + (room) => Column( + spacing: 4, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -451,7 +452,7 @@ class ChatRoomScreen extends HookConsumerWidget { (room.type == 1 && room.name == null) ? room.members!.map((e) => e.account.nick).join(', ') : room.name!, - ).fontSize(19), + ).fontSize(15), ], ), loading: () => const Text('Loading...'), diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart index 64b95d2..4fde271 100644 --- a/lib/screens/chat/room_detail.dart +++ b/lib/screens/chat/room_detail.dart @@ -89,7 +89,7 @@ class ChatDetailScreen extends HookConsumerWidget { title: Text( (currentRoom.type == 1 && currentRoom.name == null) ? currentRoom.members! - .map((e) => e.account.name) + .map((e) => e.account.nick) .join(', ') : currentRoom.name!, style: TextStyle( @@ -263,6 +263,8 @@ class _ChatMemberListSheet extends HookConsumerWidget { final memberState = ref.watch(chatMemberStateProvider(roomId)); final memberNotifier = ref.read(chatMemberStateProvider(roomId).notifier); + final roomIdentity = ref.watch(chatroomIdentityProvider(roomId)); + useEffect(() { Future(() { memberNotifier.loadMore(); @@ -355,6 +357,7 @@ class _ChatMemberListSheet extends HookConsumerWidget { final member = memberState.members[index]; return ListTile( + contentPadding: EdgeInsets.only(left: 16, right: 12), leading: ProfilePictureWidget( fileId: member.account.profile.pictureId, ), @@ -379,6 +382,55 @@ class _ChatMemberListSheet extends HookConsumerWidget { Expanded(child: Text("@${member.account.name}")), ], ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if ((roomIdentity.value?.role ?? 0) >= 50) + IconButton( + icon: const Icon(Symbols.edit), + onPressed: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: + (context) => _ChatMemberRoleSheet( + roomId: roomId, + member: member, + ), + ).then((value) { + if (value != null) { + memberNotifier.reset(); + memberNotifier.loadMore(); + } + }); + }, + ), + if ((roomIdentity.value?.role ?? 0) >= 50) + IconButton( + icon: const Icon(Symbols.delete), + onPressed: () { + showConfirmAlert( + 'removeChatMemberHint'.tr(), + 'removeChatMember'.tr(), + ).then((confirm) async { + if (confirm != true) return; + try { + final apiClient = ref.watch( + apiClientProvider, + ); + await apiClient.delete( + '/chat/$roomId/members/${member.accountId}', + ); + memberNotifier.reset(); + memberNotifier.loadMore(); + } catch (err) { + showErrorAlert(err); + } + }); + }, + ), + ], + ), ); }, ), @@ -388,3 +440,120 @@ class _ChatMemberListSheet extends HookConsumerWidget { ); } } + +class _ChatMemberRoleSheet extends HookConsumerWidget { + final String roomId; + final SnChatMember member; + + const _ChatMemberRoleSheet({required this.roomId, required this.member}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final roleController = useTextEditingController( + text: member.role.toString(), + ); + + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: EdgeInsets.only( + top: 16, + left: 20, + right: 16, + bottom: 12, + ), + child: Row( + children: [ + Text( + 'memberRoleEdit'.tr(args: [member.account.name]), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: -0.5, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Symbols.close), + onPressed: () => Navigator.pop(context), + style: IconButton.styleFrom( + minimumSize: const Size(36, 36), + ), + ), + ], + ), + ), + const Divider(height: 1), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return const [100, 50, 0]; + } + final int? value = int.tryParse(textEditingValue.text); + if (value == null) return const [100, 50, 0]; + return [100, 50, 0].where( + (option) => + option.toString().contains(textEditingValue.text), + ); + }, + onSelected: (int selection) { + roleController.text = selection.toString(); + }, + fieldViewBuilder: ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + return TextField( + controller: controller, + focusNode: focusNode, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'memberRole'.tr(), + helperText: 'memberRoleHint'.tr(), + ), + onTapOutside: (event) => focusNode.unfocus(), + ); + }, + ), + const Gap(16), + FilledButton.icon( + onPressed: () async { + try { + final newRole = int.parse(roleController.text); + if (newRole < 0 || newRole > 100) { + throw 'Role must be between 0 and 100'; + } + + final apiClient = ref.read(apiClientProvider); + await apiClient.patch( + '/chat/$roomId/members/${member.accountId}/role', + data: newRole, + ); + + if (context.mounted) Navigator.pop(context, true); + } catch (err) { + showErrorAlert(err); + } + }, + icon: const Icon(Symbols.save), + label: const Text('saveChanges').tr(), + ), + ], + ).padding(vertical: 16, horizontal: 24), + ], + ), + ), + ); + } +}