import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import '../stream_chat_flutter.dart'; import 'channel_preview.dart'; import 'stream_channel.dart'; import 'stream_chat.dart'; /// Callback called when tapping on a channel typedef ChannelTapCallback = void Function(Channel, Widget); /// Builder used to create a custom [ChannelPreview] from a [Channel] typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png) /// /// It shows the list of current channels. /// /// ```dart /// class ChannelListPage extends StatelessWidget { /// @override /// Widget build(BuildContext context) { /// return Scaffold( /// body: ChannelListView( /// filter: { /// 'members': { /// '\$in': [StreamChat.of(context).user.id], /// } /// }, /// sort: [SortOption('last_message_at')], /// pagination: PaginationParams( /// limit: 20, /// ), /// channelWidget: ChannelPage(), /// ), /// ); /// } /// } /// ``` /// /// /// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels. /// The widget uses a [ListView.custom] to render the list of channels. /// /// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. /// Modify it to change the widget appearance. class ChannelListView extends StatefulWidget { ChannelListView({ Key key, this.filter, this.options, this.sort, this.pagination, this.onChannelTap, this.channelWidget, this.channelPreviewBuilder, this.errorBuilder, }) : super(key: key); final Widget Function(Error error) errorBuilder; /// The query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. /// You can also filter other built-in channel fields. final Map filter; /// Query channels options. /// /// state: if true returns the Channel state /// watch: if true listen to changes to this Channel in real time. final Map options; /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can be provided. /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// Direction can be ascending or descending. final List sort; /// Pagination parameters /// limit: the number of channels to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel final PaginationParams pagination; /// Function called when tapping on a channel /// By default it calls [Navigator.push] building a [MaterialPageRoute] /// with the widget [channelWidget] as child. final ChannelTapCallback onChannelTap; /// Widget used when opening a channel final Widget channelWidget; /// Builder used to create a custom channel preview final ChannelPreviewBuilder channelPreviewBuilder; @override _ChannelListViewState createState() => _ChannelListViewState(); } class _ChannelListViewState extends State { final ScrollController _scrollController = ScrollController(); @override Widget build(BuildContext context) { final streamChat = StreamChat.of(context); return RefreshIndicator( onRefresh: () async { return streamChat.queryChannels( filter: widget.filter, sortOptions: widget.sort, paginationParams: widget.pagination, options: widget.options, ); }, child: StreamBuilder>( stream: streamChat.channelsStream, builder: (context, snapshot) { if (false && snapshot.hasError) { if (snapshot.error is Error) { print((snapshot.error as Error).stackTrace); } if (widget.errorBuilder != null) { return widget.errorBuilder(snapshot.error); } var message = snapshot.error.toString(); if (snapshot.error is DioError) { final dioError = snapshot.error as DioError; if (dioError.type == DioErrorType.RESPONSE) { message = dioError.message; } else { message = 'Check your connection and retry'; } } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text.rich( TextSpan( children: [ WidgetSpan( child: Padding( padding: const EdgeInsets.only( right: 2.0, ), child: Icon(Icons.error_outline), ), ), TextSpan(text: 'Error loading channels'), ], ), style: Theme.of(context).textTheme.title, ), Padding( padding: const EdgeInsets.only( top: 16.0, ), child: Text(message), ), FlatButton( onPressed: () { streamChat.queryChannels( filter: widget.filter, sortOptions: widget.sort, paginationParams: widget.pagination, options: widget.options, ); }, child: Text('Retry'), ), ], ), ); } if (!snapshot.hasData) { return Center( child: CircularProgressIndicator(), ); } final channels = snapshot.data; return ListView.custom( physics: AlwaysScrollableScrollPhysics(), controller: _scrollController, childrenDelegate: SliverChildBuilderDelegate( (context, i) { return _itemBuilder(context, i, channels); }, childCount: (channels.length * 2) + 1, findChildIndexCallback: (key) { final ValueKey valueKey = key; final index = channels.indexWhere( (channel) => 'CHANNEL-${channel.id}' == valueKey.value); return index != -1 ? (index * 2) : null; }, ), ); }), ); } Widget _itemBuilder(context, int i, List channels) { if (i % 2 != 0) { return _separatorBuilder(context, i); } i = i ~/ 2; final streamChat = StreamChat.of(context); if (i < channels.length) { final channel = channels[i]; final channelClient = streamChat.channels.firstWhere( (c) => c.cid == channel.cid, orElse: () => null, ); ChannelTapCallback onTap; if (widget.onChannelTap != null) { onTap = widget.onChannelTap; } else { onTap = (client, _) { Navigator.push( context, MaterialPageRoute( builder: (context) { return StreamChannel( child: widget.channelWidget, channel: client, ); }, ), ); }; } return StreamChannel( key: ValueKey('CHANNEL-${channelClient.id}'), channel: channelClient, child: StreamBuilder( initialData: channelClient.updatedAt, stream: channelClient.updatedAtStream, builder: (context, snapshot) { Widget child; if (widget.channelPreviewBuilder != null) { child = Stack( children: [ widget.channelPreviewBuilder( context, channelClient, ), Positioned.fill( child: Material( color: Colors.transparent, child: InkWell( onTap: () { onTap(channelClient, widget.channelWidget); }, ), ), ), ], ); } else { child = ChannelPreview( channel: channelClient, onTap: (channelClient) { onTap(channelClient, widget.channelWidget); }, ); } return child; }, ), ); } else { return _buildQueryProgressIndicator(context, streamChat); } } Widget _buildQueryProgressIndicator(context, StreamChatState streamChat) { return StreamBuilder( stream: streamChat.queryChannelsLoading, initialData: false, builder: (context, snapshot) { if (snapshot.hasError) { return Container( color: Color(0xffd0021B).withAlpha(26), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Center( child: Text('Error loading channels'), ), ), ); } return Container( height: 100, padding: EdgeInsets.all(32), child: Center( child: snapshot.data ? CircularProgressIndicator() : Container(), ), ); }); } Widget _separatorBuilder(context, i) { return Container( height: 1, color: Colors.black.withOpacity(0.1), margin: EdgeInsets.symmetric(horizontal: 16), ); } void _listenChannelPagination(StreamChatState streamChat) { if (_scrollController.position.maxScrollExtent == _scrollController.offset) { streamChat.queryChannels( filter: widget.filter, sortOptions: widget.sort, paginationParams: widget.pagination.copyWith( offset: streamChat.channels.length, ), options: widget.options, ); } } @override void initState() { super.initState(); final streamChat = StreamChat.of(context); streamChat.queryChannels( filter: widget.filter, sortOptions: widget.sort, paginationParams: widget.pagination, options: widget.options, ); _scrollController.addListener(() { _listenChannelPagination(streamChat); }); } }