From 3bcdc6728594ad1f537f1672684cb1d3e0b4e3a8 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 7 May 2024 23:07:51 +0800 Subject: [PATCH] :recycle: Better chat connection method --- lib/providers/chat.dart | 100 ++++++++++++++++++- lib/screens/chat/chat.dart | 137 ++++++++------------------ lib/screens/chat/chat_list.dart | 12 +-- lib/widgets/chat/chat_maintainer.dart | 99 ------------------- lib/widgets/chat/message_editor.dart | 2 +- 5 files changed, 142 insertions(+), 208 deletions(-) delete mode 100644 lib/widgets/chat/chat_maintainer.dart diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart index 23b9694..58adcbf 100644 --- a/lib/providers/chat.dart +++ b/lib/providers/chat.dart @@ -3,11 +3,15 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:livekit_client/livekit_client.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:solian/models/call.dart'; import 'package:solian/models/channel.dart'; +import 'package:solian/models/message.dart'; +import 'package:solian/models/packet.dart'; +import 'package:solian/models/pagination.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/utils/service_url.dart'; import 'package:solian/widgets/chat/call/exts.dart'; @@ -22,8 +26,11 @@ class ChatProvider extends ChangeNotifier { Call? ongoingCall; Channel? focusChannel; + String? focusChannelRealm; ChatCallInstance? currentCall; + PagingController? historyPagingController; + Future connect(AuthProvider auth) async { if (auth.client == null) await auth.loadClient(); if (!await auth.isAuthorized()) return null; @@ -39,19 +46,107 @@ class ChatProvider extends ChangeNotifier { ); final channel = WebSocketChannel.connect(uri); - await channel.ready; + + isOpened = true; + + channel.stream.listen( + (event) { + final result = NetworkPackage.fromJson(jsonDecode(event)); + if (focusChannel == null || historyPagingController == null) return; + switch (result.method) { + case 'messages.new': + final payload = Message.fromJson(result.payload!); + if (payload.channelId == focusChannel?.id) { + historyPagingController?.itemList?.insert(0, payload); + } + break; + case 'messages.update': + final payload = Message.fromJson(result.payload!); + if (payload.channelId == focusChannel?.id) { + historyPagingController?.itemList = + historyPagingController?.itemList?.map((x) => x.id == payload.id ? payload : x).toList(); + } + break; + case 'messages.burnt': + final payload = Message.fromJson(result.payload!); + if (payload.channelId == focusChannel?.id) { + historyPagingController?.itemList = + historyPagingController?.itemList?.where((x) => x.id != payload.id).toList(); + } + break; + case 'calls.new': + final payload = Call.fromJson(result.payload!); + if (payload.channelId == focusChannel?.id) { + setOngoingCall(payload); + } + break; + case 'calls.end': + final payload = Call.fromJson(result.payload!); + if (payload.channelId == focusChannel?.id) { + setOngoingCall(null); + } + break; + } + notifyListeners(); + }, + onError: (_, __) => connect(auth), + onDone: () => connect(auth), + ); return channel; } - Future fetchChannel(AuthProvider auth, String alias, String realm) async { + Future fetchMessages(int pageKey, BuildContext context) async { + final auth = context.read(); + if (!await auth.isAuthorized()) return; + if (focusChannel == null || focusChannelRealm == null) return; + + final offset = pageKey; + const take = 10; + + var uri = getRequestUri( + 'messaging', + '/api/channels/$focusChannelRealm/${focusChannel!.alias}/messages?take=$take&offset=$offset', + ); + + var res = await auth.client!.get(uri); + if (res.statusCode == 200) { + final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); + final items = result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty(); + final isLastPage = (result.count - pageKey) < take; + if (isLastPage || result.data == null) { + historyPagingController!.appendLastPage(items); + } else { + final nextPageKey = pageKey + items.length; + historyPagingController!.appendPage(items, nextPageKey); + } + } else if (res.statusCode == 403) { + historyPagingController!.appendLastPage([]); + } else { + historyPagingController!.error = utf8.decode(res.bodyBytes); + } + } + + Future fetchChannel(BuildContext context, AuthProvider auth, String alias, String realm) async { + if (focusChannel != null) { + unFocus(); + } + var uri = getRequestUri('messaging', '/api/channels/$realm/$alias/availability'); var res = await auth.client!.get(uri); if (res.statusCode == 200 || res.statusCode == 403) { final result = jsonDecode(utf8.decode(res.bodyBytes)); focusChannel = Channel.fromJson(result); focusChannel?.isAvailable = res.statusCode == 200; + focusChannelRealm = realm; + + if (historyPagingController == null) { + historyPagingController = PagingController(firstPageKey: 0); + historyPagingController?.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); + } + notifyListeners(); + return focusChannel!; } else { var message = utf8.decode(res.bodyBytes); @@ -110,6 +205,7 @@ class ChatProvider extends ChangeNotifier { void unFocus() { currentCall = null; focusChannel = null; + historyPagingController = null; notifyListeners(); } } diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index ffe5b8b..c354a66 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -5,14 +5,12 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:solian/models/message.dart'; -import 'package:solian/models/pagination.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/chat.dart'; import 'package:solian/router.dart'; import 'package:solian/utils/service_url.dart'; import 'package:solian/utils/theme.dart'; import 'package:solian/widgets/chat/channel_action.dart'; -import 'package:solian/widgets/chat/chat_maintainer.dart'; import 'package:solian/widgets/chat/message.dart'; import 'package:solian/widgets/chat/message_action.dart'; import 'package:solian/widgets/chat/message_editor.dart'; @@ -41,12 +39,12 @@ class ChatScreen extends StatelessWidget { call: chat.ongoingCall, channel: chat.focusChannel!, realm: realm, - onUpdate: () => chat.fetchChannel(auth, chat.focusChannel!.alias, realm), + onUpdate: () => chat.fetchChannel(context, auth, chat.focusChannel!.alias, realm), ), ChannelManageAction( channel: chat.focusChannel!, realm: realm, - onUpdate: () => chat.fetchChannel(auth, chat.focusChannel!.alias, realm), + onUpdate: () => chat.fetchChannel(context, auth, chat.focusChannel!.alias, realm), ), ] : [], @@ -71,40 +69,8 @@ class ChatWidget extends StatefulWidget { class _ChatWidgetState extends State { bool _isReady = false; - final PagingController _pagingController = PagingController(firstPageKey: 0); - late final ChatProvider _chat; - Future fetchMessages(int pageKey, BuildContext context) async { - final auth = context.read(); - if (!await auth.isAuthorized()) return; - - final offset = pageKey; - const take = 10; - - var uri = getRequestUri( - 'messaging', - '/api/channels/${widget.realm}/${widget.alias}/messages?take=$take&offset=$offset', - ); - - var res = await auth.client!.get(uri); - if (res.statusCode == 200) { - final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); - final items = result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty(); - final isLastPage = (result.count - pageKey) < take; - if (isLastPage || result.data == null) { - _pagingController.appendLastPage(items); - } else { - final nextPageKey = pageKey + items.length; - _pagingController.appendPage(items, nextPageKey); - } - } else if (res.statusCode == 403) { - _pagingController.appendLastPage([]); - } else { - _pagingController.error = utf8.decode(res.bodyBytes); - } - } - Future joinChannel() async { final auth = context.read(); if (!await auth.isAuthorized()) return; @@ -117,7 +83,7 @@ class _ChatWidgetState extends State { var res = await auth.client!.post(uri); if (res.statusCode == 200) { setState(() {}); - _pagingController.refresh(); + _chat.historyPagingController?.refresh(); } else { var message = utf8.decode(res.bodyBytes); context.showErrorDialog(message).then((_) { @@ -133,24 +99,6 @@ class _ChatWidgetState extends State { return a.createdAt.difference(b.createdAt).inMinutes <= 5; } - void addMessage(Message item) { - setState(() { - _pagingController.itemList?.insert(0, item); - }); - } - - void updateMessage(Message item) { - setState(() { - _pagingController.itemList = _pagingController.itemList?.map((x) => x.id == item.id ? item : x).toList(); - }); - } - - void deleteMessage(Message item) { - setState(() { - _pagingController.itemList = _pagingController.itemList?.where((x) => x.id != item.id).toList(); - }); - } - Message? _editingItem; Message? _replyingItem; @@ -207,14 +155,15 @@ class _ChatWidgetState extends State { @override void initState() { - _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); - super.initState(); Future.delayed(Duration.zero, () async { final auth = context.read(); + + if (!_chat.isOpened) await _chat.connect(auth); + _chat.fetchOngoingCall(widget.alias, widget.realm); - _chat.fetchChannel(auth, widget.alias, widget.realm).then((result) { + _chat.fetchChannel(context, auth, widget.alias, widget.realm).then((result) { if (result.isAvailable == false) { showUnavailableDialog(); } @@ -227,10 +176,10 @@ class _ChatWidgetState extends State { Widget chatHistoryBuilder(context, item, index) { bool isMerged = false, hasMerged = false; if (index > 0) { - hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item); + hasMerged = getMessageMergeable(_chat.historyPagingController?.itemList?[index - 1], item); } - if (index + 1 < (_pagingController.itemList?.length ?? 0)) { - isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]); + if (index + 1 < (_chat.historyPagingController?.itemList?.length ?? 0)) { + isMerged = getMessageMergeable(item, _chat.historyPagingController?.itemList?[index + 1]); } return InkWell( child: Container( @@ -279,48 +228,40 @@ class _ChatWidgetState extends State { ], ); - if (_chat.focusChannel == null) { + if (_chat.focusChannel == null || _chat.historyPagingController == null) { return const Center(child: CircularProgressIndicator()); } - return ChatMaintainer( - channel: _chat.focusChannel!, - child: Stack( - children: [ - Column( - children: [ - Expanded( - child: PagedListView( - reverse: true, - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - animateTransitions: true, - transitionDuration: 350.ms, - itemBuilder: chatHistoryBuilder, - noItemsFoundIndicatorBuilder: (_) => Container(), - ), + return Stack( + children: [ + Column( + children: [ + Expanded( + child: PagedListView( + reverse: true, + pagingController: _chat.historyPagingController!, + builderDelegate: PagedChildBuilderDelegate( + animateTransitions: true, + transitionDuration: 350.ms, + itemBuilder: chatHistoryBuilder, + noItemsFoundIndicatorBuilder: (_) => Container(), ), ), - ChatMessageEditor( - realm: widget.realm, - channel: widget.alias, - editing: _editingItem, - replying: _replyingItem, - onReset: () => setState(() { - _editingItem = null; - _replyingItem = null; - }), - ), - ], - ), - _chat.ongoingCall != null ? callBanner.animate().slideY() : Container(), - ], - ), - onInsertMessage: (message) => addMessage(message), - onUpdateMessage: (message) => updateMessage(message), - onDeleteMessage: (message) => deleteMessage(message), - onCallStarted: (call) => _chat.setOngoingCall(call), - onCallEnded: () => _chat.setOngoingCall(null), + ), + ChatMessageEditor( + realm: widget.realm, + channel: widget.alias, + editing: _editingItem, + replying: _replyingItem, + onReset: () => setState(() { + _editingItem = null; + _replyingItem = null; + }), + ), + ], + ), + _chat.ongoingCall != null ? callBanner.animate().slideY() : Container(), + ], ); } } diff --git a/lib/screens/chat/chat_list.dart b/lib/screens/chat/chat_list.dart index 8ce03c6..c1b779c 100644 --- a/lib/screens/chat/chat_list.dart +++ b/lib/screens/chat/chat_list.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:solian/models/channel.dart'; import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/chat.dart'; import 'package:solian/router.dart'; import 'package:solian/utils/service_url.dart'; import 'package:solian/utils/theme.dart'; @@ -85,6 +86,7 @@ class _ChatListWidgetState extends State { @override Widget build(BuildContext context) { final auth = context.read(); + final chat = context.watch(); return Scaffold( floatingActionButton: FutureBuilder( @@ -123,14 +125,8 @@ class _ChatListWidgetState extends State { subtitle: Text(element.description), onTap: () async { String? result; - if (SolianRouter.currentRoute.name == 'chat.channel') { - result = await SolianRouter.router.pushReplacementNamed( - widget.realm == null ? 'chat.channel' : 'realms.chat.channel', - pathParameters: { - 'channel': element.alias, - ...(widget.realm == null ? {} : {'realm': widget.realm!}), - }, - ); + if (['chat.channel', 'realms.chat.channel'].contains(SolianRouter.currentRoute.name)) { + chat.fetchChannel(context, auth, element.alias, widget.realm!); } else { result = await SolianRouter.router.pushNamed( widget.realm == null ? 'chat.channel' : 'realms.chat.channel', diff --git a/lib/widgets/chat/chat_maintainer.dart b/lib/widgets/chat/chat_maintainer.dart deleted file mode 100644 index 23f0e95..0000000 --- a/lib/widgets/chat/chat_maintainer.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:solian/models/call.dart'; -import 'package:solian/models/channel.dart'; -import 'package:solian/models/message.dart'; -import 'package:solian/models/packet.dart'; -import 'package:solian/providers/auth.dart'; -import 'package:solian/providers/chat.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ChatMaintainer extends StatefulWidget { - final Widget child; - final Channel channel; - final Function(Message val) onInsertMessage; - final Function(Message val) onUpdateMessage; - final Function(Message val) onDeleteMessage; - final Function(Call val) onCallStarted; - final Function() onCallEnded; - - const ChatMaintainer({ - super.key, - required this.child, - required this.channel, - required this.onInsertMessage, - required this.onUpdateMessage, - required this.onDeleteMessage, - required this.onCallStarted, - required this.onCallEnded, - }); - - @override - State createState() => _ChatMaintainerState(); -} - -class _ChatMaintainerState extends State { - void connect() { - ScaffoldMessenger.of(context).clearSnackBars(); - - final notify = ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.connectingServer), - duration: const Duration(minutes: 1), - ), - ); - - final auth = context.read(); - final chat = context.read(); - - chat.connect(auth).then((snapshot) { - snapshot!.stream.listen( - (event) { - final result = NetworkPackage.fromJson(jsonDecode(event)); - switch (result.method) { - case 'messages.new': - final payload = Message.fromJson(result.payload!); - if (payload.channelId == widget.channel.id) widget.onInsertMessage(payload); - break; - case 'messages.update': - final payload = Message.fromJson(result.payload!); - if (payload.channelId == widget.channel.id) widget.onUpdateMessage(payload); - break; - case 'messages.burnt': - final payload = Message.fromJson(result.payload!); - if (payload.channelId == widget.channel.id) widget.onDeleteMessage(payload); - break; - case 'calls.new': - final payload = Call.fromJson(result.payload!); - if (payload.channelId == widget.channel.id) widget.onCallStarted(payload); - break; - case 'calls.end': - final payload = Call.fromJson(result.payload!); - if (payload.channelId == widget.channel.id) widget.onCallEnded(); - break; - } - }, - onError: (_, __) => connect(), - onDone: () => connect(), - ); - - notify.close(); - }); - } - - @override - void initState() { - Future.delayed(Duration.zero, () { - connect(); - }); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} diff --git a/lib/widgets/chat/message_editor.dart b/lib/widgets/chat/message_editor.dart index 049751b..b1e5390 100644 --- a/lib/widgets/chat/message_editor.dart +++ b/lib/widgets/chat/message_editor.dart @@ -38,7 +38,7 @@ class _ChatMessageEditorState extends State { final _textController = TextEditingController(); final _focusNode = FocusNode(); - List _pendingMessages = List.empty(growable: true); + final List _pendingMessages = List.empty(growable: true); int? _prevEditingId;