From c301b51c8381e59e34f78656d1d6fb1df0c86d4e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Dec 2021 17:49:50 +0530 Subject: [PATCH] refactor(ui): improve channel list and controller Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 8 ++ .../stream_channel_list_controller.dart | 29 ++++ .../stream_channel_list_tile.dart | 84 ++++++++--- .../stream_channel_list_view.dart | 136 ++++++++++-------- 4 files changed, 178 insertions(+), 79 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 51034d3a..e2660099 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1833,6 +1833,14 @@ class ChannelClientState { (watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), ); + /// Channel member for the current user. + Member? get currentUserMember => members.firstWhereOrNull( + (m) => m.user?.id == _channel.client.state.currentUser?.id, + ); + + /// User role for the current user. + String? get currentUserRole => currentUserMember?.role; + /// Channel read list. List get read => _channelState.read; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 5dcbeaa2..c94ac616 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -176,6 +176,32 @@ class StreamChannelListController extends PagedValueNotifier { return channel; } + /// Leaves the [channel] and updates the list. + Future leaveChannel(Channel channel) async { + final user = client.state.currentUser; + assert(user != null, 'You must be logged in to leave a channel.'); + await channel.removeMembers([user!.id]); + } + + /// Deletes the [channel] and updates the list. + Future deleteChannel(Channel channel) async { + await channel.delete(); + } + + /// Mutes the [channel] and updates the list. + Future muteChannel(Channel channel) async { + await channel.mute(); + } + + /// Un-mutes the [channel] and updates the list. + Future unmuteChannel(Channel channel) async { + await channel.unmute(); + } + + /// Event listener, which can be set in order to listen + /// [client] web-socket events. + bool Function(Event event)? eventListener; + StreamSubscription? _channelEventSubscription; // Subscribes to the channel list events. @@ -185,6 +211,9 @@ class StreamChannelListController extends PagedValueNotifier { } _channelEventSubscription = client.on().listen((event) { + // Returns early if the event is already handled by the listener. + if (eventListener?.call(event) ?? false) return; + final eventType = event.type; if (eventType == EventType.channelDeleted) { _eventHandler.onChannelDeleted(event, this); diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index a3d0f67a..d58e1441 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -31,10 +31,14 @@ class StreamChannelListTile extends StatelessWidget { this.leading, this.title, this.subtitle, + this.trailing, this.onTap, this.onLongPress, + this.tileColor, this.visualDensity = VisualDensity.compact, this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + this.unreadIndicatorBuilder, + this.sendingIndicatorBuilder, }) : assert( channel.state != null, 'Channel ${channel.id} is not initialized', @@ -53,12 +57,23 @@ class StreamChannelListTile extends StatelessWidget { /// Additional content displayed below the title. final Widget? subtitle; + /// A widget to display at the end of tile. + final Widget? trailing; + /// Called when the user taps this list tile. final GestureTapCallback? onTap; /// Called when the user long-presses on this list tile. final GestureLongPressCallback? onLongPress; + /// {@template flutter.material.ListTile.tileColor} + /// Defines the background color of `ListTile` when [selected] is false. + /// + /// When the value is null, the `tileColor` is set to [ListTileTheme.tileColor] + /// if it's not null and to [Colors.transparent] if it's null. + /// {@endtemplate} + final Color? tileColor; + /// Defines how compact the list tile's layout will be. /// /// {@macro flutter.material.themedata.visualDensity} @@ -77,6 +92,40 @@ class StreamChannelListTile extends StatelessWidget { /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. final EdgeInsetsGeometry contentPadding; + /// The widget builder for the unread indicator. + final WidgetBuilder? unreadIndicatorBuilder; + + /// The widget builder for the sending indicator. + /// + /// `Message` is the last message in the channel, Use it to determine the + /// status using [Message.status]. + final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamChannelListTile copyWith({ + Key? key, + Channel? channel, + Widget? leading, + Widget? title, + Widget? subtitle, + VoidCallback? onTap, + VoidCallback? onLongPress, + VisualDensity? visualDensity, + EdgeInsetsGeometry? contentPadding, + }) => + StreamChannelListTile( + key: key ?? this.key, + channel: channel ?? this.channel, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); + @override Widget build(BuildContext context) { final channelState = channel.state!; @@ -101,6 +150,12 @@ class StreamChannelListTile extends StatelessWidget { textStyle: channelPreviewTheme.subtitleStyle, ); + final trailing = this.trailing ?? + ChannelLastMessageDate( + channel: channel, + textStyle: channelPreviewTheme.lastMessageAtStyle, + ); + return BetterStreamBuilder( stream: channel.isMutedStream, initialData: channel.isMuted, @@ -113,6 +168,7 @@ class StreamChannelListTile extends StatelessWidget { visualDensity: visualDensity, contentPadding: contentPadding, leading: leading, + tileColor: tileColor, title: Row( children: [ Expanded(child: title), @@ -125,7 +181,8 @@ class StreamChannelListTile extends StatelessWidget { !members.any((it) => it.user!.id == currentUser.id)) { return const Offstage(); } - return UnreadIndicator(cid: channel.cid); + return unreadIndicatorBuilder?.call(context) ?? + UnreadIndicator(cid: channel.cid); }, ), ], @@ -154,24 +211,19 @@ class StreamChannelListTile extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 4), - child: SendingIndicator( - message: lastMessage, - size: channelPreviewTheme.indicatorIconSize, - isMessageRead: channelState.read - .where((it) => it.user.id != currentUser.id) - .where( - (it) => it.lastRead.isAfter(lastMessage.createdAt), - ) - .isNotEmpty, - ), + child: + sendingIndicatorBuilder?.call(context, lastMessage) ?? + SendingIndicator( + message: lastMessage, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: channelState + .currentUserRead!.lastRead + .isAfter(lastMessage.createdAt), + ), ); }, ), - ChannelLastMessageDate( - channel: channel, - textStyle: channelPreviewTheme.lastMessageAtStyle, - ), - // trailing ?? _buildDate(context), + trailing, ], ), ), diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index 67d4ad56..1527493e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -19,6 +19,7 @@ Widget defaultSeparatorBuilder(BuildContext context, int index) => typedef StreamChannelListViewItemBuilder = Widget Function( BuildContext context, Channel channel, + StreamChannelListTile defaultWidget, ); /// A [ListView] that shows a list of [Channel]s, @@ -51,6 +52,9 @@ class StreamChannelListView extends StatefulWidget { required this.controller, this.itemBuilder, this.separatorBuilder = defaultSeparatorBuilder, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, this.onChannelTap, this.onChannelLongPress, this.padding, @@ -71,13 +75,29 @@ class StreamChannelListView extends StatefulWidget { /// A builder that is called to build items in the [ListView]. /// - /// The `index` parameter is the index of the list tile in the list and the - /// `channel` parameter is the [Channel] at that position. + /// The `channel` parameter is the [Channel] at this position in the list + /// and the `defaultWidget` is the default widget used + /// i.e: [StreamChannelListTile]. final StreamChannelListViewItemBuilder? itemBuilder; /// A builder that is called to build the list separator. final IndexedWidgetBuilder separatorBuilder; + /// A builder that is called to build the empty state of the list. + /// + /// If not provider, [StreamChannelListEmptyWidget] will be used. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + /// + /// If not provided, [StreamChannelListLoadingTile] will be used. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + /// + /// If not provided, [StreamChannelListErrorWidget] will be used. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + /// Called when the user taps this list tile. final void Function(Channel)? onChannelTap; @@ -250,12 +270,13 @@ class _StreamChannelListViewState extends State { builder: (context, value, _) => value.when( (channels, nextPageKey, error) { if (channels.isEmpty) { - return const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: StreamChannelListEmpty(), - ), - ); + return widget.emptyBuilder?.call(context) ?? + const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: StreamChannelListEmptyWidget(), + ), + ); } return ListView.separated( @@ -290,47 +311,61 @@ class _StreamChannelListViewState extends State { if (index == channels.length) { if (error != null) { - return ChannelListLoadMoreError( + return StreamChannelListLoadMoreError( onTap: _controller.retry, ); } return const Center( child: Padding( padding: EdgeInsets.all(16), - child: ChannelListLoadMoreIndicator(), + child: StreamChannelListLoadMoreIndicator(), ), ); } final channel = channels[index]; - final itemBuilder = widget.itemBuilder; - if (itemBuilder != null) return itemBuilder(context, channel); final onTap = widget.onChannelTap; final onLongPress = widget.onChannelLongPress; - return StreamChannelListTile( + final streamChannelListTile = StreamChannelListTile( channel: channel, onTap: onTap == null ? null : () => onTap(channel), onLongPress: onLongPress == null ? null : () => onLongPress(channel), ); + + final itemBuilder = widget.itemBuilder; + + if (itemBuilder != null) { + return itemBuilder( + context, + channel, + streamChannelListTile, + ); + } + + return streamChannelListTile; }, ); }, - loading: () => ListView.separated( - padding: widget.padding, - physics: widget.physics, - reverse: widget.reverse, - itemCount: 25, - separatorBuilder: widget.separatorBuilder, - itemBuilder: (_, __) => const StreamChannelListLoadingTile(), - ), - error: (error) => Center( - child: StreamChannelListError( - onPressed: _controller.refresh, - ), - ), + loading: () => + widget.loadingBuilder?.call(context) ?? + ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + itemCount: 25, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + ), + error: (error) => + widget.errorBuilder?.call(context, error) ?? + Center( + child: StreamChannelListErrorWidget( + onPressed: _controller.refresh, + ), + ), ), ); } @@ -338,9 +373,9 @@ class _StreamChannelListViewState extends State { /// A [StreamChannelListTile] that can be used in a [ListView] to show a /// loading tile while waiting for the [StreamChannelListController] to load /// more channels. -class ChannelListLoadMoreIndicator extends StatelessWidget { - /// Creates a new instance of [ChannelListLoadMoreIndicator]. - const ChannelListLoadMoreIndicator({Key? key}) : super(key: key); +class StreamChannelListLoadMoreIndicator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadMoreIndicator]. + const StreamChannelListLoadMoreIndicator({Key? key}) : super(key: key); @override Widget build(BuildContext context) => const SizedBox( @@ -352,9 +387,9 @@ class ChannelListLoadMoreIndicator extends StatelessWidget { /// A [StreamChannelListTile] that is used to display the error indicator when /// loading more channels fails. -class ChannelListLoadMoreError extends StatelessWidget { - /// Creates a new instance of [ChannelListLoadMoreError]. - const ChannelListLoadMoreError({ +class StreamChannelListLoadMoreError extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadMoreError]. + const StreamChannelListLoadMoreError({ Key? key, this.onTap, }) : super(key: key); @@ -407,9 +442,9 @@ class StreamChannelListSeparator extends StatelessWidget { /// A widget that is used to display an error screen /// when [StreamChannelListController] fails to load initial channels. -class StreamChannelListError extends StatelessWidget { - /// Creates a new instance of [StreamChannelListError] widget. - const StreamChannelListError({ +class StreamChannelListErrorWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListErrorWidget] widget. + const StreamChannelListErrorWidget({ Key? key, this.onPressed, }) : super(key: key); @@ -445,15 +480,9 @@ class StreamChannelListError extends StatelessWidget { /// A widget that is used to display an empty state when /// [StreamChannelListController] loads zero channels. -class StreamChannelListEmpty extends StatelessWidget { - /// Creates a new instance of [StreamChannelListEmpty] widget. - const StreamChannelListEmpty({ - Key? key, - this.onPressed, - }) : super(key: key); - - /// The callback to invoke when the user taps on the start a chat button. - final VoidCallback? onPressed; +class StreamChannelListEmptyWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListEmptyWidget] widget. + const StreamChannelListEmptyWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -461,7 +490,6 @@ class StreamChannelListEmpty extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Spacer(), StreamSvgIcon.message( size: 148, color: chatThemeData.colorTheme.disabled, @@ -471,24 +499,6 @@ class StreamChannelListEmpty extends StatelessWidget { context.translations.letsStartChattingLabel, style: chatThemeData.textTheme.headline, ), - const SizedBox(height: 8), - Text( - context.translations.sendingFirstMessageLabel, - textAlign: TextAlign.center, - style: chatThemeData.textTheme.body.copyWith( - color: chatThemeData.colorTheme.textLowEmphasis, - ), - ), - const Spacer(), - TextButton( - onPressed: onPressed, - child: Text( - context.translations.startAChatLabel, - style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentPrimary, - ), - ), - ), ], ); }