LCOV - code coverage report
Current view: top level - src - stream_channel.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 94 133 70.7 %
Date: 2021-05-27 10:59:48 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : 
       3             : import 'package:collection/collection.dart';
       4             : import 'package:flutter/foundation.dart';
       5             : import 'package:flutter/material.dart';
       6             : import 'package:rxdart/rxdart.dart';
       7             : import 'package:stream_chat/stream_chat.dart';
       8             : 
       9             : /// Specifies query direction for pagination
      10          10 : enum QueryDirection {
      11             :   /// Query earlier messages
      12             :   top,
      13             : 
      14             :   /// Query later messages
      15             :   bottom,
      16             : }
      17             : 
      18             : /// Widget used to provide information about the channel to the widget tree
      19             : ///
      20             : /// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
      21             : class StreamChannel extends StatefulWidget {
      22             :   /// Creates a new instance of [StreamChannel]. Both [child] and [client] must
      23             :   /// be supplied and not null.
      24           2 :   const StreamChannel({
      25             :     Key? key,
      26             :     required this.child,
      27             :     required this.channel,
      28             :     this.showLoading = true,
      29             :     this.initialMessageId,
      30           2 :   }) : super(key: key);
      31             : 
      32             :   /// The child of the widget
      33             :   final Widget child;
      34             : 
      35             :   /// [channel] specifies the channel with which child should be wrapped
      36             :   final Channel channel;
      37             : 
      38             :   /// Shows a loading indicator
      39             :   final bool showLoading;
      40             : 
      41             :   /// If passed the channel will load from this particular message.
      42             :   final String? initialMessageId;
      43             : 
      44             :   /// Use this method to get the current [StreamChannelState] instance
      45           1 :   static StreamChannelState of(BuildContext context) {
      46             :     StreamChannelState? streamChannelState;
      47             : 
      48           1 :     streamChannelState = context.findAncestorStateOfType<StreamChannelState>();
      49             : 
      50             :     assert(
      51           1 :       streamChannelState != null,
      52             :       'You must have a StreamChannel widget at the top of your widget tree',
      53             :     );
      54             : 
      55             :     return streamChannelState!;
      56             :   }
      57             : 
      58           2 :   @override
      59           2 :   StreamChannelState createState() => StreamChannelState();
      60             : }
      61             : 
      62             : // ignore: public_member_api_docs
      63             : class StreamChannelState extends State<StreamChannel> {
      64             :   /// Current channel
      65           6 :   Channel get channel => widget.channel;
      66             : 
      67             :   /// InitialMessageId
      68           6 :   String? get initialMessageId => widget.initialMessageId;
      69             : 
      70             :   /// Current channel state stream
      71           0 :   Stream<ChannelState>? get channelStateStream =>
      72           0 :       widget.channel.state?.channelStateStream;
      73             : 
      74             :   final _queryTopMessagesController = BehaviorSubject.seeded(false);
      75             :   final _queryBottomMessagesController = BehaviorSubject.seeded(false);
      76             : 
      77             :   /// The stream notifying the state of [_queryTopMessages] call
      78           0 :   Stream<bool> get queryTopMessages => _queryTopMessagesController.stream;
      79             : 
      80             :   /// The stream notifying the state of [_queryBottomMessages] call
      81           0 :   Stream<bool> get queryBottomMessages => _queryBottomMessagesController.stream;
      82             : 
      83             :   bool _topPaginationEnded = false;
      84             :   bool _bottomPaginationEnded = false;
      85             : 
      86           1 :   Future<void> _queryTopMessages({
      87             :     int limit = 20,
      88             :     bool preferOffline = false,
      89             :   }) async {
      90           1 :     if (_topPaginationEnded ||
      91           3 :         _queryTopMessagesController.value == true ||
      92           2 :         channel.state == null) {
      93             :       return;
      94             :     }
      95           2 :     _queryTopMessagesController.add(true);
      96             : 
      97           4 :     if (channel.state!.messages.isEmpty) {
      98           0 :       return _queryTopMessagesController.add(false);
      99             :     }
     100             : 
     101           4 :     final oldestMessage = channel.state!.messages.first;
     102             : 
     103             :     try {
     104           1 :       final state = await queryBeforeMessage(
     105           1 :         oldestMessage.id,
     106             :         limit: limit,
     107             :         preferOffline: preferOffline,
     108             :       );
     109           0 :       if (state.messages.isEmpty || state.messages.length < limit) {
     110           0 :         _topPaginationEnded = true;
     111             :       }
     112           0 :       _queryTopMessagesController.add(false);
     113             :     } catch (e, stk) {
     114           2 :       _queryTopMessagesController.addError(e, stk);
     115             :     }
     116             :   }
     117             : 
     118           0 :   Future<void> _queryBottomMessages({
     119             :     int limit = 20,
     120             :     bool preferOffline = false,
     121             :   }) async {
     122           0 :     if (_bottomPaginationEnded ||
     123           0 :         _queryBottomMessagesController.value == true ||
     124           0 :         channel.state == null ||
     125           0 :         channel.state!.isUpToDate == true) return;
     126           0 :     _queryBottomMessagesController.add(true);
     127             : 
     128           0 :     if (channel.state!.messages.isEmpty) {
     129           0 :       return _queryBottomMessagesController.add(false);
     130             :     }
     131             : 
     132           0 :     final recentMessage = channel.state!.messages.last;
     133             : 
     134             :     try {
     135           0 :       final state = await queryAfterMessage(
     136           0 :         recentMessage.id,
     137             :         limit: limit,
     138             :         preferOffline: preferOffline,
     139             :       );
     140           0 :       if (state.messages.isEmpty || state.messages.length < limit) {
     141           0 :         _bottomPaginationEnded = true;
     142             :       }
     143           0 :       _queryBottomMessagesController.add(false);
     144             :     } catch (e, stk) {
     145           0 :       _queryBottomMessagesController.addError(e, stk);
     146             :     }
     147             :   }
     148             : 
     149             :   /// Calls [channel.query] updating [queryMessage] stream
     150           1 :   Future<void> queryMessages({QueryDirection? direction = QueryDirection.top}) {
     151           2 :     if (direction == QueryDirection.top) return _queryTopMessages();
     152           0 :     return _queryBottomMessages();
     153             :   }
     154             : 
     155             :   /// Calls [channel.getReplies] updating [queryMessage] stream
     156           1 :   Future<void> getReplies(
     157             :     String parentId, {
     158             :     int limit = 50,
     159             :     bool preferOffline = false,
     160             :   }) async {
     161           1 :     if (_topPaginationEnded ||
     162           3 :         _queryTopMessagesController.value == true ||
     163           2 :         channel.state == null) return;
     164           2 :     _queryTopMessagesController.add(true);
     165             : 
     166             :     Message? message;
     167           4 :     if (channel.state!.threads.containsKey(parentId)) {
     168           4 :       final thread = channel.state!.threads[parentId]!;
     169           1 :       if (thread.isNotEmpty) {
     170           1 :         message = thread.first;
     171             :       }
     172             :     }
     173             : 
     174             :     try {
     175           2 :       final response = await channel.getReplies(
     176             :         parentId,
     177           1 :         PaginationParams(
     178           1 :           lessThan: message?.id,
     179             :           limit: limit,
     180             :         ),
     181             :         preferOffline: preferOffline,
     182             :       );
     183           0 :       if (response.messages.isEmpty || response.messages.length < limit) {
     184           0 :         _topPaginationEnded = true;
     185             :       }
     186           0 :       _queryTopMessagesController.add(false);
     187             :     } catch (e, stk) {
     188           2 :       _queryTopMessagesController.addError(e, stk);
     189             :     }
     190             :   }
     191             : 
     192             :   /// Query the channel members and watchers
     193           0 :   Future<void> queryMembersAndWatchers() async {
     194           0 :     final _members = channel.state?.members;
     195             :     if (_members != null) {
     196           0 :       await widget.channel.query(
     197           0 :         membersPagination: PaginationParams(
     198           0 :           offset: _members.length,
     199             :           limit: 100,
     200             :         ),
     201           0 :         watchersPagination: PaginationParams(
     202           0 :           offset: _members.length,
     203             :           limit: 100,
     204             :         ),
     205             :       );
     206             :     } else {
     207             :       return;
     208             :     }
     209             :   }
     210             : 
     211             :   /// Loads channel at specific message
     212           1 :   Future<void> loadChannelAtMessage(
     213             :     String? messageId, {
     214             :     int before = 20,
     215             :     int after = 20,
     216             :     bool preferOffline = false,
     217             :   }) =>
     218           1 :       _queryAtMessage(
     219             :         messageId: messageId,
     220             :         before: before,
     221             :         after: after,
     222             :         preferOffline: preferOffline,
     223             :       );
     224             : 
     225           2 :   Future<List<ChannelState>> _queryAtMessage({
     226             :     String? messageId,
     227             :     int before = 20,
     228             :     int after = 20,
     229             :     bool preferOffline = false,
     230             :   }) async {
     231           4 :     if (channel.state == null) return [];
     232           6 :     channel.state!.isUpToDate = false;
     233           6 :     channel.state!.truncate();
     234             : 
     235             :     if (messageId == null) {
     236           3 :       await channel.query(
     237           1 :         messagesPagination: PaginationParams(
     238             :           limit: before,
     239             :         ),
     240             :         preferOffline: preferOffline,
     241             :       );
     242           3 :       channel.state!.isUpToDate = true;
     243           1 :       return [];
     244             :     }
     245             : 
     246           2 :     return Future.wait([
     247           1 :       queryBeforeMessage(
     248             :         messageId,
     249             :         limit: before,
     250             :         preferOffline: preferOffline,
     251             :       ),
     252           1 :       queryAfterMessage(
     253             :         messageId,
     254             :         limit: after,
     255             :         preferOffline: preferOffline,
     256             :       ),
     257             :     ]);
     258             :   }
     259             : 
     260             :   ///
     261           2 :   Future<ChannelState> queryBeforeMessage(
     262             :     String messageId, {
     263             :     int limit = 20,
     264             :     bool preferOffline = false,
     265             :   }) =>
     266           4 :       channel.query(
     267           2 :         messagesPagination: PaginationParams(
     268             :           lessThan: messageId,
     269             :           limit: limit,
     270             :         ),
     271             :         preferOffline: preferOffline,
     272             :       );
     273             : 
     274             :   ///
     275           1 :   Future<ChannelState> queryAfterMessage(
     276             :     String messageId, {
     277             :     int limit = 20,
     278             :     bool preferOffline = false,
     279             :   }) async {
     280           3 :     final state = await channel.query(
     281           1 :       messagesPagination: PaginationParams(
     282             :         greaterThanOrEqual: messageId,
     283             :         limit: limit,
     284             :       ),
     285             :       preferOffline: preferOffline,
     286             :     );
     287           5 :     if (state.messages.isEmpty || state.messages.length < limit) {
     288           3 :       channel.state?.isUpToDate = true;
     289             :     }
     290             :     return state;
     291             :   }
     292             : 
     293             :   ///
     294           0 :   Future<Message> getMessage(String messageId) async {
     295           0 :     var message = channel.state?.messages.firstWhereOrNull(
     296           0 :       (it) => it.id == messageId,
     297             :     );
     298             :     if (message == null) {
     299           0 :       final response = await channel.getMessagesById([messageId]);
     300           0 :       message = response.messages.first;
     301             :     }
     302             :     return message;
     303             :   }
     304             : 
     305             :   /// Reloads the channel with latest message
     306           2 :   Future<void> reloadChannel() => _queryAtMessage(before: 30);
     307             : 
     308             :   late List<Future<bool>> _futures;
     309             : 
     310           1 :   Future<bool> get _loadChannelAtMessage async {
     311             :     try {
     312           3 :       await loadChannelAtMessage(initialMessageId);
     313             :       return true;
     314             :     } catch (_) {
     315             :       rethrow;
     316             :     }
     317             :   }
     318             : 
     319           2 :   @override
     320             :   void initState() {
     321           2 :     super.initState();
     322           2 :     _populateFutures();
     323             :   }
     324             : 
     325           2 :   void _populateFutures() {
     326          10 :     _futures = [widget.channel.initialized];
     327           2 :     if (initialMessageId != null) {
     328           3 :       _futures.add(_loadChannelAtMessage);
     329             :     }
     330             :   }
     331             : 
     332           1 :   @override
     333             :   void didUpdateWidget(covariant StreamChannel oldWidget) {
     334           3 :     if (oldWidget.initialMessageId != initialMessageId) {
     335           1 :       _populateFutures();
     336             :     }
     337           1 :     super.didUpdateWidget(oldWidget);
     338             :   }
     339             : 
     340           2 :   @override
     341             :   void dispose() {
     342           4 :     _queryTopMessagesController.close();
     343           4 :     _queryBottomMessagesController.close();
     344           2 :     super.dispose();
     345             :   }
     346             : 
     347           2 :   @override
     348             :   Widget build(BuildContext context) {
     349           2 :     Widget child = FutureBuilder<List<bool>>(
     350           4 :       future: Future.wait(_futures),
     351           2 :       initialData: [
     352           4 :         channel.state != null,
     353           3 :         if (initialMessageId != null) false,
     354             :       ],
     355           2 :       builder: (context, snapshot) {
     356           2 :         if (snapshot.hasError) {
     357           2 :           var message = snapshot.error.toString();
     358           2 :           if (snapshot.error is DioError) {
     359           1 :             final dioError = snapshot.error as DioError?;
     360           2 :             if (dioError?.type == DioErrorType.response) {
     361           1 :               message = dioError!.message;
     362             :             } else {
     363             :               message = 'Check your connection and retry';
     364             :             }
     365             :           }
     366           2 :           return Center(child: Text(message));
     367             :         }
     368           4 :         final initialized = snapshot.data![0];
     369             :         // ignore: avoid_bool_literals_in_conditional_expressions
     370           4 :         final dataLoaded = initialMessageId == null ? true : snapshot.data![1];
     371           4 :         if (widget.showLoading && (!initialized || !dataLoaded)) {
     372             :           return const Center(
     373             :             child: CircularProgressIndicator(),
     374             :           );
     375             :         }
     376           4 :         return widget.child;
     377             :       },
     378             :     );
     379           2 :     if (initialMessageId != null) {
     380           1 :       child = Material(child: child);
     381             :     }
     382             :     return child;
     383             :   }
     384             : }

Generated by: LCOV version 1.14