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 : }
|