Notify level in channel

This commit is contained in:
LittleSheep 2024-06-09 00:09:01 +08:00
parent 6acbd1ee9e
commit 0f24ac03f7
12 changed files with 178 additions and 39 deletions

View File

@ -31,8 +31,6 @@ class ChatCallProvider extends GetxController {
Rx<MediaDevice?> videoDevice = Rx(null); Rx<MediaDevice?> videoDevice = Rx(null);
Rx<MediaDevice?> audioDevice = Rx(null); Rx<MediaDevice?> audioDevice = Rx(null);
final VideoParameters videoParameters = VideoParametersPresets.h720_169;
late Room room; late Room room;
late EventsListener<RoomEvent> listener; late EventsListener<RoomEvent> listener;
@ -105,29 +103,27 @@ class ChatCallProvider extends GetxController {
await room.connect( await room.connect(
url, url,
token, token,
roomOptions: RoomOptions( roomOptions: const RoomOptions(
dynacast: true, dynacast: true,
adaptiveStream: true, adaptiveStream: true,
defaultAudioPublishOptions: const AudioPublishOptions( defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice', name: 'call_voice',
stream: 'call_stream', stream: 'call_stream',
), ),
defaultVideoPublishOptions: const VideoPublishOptions( defaultVideoPublishOptions: VideoPublishOptions(
name: 'call_video', name: 'call_video',
stream: 'call_stream', stream: 'call_stream',
simulcast: true, simulcast: true,
backupVideoCodec: BackupVideoCodec(enabled: true), backupVideoCodec: BackupVideoCodec(enabled: true),
), ),
defaultScreenShareCaptureOptions: const ScreenShareCaptureOptions( defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
useiOSBroadcastExtension: true, useiOSBroadcastExtension: true,
params: VideoParameters( params: VideoParametersPresets.screenShareH1080FPS30,
dimensions: VideoDimensionsPresets.h1080_169, ),
encoding: defaultCameraCaptureOptions: CameraCaptureOptions(
VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30), maxFrameRate: 30,
), params: VideoParametersPresets.h1080_169,
), ),
defaultCameraCaptureOptions:
CameraCaptureOptions(maxFrameRate: 30, params: videoParameters),
), ),
fastConnectOptions: FastConnectOptions( fastConnectOptions: FastConnectOptions(
microphone: TrackOption(track: audioTrack.value), microphone: TrackOption(track: audioTrack.value),
@ -334,7 +330,7 @@ class ChatCallProvider extends GetxController {
videoTrack.value = await LocalVideoTrack.createCameraTrack( videoTrack.value = await LocalVideoTrack.createCameraTrack(
CameraCaptureOptions( CameraCaptureOptions(
deviceId: videoDevice.value!.deviceId, deviceId: videoDevice.value!.deviceId,
params: videoParameters, params: VideoParametersPresets.h1080_169,
), ),
); );
await videoTrack.value!.start(); await videoTrack.value!.start();

View File

@ -19,6 +19,20 @@ class ChannelProvider extends GetxController {
return resp; return resp;
} }
Future<Response> getMyChannelProfile(String alias, {String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = auth.configureClient(service: 'messaging');
final resp = await client.get('/api/channels/$realm/$alias/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response?> getChannelOngoingCall(String alias, Future<Response?> getChannelOngoingCall(String alias,
{String realm = 'global'}) async { {String realm = 'global'}) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();

View File

@ -1,5 +1,4 @@
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart'; import 'package:solian/screens/about.dart';
import 'package:solian/screens/account.dart'; import 'package:solian/screens/account.dart';
@ -102,10 +101,14 @@ abstract class AppRouter {
GoRoute( GoRoute(
path: '/chat/:alias/detail', path: '/chat/:alias/detail',
name: 'channelDetail', name: 'channelDetail',
builder: (context, state) => ChannelDetailScreen( builder: (context, state) {
channel: state.extra as Channel, final arguments = state.extra as ChannelDetailArguments;
realm: state.uri.queryParameters['realm'] ?? 'global', return ChannelDetailScreen(
), channel: arguments.channel,
profile: arguments.profile,
realm: state.uri.queryParameters['realm'] ?? 'global',
);
},
), ),
], ],
), ),

View File

@ -33,7 +33,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
if (markList.isNotEmpty) { if (markList.isNotEmpty) {
final client = auth.configureClient(service: 'passport'); final client = auth.configureClient(service: 'passport');
await client.put('/api/notifications/batch/read', {'messages': markList}); await client.put('/api/notifications/batch/read', {'messages': markList});
} }
@ -129,6 +128,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
), ),
title: Text(element.subject), title: Text(element.subject),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(element.content), Text(element.content),
if (element.links != null) if (element.links != null)

View File

