Channel member management

This commit is contained in:
LittleSheep 2024-05-27 21:21:10 +08:00
parent ff9e1896b4
commit 6e09414036
3 changed files with 210 additions and 4 deletions

View File

@ -6,6 +6,7 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/widgets/channel/channel_deletion.dart'; import 'package:solian/widgets/channel/channel_deletion.dart';
import 'package:solian/widgets/channel/channel_member.dart';
class ChannelDetailScreen extends StatefulWidget { class ChannelDetailScreen extends StatefulWidget {
final String realm; final String realm;
@ -32,6 +33,18 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
}); });
} }
void showMemberList() {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) => ChannelMemberListPopup(
channel: widget.channel,
realm: widget.realm,
),
);
}
void promptLeaveChannel() async { void promptLeaveChannel() async {
final did = await showDialog( final did = await showDialog(
context: context, context: context,
@ -58,6 +71,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
final ownerActions = [ final ownerActions = [
ListTile( ListTile(
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
trailing: const Icon(Icons.chevron_right),
title: Text('channelAdjust'.tr), title: Text('channelAdjust'.tr),
onTap: () async { onTap: () async {
AppRouter.instance AppRouter.instance
@ -116,13 +130,14 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right),
title: Text('channelSettings'.tr), title: Text('channelSettings'.tr),
onTap: () {},
), ),
ListTile( ListTile(
leading: const Icon(Icons.supervisor_account), leading: const Icon(Icons.supervisor_account),
trailing: const Icon(Icons.chevron_right),
title: Text('channelMembers'.tr), title: Text('channelMembers'.tr),
onTap: () {}, onTap: () => showMemberList(),
), ),
...(_isOwned ? ownerActions : List.empty()), ...(_isOwned ? ownerActions : List.empty()),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),

View File

@ -57,6 +57,7 @@ class SolianMessages extends Translations {
'signupCaption': 'signupCaption':
'Create an account on Solarpass and then get the access of entire Solar Network!', 'Create an account on Solarpass and then get the access of entire Solar Network!',
'signout': 'Sign out', 'signout': 'Sign out',
'joinedAt': 'Joined at @date',
'riskDetection': 'Risk Detected', 'riskDetection': 'Risk Detected',
'matureContent': 'Mature Content', 'matureContent': 'Mature Content',
'matureContentCaption': 'matureContentCaption':
@ -100,6 +101,8 @@ class SolianMessages extends Translations {
'channelEncrypted': 'Encrypted Channel', 'channelEncrypted': 'Encrypted Channel',
'channelMember': 'Channel member', 'channelMember': 'Channel member',
'channelMembers': 'Channel members', 'channelMembers': 'Channel members',
'channelMembersAdd': 'Add channel members',
'channelMembersAddHint': 'Into @channel',
'channelType': 'Channel type', 'channelType': 'Channel type',
'channelTypeCommon': 'Regular', 'channelTypeCommon': 'Regular',
'channelTypeDirect': 'DM', 'channelTypeDirect': 'DM',
@ -111,7 +114,7 @@ class SolianMessages extends Translations {
'Are you sure to delete channel @channel? This action cannot be undone!', 'Are you sure to delete channel @channel? This action cannot be undone!',
'messageDecoding': 'Decoding...', 'messageDecoding': 'Decoding...',
'messageDecodeFailed': 'Unable to decode: @message', 'messageDecodeFailed': 'Unable to decode: @message',
'messageInputPlaceholder': 'Message @channel...', 'messageInputPlaceholder': 'Message @channel',
}, },
'zh_CN': { 'zh_CN': {
'hide': '隐藏', 'hide': '隐藏',
@ -162,6 +165,7 @@ class SolianMessages extends Translations {
'signupGreeting': '欢迎加入\nSolar Network', 'signupGreeting': '欢迎加入\nSolar Network',
'signupCaption': '在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!', 'signupCaption': '在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!',
'signout': '登出', 'signout': '登出',
'joinedAt': '加入于 @date',
'riskDetection': '检测到风险', 'riskDetection': '检测到风险',
'matureContent': '评级内容', 'matureContent': '评级内容',
'matureContentCaption': '该内容已被评级为家长指导级或以上,这可能说明内容包含一系列不友好的成分', 'matureContentCaption': '该内容已被评级为家长指导级或以上,这可能说明内容包含一系列不友好的成分',
@ -203,6 +207,8 @@ class SolianMessages extends Translations {
'channelEncrypted': '加密频道', 'channelEncrypted': '加密频道',
'channelMember': '频道成员', 'channelMember': '频道成员',
'channelMembers': '频道成员', 'channelMembers': '频道成员',
'channelMembersAdd': '添加频道成员',
'channelMembersAddHint': '到 @channel',
'channelType': '频道类型', 'channelType': '频道类型',
'channelTypeCommon': '普通频道', 'channelTypeCommon': '普通频道',
'channelTypeDirect': '私信聊天', 'channelTypeDirect': '私信聊天',
@ -213,7 +219,7 @@ class SolianMessages extends Translations {
'channelDeletionConfirmCaption': '你确认要删除频道 @channel 吗?该操作不可撤销。', 'channelDeletionConfirmCaption': '你确认要删除频道 @channel 吗?该操作不可撤销。',
'messageDecoding': '解码信息中…', 'messageDecoding': '解码信息中…',
'messageDecodeFailed': '解码信息失败:@message', 'messageDecodeFailed': '解码信息失败:@message',
'messageInputPlaceholder': '在 @channel 发信息', 'messageInputPlaceholder': '在 @channel 发信息',
} }
}; };
} }

View File

@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/friend_select.dart';
class ChannelMemberListPopup extends StatefulWidget {
final Channel channel;
final String realm;
const ChannelMemberListPopup({
super.key,
required this.channel,
required this.realm,
});
@override
State<ChannelMemberListPopup> createState() => _ChannelMemberListPopupState();
}
class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
bool _isBusy = true;
int? _accountId;
List<ChannelMember> _members = List.empty();
void getProfile() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
final prof = await auth.getProfile();
setState(() => _accountId = prof.body['id']);
}
void getMembers() async {
setState(() => _isBusy = true);
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
final resp = await client
.get('/api/channels/${widget.realm}/${widget.channel.alias}/members');
if (resp.statusCode == 200) {
setState(() {
_members = resp.body
.map((x) => ChannelMember.fromJson(x))
.toList()
.cast<ChannelMember>();
});
} else {
context.showErrorDialog(resp.bodyString);
}
setState(() => _isBusy = false);
}
void promptAddMember() async {
final input = await showModalBottomSheet(
context: context,
builder: (context) {
return FriendSelect(title: 'channelMembersAdd'.tr);
},
);
if (input == null) return;
addMember(input.name);
}
void addMember(String username) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
setState(() => _isBusy = true);
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.post(
'/api/channels/${widget.realm}/${widget.channel.alias}/members',
{'target': username},
);
if (resp.statusCode == 200) {
getMembers();
} else {
context.showErrorDialog(resp.bodyString);
}
setState(() => _isBusy = false);
}
void removeMember(ChannelMember item) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
setState(() => _isBusy = true);
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.request(
'/api/channels/${widget.realm}/${widget.channel.alias}/members',
'delete',
body: {'target': item.account.name},
);
if (resp.statusCode == 200) {
getMembers();
} else {
context.showErrorDialog(resp.bodyString);
}
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
getProfile();
getMembers();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'channelMembers'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: const Icon(Icons.person_add),
title: Text('channelMembersAdd'.tr),
subtitle: Text(
'channelMembersAddHint'
.trParams({'channel': '#${widget.channel.alias}'}),
),
onTap: () => promptAddMember(),
),
Expanded(
child: ListView.builder(
itemCount: _members.length,
itemBuilder: (context, index) {
var element = _members[index];
return ListTile(
title: Text(element.account.nick),
subtitle: Text(element.account.name),
leading: AccountAvatar(content: element.account.avatar),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
const IconButton(
color: Colors.teal,
icon: Icon(Icons.admin_panel_settings),
onPressed: null,
),
IconButton(
color: Colors.red,
icon: const Icon(Icons.remove_circle),
onPressed: element.account.externalId == _accountId
? null
: () => removeMember(element),
),
],
),
);
},
),
),
],
),
);
}
}