refactor(ui): improve channel list and controller

Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
Sahil Kumar
2021-12-08 17:49:50 +05:30
committed by xsahil03x
parent 093a668546
commit c301b51c83
4 changed files with 178 additions and 79 deletions
@@ -1833,6 +1833,14 @@ class ChannelClientState {
(watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), (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. /// Channel read list.
List<Read> get read => _channelState.read; List<Read> get read => _channelState.read;
@@ -176,6 +176,32 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
return channel; return channel;
} }
/// Leaves the [channel] and updates the list.
Future<void> 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<void> deleteChannel(Channel channel) async {
await channel.delete();
}
/// Mutes the [channel] and updates the list.
Future<void> muteChannel(Channel channel) async {
await channel.mute();
}
/// Un-mutes the [channel] and updates the list.
Future<void> 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<Event>? _channelEventSubscription; StreamSubscription<Event>? _channelEventSubscription;
// Subscribes to the channel list events. // Subscribes to the channel list events.
@@ -185,6 +211,9 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
} }
_channelEventSubscription = client.on().listen((event) { _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; final eventType = event.type;
if (eventType == EventType.channelDeleted) { if (eventType == EventType.channelDeleted) {
_eventHandler.onChannelDeleted(event, this); _eventHandler.onChannelDeleted(event, this);
@@ -31,10 +31,14 @@ class StreamChannelListTile extends StatelessWidget {
this.leading, this.leading,
this.title, this.title,
this.subtitle, this.subtitle,
this.trailing,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
this.tileColor,
this.visualDensity = VisualDensity.compact, this.visualDensity = VisualDensity.compact,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), this.contentPadding = const EdgeInsets.symmetric(horizontal: 8),
this.unreadIndicatorBuilder,
this.sendingIndicatorBuilder,
}) : assert( }) : assert(
channel.state != null, channel.state != null,
'Channel ${channel.id} is not initialized', 'Channel ${channel.id} is not initialized',
@@ -53,12 +57,23 @@ class StreamChannelListTile extends StatelessWidget {
/// Additional content displayed below the title. /// Additional content displayed below the title.
final Widget? subtitle; final Widget? subtitle;
/// A widget to display at the end of tile.
final Widget? trailing;
/// Called when the user taps this list tile. /// Called when the user taps this list tile.
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
/// Called when the user long-presses on this list tile. /// Called when the user long-presses on this list tile.
final GestureLongPressCallback? onLongPress; 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. /// Defines how compact the list tile's layout will be.
/// ///
/// {@macro flutter.material.themedata.visualDensity} /// {@macro flutter.material.themedata.visualDensity}
@@ -77,6 +92,40 @@ class StreamChannelListTile extends StatelessWidget {
/// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used.
final EdgeInsetsGeometry contentPadding; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channelState = channel.state!; final channelState = channel.state!;
@@ -101,6 +150,12 @@ class StreamChannelListTile extends StatelessWidget {
textStyle: channelPreviewTheme.subtitleStyle, textStyle: channelPreviewTheme.subtitleStyle,
); );
final trailing = this.trailing ??
ChannelLastMessageDate(
channel: channel,
textStyle: channelPreviewTheme.lastMessageAtStyle,
);
return BetterStreamBuilder<bool>( return BetterStreamBuilder<bool>(
stream: channel.isMutedStream, stream: channel.isMutedStream,
initialData: channel.isMuted, initialData: channel.isMuted,
@@ -113,6 +168,7 @@ class StreamChannelListTile extends StatelessWidget {
visualDensity: visualDensity, visualDensity: visualDensity,
contentPadding: contentPadding, contentPadding: contentPadding,
leading: leading, leading: leading,
tileColor: tileColor,
title: Row( title: Row(
children: [ children: [
Expanded(child: title), Expanded(child: title),
@@ -125,7 +181,8 @@ class StreamChannelListTile extends StatelessWidget {
!members.any((it) => it.user!.id == currentUser.id)) { !members.any((it) => it.user!.id == currentUser.id)) {
return const Offstage(); 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( return Padding(
padding: const EdgeInsets.only(right: 4), padding: const EdgeInsets.only(right: 4),
child: SendingIndicator( child:
message: lastMessage, sendingIndicatorBuilder?.call(context, lastMessage) ??
size: channelPreviewTheme.indicatorIconSize, SendingIndicator(
isMessageRead: channelState.read message: lastMessage,
.where((it) => it.user.id != currentUser.id) size: channelPreviewTheme.indicatorIconSize,
.where( isMessageRead: channelState
(it) => it.lastRead.isAfter(lastMessage.createdAt), .currentUserRead!.lastRead
) .isAfter(lastMessage.createdAt),
.isNotEmpty, ),
),
); );
}, },
), ),
ChannelLastMessageDate( trailing,
channel: channel,
textStyle: channelPreviewTheme.lastMessageAtStyle,
),
// trailing ?? _buildDate(context),
], ],
), ),
), ),
@@ -19,6 +19,7 @@ Widget defaultSeparatorBuilder(BuildContext context, int index) =>
typedef StreamChannelListViewItemBuilder = Widget Function( typedef StreamChannelListViewItemBuilder = Widget Function(
BuildContext context, BuildContext context,
Channel channel, Channel channel,
StreamChannelListTile defaultWidget,
); );
/// A [ListView] that shows a list of [Channel]s, /// A [ListView] that shows a list of [Channel]s,
@@ -51,6 +52,9 @@ class StreamChannelListView extends StatefulWidget {
required this.controller, required this.controller,
this.itemBuilder, this.itemBuilder,
this.separatorBuilder = defaultSeparatorBuilder, this.separatorBuilder = defaultSeparatorBuilder,
this.emptyBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onChannelTap, this.onChannelTap,
this.onChannelLongPress, this.onChannelLongPress,
this.padding, this.padding,
@@ -71,13 +75,29 @@ class StreamChannelListView extends StatefulWidget {
/// A builder that is called to build items in the [ListView]. /// 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 /// The `channel` parameter is the [Channel] at this position in the list
/// `channel` parameter is the [Channel] at that position. /// and the `defaultWidget` is the default widget used
/// i.e: [StreamChannelListTile].
final StreamChannelListViewItemBuilder? itemBuilder; final StreamChannelListViewItemBuilder? itemBuilder;
/// A builder that is called to build the list separator. /// A builder that is called to build the list separator.
final IndexedWidgetBuilder separatorBuilder; 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. /// Called when the user taps this list tile.
final void Function(Channel)? onChannelTap; final void Function(Channel)? onChannelTap;
@@ -250,12 +270,13 @@ class _StreamChannelListViewState extends State<StreamChannelListView> {
builder: (context, value, _) => value.when( builder: (context, value, _) => value.when(
(channels, nextPageKey, error) { (channels, nextPageKey, error) {
if (channels.isEmpty) { if (channels.isEmpty) {
return const Center( return widget.emptyBuilder?.call(context) ??
child: Padding( const Center(
padding: EdgeInsets.all(8), child: Padding(
child: StreamChannelListEmpty(), padding: EdgeInsets.all(8),
), child: StreamChannelListEmptyWidget(),
); ),
);
} }
return ListView.separated( return ListView.separated(
@@ -290,47 +311,61 @@ class _StreamChannelListViewState extends State<StreamChannelListView> {
if (index == channels.length) { if (index == channels.length) {
if (error != null) { if (error != null) {
return ChannelListLoadMoreError( return StreamChannelListLoadMoreError(
onTap: _controller.retry, onTap: _controller.retry,
); );
} }
return const Center( return const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: ChannelListLoadMoreIndicator(), child: StreamChannelListLoadMoreIndicator(),
), ),
); );
} }
final channel = channels[index]; final channel = channels[index];
final itemBuilder = widget.itemBuilder;
if (itemBuilder != null) return itemBuilder(context, channel);
final onTap = widget.onChannelTap; final onTap = widget.onChannelTap;
final onLongPress = widget.onChannelLongPress; final onLongPress = widget.onChannelLongPress;
return StreamChannelListTile( final streamChannelListTile = StreamChannelListTile(
channel: channel, channel: channel,
onTap: onTap == null ? null : () => onTap(channel), onTap: onTap == null ? null : () => onTap(channel),
onLongPress: onLongPress:
onLongPress == null ? null : () => onLongPress(channel), onLongPress == null ? null : () => onLongPress(channel),
); );
final itemBuilder = widget.itemBuilder;
if (itemBuilder != null) {
return itemBuilder(
context,
channel,
streamChannelListTile,
);
}
return streamChannelListTile;
}, },
); );
}, },
loading: () => ListView.separated( loading: () =>
padding: widget.padding, widget.loadingBuilder?.call(context) ??
physics: widget.physics, ListView.separated(
reverse: widget.reverse, padding: widget.padding,
itemCount: 25, physics: widget.physics,
separatorBuilder: widget.separatorBuilder, reverse: widget.reverse,
itemBuilder: (_, __) => const StreamChannelListLoadingTile(), itemCount: 25,
), separatorBuilder: widget.separatorBuilder,
error: (error) => Center( itemBuilder: (_, __) => const StreamChannelListLoadingTile(),
child: StreamChannelListError( ),
onPressed: _controller.refresh, error: (error) =>
), widget.errorBuilder?.call(context, error) ??
), Center(
child: StreamChannelListErrorWidget(
onPressed: _controller.refresh,
),
),
), ),
); );
} }
@@ -338,9 +373,9 @@ class _StreamChannelListViewState extends State<StreamChannelListView> {
/// A [StreamChannelListTile] that can be used in a [ListView] to show a /// A [StreamChannelListTile] that can be used in a [ListView] to show a
/// loading tile while waiting for the [StreamChannelListController] to load /// loading tile while waiting for the [StreamChannelListController] to load
/// more channels. /// more channels.
class ChannelListLoadMoreIndicator extends StatelessWidget { class StreamChannelListLoadMoreIndicator extends StatelessWidget {
/// Creates a new instance of [ChannelListLoadMoreIndicator]. /// Creates a new instance of [StreamChannelListLoadMoreIndicator].
const ChannelListLoadMoreIndicator({Key? key}) : super(key: key); const StreamChannelListLoadMoreIndicator({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) => const SizedBox( 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 /// A [StreamChannelListTile] that is used to display the error indicator when
/// loading more channels fails. /// loading more channels fails.
class ChannelListLoadMoreError extends StatelessWidget { class StreamChannelListLoadMoreError extends StatelessWidget {
/// Creates a new instance of [ChannelListLoadMoreError]. /// Creates a new instance of [StreamChannelListLoadMoreError].
const ChannelListLoadMoreError({ const StreamChannelListLoadMoreError({
Key? key, Key? key,
this.onTap, this.onTap,
}) : super(key: key); }) : super(key: key);
@@ -407,9 +442,9 @@ class StreamChannelListSeparator extends StatelessWidget {
/// A widget that is used to display an error screen /// A widget that is used to display an error screen
/// when [StreamChannelListController] fails to load initial channels. /// when [StreamChannelListController] fails to load initial channels.
class StreamChannelListError extends StatelessWidget { class StreamChannelListErrorWidget extends StatelessWidget {
/// Creates a new instance of [StreamChannelListError] widget. /// Creates a new instance of [StreamChannelListErrorWidget] widget.
const StreamChannelListError({ const StreamChannelListErrorWidget({
Key? key, Key? key,
this.onPressed, this.onPressed,
}) : super(key: key); }) : super(key: key);
@@ -445,15 +480,9 @@ class StreamChannelListError extends StatelessWidget {
/// A widget that is used to display an empty state when /// A widget that is used to display an empty state when
/// [StreamChannelListController] loads zero channels. /// [StreamChannelListController] loads zero channels.
class StreamChannelListEmpty extends StatelessWidget { class StreamChannelListEmptyWidget extends StatelessWidget {
/// Creates a new instance of [StreamChannelListEmpty] widget. /// Creates a new instance of [StreamChannelListEmptyWidget] widget.
const StreamChannelListEmpty({ const StreamChannelListEmptyWidget({Key? key}) : super(key: key);
Key? key,
this.onPressed,
}) : super(key: key);
/// The callback to invoke when the user taps on the start a chat button.
final VoidCallback? onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -461,7 +490,6 @@ class StreamChannelListEmpty extends StatelessWidget {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Spacer(),
StreamSvgIcon.message( StreamSvgIcon.message(
size: 148, size: 148,
color: chatThemeData.colorTheme.disabled, color: chatThemeData.colorTheme.disabled,
@@ -471,24 +499,6 @@ class StreamChannelListEmpty extends StatelessWidget {
context.translations.letsStartChattingLabel, context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline, 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,
),
),
),
], ],
); );
} }