diff --git a/.idea/dictionaries/salvatoregiordano.xml b/.idea/dictionaries/salvatoregiordano.xml
new file mode 100644
index 00000000..afc682e7
--- /dev/null
+++ b/.idea/dictionaries/salvatoregiordano.xml
@@ -0,0 +1,7 @@
+
+
+
+ typings
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 4b2c7bf3..16c6fe9c 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -2,18 +2,17 @@
-
-
-
+
+
+
+
+
+
-
-
-
+
+
-
-
-
@@ -26,38 +25,77 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -65,7 +103,7 @@
-
+
@@ -75,61 +113,15 @@
-
-
-
-
-
-
-
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -152,6 +144,9 @@
onThreadSelect
build
channelClient
+ dispose
+ CHANNEL-
+ _userController
@@ -162,17 +157,22 @@
-
-
+
+
+
+
+
+
+
@@ -185,6 +185,7 @@
+
@@ -206,11 +207,6 @@
-
-
-
-
-
@@ -244,14 +240,6 @@
-
-
-
-
-
-
-
-
@@ -324,6 +312,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -333,13 +346,6 @@
-
-
-
-
-
-
-
@@ -350,14 +356,7 @@
-
-
-
-
-
-
-
-
+
@@ -374,13 +373,6 @@
-
-
-
-
-
-
-
@@ -411,43 +403,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -455,5 +410,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/lib/multiple_conversation.dart b/example/lib/multiple_conversation.dart
new file mode 100644
index 00000000..36855ddb
--- /dev/null
+++ b/example/lib/multiple_conversation.dart
@@ -0,0 +1,78 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+
+void main() async {
+ final client = Client('qk4nn7rpcn75');
+
+ await client.setUser(
+ User(id: "wild-breeze-7"),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo',
+ );
+
+ runApp(StreamChat(
+ client: client,
+ child: MyApp(),
+ ));
+}
+
+class MyApp extends StatefulWidget {
+ @override
+ _MyAppState createState() => _MyAppState();
+}
+
+class _MyAppState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ home: ChannelListPage(),
+ );
+ }
+}
+
+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,
+ ),
+ onChannelTap: (channelClient) {
+ Navigator.push(context, MaterialPageRoute(builder: (context) {
+ return ChannelPage(channelClient);
+ }));
+ },
+ ),
+ );
+ }
+}
+
+class ChannelPage extends StatelessWidget {
+ ChannelPage(this.channelClient);
+
+ final ChannelClient channelClient;
+
+ @override
+ Widget build(BuildContext context) {
+ return StreamChannel(
+ channelClient: channelClient,
+ child: Scaffold(
+ body: Column(
+ children: [
+ Expanded(child: MessageListView()),
+ MessageInput(),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/example/lib/single_conversation.dart b/example/lib/single_conversation.dart
index 2c2718a3..153bfd0f 100644
--- a/example/lib/single_conversation.dart
+++ b/example/lib/single_conversation.dart
@@ -1,74 +1,36 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
-void main() {
+void main() async {
final client = Client('qk4nn7rpcn75');
- runApp(StreamChat(
- client: client,
- child: MyApp(),
- ));
-}
-class MyApp extends StatefulWidget {
- @override
- _MyAppState createState() => _MyAppState();
-}
+ await client.setUser(
+ User(id: "wild-breeze-7"),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo',
+ );
-class _MyAppState extends State {
- ChannelClient _channelClient;
+ final channelClient = client.channel('messaging', id: 'godevs');
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- primarySwatch: Colors.blue,
- ),
- home: ChannelPage(_channelClient),
- );
- }
+ channelClient.watch();
- @override
- void initState() {
- super.initState();
-
- final streamChat = StreamChat.of(context);
- _channelClient = streamChat.client.channel('messaging', id: 'godevs');
-
- streamChat
- .setUser(
- User(id: "wild-breeze-7"),
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo',
- )
- .then((_) {
- _channelClient.watch();
- });
- }
-
- @override
- void dispose() {
- StreamChat.of(context).dispose();
- super.dispose();
- }
-}
-
-class ChannelPage extends StatelessWidget {
- ChannelPage(this.channelClient);
-
- final ChannelClient channelClient;
-
- @override
- Widget build(BuildContext context) {
- return StreamChannel(
- channelClient: channelClient,
- child: Scaffold(
- body: Column(
- children: [
- Expanded(child: MessageListView()),
- MessageInput(),
- ],
+ runApp(
+ MaterialApp(
+ home: StreamChat(
+ client: client,
+ child: StreamChannel(
+ channelClient: channelClient,
+ child: Scaffold(
+ body: Column(
+ children: [
+ Expanded(
+ child: MessageListView(),
+ ),
+ MessageInput(),
+ ],
+ ),
+ ),
),
),
- );
- }
+ ),
+ );
}
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
index 9efb415c..a846cfb6 100644
--- a/example/test/widget_test.dart
+++ b/example/test/widget_test.dart
@@ -7,7 +7,7 @@
import 'package:example/single_conversation.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_testdart';
+import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart
new file mode 100644
index 00000000..f28db180
--- /dev/null
+++ b/lib/src/channel_image.dart
@@ -0,0 +1,25 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+class ChannelImage extends StatelessWidget {
+ const ChannelImage({
+ Key key,
+ @required this.channel,
+ }) : super(key: key);
+
+ final Channel channel;
+
+ @override
+ Widget build(BuildContext context) {
+ return CircleAvatar(
+ radius: 20,
+ backgroundImage: channel.extraData.containsKey('image')
+ ? CachedNetworkImageProvider(channel.extraData['image'] as String)
+ : null,
+ child: channel.extraData.containsKey('image')
+ ? null
+ : Text(channel.config.name[0]),
+ );
+ }
+}
diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart
new file mode 100644
index 00000000..3781db08
--- /dev/null
+++ b/lib/src/channel_list_view.dart
@@ -0,0 +1,177 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+import 'channel_preview.dart';
+import 'stream_channel.dart';
+import 'stream_chat.dart';
+
+typedef ChannelPreviewBuilder = Widget Function(BuildContext, ChannelState);
+typedef ChannelTapCallback = void Function(ChannelClient);
+
+class ChannelListView extends StatefulWidget {
+ ChannelListView({
+ Key key,
+ ChannelPreviewBuilder channelPreviewBuilder,
+ this.filter,
+ this.options,
+ this.sort,
+ this.pagination,
+ this.onChannelTap,
+ }) : _channelPreviewBuilder = channelPreviewBuilder,
+ super(key: key);
+
+ final Map filter;
+ final Map options;
+ final List sort;
+ final PaginationParams pagination;
+ final ScrollController _scrollController = ScrollController();
+ final ChannelTapCallback onChannelTap;
+
+ final ChannelPreviewBuilder _channelPreviewBuilder;
+
+ @override
+ _ChannelListViewState createState() => _ChannelListViewState();
+}
+
+class _ChannelListViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final streamChat = StreamChat.of(context);
+ return RefreshIndicator(
+ onRefresh: () async {
+ streamChat.clearChannels();
+ return streamChat.queryChannels(
+ filter: widget.filter,
+ sortOptions: widget.sort,
+ paginationParams: widget.pagination,
+ options: widget.options,
+ );
+ },
+ child: StreamBuilder>(
+ stream: streamChat.channelsStream,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+
+ if (snapshot.hasError) {
+ print((snapshot.error as Error).stackTrace);
+ return Center(
+ child: Text(snapshot.error.toString()),
+ );
+ }
+
+ final channelsStates = snapshot.data;
+ return ListView.custom(
+ physics: AlwaysScrollableScrollPhysics(),
+ controller: widget._scrollController,
+ childrenDelegate: SliverChildBuilderDelegate(
+ (context, i) {
+ return _itemBuilder(context, i, channelsStates);
+ },
+ childCount: (channelsStates.length * 2) + 1,
+ findChildIndexCallback: (key) {
+ final ValueKey valueKey = key;
+ final index = channelsStates.indexWhere(
+ (cs) => 'CHANNEL-${cs.channel.id}' == valueKey.value);
+ return index != -1 ? (index * 2) : null;
+ },
+ ),
+ );
+ }),
+ );
+ }
+
+ Widget _itemBuilder(context, int i, List channelsStates) {
+ if (i % 2 != 0) {
+ return _separatorBuilder(context, i);
+ }
+
+ i = i ~/ 2;
+
+ final streamChat = StreamChat.of(context);
+ if (i < channelsStates.length) {
+ final channelState = channelsStates[i];
+
+ final channelClient =
+ streamChat.client.channelClients[channelState.channel.id];
+
+ Widget child;
+ if (widget._channelPreviewBuilder != null) {
+ child = widget._channelPreviewBuilder(context, channelState);
+ } else {
+ child = ChannelPreview(
+ onTap: widget?.onChannelTap != null
+ ? () {
+ widget?.onChannelTap(channelClient);
+ }
+ : null,
+ );
+ }
+
+ return StreamChannel(
+ key: ValueKey('CHANNEL-${channelClient.id}'),
+ child: child,
+ channelClient: channelClient,
+ );
+ } else {
+ return _buildQueryProgressIndicator(context, streamChat);
+ }
+ }
+
+ Widget _buildQueryProgressIndicator(context, StreamChat streamChat) {
+ return StreamBuilder(
+ stream: streamChat.queryChannelsLoading,
+ initialData: false,
+ builder: (context, snapshot) {
+ 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(StreamChat streamChat) {
+ if (widget._scrollController.position.maxScrollExtent ==
+ widget._scrollController.position.pixels) {
+ 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,
+ );
+
+ widget._scrollController.addListener(() {
+ _listenChannelPagination(streamChat);
+ });
+ }
+}
diff --git a/lib/src/channel_name_text.dart b/lib/src/channel_name_text.dart
new file mode 100644
index 00000000..f31f38c5
--- /dev/null
+++ b/lib/src/channel_name_text.dart
@@ -0,0 +1,19 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+class ChannelNameText extends StatelessWidget {
+ const ChannelNameText({
+ Key key,
+ this.channel,
+ }) : super(key: key);
+
+ final Channel channel;
+
+ @override
+ Widget build(BuildContext context) {
+ return Text(
+ channel.extraData['name'] as String ?? channel.config.name,
+ style: Theme.of(context).textTheme.body2,
+ );
+ }
+}
diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart
new file mode 100644
index 00000000..096e72ec
--- /dev/null
+++ b/lib/src/channel_preview.dart
@@ -0,0 +1,148 @@
+import 'package:date_format/date_format.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+import 'channel_image.dart';
+import 'channel_name_text.dart';
+import 'stream_channel.dart';
+import 'stream_chat.dart';
+
+class ChannelPreview extends StatelessWidget {
+ final VoidCallback onTap;
+
+ const ChannelPreview({
+ Key key,
+ @required this.onTap,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final streamChannel = StreamChannel.of(context);
+ return StreamBuilder(
+ stream: streamChannel.channelClient.state.channelStateStream,
+ initialData: streamChannel.channelState,
+ builder: (context, snapshot) {
+ final channelState = snapshot.data;
+ return _buildChannelPreview(
+ context,
+ channelState,
+ streamChannel,
+ );
+ });
+ }
+
+ StreamChannel _buildChannelPreview(
+ BuildContext context,
+ ChannelState channelState,
+ StreamChannelState streamChannel,
+ ) {
+ return StreamChannel(
+ channelClient:
+ StreamChat.of(context).client.channelClients[channelState.channel.id],
+ child: ListTile(
+ onTap: () {
+ if (onTap != null) {
+ onTap();
+ }
+ },
+ leading: ChannelImage(
+ channel: channelState.channel,
+ ),
+ title: ChannelNameText(
+ channel: channelState.channel,
+ ),
+ subtitle: _buildSubtitle(
+ streamChannel,
+ ),
+ trailing: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ _buildDate(context, channelState.channel.lastMessageAt),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Text _buildDate(BuildContext context, DateTime lastMessageAt) {
+ String stringDate;
+ final now = DateTime.now();
+
+ if (now.year != lastMessageAt.year ||
+ now.month != lastMessageAt.month ||
+ now.day != lastMessageAt.day) {
+ stringDate =
+ '${lastMessageAt.day}/${lastMessageAt.month}/${lastMessageAt.year}';
+ stringDate = formatDate(lastMessageAt, [dd, '/', mm, '/', yyyy]);
+ } else {
+ stringDate = '${lastMessageAt.hour}:${lastMessageAt.minute}';
+ stringDate = formatDate(lastMessageAt, [HH, ':', nn]);
+ }
+
+ return Text(
+ stringDate,
+ style: Theme.of(context).textTheme.caption,
+ );
+ }
+
+ Widget _buildSubtitle(
+ StreamChannelState streamChannel,
+ ) {
+ return StreamBuilder>(
+ stream: streamChannel.channelClient.state.typingEventsStream,
+ initialData: [],
+ builder: (context, snapshot) {
+ final typings = snapshot.data;
+ final opacity =
+ streamChannel.channelClient.state.unreadCount > .0 ? 1.0 : 0.5;
+ return typings.isNotEmpty
+ ? _buildTypings(typings, context, opacity)
+ : _buildLastMessage(context, streamChannel, opacity);
+ });
+ }
+
+ Widget _buildLastMessage(
+ BuildContext context, StreamChannelState streamChannel, double opacity) {
+ final lastMessage = streamChannel.channelState.messages.isNotEmpty
+ ? streamChannel.channelState.messages.last
+ : null;
+ if (lastMessage == null) {
+ return SizedBox.fromSize(
+ size: Size.zero,
+ );
+ }
+
+ final prefix = lastMessage.attachments
+ .map((e) {
+ if (e.type == 'image') {
+ return '📷';
+ } else if (e.type == 'video') {
+ return '🎬';
+ }
+ return null;
+ })
+ .where((e) => e != null)
+ .join(' ');
+
+ return Text(
+ '$prefix ${lastMessage.text ?? ''}',
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context).textTheme.caption.copyWith(
+ color: Colors.black.withOpacity(opacity),
+ ),
+ );
+ }
+
+ Text _buildTypings(List typings, BuildContext context, double opacity) {
+ return Text(
+ '${typings.map((u) => u.extraData.containsKey('name') ? u.extraData['name'] : u.id).join(',')} ${typings.length == 1 ? 'is' : 'are'} typing...',
+ maxLines: 1,
+ style: Theme.of(context).textTheme.caption.copyWith(
+ color: Colors.black.withOpacity(opacity),
+ ),
+ );
+ }
+}
diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart
index 899b1ba2..86e7374b 100644
--- a/lib/src/message_list_view.dart
+++ b/lib/src/message_list_view.dart
@@ -256,7 +256,7 @@ class _MessageListViewState extends State {
_messages = newMessages;
});
} else if (newMessages.first.user.id ==
- streamChannel.channelClient.client.user.id) {
+ streamChannel.channelClient.client.state.user.id) {
_scrollController.jumpTo(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart
index 8b81c85a..bbb96440 100644
--- a/lib/src/stream_channel.dart
+++ b/lib/src/stream_channel.dart
@@ -30,18 +30,18 @@ class StreamChannel extends StatefulWidget {
}
@override
- StreamChannelState createState() => StreamChannelState(channelClient);
+ StreamChannelState createState() => StreamChannelState();
}
class StreamChannelState extends State {
- final ChannelClient channelClient;
+ StreamChannelState();
- StreamChannelState(this.channelClient);
+ ChannelClient get channelClient => widget.channelClient;
- ChannelState get channelState => channelClient.state.channelState;
+ ChannelState get channelState => widget.channelClient.state.channelState;
Stream get channelStateStream =>
- channelClient.state.channelStateStream;
+ widget.channelClient.state.channelStateStream;
final BehaviorSubject _queryMessageController = BehaviorSubject();
@@ -55,7 +55,7 @@ class StreamChannelState extends State {
firstId = channelState.messages.first.id;
}
- channelClient
+ widget.channelClient
.query(
messagesPagination: PaginationParams(
lessThan: firstId,
@@ -73,11 +73,11 @@ class StreamChannelState extends State {
_queryMessageController.add(true);
String firstId;
- if (channelClient.state.threads.containsKey(parentId)) {
- firstId = channelClient.state.threads[parentId].first.id;
+ if (widget.channelClient.state.threads.containsKey(parentId)) {
+ firstId = widget.channelClient.state.threads[parentId].first.id;
}
- return channelClient
+ return widget.channelClient
.getReplies(
parentId,
PaginationParams(
@@ -95,29 +95,26 @@ class StreamChannelState extends State {
@override
void dispose() {
_queryMessageController.close();
- channelClient.dispose();
+ widget.channelClient.dispose();
super.dispose();
}
- @override
- void initState() {
- super.initState();
- }
-
@override
Widget build(BuildContext context) {
- if (channelClient == null) {
+ if (widget.channelClient == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return FutureBuilder(
- future: channelClient.initialized,
+ future: widget.channelClient.initialized,
builder: (context, snapshot) {
if (!snapshot.hasData) {
- return Center(
- child: CircularProgressIndicator(),
+ return Scaffold(
+ body: Center(
+ child: CircularProgressIndicator(),
+ ),
);
} else if (snapshot.hasError) {
return Center(
diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart
index db6328d3..79d447e3 100644
--- a/lib/src/stream_chat.dart
+++ b/lib/src/stream_chat.dart
@@ -27,25 +27,9 @@ class StreamChat extends InheritedWidget {
}));
}
- User get user => _userController.value;
+ User get user => client.state.user;
- Stream get userStream => _userController.stream;
- final BehaviorSubject _userController = BehaviorSubject();
-
- Future setUser(User newUser, [String token]) async {
- _userController.sink.add(null);
-
- try {
- if (token != null) {
- await client.setUser(newUser, token);
- } else {
- await client.setUserWithProvider(newUser);
- }
- _userController.sink.add(newUser);
- } catch (e, stack) {
- _userController.sink.addError(e, stack);
- }
- }
+ Stream get userStream => client.state.userStream;
Stream> get channelsStream => _channelsController.stream;
final BehaviorSubject> _channelsController =
@@ -91,7 +75,6 @@ class StreamChat extends InheritedWidget {
void dispose() {
client.dispose();
_subscriptions.forEach((s) => s.cancel());
- _userController.close();
_queryChannelsLoadingController.close();
_channelsController.close();
}
diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart
index d339da3f..05bf53da 100644
--- a/lib/stream_chat_flutter.dart
+++ b/lib/stream_chat_flutter.dart
@@ -1,5 +1,6 @@
export 'package:stream_chat/stream_chat.dart';
+export 'src/channel_list_view.dart';
export 'src/message_input.dart';
export 'src/message_list_view.dart';
export 'src/message_widget.dart';