Line data Source code
1 : import 'dart:async'; 2 : 3 : import 'package:flutter/cupertino.dart'; 4 : import 'package:flutter/foundation.dart'; 5 : import 'package:flutter/material.dart'; 6 : import 'package:stream_chat/stream_chat.dart'; 7 : import 'package:stream_chat_flutter_core/src/stream_channel.dart'; 8 : import 'package:stream_chat_flutter_core/src/typedef.dart'; 9 : 10 : /// [MessageListCore] is a simplified class that allows fetching a list of 11 : /// messages while exposing UI builders. 12 : /// 13 : /// A [MessageListController] is used to paginate data. 14 : /// 15 : /// ```dart 16 : /// class ChannelPage extends StatelessWidget { 17 : /// const ChannelPage({ 18 : /// Key key, 19 : /// }) : super(key: key); 20 : /// 21 : /// @override 22 : /// Widget build(BuildContext context) { 23 : /// return Scaffold( 24 : /// body: Column( 25 : /// children: <Widget>[ 26 : /// Expanded( 27 : /// child: MessageListCore( 28 : /// emptyBuilder: (context) { 29 : /// return Center( 30 : /// child: Text('Nothing here...'), 31 : /// ); 32 : /// }, 33 : /// loadingBuilder: (context) { 34 : /// return Center( 35 : /// child: CircularProgressIndicator(), 36 : /// ); 37 : /// }, 38 : /// messageListBuilder: (context, list) { 39 : /// return MessagesPage(list); 40 : /// }, 41 : /// errorWidgetBuilder: (context, err) { 42 : /// return Center( 43 : /// child: Text('Error'), 44 : /// ); 45 : /// }, 46 : /// ), 47 : /// ), 48 : /// ], 49 : /// ), 50 : /// ); 51 : /// } 52 : /// } 53 : /// ``` 54 : /// 55 : /// 56 : /// Make sure to have a [StreamChannel] ancestor in order to provide the 57 : /// information about the channels. 58 : /// 59 : /// The widget uses a [ListView.custom] to render the list of channels. 60 : /// 61 : class MessageListCore extends StatefulWidget { 62 : /// Instantiate a new [MessageListView]. 63 1 : const MessageListCore({ 64 : Key? key, 65 : required this.loadingBuilder, 66 : required this.emptyBuilder, 67 : required this.messageListBuilder, 68 : required this.errorWidgetBuilder, 69 : this.showScrollToBottom = true, 70 : this.parentMessage, 71 : this.messageListController, 72 : this.messageFilter, 73 1 : }) : super(key: key); 74 : 75 : /// A [MessageListController] allows pagination. 76 : /// Use [ChannelListController.paginateData] pagination. 77 : final MessageListController? messageListController; 78 : 79 : /// Function called when messages are fetched 80 : final Widget Function(BuildContext, List<Message>) messageListBuilder; 81 : 82 : /// Function used to build a loading widget 83 : final WidgetBuilder loadingBuilder; 84 : 85 : /// Function used to build an empty widget 86 : final WidgetBuilder emptyBuilder; 87 : 88 : /// Callback triggered when an error occurs while performing the given 89 : /// request. 90 : /// 91 : /// This parameter can be used to display an error message to users in the 92 : /// event of a connection failure. 93 : final ErrorBuilder errorWidgetBuilder; 94 : 95 : /// If true will show a scroll to bottom message when there are new messages 96 : /// and the scroll offset is not zero. 97 : final bool showScrollToBottom; 98 : 99 : /// If the current message belongs to a `thread`, this property represents the 100 : /// first message or the parent of the conversation. 101 : final Message? parentMessage; 102 : 103 : /// Predicate used to filter messages 104 : final bool Function(Message)? messageFilter; 105 : 106 1 : @override 107 1 : MessageListCoreState createState() => MessageListCoreState(); 108 : } 109 : 110 : /// The current state of the [MessageListCore]. 111 : class MessageListCoreState extends State<MessageListCore> { 112 : StreamChannelState? _streamChannel; 113 : 114 5 : bool get _upToDate => _streamChannel!.channel.state?.isUpToDate ?? true; 115 : 116 3 : bool get _isThreadConversation => widget.parentMessage != null; 117 : 118 6 : OwnUser? get _currentUser => _streamChannel!.channel.client.state.user; 119 : 120 : var _messages = <Message>[]; 121 : 122 1 : @override 123 : Widget build(BuildContext context) { 124 1 : final messagesStream = _isThreadConversation 125 4 : ? _streamChannel!.channel.state?.threadsStream 126 6 : .where((threads) => threads.containsKey(widget.parentMessage!.id)) 127 6 : .map((threads) => threads[widget.parentMessage!.id]) 128 4 : : _streamChannel!.channel.state?.messagesStream; 129 : 130 1 : bool defaultFilter(Message m) { 131 5 : final isMyMessage = m.user?.id == _currentUser?.id; 132 4 : final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true; 133 : if (isDeletedOrShadowed && !isMyMessage) return false; 134 : return true; 135 : } 136 : 137 1 : return StreamBuilder<List<Message>?>( 138 2 : stream: messagesStream?.map((messages) => 139 4 : messages?.where(widget.messageFilter ?? defaultFilter).toList( 140 : growable: false, 141 : )), 142 1 : builder: (context, snapshot) { 143 1 : if (snapshot.hasError) { 144 4 : return widget.errorWidgetBuilder(context, snapshot.error!); 145 1 : } else if (!snapshot.hasData) { 146 3 : return widget.loadingBuilder(context); 147 : } else { 148 : final messageList = 149 3 : snapshot.data?.reversed.toList(growable: false) ?? []; 150 2 : if (messageList.isEmpty && !_isThreadConversation) { 151 1 : if (_upToDate) { 152 3 : return widget.emptyBuilder(context); 153 : } 154 : } else { 155 1 : _messages = messageList; 156 : } 157 4 : return widget.messageListBuilder(context, _messages); 158 : } 159 : }, 160 : ); 161 : } 162 : 163 : /// Fetches more messages with updated pagination and updates the widget. 164 : /// 165 : /// Optionally pass the fetch direction, defaults to [QueryDirection.top] 166 1 : Future<void> paginateData({ 167 : QueryDirection direction = QueryDirection.top, 168 : }) { 169 1 : if (!_isThreadConversation) { 170 2 : return _streamChannel!.queryMessages(direction: direction); 171 : } else { 172 0 : return _streamChannel!.getReplies(widget.parentMessage!.id); 173 : } 174 : } 175 : 176 1 : @override 177 : void didChangeDependencies() { 178 2 : final newStreamChannel = StreamChannel.of(context); 179 : 180 2 : if (newStreamChannel != _streamChannel) { 181 2 : if (_streamChannel == null /*only first time*/ && _isThreadConversation) { 182 4 : newStreamChannel.getReplies(widget.parentMessage!.id); 183 : } 184 1 : _streamChannel = newStreamChannel; 185 : } 186 : 187 1 : super.didChangeDependencies(); 188 : } 189 : 190 0 : @override 191 : void didUpdateWidget(covariant MessageListCore oldWidget) { 192 0 : super.didUpdateWidget(oldWidget); 193 : 194 0 : if (widget.messageListController != oldWidget.messageListController) { 195 0 : _setupController(); 196 : } 197 : 198 0 : if (widget.parentMessage?.id != widget.parentMessage?.id) { 199 0 : if (_isThreadConversation) { 200 0 : _streamChannel!.getReplies(widget.parentMessage!.id); 201 : } 202 : } 203 : } 204 : 205 1 : @override 206 : void initState() { 207 1 : _setupController(); 208 : 209 1 : super.initState(); 210 : } 211 : 212 1 : void _setupController() { 213 2 : if (widget.messageListController != null) { 214 4 : widget.messageListController!.paginateData = paginateData; 215 : } 216 : } 217 : 218 1 : @override 219 : void dispose() { 220 1 : if (!_upToDate) { 221 2 : _streamChannel!.reloadChannel(); 222 : } 223 1 : super.dispose(); 224 : } 225 : } 226 : 227 : /// Controller used for paginating data in [ChannelListView] 228 : class MessageListController { 229 : /// Call this function to load further data 230 : Future<void> Function({QueryDirection direction})? paginateData; 231 : }