From a59de65130a2e37883c5671a922aa7a9ee419b4e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 25 Nov 2024 00:05:49 +0800 Subject: [PATCH] :lipstick: Optimization of UX in messages --- assets/translations/en.json | 4 +- assets/translations/zh.json | 4 +- ios/Runner.xcodeproj/project.pbxproj | 3 + ios/Runner/Info.plist | 2 + ios/Runner/Runner.entitlements | 2 + lib/screens/chat/call_room.dart | 8 ++- lib/screens/chat/room.dart | 39 +++++++------ lib/screens/post/post_detail.dart | 38 +++++++------ lib/screens/post/post_editor.dart | 36 +++++++----- lib/widgets/chat/chat_message.dart | 56 ++++++++++++++++--- lib/widgets/chat/chat_message_input.dart | 1 + lib/widgets/connection_indicator.dart | 71 +++++++++++++----------- 12 files changed, 170 insertions(+), 94 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 132be6d..ebc2ae5 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -228,5 +228,7 @@ "callVideoFlip": "Mirror video", "callSpeakerphoneToggle": "Toggle speakerphone", "callScreenOff": "Turn off screen share", - "callScreenOn": "Turn on screen share" + "callScreenOn": "Turn on screen share", + "callMessageEnded": "Call lasted {}", + "callMessageStarted": "Call started" } diff --git a/assets/translations/zh.json b/assets/translations/zh.json index cc3beae..8f6083b 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -228,5 +228,7 @@ "callVideoFlip": "镜像画面", "callSpeakerphoneToggle": "切换扬声器", "callScreenOff": "关闭屏幕共享", - "callScreenOn": "开启屏幕共享" + "callScreenOn": "开启屏幕共享", + "callMessageEnded": "通话持续了 {}", + "callMessageStarted": "通话开始了" } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d581ddd..bc9cc71 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -518,6 +518,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Solian; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -703,6 +704,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Solian; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -728,6 +730,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Solian; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 36584e5..3b03791 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,6 +45,8 @@ fetch remote-notification + audio + voip UILaunchStoryboardName LaunchScreen diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 903def2..29326e3 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -4,5 +4,7 @@ aps-environment development + com.apple.developer.usernotifications.communication + diff --git a/lib/screens/chat/call_room.dart b/lib/screens/chat/call_room.dart index 2bb7843..a9bd1f3 100644 --- a/lib/screens/chat/call_room.dart +++ b/lib/screens/chat/call_room.dart @@ -35,7 +35,8 @@ class _CallRoomScreenState extends State { return Stack( children: [ Container( - color: Theme.of(context).colorScheme.surfaceContainer, + color: + Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), child: call.focusTrack != null ? InteractiveParticipantWidget( isFixedAvatar: false, @@ -113,7 +114,10 @@ class _CallRoomScreenState extends State { child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), child: InteractiveParticipantWidget( - color: Theme.of(context).colorScheme.surfaceContainerHigh, + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh + .withOpacity(0.75), participant: track, onTap: () { if (track.participant.sid != diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index db0982d..4edee0c 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -138,6 +138,12 @@ class _ChatRoomScreenState extends State { ); } + bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) { + if (a == null || b == null) return false; + if (a.sender.accountId != b.sender.accountId) return false; + return a.createdAt.difference(b.createdAt).inMinutes <= 3; + } + @override void initState() { super.initState(); @@ -248,27 +254,20 @@ class _ChatRoomScreenState extends State { }, itemBuilder: (context, idx) { final message = _messageController.messages[idx]; - final nextMessage = - idx < _messageController.messages.length - 1 - ? _messageController.messages[idx + 1] - : null; - final previousMessage = - idx > 0 ? _messageController.messages[idx - 1] : null; - final canMerge = nextMessage != null && - nextMessage.senderId == message.senderId && - message.createdAt - .difference(nextMessage.createdAt) - .inMinutes - .abs() <= - 3; - final canMergePrevious = previousMessage != null && - previousMessage.senderId == message.senderId && - message.createdAt - .difference(previousMessage.createdAt) - .inMinutes - .abs() <= - 3; + bool canMerge = false, canMergePrevious = false; + if (idx > 0) { + canMergePrevious = _checkMessageMergeable( + _messageController.messages[idx - 1], + _messageController.messages[idx], + ); + } + if (idx + 1 < _messageController.messages.length) { + canMerge = _checkMessageMergeable( + _messageController.messages[idx], + _messageController.messages[idx + 1], + ); + } return ChatMessage( data: message, diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart index f1e7606..23c1d5c 100644 --- a/lib/screens/post/post_detail.dart +++ b/lib/screens/post/post_detail.dart @@ -86,22 +86,28 @@ class _PostDetailScreenState extends State { GoRouter.of(context).replaceNamed('explore'); }, ), - flexibleSpace: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_data?.body['title'] != null) - Text(_data?.body['title'] ?? 'postNoun'.tr()) - .textStyle(Theme.of(context).textTheme.titleLarge!) - .textColor(Colors.white), - if (_data?.body['title'] != null) - Text('postDetail'.tr()) - .textColor(Colors.white.withAlpha((255 * 0.9).round())) - else - Text('postDetail'.tr()) - .textStyle(Theme.of(context).textTheme.titleLarge!) - .textColor(Colors.white), - ], - ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)), + title: _data?.body['title'] != null + ? RichText( + textAlign: TextAlign.center, + text: TextSpan(children: [ + TextSpan( + text: _data?.body['title'] ?? 'postNoun'.tr(), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Colors.white), + ), + const TextSpan(text: '\n'), + TextSpan( + text: 'postDetail'.tr(), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.white), + ), + ]), + ) + : Text('postDetail').tr(), ), body: CustomScrollView( slivers: [ diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index c9ebbc8..41fb1cd 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -1,5 +1,3 @@ -import 'dart:math' as math; - import 'package:collection/collection.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -129,18 +127,28 @@ class _PostEditorScreenState extends State { Navigator.pop(context); }, ), - flexibleSpace: Column( - children: [ - Text(_writeController.title.isNotEmpty - ? _writeController.title - : 'untitled'.tr()) - .textStyle(Theme.of(context).textTheme.titleLarge!) - .textColor(Colors.white), - Text(PostWriteController.kTitleMap[widget.mode]!) - .tr() - .textColor(Colors.white.withAlpha((255 * 0.9).round())), - ], - ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)), + title: RichText( + textAlign: TextAlign.center, + text: TextSpan(children: [ + TextSpan( + text: _writeController.title.isNotEmpty + ? _writeController.title + : 'untitled'.tr(), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Colors.white), + ), + const TextSpan(text: '\n'), + TextSpan( + text: PostWriteController.kTitleMap[widget.mode]!, + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.white), + ), + ]), + ), actions: [ IconButton( icon: const Icon(Symbols.tune), diff --git a/lib/widgets/chat/chat_message.dart b/lib/widgets/chat/chat_message.dart index 09d771f..7d81dfc 100644 --- a/lib/widgets/chat/chat_message.dart +++ b/lib/widgets/chat/chat_message.dart @@ -47,8 +47,10 @@ class ChatMessage extends StatelessWidget { return SwipeTo( key: Key('chat-message-${data.id}'), iconOnLeftSwipe: Symbols.reply, + iconOnRightSwipe: Symbols.edit, swipeSensitivity: 20, onLeftSwipe: onReply != null ? (_) => onReply!(data) : null, + onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null, child: ContextMenuRegion( contextMenu: ContextMenu( entries: [ @@ -111,10 +113,6 @@ class ChatMessage extends StatelessWidget { ? data.sender.nick! : user?.nick ?? 'unknown', ).bold(), - if (data.updatedAt != data.createdAt) - Text( - 'messageEditedHint'.tr(), - ).fontSize(14).opacity(0.75).padding(left: 6), const Gap(6), Text( dateFormatter.format(data.createdAt.toLocal()), @@ -163,7 +161,10 @@ class ChatMessage extends StatelessWidget { maxHeight: 520, listPadding: const EdgeInsets.only(top: 8), ), - if (!hasMerged && !isCompact) const Gap(12), + if (!hasMerged && !isCompact) + const Gap(12) + else if (!isCompact) + const Gap(6), ], ), ), @@ -178,9 +179,18 @@ class _ChatMessageText extends StatelessWidget { @override Widget build(BuildContext context) { if (data.body['text'] != null && data.body['text'].isNotEmpty) { - return MarkdownTextContent( - content: data.body['text'], - isAutoWarp: true, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MarkdownTextContent( + content: data.body['text'], + isAutoWarp: true, + ), + if (data.updatedAt != data.createdAt) + Text( + 'messageEditedHint'.tr(), + ).fontSize(13).opacity(0.75), + ], ); } else if (data.body['attachments']?.isNotEmpty) { return Row( @@ -204,6 +214,14 @@ class _ChatMessageSystemNotify extends StatelessWidget { final SnChatMessage data; const _ChatMessageSystemNotify({super.key, required this.data}); + String _formatDuration(Duration duration) { + String negativeSign = duration.isNegative ? '-' : ''; + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60).abs()); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs()); + return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds'; + } + @override Widget build(BuildContext context) { switch (data.type) { @@ -227,6 +245,28 @@ class _ChatMessageSystemNotify extends StatelessWidget { ), ], ).opacity(0.75); + case 'calls.start': + return Row( + children: [ + const Icon(Symbols.call, size: 20), + const Gap(4), + Text( + 'callMessageStarted'.tr(), + ), + ], + ).opacity(0.75); + case 'calls.end': + return Row( + children: [ + const Icon(Symbols.call_end, size: 20), + const Gap(4), + Text( + 'callMessageEnded'.tr(args: [ + _formatDuration(Duration(seconds: data.body['last'])), + ]), + ), + ], + ).opacity(0.75); default: return Row( children: [ diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index aacce88..c8fc447 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -36,6 +36,7 @@ class ChatMessageInputState extends State { } void setEdit(SnChatMessage? value) { + _contentController.text = value?.body['text'] ?? ''; setState(() => _editingMessage = value); } diff --git a/lib/widgets/connection_indicator.dart b/lib/widgets/connection_indicator.dart index f0289ee..25a3704 100644 --- a/lib/widgets/connection_indicator.dart +++ b/lib/widgets/connection_indicator.dart @@ -17,38 +17,45 @@ class ConnectionIndicator extends StatelessWidget { builder: (context, _) { final ua = context.read(); - return Container( - padding: EdgeInsets.only( - bottom: 8, - top: MediaQuery.of(context).padding.top + 8, - left: 24, - right: 24, - ), - color: Theme.of(context).colorScheme.secondaryContainer, - child: ua.isAuthorized - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (ws.isBusy) - Text('serverConnecting').tr().textColor( - Theme.of(context).colorScheme.onSecondaryContainer) - else if (!ws.isConnected) - Text('serverDisconnected').tr().textColor( - Theme.of(context).colorScheme.onSecondaryContainer), - ], - ) - : const SizedBox.shrink(), - ) - .height( - (ws.isBusy || !ws.isConnected) && ua.isAuthorized - ? MediaQuery.of(context).padding.top + 36 - : 0, - animate: true) - .animate( - const Duration(milliseconds: 300), - Curves.easeInOut, - ); + return GestureDetector( + child: Container( + padding: EdgeInsets.only( + bottom: 8, + top: MediaQuery.of(context).padding.top + 8, + left: 24, + right: 24, + ), + color: Theme.of(context).colorScheme.secondaryContainer, + child: ua.isAuthorized + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (ws.isBusy) + Text('serverConnecting').tr().textColor( + Theme.of(context).colorScheme.onSecondaryContainer) + else if (!ws.isConnected) + Text('serverDisconnected').tr().textColor( + Theme.of(context).colorScheme.onSecondaryContainer), + ], + ) + : const SizedBox.shrink(), + ) + .height( + (ws.isBusy || !ws.isConnected) && ua.isAuthorized + ? MediaQuery.of(context).padding.top + 36 + : 0, + animate: true) + .animate( + const Duration(milliseconds: 300), + Curves.easeInOut, + ), + onTap: () { + if (!ws.isConnected && !ws.isBusy) { + ws.connect(); + } + }, + ); }, ); }