♻️ Better chat connection method
This commit is contained in:
		| @@ -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<int, Message>? historyPagingController; | ||||
|  | ||||
|   Future<WebSocketChannel?> 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<Channel> fetchChannel(AuthProvider auth, String alias, String realm) async { | ||||
|   Future<void> fetchMessages(int pageKey, BuildContext context) async { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     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<Channel> 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(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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<ChatWidget> { | ||||
|   bool _isReady = false; | ||||
|  | ||||
|   final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0); | ||||
|  | ||||
|   late final ChatProvider _chat; | ||||
|  | ||||
|   Future<void> fetchMessages(int pageKey, BuildContext context) async { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     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<void> joinChannel() async { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     if (!await auth.isAuthorized()) return; | ||||
| @@ -117,7 +83,7 @@ class _ChatWidgetState extends State<ChatWidget> { | ||||
|     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<ChatWidget> { | ||||
|     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<ChatWidget> { | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); | ||||
|  | ||||
|     super.initState(); | ||||
|  | ||||
|     Future.delayed(Duration.zero, () async { | ||||
|       final auth = context.read<AuthProvider>(); | ||||
|  | ||||
|       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<ChatWidget> { | ||||
|     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<ChatWidget> { | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     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<int, Message>( | ||||
|                   reverse: true, | ||||
|                   pagingController: _pagingController, | ||||
|                   builderDelegate: PagedChildBuilderDelegate<Message>( | ||||
|                     animateTransitions: true, | ||||
|                     transitionDuration: 350.ms, | ||||
|                     itemBuilder: chatHistoryBuilder, | ||||
|                     noItemsFoundIndicatorBuilder: (_) => Container(), | ||||
|                   ), | ||||
|     return Stack( | ||||
|       children: [ | ||||
|         Column( | ||||
|           children: [ | ||||
|             Expanded( | ||||
|               child: PagedListView<int, Message>( | ||||
|                 reverse: true, | ||||
|                 pagingController: _chat.historyPagingController!, | ||||
|                 builderDelegate: PagedChildBuilderDelegate<Message>( | ||||
|                   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(), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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<ChatListWidget> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     final chat = context.watch<ChatProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|       floatingActionButton: FutureBuilder( | ||||
| @@ -123,14 +125,8 @@ class _ChatListWidgetState extends State<ChatListWidget> { | ||||
|                   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', | ||||
|   | ||||
| @@ -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<ChatMaintainer> createState() => _ChatMaintainerState(); | ||||
| } | ||||
|  | ||||
| class _ChatMaintainerState extends State<ChatMaintainer> { | ||||
|   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<AuthProvider>(); | ||||
|     final chat = context.read<ChatProvider>(); | ||||
|  | ||||
|     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; | ||||
|   } | ||||
| } | ||||
| @@ -38,7 +38,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | ||||
|   final _textController = TextEditingController(); | ||||
|   final _focusNode = FocusNode(); | ||||
|  | ||||
|   List<int> _pendingMessages = List.empty(growable: true); | ||||
|   final List<int> _pendingMessages = List.empty(growable: true); | ||||
|  | ||||
|   int? _prevEditingId; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user