refactor(ui): improve channel list and controller
Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
+29
@@ -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);
|
||||
|
||||
+68
-16
@@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
+73
-63
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user