diff --git a/lib/providers/content/call.dart b/lib/providers/content/call.dart index 9f9b6da..a78841c 100644 --- a/lib/providers/content/call.dart +++ b/lib/providers/content/call.dart @@ -31,8 +31,6 @@ class ChatCallProvider extends GetxController { Rx videoDevice = Rx(null); Rx audioDevice = Rx(null); - final VideoParameters videoParameters = VideoParametersPresets.h720_169; - late Room room; late EventsListener listener; @@ -105,29 +103,27 @@ class ChatCallProvider extends GetxController { await room.connect( url, token, - roomOptions: RoomOptions( + roomOptions: const RoomOptions( dynacast: true, adaptiveStream: true, - defaultAudioPublishOptions: const AudioPublishOptions( + defaultAudioPublishOptions: AudioPublishOptions( name: 'call_voice', stream: 'call_stream', ), - defaultVideoPublishOptions: const VideoPublishOptions( + defaultVideoPublishOptions: VideoPublishOptions( name: 'call_video', stream: 'call_stream', simulcast: true, backupVideoCodec: BackupVideoCodec(enabled: true), ), - defaultScreenShareCaptureOptions: const ScreenShareCaptureOptions( + defaultScreenShareCaptureOptions: ScreenShareCaptureOptions( useiOSBroadcastExtension: true, - params: VideoParameters( - dimensions: VideoDimensionsPresets.h1080_169, - encoding: - VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30), - ), + params: VideoParametersPresets.screenShareH1080FPS30, + ), + defaultCameraCaptureOptions: CameraCaptureOptions( + maxFrameRate: 30, + params: VideoParametersPresets.h1080_169, ), - defaultCameraCaptureOptions: - CameraCaptureOptions(maxFrameRate: 30, params: videoParameters), ), fastConnectOptions: FastConnectOptions( microphone: TrackOption(track: audioTrack.value), @@ -334,7 +330,7 @@ class ChatCallProvider extends GetxController { videoTrack.value = await LocalVideoTrack.createCameraTrack( CameraCaptureOptions( deviceId: videoDevice.value!.deviceId, - params: videoParameters, + params: VideoParametersPresets.h1080_169, ), ); await videoTrack.value!.start(); diff --git a/lib/providers/content/channel.dart b/lib/providers/content/channel.dart index ce96d77..b99546c 100644 --- a/lib/providers/content/channel.dart +++ b/lib/providers/content/channel.dart @@ -19,6 +19,20 @@ class ChannelProvider extends GetxController { return resp; } + Future 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 getChannelOngoingCall(String alias, {String realm = 'global'}) async { final AuthProvider auth = Get.find(); diff --git a/lib/router.dart b/lib/router.dart index 0af4ad8..56f66ef 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,5 +1,4 @@ import 'package:go_router/go_router.dart'; -import 'package:solian/models/channel.dart'; import 'package:solian/models/realm.dart'; import 'package:solian/screens/about.dart'; import 'package:solian/screens/account.dart'; @@ -102,10 +101,14 @@ abstract class AppRouter { GoRoute( path: '/chat/:alias/detail', name: 'channelDetail', - builder: (context, state) => ChannelDetailScreen( - channel: state.extra as Channel, - realm: state.uri.queryParameters['realm'] ?? 'global', - ), + builder: (context, state) { + final arguments = state.extra as ChannelDetailArguments; + return ChannelDetailScreen( + channel: arguments.channel, + profile: arguments.profile, + realm: state.uri.queryParameters['realm'] ?? 'global', + ); + }, ), ], ), diff --git a/lib/screens/account/notification.dart b/lib/screens/account/notification.dart index ad04e25..4700f9a 100644 --- a/lib/screens/account/notification.dart +++ b/lib/screens/account/notification.dart @@ -33,7 +33,6 @@ class _NotificationScreenState extends State { if (markList.isNotEmpty) { final client = auth.configureClient(service: 'passport'); - await client.put('/api/notifications/batch/read', {'messages': markList}); } @@ -129,6 +128,7 @@ class _NotificationScreenState extends State { ), title: Text(element.subject), subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(element.content), if (element.links != null) diff --git a/lib/screens/auth/signin.dart b/lib/screens/auth/signin.dart index 46c6f05..a3aa1e6 100644 --- a/lib/screens/auth/signin.dart +++ b/lib/screens/auth/signin.dart @@ -25,6 +25,7 @@ class _SignInPopupState extends State { if (username.isEmpty || password.isEmpty) return; provider.signin(context, username, password).then((_) async { await showDialog( + useRootNavigator: true, context: context, builder: (context) => const PushNotifyRegisterDialog(), ); diff --git a/lib/screens/channel/call/call.dart b/lib/screens/channel/call/call.dart index 9d632d4..50432a8 100644 --- a/lib/screens/channel/call/call.dart +++ b/lib/screens/channel/call/call.dart @@ -55,6 +55,7 @@ class _CallScreenState extends State { color: Theme.of(context).colorScheme.surface, child: Scaffold( appBar: AppBar( + centerTitle: true, title: RichText( textAlign: TextAlign.center, text: TextSpan(children: [ diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index c822ba7..cdc24dc 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -15,6 +15,7 @@ import 'package:solian/providers/chat.dart'; import 'package:solian/providers/content/call.dart'; import 'package:solian/providers/content/channel.dart'; import 'package:solian/router.dart'; +import 'package:solian/screens/channel/channel_detail.dart'; import 'package:solian/theme.dart'; import 'package:solian/widgets/chat/call/call_prejoin.dart'; import 'package:solian/widgets/chat/call/chat_call_action.dart'; @@ -44,6 +45,7 @@ class _ChannelChatScreenState extends State { String? _overrideAlias; Channel? _channel; + ChannelMember? _channelProfile; Call? _ongoingCall; StreamSubscription? _subscription; @@ -61,16 +63,21 @@ class _ChannelChatScreenState extends State { setState(() => _isBusy = true); - if (overrideAlias != null) { - _overrideAlias = overrideAlias; - } + if (overrideAlias != null) _overrideAlias = overrideAlias; try { final resp = await provider.getChannel( _overrideAlias ?? widget.alias, 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) { context.showErrorDialog(e); } @@ -192,7 +199,7 @@ class _ChannelChatScreenState extends State { Message? _messageToReplying; Message? _messageToEditing; - Widget buildHistory(context, item, index) { + Widget buildHistory(context, Message item, index) { bool isMerged = false, hasMerged = false; if (index > 0) { hasMerged = checkMessageMergeable( @@ -212,8 +219,8 @@ class _ChannelChatScreenState extends State { content = Column( children: [ ChatMessage( - key: Key('m${item.replyTo.uuid}'), - item: item.replyTo, + key: Key('m${item.replyTo!.uuid}'), + item: item.replyTo!, isReply: true, ).paddingOnly(left: 24, right: 4, bottom: 2), ChatMessage( @@ -273,8 +280,11 @@ class _ChannelChatScreenState extends State { @override Widget build(BuildContext context) { if (_isBusy || _channel == null) { - return const Center( - child: CircularProgressIndicator(), + return Material( + color: Theme.of(context).colorScheme.surface, + child: const Center( + child: CircularProgressIndicator(), + ), ); } @@ -315,7 +325,10 @@ class _ChannelChatScreenState extends State { 'channelDetail', pathParameters: {'alias': widget.alias}, queryParameters: {'realm': widget.realm}, - extra: _channel, + extra: ChannelDetailArguments( + profile: _channelProfile!, + channel: _channel!, + ), ) .then((value) { if (value == false) AppRouter.instance.pop(); diff --git a/lib/screens/channel/channel_detail.dart b/lib/screens/channel/channel_detail.dart index 525d7ed..0268396 100644 --- a/lib/screens/channel/channel_detail.dart +++ b/lib/screens/channel/channel_detail.dart @@ -1,6 +1,8 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.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/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_member.dart'; +class ChannelDetailArguments { + final Channel channel; + final ChannelMember profile; + + ChannelDetailArguments({required this.channel, required this.profile}); +} + class ChannelDetailScreen extends StatefulWidget { final String realm; final Channel channel; + final ChannelMember profile; const ChannelDetailScreen({ super.key, required this.channel, required this.realm, + required this.profile, }); @override @@ -23,7 +34,10 @@ class ChannelDetailScreen extends StatefulWidget { } class _ChannelDetailScreenState extends State { + bool _isBusy = false; + bool _isOwned = false; + int _notifyLevel = 0; void checkOwner() async { final AuthProvider auth = Get.find(); @@ -59,15 +73,43 @@ class _ChannelDetailScreenState extends State { } } + 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 void initState() { + _notifyLevel = widget.profile.notify; super.initState(); - checkOwner(); } @override Widget build(BuildContext context) { + final notifyTypes = { + 0: 'channelNotifyLevelAll'.tr, + 1: 'channelNotifyLevelMentioned'.tr, + 2: 'channelNotifyLevelNone'.tr, + }; + final ownerActions = [ ListTile( leading: const Icon(Icons.edit), @@ -127,9 +169,38 @@ class _ChannelDetailScreenState extends State { child: ListView( children: [ ListTile( - leading: const Icon(Icons.settings), - trailing: const Icon(Icons.chevron_right), - title: Text('channelSettings'.tr.capitalize!), + leading: const Icon(Icons.notifications_active), + title: Text('channelNotifyLevel'.tr.capitalize!), + trailing: DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + items: notifyTypes.entries + .map((item) => DropdownMenuItem( + 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( leading: const Icon(Icons.supervisor_account), diff --git a/lib/translations.dart b/lib/translations.dart index 8f391a7..2494685 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -119,6 +119,9 @@ class SolianMessages extends Translations { 'realmAdjust': 'Realm adjustment', 'realmSettings': 'Realm settings', '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', 'realmDeletionConfirmCaption': 'Are you sure to delete realm @realm? This action cannot be undone!', @@ -145,11 +148,19 @@ class SolianMessages extends Translations { 'channelAdjust': 'Channel adjustment', 'channelDetail': 'Channel detail', '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', 'channelDeletionConfirmCaption': 'Are you sure to delete channel @channel? This action cannot be undone!', 'channelCategoryDirect': 'DM', 'channelCategoryDirectHint': 'Your direct messages', + 'channelNotifyLevel': 'Notify level', + 'channelNotifyLevelAll': 'All', + 'channelNotifyLevelMentioned': 'Only mentioned', + 'channelNotifyLevelNone': 'Ignore all', + 'channelNotifyLevelApplied': 'Your notification settings has been applied.', 'messageDecoding': 'Decoding...', 'messageDecodeFailed': 'Unable to decode: @message', 'messageInputPlaceholder': 'Message @channel', @@ -309,6 +320,8 @@ class SolianMessages extends Translations { 'realmAdjust': '调整领域', 'realmSettings': '领域设置', 'realmEditingNotify': '你正在编辑领域 @realm', + 'realmLeaveConfirm': '确认离开领域', + 'realmLeaveConfirmCaption': '你确认要离开领域 @realm 吗?你在该领域发表的内容不会被删除。', 'realmDeletionConfirm': '确认删除领域', 'realmDeletionConfirmCaption': '你确定要删除领域 @realm 嘛?该操作不可撤销。', 'channelNew': '创建新频道', @@ -334,10 +347,17 @@ class SolianMessages extends Translations { 'channelAdjust': '调整频道', 'channelDetail': '频道详情', 'channelSettings': '频道设置', + 'channelLeaveConfirm': '确认离开频道', + 'channelLeaveConfirmCaption': '你确认要离开频道 @channel 吗?你在这个频道的消息将被删除。', 'channelDeletionConfirm': '确认删除频道', 'channelDeletionConfirmCaption': '你确认要删除频道 @channel 吗?该操作不可撤销。', 'channelCategoryDirect': '私聊频道', 'channelCategoryDirectHint': '你的所有私聊频道', + 'channelNotifyLevel': '通知等级', + 'channelNotifyLevelAll': '全部通知', + 'channelNotifyLevelMentioned': '仅提及', + 'channelNotifyLevelNone': '忽略一切', + 'channelNotifyLevelApplied': '你的通知设置已经应用。', 'messageDecoding': '解码信息中…', 'messageDecodeFailed': '解码信息失败:@message', 'messageInputPlaceholder': '在 @channel 发信息', diff --git a/lib/widgets/channel/channel_deletion.dart b/lib/widgets/channel/channel_deletion.dart index 2617325..7235e35 100644 --- a/lib/widgets/channel/channel_deletion.dart +++ b/lib/widgets/channel/channel_deletion.dart @@ -65,9 +65,14 @@ class _ChannelDeletionDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text('channelDeletionConfirm'.tr), + title: Text(widget.isOwned + ? 'channelDeletionConfirm'.tr + : 'channelLeaveConfirm'.tr), content: Text( + widget.isOwned ? 'channelDeletionConfirmCaption' + .trParams({'channel': '#${widget.channel.alias}'}) : + 'channelLeaveConfirmCaption' .trParams({'channel': '#${widget.channel.alias}'}), ), actions: [ diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index 468517f..4b8efc5 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -113,7 +113,7 @@ class _ChatMessageInputState extends State { Response resp; if (_editTo != null) { 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, ); } else { @@ -171,6 +171,11 @@ class _ChatMessageInputState extends State { MaterialBanner( leading: const FaIcon(FontAwesomeIcons.reply, size: 18), dividerColor: Colors.transparent, + padding: const EdgeInsets.only(left: 20), + backgroundColor: Theme.of(context) + .colorScheme + .surfaceContainerHighest + .withOpacity(0.5), content: ChatMessage( item: _replyTo!, isContentPreviewing: true, @@ -181,6 +186,11 @@ class _ChatMessageInputState extends State { MaterialBanner( leading: const Icon(Icons.edit), dividerColor: Colors.transparent, + padding: const EdgeInsets.only(left: 20), + backgroundColor: Theme.of(context) + .colorScheme + .surfaceContainerHighest + .withOpacity(0.5), content: ChatMessage( item: _editTo!, isContentPreviewing: true, diff --git a/lib/widgets/realms/realm_deletion.dart b/lib/widgets/realms/realm_deletion.dart index 13016f2..becae65 100644 --- a/lib/widgets/realms/realm_deletion.dart +++ b/lib/widgets/realms/realm_deletion.dart @@ -61,10 +61,15 @@ class _RealmDeletionDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text('realmDeletionConfirm'.tr), + title: Text(widget.isOwned + ? 'realmDeletionConfirm'.tr + : 'channelLeaveConfirm'.tr), content: Text( - 'realmDeletionConfirmCaption' - .trParams({'realm': '#${widget.realm.alias}'}), + widget.isOwned + ? 'realmDeletionConfirmCaption' + .trParams({'realm': '#${widget.realm.alias}'}) + : 'realmLeaveConfirmCaption' + .trParams({'realm': '#${widget.realm.alias}'}), ), actions: [ TextButton(