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(),
);
/// 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<Read> get read => _channelState.read;
@@ -176,6 +176,32 @@ class StreamChannelListController extends PagedValueNotifier<int, 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;
// Subscribes to the channel list events.
@@ -185,6 +211,9 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
}
_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);
@@ -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<bool>(
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,
],
),
),
@@ -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<StreamChannelListView> {
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<StreamChannelListView> {
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<StreamChannelListView> {
/// 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,
),
),
),
],
);
}