From b808c76ea3a1ee9df16e94e78e621e2c44249520 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 6 Jul 2024 17:12:57 +0800 Subject: [PATCH] :zap: Optimized chat messages --- lib/controllers/chat_events_controller.dart | 26 +- .../message/{helper.dart => adaptor.dart} | 6 +- lib/screens/account/friend.dart | 15 +- lib/screens/channel/channel_chat.dart | 235 ++++++------------ lib/translations.dart | 8 +- lib/widgets/chat/chat_event_list.dart | 116 +++++++++ 6 files changed, 221 insertions(+), 185 deletions(-) rename lib/providers/message/{helper.dart => adaptor.dart} (95%) create mode 100644 lib/widgets/chat/chat_event_list.dart diff --git a/lib/controllers/chat_events_controller.dart b/lib/controllers/chat_events_controller.dart index 943aeb5..1f2ee18 100644 --- a/lib/controllers/chat_events_controller.dart +++ b/lib/controllers/chat_events_controller.dart @@ -2,7 +2,7 @@ import 'package:get/get.dart'; import 'package:solian/models/channel.dart'; import 'package:solian/models/event.dart'; import 'package:solian/platform.dart'; -import 'package:solian/providers/message/helper.dart'; +import 'package:solian/providers/message/adaptor.dart'; import 'package:solian/providers/message/events.dart'; class ChatEventController { @@ -57,11 +57,13 @@ class ChatEventController { totalEvents.value = result?.$2 ?? 0; if (result != null) { for (final x in result.$1.reversed) { - applyEvent(LocalEvent(x.id, x, x.channelId, x.createdAt)); + final entry = LocalEvent(x.id, x, x.channelId, x.createdAt); + insertEvent(entry); + applyEvent(entry); } } } else { - final result = await database.syncEvents( + final result = await database.syncRemoteEvents( channel, scope: scope, ); @@ -80,14 +82,16 @@ class ChatEventController { remainDepth: 3, offset: currentEvents.length, ); - totalEvents.value = result?.$2 ?? 0; if (result != null) { + totalEvents.value = result.$2; for (final x in result.$1.reversed) { - applyEvent(LocalEvent(x.id, x, x.channelId, x.createdAt)); + final entry = LocalEvent(x.id, x, x.channelId, x.createdAt); + currentEvents.add(entry); + applyEvent(entry); } } } else { - final result = await database.syncEvents( + final result = await database.syncRemoteEvents( channel, depth: 3, scope: scope, @@ -102,6 +106,7 @@ class ChatEventController { Future syncLocal(Channel channel) async { if (PlatformInfo.isWeb) return false; final data = await database.localEvents.findAllByChannel(channel.id); + currentEvents.replaceRange(0, currentEvents.length, data); for (final x in data.reversed) { applyEvent(x); } @@ -121,18 +126,21 @@ class ChatEventController { entry = await database.receiveEvent(remote); } + insertEvent(entry); applyEvent(entry); } - applyEvent(LocalEvent entry) { - if (entry.channelId != channel?.id) return; - + insertEvent(LocalEvent entry) { final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid); if (idx != -1) { currentEvents[idx] = entry; } else { currentEvents.insert(0, entry); } + } + + applyEvent(LocalEvent entry) { + if (entry.channelId != channel?.id) return; switch (entry.data.type) { case 'messages.edit': diff --git a/lib/providers/message/helper.dart b/lib/providers/message/adaptor.dart similarity index 95% rename from lib/providers/message/helper.dart rename to lib/providers/message/adaptor.dart index 9fe3d53..1ea08ee 100644 --- a/lib/providers/message/helper.dart +++ b/lib/providers/message/adaptor.dart @@ -81,7 +81,7 @@ Future<(List, int)?> getRemoteEvents( return ([...result, ...expandResult], response.count); } -extension MessageHistoryHelper on MessageHistoryDb { +extension MessageHistoryAdaptor on MessageHistoryDb { Future receiveEvent(Event remote) async { final entry = LocalEvent( remote.id, @@ -121,7 +121,7 @@ extension MessageHistoryHelper on MessageHistoryDb { return await receiveEvent(remoteRecord); } - Future<(List, int)?> syncEvents(Channel channel, + Future<(List, int)?> syncRemoteEvents(Channel channel, {String scope = 'global', depth = 10, offset = 0}) async { final lastOne = await localEvents.findLastByChannel(channel.id); @@ -145,7 +145,7 @@ extension MessageHistoryHelper on MessageHistoryDb { return data; } - Future> listMessages(Channel channel) async { + Future> listEvents(Channel channel) async { return await localEvents.findAllByChannel(channel.id); } } diff --git a/lib/screens/account/friend.dart b/lib/screens/account/friend.dart index c66cd42..87989b3 100644 --- a/lib/screens/account/friend.dart +++ b/lib/screens/account/friend.dart @@ -186,13 +186,14 @@ class _FriendScreenState extends State { showScopedListPopup('accountFriendBlocked'.tr, 2), ), ), - SliverFriendList( - accountId: _accountId!, - items: filterWithStatus(1), - onUpdate: () { - getFriendship(); - }, - ), + if (_accountId != null) + SliverFriendList( + accountId: _accountId!, + items: filterWithStatus(1), + onUpdate: () { + getFriendship(); + }, + ), const SliverToBoxAdapter( child: Divider(thickness: 0.3, height: 0.3), ), diff --git a/lib/screens/channel/channel_chat.dart b/lib/screens/channel/channel_chat.dart index 9be61ed..75261a2 100644 --- a/lib/screens/channel/channel_chat.dart +++ b/lib/screens/channel/channel_chat.dart @@ -21,7 +21,7 @@ import 'package:solian/widgets/app_bar_title.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/chat_event.dart'; -import 'package:solian/widgets/chat/chat_event_action.dart'; +import 'package:solian/widgets/chat/chat_event_list.dart'; import 'package:solian/widgets/chat/chat_message_input.dart'; import 'package:solian/widgets/current_state_action.dart'; @@ -111,7 +111,7 @@ class _ChannelChatScreenState extends State { switch (event.method) { case 'events.new': final payload = Event.fromJson(event.payload!); - _chatController.receiveEvent(payload); + _chatController.receiveEvent(payload); break; case 'calls.new': final payload = Call.fromJson(event.payload!); @@ -124,12 +124,6 @@ class _ChannelChatScreenState extends State { }); } - bool checkMessageMergeable(Event? a, Event? b) { - if (a == null || b == null) return false; - if (a.sender.account.id != b.sender.account.id) return false; - return a.createdAt.difference(b.createdAt).inMinutes <= 3; - } - void showCallPrejoin() { showModalBottomSheet( useRootNavigator: true, @@ -153,50 +147,6 @@ class _ChannelChatScreenState extends State { ); } - Widget buildHistory(context, index) { - bool isMerged = false, hasMerged = false; - if (index > 0) { - hasMerged = checkMessageMergeable( - _chatController.currentEvents[index - 1].data, - _chatController.currentEvents[index].data, - ); - } - if (index + 1 < _chatController.currentEvents.length) { - isMerged = checkMessageMergeable( - _chatController.currentEvents[index].data, - _chatController.currentEvents[index + 1].data, - ); - } - - final item = _chatController.currentEvents[index].data; - - return InkWell( - child: Container( - child: buildHistoryBody(item, isMerged: isMerged).paddingOnly( - top: !isMerged ? 8 : 0, - bottom: !hasMerged ? 8 : 0, - ), - ), - onLongPress: () { - showModalBottomSheet( - useRootNavigator: true, - context: context, - builder: (context) => ChatEventAction( - channel: _channel!, - realm: _channel!.realm, - item: item, - onEdit: () { - setState(() => _messageToEditing = item); - }, - onReply: () { - setState(() => _messageToReplying = item); - }, - ), - ); - }, - ); - } - @override void initState() { _chatController = ChatEventController(); @@ -283,120 +233,81 @@ class _ChannelChatScreenState extends State { ), ], ), - body: Stack( + body: Column( children: [ - Column( - children: [ - Expanded( - child: CustomScrollView( - reverse: true, - slivers: [ - Obx(() { - return SliverList.builder( - key: Key('chat-history#${_channel!.id}'), - itemCount: _chatController.currentEvents.length, - itemBuilder: buildHistory, - ); - }), - Obx(() { - final amount = _chatController.totalEvents - - _chatController.currentEvents.length; - - if (amount.value <= 0 || - _chatController.isLoading.isTrue) { - return const SliverToBoxAdapter(child: SizedBox()); - } - - return SliverToBoxAdapter( - child: ListTile( - tileColor: - Theme.of(context).colorScheme.surfaceContainerLow, - leading: const Icon(Icons.sync_disabled), - title: Text('messageUnsync'.tr), - subtitle: Text('messageUnsyncCaption'.trParams({ - 'count': amount.string, - })), - onTap: () { - _chatController.loadEvents( - _channel!, - widget.realm, - ); - }, - ), - ); - }), - Obx(() { - if (_chatController.isLoading.isFalse) { - return const SliverToBoxAdapter(child: SizedBox()); - } - - return SliverToBoxAdapter( - child: const LinearProgressIndicator().animate().slideY(), - ); - }), - ], - ), - ), - ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), - child: SafeArea( - child: ChatMessageInput( - edit: _messageToEditing, - reply: _messageToReplying, - realm: widget.realm, - placeholder: placeholder, - channel: _channel!, - onSent: (Event item) { - setState(() { - _chatController.addPendingEvent(item); - }); - }, - onReset: () { - setState(() { - _messageToReplying = null; - _messageToEditing = null; - }); - }, - ), - ), - ), - ), - ], - ), if (_ongoingCall != null) - Positioned( - top: 0, - left: 0, - right: 0, - child: MaterialBanner( - padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4), - leading: const Icon(Icons.call_received), - backgroundColor: Theme.of(context).colorScheme.surfaceContainer, - dividerColor: Colors.transparent, - content: Text('callOngoing'.tr), - actions: [ - Obx(() { - if (call.current.value == null) { - return TextButton( - onPressed: showCallPrejoin, - child: Text('callJoin'.tr), - ); - } else if (call.channel.value?.id == _channel?.id) { - return TextButton( - onPressed: () => call.gotoScreen(context), - child: Text('callResume'.tr), - ); - } else { - return TextButton( - onPressed: null, - child: Text('callJoin'.tr), - ); - } - }) - ], + MaterialBanner( + padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4), + leading: const Icon(Icons.call_received), + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + dividerColor: Colors.transparent, + content: Text('callOngoing'.tr), + actions: [ + Obx(() { + if (call.current.value == null) { + return TextButton( + onPressed: showCallPrejoin, + child: Text('callJoin'.tr), + ); + } else if (call.channel.value?.id == _channel?.id) { + return TextButton( + onPressed: () => call.gotoScreen(context), + child: Text('callResume'.tr), + ); + } else { + return TextButton( + onPressed: null, + child: Text('callJoin'.tr), + ); + } + }) + ], + ), + Expanded( + child: ChatEventList( + scope: widget.realm, + channel: _channel!, + chatController: _chatController, + onEdit: (item) { + setState(() => _messageToEditing = item); + }, + onReply: (item) { + setState(() => _messageToReplying = item); + }, + ), + ), + Obx(() { + if (_chatController.isLoading.isTrue) { + return const LinearProgressIndicator().animate().slideY(); + } else { + return const SizedBox(); + } + }), + ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), + child: SafeArea( + child: ChatMessageInput( + edit: _messageToEditing, + reply: _messageToReplying, + realm: widget.realm, + placeholder: placeholder, + channel: _channel!, + onSent: (Event item) { + setState(() { + _chatController.addPendingEvent(item); + }); + }, + onReset: () { + setState(() { + _messageToReplying = null; + _messageToEditing = null; + }); + }, + ), ), ), + ), ], ), ); diff --git a/lib/translations.dart b/lib/translations.dart index 0c09f3a..3e7196a 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -178,8 +178,8 @@ class SolianMessages extends Translations { 'channelNotifyLevelNone': 'Ignore all', 'channelNotifyLevelApplied': 'Your notification settings has been applied.', - 'messageUnsync': 'Messages Un-synced', - 'messageUnsyncCaption': '@count message(s) still in un-synced.', + 'messageUnSync': 'Messages Un-synced', + 'messageUnSyncCaption': '@count message(s) still in un-synced.', 'messageSending': 'Sending...', 'messageEditDesc': 'Edited message @id', 'messageDeleteDesc': 'Deleted message @id', @@ -408,8 +408,8 @@ class SolianMessages extends Translations { 'channelNotifyLevelMentioned': '仅提及', 'channelNotifyLevelNone': '忽略一切', 'channelNotifyLevelApplied': '你的通知设置已经应用。', - 'messageUnsync': '消息未同步', - 'messageUnsyncCaption': '还有 @count 条消息未同步', + 'messageUnSync': '消息未同步', + 'messageUnSyncCaption': '还有 @count 条消息未同步', 'messageSending': '消息发送中…', 'messageEditDesc': '修改了消息 @id', 'messageDeleteDesc': '删除了消息 @id', diff --git a/lib/widgets/chat/chat_event_list.dart b/lib/widgets/chat/chat_event_list.dart new file mode 100644 index 0000000..310ce5e --- /dev/null +++ b/lib/widgets/chat/chat_event_list.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/controllers/chat_events_controller.dart'; +import 'package:solian/models/channel.dart'; +import 'package:solian/models/event.dart'; +import 'package:solian/widgets/chat/chat_event.dart'; +import 'package:solian/widgets/chat/chat_event_action.dart'; + +class ChatEventList extends StatelessWidget { + final String scope; + final Channel channel; + final ChatEventController chatController; + + final Function(Event) onEdit; + final Function(Event) onReply; + + const ChatEventList({ + super.key, + this.scope = 'global', + required this.channel, + required this.chatController, + required this.onEdit, + required this.onReply, + }); + + bool checkMessageMergeable(Event? a, Event? b) { + if (a == null || b == null) return false; + if (a.sender.account.id != b.sender.account.id) return false; + return a.createdAt.difference(b.createdAt).inMinutes <= 3; + } + + @override + Widget build(BuildContext context) { + return CustomScrollView( + reverse: true, + slivers: [ + Obx(() { + return SliverList.builder( + key: Key('chat-history#${channel.id}'), + itemCount: chatController.currentEvents.length, + itemBuilder: (context, index) { + bool isMerged = false, hasMerged = false; + if (index > 0) { + hasMerged = checkMessageMergeable( + chatController.currentEvents[index - 1].data, + chatController.currentEvents[index].data, + ); + } + if (index + 1 < chatController.currentEvents.length) { + isMerged = checkMessageMergeable( + chatController.currentEvents[index].data, + chatController.currentEvents[index + 1].data, + ); + } + + final item = chatController.currentEvents[index].data; + + return InkWell( + child: RepaintBoundary( + child: ChatEvent( + key: Key('m${item.uuid}'), + item: item, + isMerged: isMerged, + chatController: chatController, + ).paddingOnly( + top: !isMerged ? 8 : 0, + bottom: !hasMerged ? 8 : 0, + ), + ), + onLongPress: () { + showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) => ChatEventAction( + channel: channel, + realm: channel.realm, + item: item, + onEdit: () { + onEdit(item); + }, + onReply: () { + onReply(item); + }, + ), + ); + }, + ); + }, + ); + }), + Obx(() { + final amount = + chatController.totalEvents - chatController.currentEvents.length; + + if (amount.value <= 0 || chatController.isLoading.isTrue) { + return const SliverToBoxAdapter(child: SizedBox()); + } + + return SliverToBoxAdapter( + child: ListTile( + tileColor: Theme.of(context).colorScheme.surfaceContainerLow, + leading: const Icon(Icons.sync_disabled), + title: Text('messageUnSync'.tr), + subtitle: Text('messageUnSyncCaption'.trParams({ + 'count': amount.string, + })), + onTap: () { + chatController.loadEvents(channel, scope); + }, + ), + ); + }), + ], + ); + } +}