Line data Source code
1 : import 'dart:async'; 2 : 3 : import 'package:flutter/foundation.dart'; 4 : import 'package:flutter/material.dart'; 5 : import 'package:rxdart/rxdart.dart'; 6 : import 'package:stream_chat/stream_chat.dart'; 7 : import 'package:stream_chat_flutter_core/src/channel_list_core.dart'; 8 : import 'package:stream_chat_flutter_core/src/stream_chat_core.dart'; 9 : 10 : /// Widget dedicated to the management of a channel list with pagination 11 : /// [ChannelsBloc] is used together with [ChannelListCore] to manage a list of 12 : /// [Channel]s with pagination, re-ordering, querying and other operations 13 : /// associated with [Channel]s. 14 : /// 15 : /// [ChannelsBloc] can be access at anytime by using the static [of] method 16 : /// using Flutter's [BuildContext]. 17 : /// 18 : /// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ 19 : class ChannelsBloc extends StatefulWidget { 20 : /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and 21 : /// not null. 22 2 : const ChannelsBloc({ 23 : Key? key, 24 : required this.child, 25 : this.lockChannelsOrder = false, 26 : this.channelsComparator, 27 : this.shouldAddChannel, 28 2 : }) : super(key: key); 29 : 30 : /// The widget child 31 : final Widget child; 32 : 33 : /// Set this to true to prevent channels to be brought to the top of the list 34 : /// when a new message arrives 35 : final bool lockChannelsOrder; 36 : 37 : /// Comparator used to sort the channels when a message.new event is received 38 : final Comparator<Channel>? channelsComparator; 39 : 40 : /// Function used to evaluate if a channel should be added to the list when a 41 : /// message.new event is received 42 : final bool Function(Event)? shouldAddChannel; 43 : 44 2 : @override 45 2 : ChannelsBlocState createState() => ChannelsBlocState(); 46 : 47 : /// Use this method to get the current [ChannelsBlocState] instance 48 1 : static ChannelsBlocState of(BuildContext context) { 49 : ChannelsBlocState? streamChatState; 50 : 51 1 : streamChatState = context.findAncestorStateOfType<ChannelsBlocState>(); 52 : 53 : assert( 54 1 : streamChatState != null, 55 : 'You must have a ChannelsBloc widget as ancestor', 56 : ); 57 : 58 : return streamChatState!; 59 : } 60 : } 61 : 62 : /// The current state of the [ChannelsBloc]. 63 : class ChannelsBlocState extends State<ChannelsBloc> 64 : with AutomaticKeepAliveClientMixin { 65 : StreamChatCoreState? _streamChatCoreState; 66 : 67 2 : @override 68 : Widget build(BuildContext context) { 69 2 : super.build(context); 70 4 : return widget.child; 71 : } 72 : 73 : /// The current channel list 74 6 : List<Channel>? get channels => _channelsController.valueOrNull; 75 : 76 : /// The current channel list as a stream 77 6 : Stream<List<Channel>> get channelsStream => _channelsController.stream; 78 : 79 : final _queryChannelsLoadingController = BehaviorSubject.seeded(false); 80 : 81 : final BehaviorSubject<List<Channel>> _channelsController = 82 : BehaviorSubject<List<Channel>>(); 83 : 84 : /// The stream notifying the state of queryChannel call 85 1 : Stream<bool> get queryChannelsLoading => 86 2 : _queryChannelsLoadingController.stream; 87 : 88 : final List<Channel> _hiddenChannels = []; 89 : 90 : bool _paginationEnded = false; 91 : 92 : final List<StreamSubscription> _subscriptions = []; 93 : 94 : /// Calls [client.queryChannels] updating [queryChannelsLoading] stream 95 2 : Future<void> queryChannels({ 96 : Filter? filter, 97 : List<SortOption<ChannelModel>>? sortOptions, 98 : PaginationParams paginationParams = const PaginationParams(limit: 30), 99 : Map<String, dynamic>? options, 100 : }) async { 101 4 : final client = _streamChatCoreState!.client; 102 : 103 4 : final clear = paginationParams.offset == 0; 104 : 105 2 : if ((!clear && _paginationEnded) || 106 6 : _queryChannelsLoadingController.value == true) { 107 : return; 108 : } 109 : 110 4 : if (_channelsController.hasValue) { 111 4 : _queryChannelsLoadingController.add(true); 112 : } 113 : 114 : try { 115 6 : final oldChannels = List<Channel>.from(channels ?? []); 116 2 : var newChannels = <Channel>[]; 117 4 : await for (final channels in client.queryChannels( 118 : filter: filter, 119 : sort: sortOptions, 120 : options: options, 121 : paginationParams: paginationParams, 122 2 : )) { 123 : newChannels = channels; 124 : if (clear) { 125 4 : _channelsController.add(channels); 126 : } else { 127 2 : final temp = oldChannels + channels; 128 4 : _channelsController.add(temp); 129 : } 130 4 : if (_channelsController.hasValue && 131 4 : _queryChannelsLoadingController.value) { 132 6 : _queryChannelsLoadingController.sink.add(false); 133 : } 134 : } 135 8 : if (newChannels.isEmpty || newChannels.length < paginationParams.limit) { 136 2 : _paginationEnded = true; 137 : } 138 : } catch (e, stk) { 139 4 : if (_channelsController.hasValue) { 140 2 : _queryChannelsLoadingController.addError(e, stk); 141 : } else { 142 4 : _channelsController.addError(e, stk); 143 : } 144 : } 145 : } 146 : 147 2 : @override 148 : void didChangeDependencies() { 149 4 : final newStreamChatCoreState = StreamChatCore.of(context); 150 : 151 4 : if (newStreamChatCoreState != _streamChatCoreState) { 152 2 : _streamChatCoreState = newStreamChatCoreState; 153 4 : final client = _streamChatCoreState!.client; 154 : 155 2 : _cancelSubscriptions(); 156 4 : if (!widget.lockChannelsOrder) { 157 4 : _subscriptions.add(client 158 2 : .on( 159 : EventType.messageNew, 160 : ) 161 3 : .listen((e) { 162 2 : final newChannels = List<Channel>.from(channels ?? []); 163 5 : final index = newChannels.indexWhere((c) => c.cid == e.cid); 164 2 : if (index != -1) { 165 1 : if (index > 0) { 166 1 : final channel = newChannels.removeAt(index); 167 1 : newChannels.insert(0, channel); 168 : } 169 4 : } else if (widget.shouldAddChannel?.call(e) == true) { 170 : final hiddenIndex = 171 6 : _hiddenChannels.indexWhere((c) => c.cid == e.cid); 172 2 : if (hiddenIndex != -1) { 173 3 : newChannels.insert(0, _hiddenChannels[hiddenIndex]); 174 2 : _hiddenChannels.removeAt(hiddenIndex); 175 : } else { 176 4 : if (client.state.channels[e.cid] != null) { 177 5 : newChannels.insert(0, client.state.channels[e.cid]!); 178 : } 179 : } 180 : } 181 : 182 2 : if (widget.channelsComparator != null) { 183 3 : newChannels.sort(widget.channelsComparator); 184 : } 185 2 : _channelsController.add(newChannels); 186 : })); 187 : } 188 : 189 2 : _subscriptions 190 7 : ..add(client.on(EventType.channelHidden).listen((event) async { 191 2 : final newChannels = List<Channel>.from(channels ?? []); 192 : final channelIndex = 193 5 : newChannels.indexWhere((c) => c.cid == event.cid); 194 2 : if (channelIndex > -1) { 195 1 : final channel = newChannels.removeAt(channelIndex); 196 2 : _hiddenChannels.add(channel); 197 2 : _channelsController.add(newChannels); 198 : } 199 : })) 200 2 : ..add(client 201 2 : .on( 202 : EventType.channelDeleted, 203 : EventType.notificationRemovedFromChannel, 204 : ) 205 3 : .listen((e) { 206 1 : final channel = e.channel; 207 3 : _channelsController.add(List.from( 208 6 : (channels ?? [])..removeWhere((c) => c.cid == channel?.cid))); 209 : })); 210 : } 211 : 212 2 : super.didChangeDependencies(); 213 : } 214 : 215 2 : @override 216 : void dispose() { 217 4 : _channelsController.close(); 218 4 : _queryChannelsLoadingController.close(); 219 2 : _cancelSubscriptions(); 220 2 : super.dispose(); 221 : } 222 : 223 2 : void _cancelSubscriptions() { 224 2 : _subscriptions 225 6 : ..forEach((s) => s.cancel()) 226 2 : ..clear(); 227 : } 228 : 229 2 : @override 230 : bool get wantKeepAlive => true; 231 : }