@ -25,6 +25,7 @@ class _SignInPopupState extends State<SignInPopup> {
if (username.isEmpty || password.isEmpty) return; if (username.isEmpty || password.isEmpty) return;
provider.signin(context, username, password).then((_) async { provider.signin(context, username, password).then((_) async {
await showDialog( await showDialog(
useRootNavigator: true,
context: context, context: context,
builder: (context) => const PushNotifyRegisterDialog(), builder: (context) => const PushNotifyRegisterDialog(),
); );

View File

@ -55,6 +55,7 @@ class _CallScreenState extends State<CallScreen> {
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true,
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,
text: TextSpan(children: [ text: TextSpan(children: [

View File

@ -15,6 +15,7 @@ import 'package:solian/providers/chat.dart';
import 'package:solian/providers/content/call.dart'; import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/chat/call/call_prejoin.dart'; import 'package:solian/widgets/chat/call/call_prejoin.dart';
import 'package:solian/widgets/chat/call/chat_call_action.dart'; import 'package:solian/widgets/chat/call/chat_call_action.dart';
@ -44,6 +45,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
String? _overrideAlias; String? _overrideAlias;
Channel? _channel; Channel? _channel;
ChannelMember? _channelProfile;
Call? _ongoingCall; Call? _ongoingCall;
StreamSubscription<NetworkPackage>? _subscription; StreamSubscription<NetworkPackage>? _subscription;
@ -61,16 +63,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
if (overrideAlias != null) { if (overrideAlias != null) _overrideAlias = overrideAlias;
_overrideAlias = overrideAlias;
}
try { try {
final resp = await provider.getChannel( final resp = await provider.getChannel(
_overrideAlias ?? widget.alias, _overrideAlias ?? widget.alias,
realm: widget.realm, realm: widget.realm,
); );
setState(() => _channel = Channel.fromJson(resp.body)); final respProfile = await provider.getMyChannelProfile(
_overrideAlias ?? widget.alias,
realm: widget.realm,
);
setState(() {
_channel = Channel.fromJson(resp.body);
_channelProfile = ChannelMember.fromJson(respProfile.body);
});
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
} }
@ -192,7 +199,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Message? _messageToReplying; Message? _messageToReplying;
Message? _messageToEditing; Message? _messageToEditing;
Widget buildHistory(context, item, index) { Widget buildHistory(context, Message item, index) {
bool isMerged = false, hasMerged = false; bool isMerged = false, hasMerged = false;
if (index > 0) { if (index > 0) {
hasMerged = checkMessageMergeable( hasMerged = checkMessageMergeable(
@ -212,8 +219,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
content = Column( content = Column(
children: [ children: [
ChatMessage( ChatMessage(
key: Key('m${item.replyTo.uuid}'), key: Key('m${item.replyTo!.uuid}'),
item: item.replyTo, item: item.replyTo!,
isReply: true, isReply: true,
).paddingOnly(left: 24, right: 4, bottom: 2), ).paddingOnly(left: 24, right: 4, bottom: 2),
ChatMessage( ChatMessage(
@ -273,8 +280,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _channel == null) { if (_isBusy || _channel == null) {
return const Center( return Material(
child: CircularProgressIndicator(), color: Theme.of(context).colorScheme.surface,
child: const Center(
child: CircularProgressIndicator(),
),
); );
} }
@ -315,7 +325,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
'channelDetail', 'channelDetail',
pathParameters: {'alias': widget.alias}, pathParameters: {'alias': widget.alias},
queryParameters: {'realm': widget.realm}, queryParameters: {'realm': widget.realm},
extra: _channel, extra: ChannelDetailArguments(
profile: _channelProfile!,
channel: _channel!,
),
) )
.then((value) { .then((value) {
if (value == false) AppRouter.instance.pop(); if (value == false) AppRouter.instance.pop();

View File

@ -1,6 +1,8 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
@ -8,14 +10,23 @@ 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'; import 'package:solian/widgets/channel/channel_member.dart';
class ChannelDetailArguments {
final Channel channel;
final ChannelMember profile;
ChannelDetailArguments({required this.channel, required this.profile});
}
class ChannelDetailScreen extends StatefulWidget { class ChannelDetailScreen extends StatefulWidget {
final String realm; final String realm;
final Channel channel; final Channel channel;
final ChannelMember profile;
const ChannelDetailScreen({ const ChannelDetailScreen({
super.key, super.key,
required this.channel, required this.channel,
required this.realm, required this.realm,
required this.profile,
}); });
@override @override
@ -23,7 +34,10 @@ class ChannelDetailScreen extends StatefulWidget {
} }
class _ChannelDetailScreenState extends State<ChannelDetailScreen> { class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
bool _isBusy = false;
bool _isOwned = false; bool _isOwned = false;
int _notifyLevel = 0;
void checkOwner() async { void checkOwner() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@ -59,15 +73,43 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
} }
} }
void applyProfileChanges() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
setState(() => _isBusy = true);
final client = auth.configureClient(service: 'messaging');
final resp = await client
.put('/api/channels/${widget.realm}/${widget.channel.alias}/members/me', {
'nick': null,
'notify_level': _notifyLevel,
});
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString);
} else {
context.showSnackbar('channelNotifyLevelApplied'.tr);
}
setState(() => _isBusy = false);
}
@override @override
void initState() { void initState() {
_notifyLevel = widget.profile.notify;
super.initState(); super.initState();
checkOwner(); checkOwner();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final notifyTypes = {
0: 'channelNotifyLevelAll'.tr,
1: 'channelNotifyLevelMentioned'.tr,
2: 'channelNotifyLevelNone'.tr,
};
final ownerActions = [ final ownerActions = [
ListTile( ListTile(
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
@ -127,9 +169,38 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
child: ListView( child: ListView(
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.notifications_active),
trailing: const Icon(Icons.chevron_right), title: Text('channelNotifyLevel'.tr.capitalize!),
title: Text('channelSettings'.tr.capitalize!), trailing: DropdownButtonHideUnderline(
child: DropdownButton2<int>(
isExpanded: true,
items: notifyTypes.entries
.map((item) => DropdownMenuItem<int>(
enabled: !_isBusy,
value: item.key,
child: Text(
item.value,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
value: _notifyLevel,
onChanged: (int? value) {
setState(() => _notifyLevel = value ?? 0);
applyProfileChanges();
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.only(left: 16, right: 1),
height: 40,
width: 140,
),
menuItemStyleData: const MenuItemStyleData(
height: 40,
),
),
),
), ),
ListTile( ListTile(
leading: const Icon(Icons.supervisor_account), leading: const Icon(Icons.supervisor_account),

View File

@ -119,6 +119,9 @@ class SolianMessages extends Translations {
'realmAdjust': 'Realm adjustment', 'realmAdjust': 'Realm adjustment',
'realmSettings': 'Realm settings', 'realmSettings': 'Realm settings',
'realmEditingNotify': 'You\'re editing realm @realm', 'realmEditingNotify': 'You\'re editing realm @realm',
'realmLeaveConfirm': 'Confirm realm quit',
'realmLeaveConfirmCaption':
'Are you sure you want leave realm @realm? Your content published in this realm will not be deleted.',
'realmDeletionConfirm': 'Confirm realm deletion', 'realmDeletionConfirm': 'Confirm realm deletion',
'realmDeletionConfirmCaption': 'realmDeletionConfirmCaption':
'Are you sure to delete realm @realm? This action cannot be undone!', 'Are you sure to delete realm @realm? This action cannot be undone!',
@ -145,11 +148,19 @@ class SolianMessages extends Translations {
'channelAdjust': 'Channel adjustment', 'channelAdjust': 'Channel adjustment',
'channelDetail': 'Channel detail', 'channelDetail': 'Channel detail',
'channelSettings': 'Channel settings', 'channelSettings': 'Channel settings',
'channelLeaveConfirm': 'Confirm channel quit',
'channelLeaveConfirmCaption':
'Are you sure to leave channel @channel? All your messages will be deleted!',
'channelDeletionConfirm': 'Confirm channel deletion', 'channelDeletionConfirm': 'Confirm channel deletion',
'channelDeletionConfirmCaption': 'channelDeletionConfirmCaption':
'Are you sure to delete channel @channel? This action cannot be undone!', 'Are you sure to delete channel @channel? This action cannot be undone!',
'channelCategoryDirect': 'DM', 'channelCategoryDirect': 'DM',
'channelCategoryDirectHint': 'Your direct messages', 'channelCategoryDirectHint': 'Your direct messages',
'channelNotifyLevel': 'Notify level',
'channelNotifyLevelAll': 'All',
'channelNotifyLevelMentioned': 'Only mentioned',
'channelNotifyLevelNone': 'Ignore all',
'channelNotifyLevelApplied': 'Your notification settings has been applied.',
'messageDecoding': 'Decoding...', 'messageDecoding': 'Decoding...',
'messageDecodeFailed': 'Unable to decode: @message', 'messageDecodeFailed': 'Unable to decode: @message',
'messageInputPlaceholder': 'Message @channel', 'messageInputPlaceholder': 'Message @channel',
@ -309,6 +320,8 @@ class SolianMessages extends Translations {
'realmAdjust': '调整领域', 'realmAdjust': '调整领域',
'realmSettings': '领域设置', 'realmSettings': '领域设置',
'realmEditingNotify': '你正在编辑领域 @realm', 'realmEditingNotify': '你正在编辑领域 @realm',
'realmLeaveConfirm': '确认离开领域',
'realmLeaveConfirmCaption': '你确认要离开领域 @realm 吗?你在该领域发表的内容不会被删除。',
'realmDeletionConfirm': '确认删除领域', 'realmDeletionConfirm': '确认删除领域',
'realmDeletionConfirmCaption': '你确定要删除领域 @realm 嘛?该操作不可撤销。', 'realmDeletionConfirmCaption': '你确定要删除领域 @realm 嘛?该操作不可撤销。',
'channelNew': '创建新频道', 'channelNew': '创建新频道',
@ -334,10 +347,17 @@ class SolianMessages extends Translations {
'channelAdjust': '调整频道', 'channelAdjust': '调整频道',
'channelDetail': '频道详情', 'channelDetail': '频道详情',
'channelSettings': '频道设置', 'channelSettings': '频道设置',
'channelLeaveConfirm': '确认离开频道',
'channelLeaveConfirmCaption': '你确认要离开频道 @channel 吗?你在这个频道的消息将被删除。',
'channelDeletionConfirm': '确认删除频道', 'channelDeletionConfirm': '确认删除频道',
'channelDeletionConfirmCaption': '你确认要删除频道 @channel 吗?该操作不可撤销。', 'channelDeletionConfirmCaption': '你确认要删除频道 @channel 吗?该操作不可撤销。',
'channelCategoryDirect': '私聊频道', 'channelCategoryDirect': '私聊频道',
'channelCategoryDirectHint': '你的所有私聊频道', 'channelCategoryDirectHint': '你的所有私聊频道',
'channelNotifyLevel': '通知等级',
'channelNotifyLevelAll': '全部通知',
'channelNotifyLevelMentioned': '仅提及',
'channelNotifyLevelNone': '忽略一切',
'channelNotifyLevelApplied': '你的通知设置已经应用。',
'messageDecoding': '解码信息中…', 'messageDecoding': '解码信息中…',
'messageDecodeFailed': '解码信息失败:@message', 'messageDecodeFailed': '解码信息失败:@message',
'messageInputPlaceholder': '在 @channel 发信息', 'messageInputPlaceholder': '在 @channel 发信息',

View File

@ -65,9 +65,14 @@ class _ChannelDeletionDialogState extends State<ChannelDeletionDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text('channelDeletionConfirm'.tr), title: Text(widget.isOwned
? 'channelDeletionConfirm'.tr
: 'channelLeaveConfirm'.tr),
content: Text( content: Text(
widget.isOwned ?
'channelDeletionConfirmCaption' 'channelDeletionConfirmCaption'
.trParams({'channel': '#${widget.channel.alias}'}) :
'channelLeaveConfirmCaption'
.trParams({'channel': '#${widget.channel.alias}'}), .trParams({'channel': '#${widget.channel.alias}'}),
), ),
actions: <Widget>[ actions: <Widget>[

View File

@ -113,7 +113,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
Response resp; Response resp;
if (_editTo != null) { if (_editTo != null) {
resp = await client.put( resp = await client.put(
'/api/channels/${widget.realm}/${widget.channel.alias}/messages/${widget.edit!.id}', '/api/channels/${widget.realm}/${widget.channel.alias}/messages/${_editTo!.id}',
payload, payload,
); );
} else { } else {
@ -171,6 +171,11 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
MaterialBanner( MaterialBanner(
leading: const FaIcon(FontAwesomeIcons.reply, size: 18), leading: const FaIcon(FontAwesomeIcons.reply, size: 18),
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
padding: const EdgeInsets.only(left: 20),
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withOpacity(0.5),
content: ChatMessage( content: ChatMessage(
item: _replyTo!, item: _replyTo!,
isContentPreviewing: true, isContentPreviewing: true,
@ -181,6 +186,11 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
MaterialBanner( MaterialBanner(
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
padding: const EdgeInsets.only(left: 20),
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withOpacity(0.5),
content: ChatMessage( content: ChatMessage(
item: _editTo!, item: _editTo!,
isContentPreviewing: true, isContentPreviewing: true,

View File

@ -61,10 +61,15 @@ class _RealmDeletionDialogState extends State<RealmDeletionDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text('realmDeletionConfirm'.tr), title: Text(widget.isOwned
? 'realmDeletionConfirm'.tr
: 'channelLeaveConfirm'.tr),
content: Text( content: Text(
'realmDeletionConfirmCaption' widget.isOwned
.trParams({'realm': '#${widget.realm.alias}'}), ? 'realmDeletionConfirmCaption'
.trParams({'realm': '#${widget.realm.alias}'})
: 'realmLeaveConfirmCaption'
.trParams({'realm': '#${widget.realm.alias}'}),
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(