feat(ui, core): add remaining v4 list-views
Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
@@ -115,9 +115,11 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
|||||||
|
|
||||||
Widget _channelPreviewBuilder(
|
Widget _channelPreviewBuilder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Channel channel,
|
List<Channel> channels,
|
||||||
|
int index,
|
||||||
StreamChannelListTile defaultTile,
|
StreamChannelListTile defaultTile,
|
||||||
) {
|
) {
|
||||||
|
final channel = channels[index];
|
||||||
final lastMessage = channel.state?.messages.reversed.firstWhereOrNull(
|
final lastMessage = channel.state?.messages.reversed.firstWhereOrNull(
|
||||||
(message) => !message.isDeleted,
|
(message) => !message.isDeleted,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,23 +2,20 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// {@macro message_search_item}
|
|
||||||
@Deprecated("Use 'StreamMessageSearchItem' instead")
|
|
||||||
typedef MessageSearchItem = StreamMessageSearchItem;
|
|
||||||
|
|
||||||
/// {@template message_search_item}
|
/// {@template message_search_item}
|
||||||
/// It shows the current [Message] preview.
|
/// It shows the current [Message] preview.
|
||||||
///
|
///
|
||||||
/// Usually you don't use this widget as it's the default item used by
|
/// Usually you don't use this widget as it's the default item used by
|
||||||
/// [StreamMessageSearchListView].
|
/// [MessageSearchListView].
|
||||||
///
|
///
|
||||||
/// The widget renders the ui based on the first ancestor of type
|
/// The widget renders the ui based on the first ancestor of type
|
||||||
/// [StreamChatTheme].
|
/// [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class StreamMessageSearchItem extends StatelessWidget {
|
@Deprecated("Use 'StreamMessageSearchItem' instead")
|
||||||
|
class MessageSearchItem extends StatelessWidget {
|
||||||
/// Instantiate a new MessageSearchItem
|
/// Instantiate a new MessageSearchItem
|
||||||
const StreamMessageSearchItem({
|
const MessageSearchItem({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.getMessageResponse,
|
required this.getMessageResponse,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
@@ -31,7 +28,7 @@ class StreamMessageSearchItem extends StatelessWidget {
|
|||||||
/// Function called when tapping this widget
|
/// Function called when tapping this widget
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
/// If true the [StreamMessageSearchItem] will show the current online Status
|
/// If true the [MessageSearchItem] will show the current online Status
|
||||||
final bool showOnlineStatus;
|
final bool showOnlineStatus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ typedef MessageSearchItemBuilder = Widget Function(
|
|||||||
GetMessageResponse,
|
GetMessageResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Builder used when [StreamMessageSearchListView] is empty
|
/// Builder used when [MessageSearchListView] is empty
|
||||||
typedef EmptyMessageSearchBuilder = Widget Function(
|
typedef EmptyMessageSearchBuilder = Widget Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String searchQuery,
|
String searchQuery,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// {@macro message_search_list_view}
|
|
||||||
@Deprecated("Use 'StreamMessageSearchListView' instead")
|
|
||||||
typedef MessageSearchListView = StreamMessageSearchListView;
|
|
||||||
|
|
||||||
/// {@template message_search_list_view}
|
/// {@template message_search_list_view}
|
||||||
/// It shows the list of searched messages.
|
/// It shows the list of searched messages.
|
||||||
///
|
///
|
||||||
@@ -52,9 +48,10 @@ typedef MessageSearchListView = StreamMessageSearchListView;
|
|||||||
/// [StreamChatTheme].
|
/// [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class StreamMessageSearchListView extends StatefulWidget {
|
@Deprecated("Use 'StreamMessageSearchListView' instead")
|
||||||
|
class MessageSearchListView extends StatefulWidget {
|
||||||
/// Instantiate a new MessageSearchListView
|
/// Instantiate a new MessageSearchListView
|
||||||
const StreamMessageSearchListView({
|
const MessageSearchListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.filters,
|
required this.filters,
|
||||||
this.messageQuery,
|
this.messageQuery,
|
||||||
@@ -101,7 +98,7 @@ class StreamMessageSearchListView extends StatefulWidget {
|
|||||||
/// Builder used to create a custom item preview
|
/// Builder used to create a custom item preview
|
||||||
final MessageSearchItemBuilder? itemBuilder;
|
final MessageSearchItemBuilder? itemBuilder;
|
||||||
|
|
||||||
/// Function called when tapping on a [StreamMessageSearchItem]
|
/// Function called when tapping on a [MessageSearchItem]
|
||||||
final MessageSearchItemTapCallback? onItemTap;
|
final MessageSearchItemTapCallback? onItemTap;
|
||||||
|
|
||||||
/// Builder used to create a custom item separator
|
/// Builder used to create a custom item separator
|
||||||
@@ -135,12 +132,10 @@ class StreamMessageSearchListView extends StatefulWidget {
|
|||||||
final MessageSearchListController? messageSearchListController;
|
final MessageSearchListController? messageSearchListController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_StreamMessageSearchListViewState createState() =>
|
_MessageSearchListViewState createState() => _MessageSearchListViewState();
|
||||||
_StreamMessageSearchListViewState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StreamMessageSearchListViewState
|
class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||||
extends State<StreamMessageSearchListView> {
|
|
||||||
late final _defaultController = MessageSearchListController();
|
late final _defaultController = MessageSearchListController();
|
||||||
|
|
||||||
MessageSearchListController get _messageSearchListController =>
|
MessageSearchListController get _messageSearchListController =>
|
||||||
@@ -226,7 +221,7 @@ class _StreamMessageSearchListViewState
|
|||||||
if (widget.itemBuilder != null) {
|
if (widget.itemBuilder != null) {
|
||||||
return widget.itemBuilder!(context, getMessageResponse);
|
return widget.itemBuilder!(context, getMessageResponse);
|
||||||
}
|
}
|
||||||
return StreamMessageSearchItem(
|
return MessageSearchItem(
|
||||||
getMessageResponse: getMessageResponse,
|
getMessageResponse: getMessageResponse,
|
||||||
onTap: () => widget.onItemTap!(getMessageResponse),
|
onTap: () => widget.onItemTap!(getMessageResponse),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ class StreamChatThemeData {
|
|||||||
/// Theme configuration for the [StreamUserListView] widget.
|
/// Theme configuration for the [StreamUserListView] widget.
|
||||||
final StreamUserListViewThemeData userListViewTheme;
|
final StreamUserListViewThemeData userListViewTheme;
|
||||||
|
|
||||||
/// Theme configuration for the [StreamMessageSearchListView] widget.
|
/// Theme configuration for the [MessageSearchListView] widget.
|
||||||
final StreamMessageSearchListViewThemeData messageSearchListViewTheme;
|
final StreamMessageSearchListViewThemeData messageSearchListViewTheme;
|
||||||
|
|
||||||
/// Creates a copy of [StreamChatThemeData] with specified attributes
|
/// Creates a copy of [StreamChatThemeData] with specified attributes
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ typedef UserTapCallback = void Function(User, Widget?);
|
|||||||
/// Builder used to create a custom [ListUserItem] from a [User]
|
/// Builder used to create a custom [ListUserItem] from a [User]
|
||||||
typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
|
typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
|
||||||
|
|
||||||
/// {@macro user_list_view}
|
|
||||||
@Deprecated("Use 'StreamUserListView' instead")
|
|
||||||
typedef UserListView = StreamUserListView;
|
|
||||||
|
|
||||||
/// {@template user_list_view}
|
/// {@template user_list_view}
|
||||||
/// It shows the list of current users.
|
/// It shows the list of current users.
|
||||||
///
|
///
|
||||||
@@ -47,9 +43,10 @@ typedef UserListView = StreamUserListView;
|
|||||||
/// type [StreamChatTheme].
|
/// type [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class StreamUserListView extends StatefulWidget {
|
@Deprecated("Use 'StreamUserListView' instead")
|
||||||
|
class UserListView extends StatefulWidget {
|
||||||
/// Instantiate a new UserListView
|
/// Instantiate a new UserListView
|
||||||
StreamUserListView({
|
UserListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.filter = const Filter.empty(),
|
this.filter = const Filter.empty(),
|
||||||
this.sort,
|
this.sort,
|
||||||
@@ -165,10 +162,10 @@ class StreamUserListView extends StatefulWidget {
|
|||||||
final UserListController? userListController;
|
final UserListController? userListController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_StreamUserListViewState createState() => _StreamUserListViewState();
|
_UserListViewState createState() => _UserListViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StreamUserListViewState extends State<StreamUserListView>
|
class _UserListViewState extends State<UserListView>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
bool get _isListView => widget.crossAxisCount == 1;
|
bool get _isListView => widget.crossAxisCount == 1;
|
||||||
|
|
||||||
|
|||||||
+5
-88
@@ -1,6 +1,7 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/v4/stream_message_preview_text.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// A widget that displays a channel preview.
|
/// A widget that displays a channel preview.
|
||||||
@@ -351,95 +352,11 @@ class ChannelLastMessageText extends StatelessWidget {
|
|||||||
|
|
||||||
if (lastMessage == null) return const Offstage();
|
if (lastMessage == null) return const Offstage();
|
||||||
|
|
||||||
final lastMessageText = lastMessage
|
return StreamMessagePreviewText(
|
||||||
.translate(channel.client.state.currentUser?.language ?? 'en')
|
message: lastMessage,
|
||||||
.replaceMentions(linkify: false)
|
textStyle: textStyle,
|
||||||
.text;
|
language: channel.client.state.currentUser?.language,
|
||||||
final lastMessageAttachments = lastMessage.attachments;
|
|
||||||
final lastMessageMentionedUsers = lastMessage.mentionedUsers;
|
|
||||||
|
|
||||||
final mentionedUsersRegex = RegExp(
|
|
||||||
lastMessageMentionedUsers.map((it) => '@${it.name}').join('|'),
|
|
||||||
caseSensitive: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
final messageTextParts = [
|
|
||||||
...lastMessageAttachments.map((it) {
|
|
||||||
if (it.type == 'image') {
|
|
||||||
return '📷';
|
|
||||||
} else if (it.type == 'video') {
|
|
||||||
return '🎬';
|
|
||||||
} else if (it.type == 'giphy') {
|
|
||||||
return '[GIF]';
|
|
||||||
}
|
|
||||||
return it == lastMessage.attachments.last
|
|
||||||
? (it.title ?? 'File')
|
|
||||||
: '${it.title ?? 'File'} , ';
|
|
||||||
}),
|
|
||||||
if (lastMessageText != null)
|
|
||||||
if (lastMessageMentionedUsers.isNotEmpty)
|
|
||||||
...mentionedUsersRegex.allMatchesWithSep(lastMessageText)
|
|
||||||
else
|
|
||||||
lastMessageText,
|
|
||||||
];
|
|
||||||
|
|
||||||
final fontStyle = (lastMessage.isSystem || lastMessage.isDeleted)
|
|
||||||
? FontStyle.italic
|
|
||||||
: FontStyle.normal;
|
|
||||||
|
|
||||||
final regularTextStyle = textStyle?.copyWith(fontStyle: fontStyle);
|
|
||||||
|
|
||||||
final mentionsTextStyle = textStyle?.copyWith(
|
|
||||||
fontStyle: fontStyle,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
);
|
|
||||||
|
|
||||||
final spans = [
|
|
||||||
for (final part in messageTextParts)
|
|
||||||
if (lastMessageMentionedUsers.isNotEmpty &&
|
|
||||||
lastMessageMentionedUsers.any((it) => '@${it.name}' == part))
|
|
||||||
TextSpan(
|
|
||||||
text: part,
|
|
||||||
style: mentionsTextStyle,
|
|
||||||
)
|
|
||||||
else if (lastMessageAttachments.isNotEmpty &&
|
|
||||||
lastMessageAttachments
|
|
||||||
.where((it) => it.title != null)
|
|
||||||
.any((it) => it.title == part))
|
|
||||||
TextSpan(
|
|
||||||
text: part,
|
|
||||||
style: regularTextStyle?.copyWith(
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
TextSpan(
|
|
||||||
text: part,
|
|
||||||
style: regularTextStyle,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Text.rich(
|
|
||||||
TextSpan(children: spans),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _RegExpX on RegExp {
|
|
||||||
List<String> allMatchesWithSep(String input, [int start = 0]) {
|
|
||||||
final result = <String>[];
|
|
||||||
for (final match in allMatches(input, start)) {
|
|
||||||
result.add(input.substring(start, match.start));
|
|
||||||
// ignore: cascade_invocations
|
|
||||||
result.add(match[0]!);
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
start = match.end;
|
|
||||||
}
|
|
||||||
result.add(input.substring(start));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+79
-136
@@ -5,19 +5,21 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
|||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart';
|
import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart';
|
||||||
import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart';
|
import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/v4/stream_list_view_indexed_widget_builder.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
/// Default separator builder for [StreamChannelListView].
|
/// Default separator builder for [StreamChannelListView].
|
||||||
Widget defaultSeparatorBuilder(BuildContext context, int index) =>
|
Widget defaultChannelListViewSeparatorBuilder(
|
||||||
|
BuildContext context,
|
||||||
|
List<Channel> items,
|
||||||
|
int index,
|
||||||
|
) =>
|
||||||
const StreamChannelListSeparator();
|
const StreamChannelListSeparator();
|
||||||
|
|
||||||
/// Signature for the item builder that creates the children of the
|
/// Signature for the item builder that creates the children of the
|
||||||
/// [StreamChannelListView].
|
/// [StreamChannelListView].
|
||||||
typedef StreamChannelListViewItemBuilder = Widget Function(
|
typedef StreamChannelListViewIndexedWidgetBuilder
|
||||||
BuildContext context,
|
= StreamListViewIndexedWidgetBuilder<Channel, StreamChannelListTile>;
|
||||||
Channel channel,
|
|
||||||
StreamChannelListTile defaultWidget,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A [ListView] that shows a list of [Channel]s,
|
/// A [ListView] that shows a list of [Channel]s,
|
||||||
/// it uses [StreamChannelListTile] as a default item.
|
/// it uses [StreamChannelListTile] as a default item.
|
||||||
@@ -42,18 +44,19 @@ typedef StreamChannelListViewItemBuilder = Widget Function(
|
|||||||
/// See also:
|
/// See also:
|
||||||
/// * [StreamChannelListTile]
|
/// * [StreamChannelListTile]
|
||||||
/// * [StreamChannelListController]
|
/// * [StreamChannelListController]
|
||||||
class StreamChannelListView extends StatefulWidget {
|
class StreamChannelListView extends StatelessWidget {
|
||||||
/// Creates a new instance of [StreamChannelListView].
|
/// Creates a new instance of [StreamChannelListView].
|
||||||
const StreamChannelListView({
|
const StreamChannelListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.itemBuilder,
|
this.itemBuilder,
|
||||||
this.separatorBuilder = defaultSeparatorBuilder,
|
this.separatorBuilder = defaultChannelListViewSeparatorBuilder,
|
||||||
this.emptyBuilder,
|
this.emptyBuilder,
|
||||||
this.loadingBuilder,
|
this.loadingBuilder,
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
this.onChannelTap,
|
this.onChannelTap,
|
||||||
this.onChannelLongPress,
|
this.onChannelLongPress,
|
||||||
|
this.loadMoreTriggerIndex = 3,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.physics,
|
this.physics,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
@@ -75,14 +78,14 @@ class StreamChannelListView extends StatefulWidget {
|
|||||||
/// The `channel` parameter is the [Channel] at this position in the list
|
/// The `channel` parameter is the [Channel] at this position in the list
|
||||||
/// and the `defaultWidget` is the default widget used
|
/// and the `defaultWidget` is the default widget used
|
||||||
/// i.e: [StreamChannelListTile].
|
/// i.e: [StreamChannelListTile].
|
||||||
final StreamChannelListViewItemBuilder? itemBuilder;
|
final StreamChannelListViewIndexedWidgetBuilder? 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 PagedValueListViewIndexedWidgetBuilder<Channel> separatorBuilder;
|
||||||
|
|
||||||
/// A builder that is called to build the empty state of the list.
|
/// A builder that is called to build the empty state of the list.
|
||||||
///
|
///
|
||||||
/// If not provider, [StreamChannelListEmptyWidget] will be used.
|
/// If not provided, [StreamChannelListEmptyWidget] will be used.
|
||||||
final WidgetBuilder? emptyBuilder;
|
final WidgetBuilder? emptyBuilder;
|
||||||
|
|
||||||
/// A builder that is called to build the loading state of the list.
|
/// A builder that is called to build the loading state of the list.
|
||||||
@@ -101,6 +104,9 @@ class StreamChannelListView extends StatefulWidget {
|
|||||||
/// Called when the user long-presses on this list tile.
|
/// Called when the user long-presses on this list tile.
|
||||||
final void Function(Channel)? onChannelLongPress;
|
final void Function(Channel)? onChannelLongPress;
|
||||||
|
|
||||||
|
/// The index to take into account when triggering [controller.loadMore].
|
||||||
|
final int loadMoreTriggerIndex;
|
||||||
|
|
||||||
/// The amount of space by which to inset the children.
|
/// The amount of space by which to inset the children.
|
||||||
final EdgeInsetsGeometry? padding;
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
@@ -235,135 +241,72 @@ class StreamChannelListView extends StatefulWidget {
|
|||||||
final String? restorationId;
|
final String? restorationId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_StreamChannelListViewState createState() => _StreamChannelListViewState();
|
Widget build(BuildContext context) => PagedValueListView<int, Channel>(
|
||||||
}
|
padding: padding,
|
||||||
|
physics: physics,
|
||||||
|
reverse: reverse,
|
||||||
|
controller: controller,
|
||||||
|
primary: primary,
|
||||||
|
shrinkWrap: shrinkWrap,
|
||||||
|
keyboardDismissBehavior: keyboardDismissBehavior,
|
||||||
|
restorationId: restorationId,
|
||||||
|
dragStartBehavior: dragStartBehavior,
|
||||||
|
cacheExtent: cacheExtent,
|
||||||
|
loadMoreTriggerIndex: loadMoreTriggerIndex,
|
||||||
|
separatorBuilder: separatorBuilder,
|
||||||
|
itemBuilder: (context, channels, index) {
|
||||||
|
final channel = channels[index];
|
||||||
|
final onTap = onChannelTap;
|
||||||
|
final onLongPress = onChannelLongPress;
|
||||||
|
|
||||||
class _StreamChannelListViewState extends State<StreamChannelListView> {
|
final streamChannelListTile = StreamChannelListTile(
|
||||||
StreamChannelListController get _controller => widget.controller;
|
channel: channel,
|
||||||
|
onTap: onTap == null ? null : () => onTap(channel),
|
||||||
|
onLongPress:
|
||||||
|
onLongPress == null ? null : () => onLongPress(channel),
|
||||||
|
);
|
||||||
|
|
||||||
// Avoids duplicate requests on rebuilds.
|
return itemBuilder?.call(
|
||||||
bool _hasRequestedNextPage = false;
|
context,
|
||||||
|
channels,
|
||||||
@override
|
index,
|
||||||
void initState() {
|
streamChannelListTile,
|
||||||
super.initState();
|
) ??
|
||||||
_controller.doInitialLoad();
|
streamChannelListTile;
|
||||||
}
|
},
|
||||||
|
emptyBuilder: (context) =>
|
||||||
@override
|
emptyBuilder?.call(context) ??
|
||||||
void didUpdateWidget(covariant StreamChannelListView oldWidget) {
|
const Center(
|
||||||
super.didUpdateWidget(oldWidget);
|
child: Padding(
|
||||||
if (_controller != oldWidget.controller) {
|
padding: EdgeInsets.all(8),
|
||||||
// reset duplicate requests flag
|
child: StreamChannelListEmptyWidget(),
|
||||||
_hasRequestedNextPage = false;
|
|
||||||
_controller.doInitialLoad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) =>
|
|
||||||
PagedValueListenableBuilder<int, Channel>(
|
|
||||||
valueListenable: widget.controller,
|
|
||||||
builder: (context, value, _) => value.when(
|
|
||||||
(channels, nextPageKey, error) {
|
|
||||||
if (channels.isEmpty) {
|
|
||||||
return widget.emptyBuilder?.call(context) ??
|
|
||||||
const Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(8),
|
|
||||||
child: StreamChannelListEmptyWidget(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
padding: widget.padding,
|
|
||||||
physics: widget.physics,
|
|
||||||
reverse: widget.reverse,
|
|
||||||
controller: widget.scrollController,
|
|
||||||
primary: widget.primary,
|
|
||||||
shrinkWrap: widget.shrinkWrap,
|
|
||||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
|
||||||
restorationId: widget.restorationId,
|
|
||||||
dragStartBehavior: widget.dragStartBehavior,
|
|
||||||
cacheExtent: widget.cacheExtent,
|
|
||||||
itemCount: value.itemCount,
|
|
||||||
separatorBuilder: widget.separatorBuilder,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (!_hasRequestedNextPage) {
|
|
||||||
final newPageRequestTriggerIndex = channels.length - 3;
|
|
||||||
final isBuildingTriggerIndexItem =
|
|
||||||
index == newPageRequestTriggerIndex;
|
|
||||||
if (nextPageKey != null && isBuildingTriggerIndexItem) {
|
|
||||||
// Schedules the request for the end of this frame.
|
|
||||||
WidgetsBinding.instance?.addPostFrameCallback((_) async {
|
|
||||||
if (error == null) {
|
|
||||||
await _controller.loadMore(nextPageKey);
|
|
||||||
}
|
|
||||||
_hasRequestedNextPage = false;
|
|
||||||
});
|
|
||||||
_hasRequestedNextPage = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index == channels.length) {
|
|
||||||
if (error != null) {
|
|
||||||
return StreamChannelListLoadMoreError(
|
|
||||||
onTap: _controller.retry,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
child: StreamChannelListLoadMoreIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final channel = channels[index];
|
|
||||||
|
|
||||||
final onTap = widget.onChannelTap;
|
|
||||||
final onLongPress = widget.onChannelLongPress;
|
|
||||||
|
|
||||||
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: () =>
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
loadMoreErrorBuilder: (context, error) =>
|
||||||
|
StreamChannelListLoadMoreError(onTap: controller.retry),
|
||||||
|
loadMoreIndicatorBuilder: (context) => const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: StreamChannelListLoadMoreIndicator(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
loadingBuilder: (context) =>
|
||||||
|
loadingBuilder?.call(context) ??
|
||||||
|
ListView.separated(
|
||||||
|
padding: padding,
|
||||||
|
physics: physics,
|
||||||
|
reverse: reverse,
|
||||||
|
itemCount: 25,
|
||||||
|
separatorBuilder: (_, __) => const StreamChannelListSeparator(),
|
||||||
|
itemBuilder: (_, __) => const StreamChannelListLoadingTile(),
|
||||||
|
),
|
||||||
|
errorBuilder: (context, error) =>
|
||||||
|
errorBuilder?.call(context, error) ??
|
||||||
|
Center(
|
||||||
|
child: StreamChannelListErrorWidget(
|
||||||
|
onPressed: controller.refresh,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/v4/stream_message_preview_text.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// A widget that displays a message search item.
|
||||||
|
///
|
||||||
|
/// This widget is intended to be used as a
|
||||||
|
/// Tile in [StreamMessageSearchListView].
|
||||||
|
///
|
||||||
|
/// It displays the message's text, channel, sender, and timestamp.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [StreamMessageSearchListView]
|
||||||
|
/// * [StreamUserAvatar]
|
||||||
|
class StreamMessageSearchListTile extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamMessageSearchListTile].
|
||||||
|
const StreamMessageSearchListTile({
|
||||||
|
Key? key,
|
||||||
|
required this.messageResponse,
|
||||||
|
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),
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The message response to display.
|
||||||
|
final GetMessageResponse messageResponse;
|
||||||
|
|
||||||
|
/// A widget to display before the title.
|
||||||
|
final Widget? leading;
|
||||||
|
|
||||||
|
/// The primary content of the list tile.
|
||||||
|
final Widget? title;
|
||||||
|
|
||||||
|
/// 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 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}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
|
||||||
|
/// widgets within a [Theme].
|
||||||
|
final VisualDensity visualDensity;
|
||||||
|
|
||||||
|
/// The tile's internal padding.
|
||||||
|
///
|
||||||
|
/// Insets a [ListTile]'s contents: its [leading], [title], [subtitle],
|
||||||
|
/// and [trailing] widgets.
|
||||||
|
///
|
||||||
|
/// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used.
|
||||||
|
final EdgeInsetsGeometry contentPadding;
|
||||||
|
|
||||||
|
/// Creates a copy of this tile but with the given fields replaced with
|
||||||
|
/// the new values.
|
||||||
|
StreamMessageSearchListTile copyWith({
|
||||||
|
Key? key,
|
||||||
|
GetMessageResponse? messageResponse,
|
||||||
|
Widget? leading,
|
||||||
|
Widget? title,
|
||||||
|
Widget? subtitle,
|
||||||
|
Widget? trailing,
|
||||||
|
GestureTapCallback? onTap,
|
||||||
|
GestureLongPressCallback? onLongPress,
|
||||||
|
Color? tileColor,
|
||||||
|
VisualDensity? visualDensity,
|
||||||
|
EdgeInsetsGeometry? contentPadding,
|
||||||
|
}) =>
|
||||||
|
StreamMessageSearchListTile(
|
||||||
|
key: key ?? this.key,
|
||||||
|
messageResponse: messageResponse ?? this.messageResponse,
|
||||||
|
leading: leading ?? this.leading,
|
||||||
|
title: title ?? this.title,
|
||||||
|
subtitle: subtitle ?? this.subtitle,
|
||||||
|
trailing: trailing ?? this.trailing,
|
||||||
|
onTap: onTap ?? this.onTap,
|
||||||
|
onLongPress: onLongPress ?? this.onLongPress,
|
||||||
|
tileColor: tileColor ?? this.tileColor,
|
||||||
|
visualDensity: visualDensity ?? this.visualDensity,
|
||||||
|
contentPadding: contentPadding ?? this.contentPadding,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final message = messageResponse.message;
|
||||||
|
final user = message.user!;
|
||||||
|
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
|
||||||
|
|
||||||
|
final leading = this.leading ??
|
||||||
|
StreamUserAvatar(
|
||||||
|
user: user,
|
||||||
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final title = this.title ??
|
||||||
|
MessageSearchListTileTitle(
|
||||||
|
messageResponse: messageResponse,
|
||||||
|
textStyle: channelPreviewTheme.titleStyle,
|
||||||
|
);
|
||||||
|
|
||||||
|
final subtitle = this.subtitle ??
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: StreamMessagePreviewText(
|
||||||
|
message: message,
|
||||||
|
textStyle: channelPreviewTheme.subtitleStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
MessageSearchTileMessageDate(
|
||||||
|
message: message,
|
||||||
|
textStyle: channelPreviewTheme.lastMessageAtStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
visualDensity: visualDensity,
|
||||||
|
contentPadding: contentPadding,
|
||||||
|
tileColor: tileColor,
|
||||||
|
leading: leading,
|
||||||
|
trailing: trailing,
|
||||||
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageSearchListTileTitle extends StatelessWidget {
|
||||||
|
const MessageSearchListTileTitle({
|
||||||
|
Key? key,
|
||||||
|
required this.messageResponse,
|
||||||
|
this.textStyle,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final GetMessageResponse messageResponse;
|
||||||
|
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final user = messageResponse.message.user!;
|
||||||
|
final channel = messageResponse.channel;
|
||||||
|
final channelName = channel?.extraData['name'];
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
user.id == StreamChat.of(context).currentUser?.id
|
||||||
|
? context.translations.youText
|
||||||
|
: user.name,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
if (channelName != null) ...[
|
||||||
|
Text(
|
||||||
|
' ${context.translations.inText} ',
|
||||||
|
style: textStyle?.copyWith(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
channelName as String,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageSearchTileMessageDate extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [MessageSearchTileMessageDate].
|
||||||
|
const MessageSearchTileMessageDate({
|
||||||
|
Key? key,
|
||||||
|
required this.message,
|
||||||
|
this.textStyle,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The searched message response.
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// The text style to use for the date.
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final createdAt = message.createdAt;
|
||||||
|
String stringDate;
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (now.year != createdAt.year ||
|
||||||
|
now.month != createdAt.month ||
|
||||||
|
now.day != createdAt.day) {
|
||||||
|
stringDate = Jiffy(createdAt.toLocal()).yMd;
|
||||||
|
} else {
|
||||||
|
stringDate = Jiffy(createdAt.toLocal()).jm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
stringDate,
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+446
@@ -0,0 +1,446 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/v4/stream_list_view_indexed_widget_builder.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// Default separator builder for [StreamMessageSearchListView].
|
||||||
|
Widget defaultMessageSearchListViewSeparatorBuilder(
|
||||||
|
BuildContext context,
|
||||||
|
List<GetMessageResponse> responses,
|
||||||
|
int index,
|
||||||
|
) =>
|
||||||
|
const StreamMessageSearchListSeparator();
|
||||||
|
|
||||||
|
/// Signature for the item builder that creates the children of the
|
||||||
|
/// [StreamMessageSearchListView].
|
||||||
|
typedef StreamMessageSearchListViewIndexedWidgetBuilder
|
||||||
|
= StreamListViewIndexedWidgetBuilder<GetMessageResponse,
|
||||||
|
StreamMessageSearchListTile>;
|
||||||
|
|
||||||
|
/// A [ListView] that shows a list of [GetMessageResponse]s,
|
||||||
|
/// it uses [StreamMessageSearchListTile] as a default item.
|
||||||
|
///
|
||||||
|
/// This is the new version of [MessageSearchListView] that uses
|
||||||
|
/// [StreamMessageSearchListController].
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// StreamMessageSearchListView(
|
||||||
|
/// controller: controller,
|
||||||
|
/// onMessageTap: (user) {
|
||||||
|
/// // Handle user tap event
|
||||||
|
/// },
|
||||||
|
/// onMessageLongPress: (user) {
|
||||||
|
/// // Handle user long press event
|
||||||
|
/// },
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [StreamMessageSearchListTile]
|
||||||
|
/// * [StreamMessageSearchListController]
|
||||||
|
class StreamMessageSearchListView extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamMessageSearchListView].
|
||||||
|
const StreamMessageSearchListView({
|
||||||
|
Key? key,
|
||||||
|
required this.controller,
|
||||||
|
this.itemBuilder,
|
||||||
|
this.separatorBuilder = defaultMessageSearchListViewSeparatorBuilder,
|
||||||
|
this.emptyBuilder,
|
||||||
|
this.loadingBuilder,
|
||||||
|
this.errorBuilder,
|
||||||
|
this.onMessageTap,
|
||||||
|
this.onMessageLongPress,
|
||||||
|
this.loadMoreTriggerIndex = 3,
|
||||||
|
this.padding,
|
||||||
|
this.physics,
|
||||||
|
this.reverse = false,
|
||||||
|
this.scrollController,
|
||||||
|
this.primary,
|
||||||
|
this.scrollBehavior,
|
||||||
|
this.shrinkWrap = false,
|
||||||
|
this.cacheExtent,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
|
this.restorationId,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The [StreamUserListController] used to control the list of
|
||||||
|
/// searched messages.
|
||||||
|
final StreamMessageSearchListController controller;
|
||||||
|
|
||||||
|
/// A builder that is called to build items in the [ListView].
|
||||||
|
///
|
||||||
|
/// The `messageResponse` parameter is the [GetMessageResponse] at this
|
||||||
|
/// position in the list and the `defaultWidget` is the default widget used
|
||||||
|
/// i.e: [StreamMessageSearchListTile].
|
||||||
|
final StreamMessageSearchListViewIndexedWidgetBuilder? itemBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the list separator.
|
||||||
|
final PagedValueListViewIndexedWidgetBuilder<GetMessageResponse>
|
||||||
|
separatorBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the empty state of the list.
|
||||||
|
///
|
||||||
|
/// If not provided, [StreamMessageSearchListEmptyWidget] will be used.
|
||||||
|
final WidgetBuilder? emptyBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the loading state of the list.
|
||||||
|
///
|
||||||
|
/// If not provided, [StreamMessageSearchListLoadingTile] will be used.
|
||||||
|
final WidgetBuilder? loadingBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the error state of the list.
|
||||||
|
///
|
||||||
|
/// If not provided, [StreamMessageSearchListErrorWidget] will be used.
|
||||||
|
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
|
||||||
|
|
||||||
|
/// Called when the user taps this list tile.
|
||||||
|
final void Function(GetMessageResponse)? onMessageTap;
|
||||||
|
|
||||||
|
/// Called when the user long-presses on this list tile.
|
||||||
|
final void Function(GetMessageResponse)? onMessageLongPress;
|
||||||
|
|
||||||
|
/// The index to take into account when triggering [controller.loadMore].
|
||||||
|
final int loadMoreTriggerIndex;
|
||||||
|
|
||||||
|
/// The amount of space by which to inset the children.
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.reverse}
|
||||||
|
/// Whether the scroll view scrolls in the reading direction.
|
||||||
|
///
|
||||||
|
/// For example, if [scrollDirection] is [Axis.vertical], then the scroll view
|
||||||
|
/// scrolls from top to bottom when [reverse] is false and from bottom to top
|
||||||
|
/// when [reverse] is true.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.controller}
|
||||||
|
/// An object that can be used to control the position to which this scroll
|
||||||
|
/// view is scrolled.
|
||||||
|
///
|
||||||
|
/// Must be null if [primary] is true.
|
||||||
|
///
|
||||||
|
/// A [ScrollController] serves several purposes. It can be used to control
|
||||||
|
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
|
||||||
|
/// It can be used to control whether the scroll view should automatically
|
||||||
|
/// save and restore its scroll position in the [PageStorage] (see
|
||||||
|
/// [ScrollController.keepScrollOffset]). It can be used to read the current
|
||||||
|
/// scroll position (see [ScrollController.offset]), or change it (see
|
||||||
|
/// [ScrollController.animateTo]).
|
||||||
|
/// {@endtemplate}
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.primary}
|
||||||
|
/// Whether this is the primary scroll view associated with the parent
|
||||||
|
/// [PrimaryScrollController].
|
||||||
|
///
|
||||||
|
/// When this is true, the scroll view is scrollable even if it does not have
|
||||||
|
/// sufficient content to actually scroll. Otherwise, by default the user can
|
||||||
|
/// only scroll the view if it has sufficient content. See [physics].
|
||||||
|
///
|
||||||
|
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
|
||||||
|
/// ScrollAction is not handled by an otherwise focused part of the
|
||||||
|
/// application, the ScrollAction will be evaluated using this scroll view,
|
||||||
|
/// for example, when executing [Shortcuts] key events like page up and down.
|
||||||
|
///
|
||||||
|
/// On iOS, this also identifies the scroll view that will scroll to top in
|
||||||
|
/// response to a tap in the status bar.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// Defaults to true when [scrollController] is null.
|
||||||
|
final bool? primary;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.shadow.scrollBehavior}
|
||||||
|
///
|
||||||
|
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
|
||||||
|
/// [ScrollPhysics] is provided in [physics], it will take precedence,
|
||||||
|
/// followed by [scrollBehavior], and then the inherited ancestor
|
||||||
|
/// [ScrollBehavior].
|
||||||
|
final ScrollBehavior? scrollBehavior;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.shrinkWrap}
|
||||||
|
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||||
|
/// determined by the contents being viewed.
|
||||||
|
///
|
||||||
|
/// If the scroll view does not shrink wrap, then the scroll view will expand
|
||||||
|
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
|
||||||
|
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
|
||||||
|
/// be true.
|
||||||
|
///
|
||||||
|
/// Shrink wrapping the content of the scroll view is significantly more
|
||||||
|
/// expensive than expanding to the maximum allowed size because the content
|
||||||
|
/// can expand and contract during scrolling, which means the size of the
|
||||||
|
/// scroll view needs to be recomputed whenever the scroll position changes.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.physics}
|
||||||
|
/// How the scroll view should respond to user input.
|
||||||
|
///
|
||||||
|
/// For example, determines how the scroll view continues to animate after the
|
||||||
|
/// user stops dragging the scroll view.
|
||||||
|
///
|
||||||
|
/// Defaults to matching platform conventions. Furthermore, if [primary] is
|
||||||
|
/// false, then the user cannot scroll if there is insufficient content to
|
||||||
|
/// scroll, while if [primary] is true, they can always attempt to scroll.
|
||||||
|
///
|
||||||
|
/// To force the scroll view to always be scrollable even if there is
|
||||||
|
/// insufficient content, as if [primary] was true but without necessarily
|
||||||
|
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
|
||||||
|
/// object, as in:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// To force the scroll view to use the default platform conventions and not
|
||||||
|
/// be scrollable if there is insufficient content, regardless of the value of
|
||||||
|
/// [primary], provide an explicit [ScrollPhysics] object, as in:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// physics: const ScrollPhysics(),
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The physics can be changed dynamically (by providing a new object in a
|
||||||
|
/// subsequent build), but new physics will only take effect if the _class_ of
|
||||||
|
/// the provided object changes. Merely constructing a new instance with a
|
||||||
|
/// different configuration is insufficient to cause the physics to be
|
||||||
|
/// reapplied. (This is because the final object used is generated
|
||||||
|
/// dynamically, which can be relatively expensive, and it would be
|
||||||
|
/// inefficient to speculatively create this object each frame to see if the
|
||||||
|
/// physics should be updated.)
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
|
||||||
|
/// [ScrollPhysics] provided by that behavior will take precedence after
|
||||||
|
/// [physics].
|
||||||
|
final ScrollPhysics? physics;
|
||||||
|
|
||||||
|
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
|
||||||
|
final double? cacheExtent;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.restorationId}
|
||||||
|
final String? restorationId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) =>
|
||||||
|
PagedValueListView<String, GetMessageResponse>(
|
||||||
|
padding: padding,
|
||||||
|
physics: physics,
|
||||||
|
reverse: reverse,
|
||||||
|
controller: controller,
|
||||||
|
primary: primary,
|
||||||
|
shrinkWrap: shrinkWrap,
|
||||||
|
keyboardDismissBehavior: keyboardDismissBehavior,
|
||||||
|
restorationId: restorationId,
|
||||||
|
dragStartBehavior: dragStartBehavior,
|
||||||
|
cacheExtent: cacheExtent,
|
||||||
|
loadMoreTriggerIndex: loadMoreTriggerIndex,
|
||||||
|
separatorBuilder: separatorBuilder,
|
||||||
|
itemBuilder: (context, messageResponses, index) {
|
||||||
|
final messageResponse = messageResponses[index];
|
||||||
|
final onTap = onMessageTap;
|
||||||
|
final onLongPress = onMessageLongPress;
|
||||||
|
|
||||||
|
final streamUserListTile = StreamMessageSearchListTile(
|
||||||
|
messageResponse: messageResponse,
|
||||||
|
onTap: onTap == null ? null : () => onTap(messageResponse),
|
||||||
|
onLongPress:
|
||||||
|
onLongPress == null ? null : () => onLongPress(messageResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
return itemBuilder?.call(
|
||||||
|
context,
|
||||||
|
messageResponses,
|
||||||
|
index,
|
||||||
|
streamUserListTile,
|
||||||
|
) ??
|
||||||
|
streamUserListTile;
|
||||||
|
},
|
||||||
|
emptyBuilder: (context) =>
|
||||||
|
emptyBuilder?.call(context) ??
|
||||||
|
const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: StreamMessageSearchListEmptyWidget(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loadMoreErrorBuilder: (context, error) =>
|
||||||
|
StreamMessageSearchListLoadMoreError(onTap: controller.retry),
|
||||||
|
loadMoreIndicatorBuilder: (context) => const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: StreamMessageSearchListLoadMoreIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loadingBuilder: (context) =>
|
||||||
|
loadingBuilder?.call(context) ??
|
||||||
|
ListView.separated(
|
||||||
|
padding: padding,
|
||||||
|
physics: physics,
|
||||||
|
reverse: reverse,
|
||||||
|
itemCount: 25,
|
||||||
|
separatorBuilder: (_, __) =>
|
||||||
|
const StreamMessageSearchListSeparator(),
|
||||||
|
itemBuilder: (_, __) => const StreamChannelListLoadingTile(),
|
||||||
|
),
|
||||||
|
errorBuilder: (context, error) =>
|
||||||
|
errorBuilder?.call(context, error) ??
|
||||||
|
Center(
|
||||||
|
child: StreamMessageSearchListErrorWidget(
|
||||||
|
onPressed: controller.refresh,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [StreamMessageSearchListTile] that can be used in a [ListView] to show a
|
||||||
|
/// loading tile while waiting for the [StreamMessageSearchListController] to
|
||||||
|
/// load more messages.
|
||||||
|
class StreamMessageSearchListLoadMoreIndicator extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamMessageSearchListLoadMoreIndicator].
|
||||||
|
const StreamMessageSearchListLoadMoreIndicator({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [StreamMessageSearchListTile] that is used to display the error indicator
|
||||||
|
/// when loading more messages fails.
|
||||||
|
class StreamMessageSearchListLoadMoreError extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamMessageSearchListLoadMoreError].
|
||||||
|
const StreamMessageSearchListLoadMoreError({
|
||||||
|
Key? key,
|
||||||
|
this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The callback to invoke when the user taps on the error indicator.
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
color: theme.colorTheme.textLowEmphasis.withOpacity(0.9),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.translations.loadingChannelsError,
|
||||||
|
style: theme.textTheme.body.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamSvgIcon.retry(color: Colors.white),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that is used to display a separator between
|
||||||
|
/// [StreamMessageSearchListTile] items.
|
||||||
|
class StreamMessageSearchListSeparator extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamMessageSearchListSeparator].
|
||||||
|
const StreamMessageSearchListSeparator({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
|
||||||
|
return Container(
|
||||||
|
height: 1,
|
||||||
|
color: effect.color!.withOpacity(effect.alpha ?? 1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that is used to display an error screen
|
||||||
|
/// when [StreamMessageSearchListController] fails to load initial messages.
|
||||||
|
class StreamMessageSearchListErrorWidget extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamMessageSearchListErrorWidget] widget.
|
||||||
|
const StreamMessageSearchListErrorWidget({
|
||||||
|
Key? key,
|
||||||
|
this.onPressed,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The callback to invoke when the user taps on the retry button.
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
const WidgetSpan(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: 2),
|
||||||
|
child: Icon(Icons.error_outline),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: context.translations.loadingChannelsError),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Text(context.translations.retryLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that is used to display an empty state when
|
||||||
|
/// [StreamMessageSearchListController] loads zero messages.
|
||||||
|
class StreamMessageSearchListEmptyWidget extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamMessageSearchListEmptyWidget] widget.
|
||||||
|
const StreamMessageSearchListEmptyWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
StreamSvgIcon.message(
|
||||||
|
size: 148,
|
||||||
|
color: chatThemeData.colorTheme.disabled,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
Text(
|
||||||
|
context.translations.letsStartChattingLabel,
|
||||||
|
style: chatThemeData.textTheme.headline,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
typedef StreamListViewIndexedWidgetBuilder<ItemType, WidgetType extends Widget>
|
||||||
|
= Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
List<ItemType> items,
|
||||||
|
int index,
|
||||||
|
WidgetType defaultWidget,
|
||||||
|
);
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// A widget that renders a preview of the message text.
|
||||||
|
class StreamMessagePreviewText extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamMessagePreviewText].
|
||||||
|
const StreamMessagePreviewText({
|
||||||
|
Key? key,
|
||||||
|
required this.message,
|
||||||
|
this.language,
|
||||||
|
this.textStyle,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The message to display.
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// The language to use for translations.
|
||||||
|
final String? language;
|
||||||
|
|
||||||
|
/// The style to use for the text.
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final messageText = message
|
||||||
|
.translate(language ?? 'en')
|
||||||
|
.replaceMentions(linkify: false)
|
||||||
|
.text;
|
||||||
|
final messageAttachments = message.attachments;
|
||||||
|
final messageMentionedUsers = message.mentionedUsers;
|
||||||
|
|
||||||
|
final mentionedUsersRegex = RegExp(
|
||||||
|
messageMentionedUsers.map((it) => '@${it.name}').join('|'),
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final messageTextParts = [
|
||||||
|
...messageAttachments.map((it) {
|
||||||
|
if (it.type == 'image') {
|
||||||
|
return '📷';
|
||||||
|
} else if (it.type == 'video') {
|
||||||
|
return '🎬';
|
||||||
|
} else if (it.type == 'giphy') {
|
||||||
|
return '[GIF]';
|
||||||
|
}
|
||||||
|
return it == message.attachments.last
|
||||||
|
? (it.title ?? 'File')
|
||||||
|
: '${it.title ?? 'File'} , ';
|
||||||
|
}),
|
||||||
|
if (messageText != null)
|
||||||
|
if (messageMentionedUsers.isNotEmpty)
|
||||||
|
...mentionedUsersRegex.allMatchesWithSep(messageText)
|
||||||
|
else
|
||||||
|
messageText,
|
||||||
|
];
|
||||||
|
|
||||||
|
final fontStyle = (message.isSystem || message.isDeleted)
|
||||||
|
? FontStyle.italic
|
||||||
|
: FontStyle.normal;
|
||||||
|
|
||||||
|
final regularTextStyle = textStyle?.copyWith(fontStyle: fontStyle);
|
||||||
|
|
||||||
|
final mentionsTextStyle = textStyle?.copyWith(
|
||||||
|
fontStyle: fontStyle,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
|
|
||||||
|
final spans = [
|
||||||
|
for (final part in messageTextParts)
|
||||||
|
if (messageMentionedUsers.isNotEmpty &&
|
||||||
|
messageMentionedUsers.any((it) => '@${it.name}' == part))
|
||||||
|
TextSpan(
|
||||||
|
text: part,
|
||||||
|
style: mentionsTextStyle,
|
||||||
|
)
|
||||||
|
else if (messageAttachments.isNotEmpty &&
|
||||||
|
messageAttachments
|
||||||
|
.where((it) => it.title != null)
|
||||||
|
.any((it) => it.title == part))
|
||||||
|
TextSpan(
|
||||||
|
text: part,
|
||||||
|
style: regularTextStyle?.copyWith(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
TextSpan(
|
||||||
|
text: part,
|
||||||
|
style: regularTextStyle,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(children: spans),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _RegExpX on RegExp {
|
||||||
|
List<String> allMatchesWithSep(String input, [int start = 0]) {
|
||||||
|
final result = <String>[];
|
||||||
|
for (final match in allMatches(input, start)) {
|
||||||
|
result.add(input.substring(start, match.start));
|
||||||
|
// ignore: cascade_invocations
|
||||||
|
result.add(match[0]!);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
start = match.end;
|
||||||
|
}
|
||||||
|
result.add(input.substring(start));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:jiffy/jiffy.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||||
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
|
||||||
|
show User;
|
||||||
|
|
||||||
|
/// A widget that displays a user.
|
||||||
|
///
|
||||||
|
/// This widget is intended to be used as a Tile in [StreamUserListView]
|
||||||
|
///
|
||||||
|
/// It shows the user's avatar, name and last message.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [StreamUserListView]
|
||||||
|
/// * [StreamUserAvatar]
|
||||||
|
class StreamUserListTile extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamUserListTile].
|
||||||
|
const StreamUserListTile({
|
||||||
|
Key? key,
|
||||||
|
required this.user,
|
||||||
|
this.leading,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.selected = false,
|
||||||
|
this.selectedWidget,
|
||||||
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
|
this.tileColor,
|
||||||
|
this.visualDensity = VisualDensity.compact,
|
||||||
|
this.contentPadding = const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The user to display.
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
/// A widget to display before the title.
|
||||||
|
final Widget? leading;
|
||||||
|
|
||||||
|
/// The primary content of the list tile.
|
||||||
|
final Widget? title;
|
||||||
|
|
||||||
|
/// Additional content displayed below the title.
|
||||||
|
final Widget? subtitle;
|
||||||
|
|
||||||
|
/// A widget to display at the end of tile.
|
||||||
|
final Widget? selectedWidget;
|
||||||
|
|
||||||
|
/// If this tile is also [enabled] then icons and text are rendered with the same color.
|
||||||
|
///
|
||||||
|
/// By default the selected color is the theme's primary color. The selected color
|
||||||
|
/// can be overridden with a [ListTileTheme].
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// Here is an example of using a [StatefulWidget] to keep track of the
|
||||||
|
/// selected index, and using that to set the `selected` property on the
|
||||||
|
/// corresponding [ListTile].
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/list_tile/list_tile.selected.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
final bool selected;
|
||||||
|
|
||||||
|
/// 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 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}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
|
||||||
|
/// widgets within a [Theme].
|
||||||
|
final VisualDensity visualDensity;
|
||||||
|
|
||||||
|
/// The tile's internal padding.
|
||||||
|
///
|
||||||
|
/// Insets a [ListTile]'s contents: its [leading], [title], [subtitle],
|
||||||
|
/// and [trailing] widgets.
|
||||||
|
///
|
||||||
|
/// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used.
|
||||||
|
final EdgeInsetsGeometry contentPadding;
|
||||||
|
|
||||||
|
/// Creates a copy of this tile but with the given fields replaced with
|
||||||
|
/// the new values.
|
||||||
|
StreamUserListTile copyWith({
|
||||||
|
Key? key,
|
||||||
|
User? user,
|
||||||
|
Widget? leading,
|
||||||
|
Widget? title,
|
||||||
|
Widget? subtitle,
|
||||||
|
Widget? selectedWidget,
|
||||||
|
bool? selected,
|
||||||
|
GestureTapCallback? onTap,
|
||||||
|
GestureLongPressCallback? onLongPress,
|
||||||
|
Color? tileColor,
|
||||||
|
VisualDensity? visualDensity,
|
||||||
|
EdgeInsetsGeometry? contentPadding,
|
||||||
|
}) =>
|
||||||
|
StreamUserListTile(
|
||||||
|
key: key ?? this.key,
|
||||||
|
user: user ?? this.user,
|
||||||
|
leading: leading ?? this.leading,
|
||||||
|
title: title ?? this.title,
|
||||||
|
subtitle: subtitle ?? this.subtitle,
|
||||||
|
selectedWidget: selectedWidget ?? this.selectedWidget,
|
||||||
|
selected: selected ?? this.selected,
|
||||||
|
onTap: onTap ?? this.onTap,
|
||||||
|
onLongPress: onLongPress ?? this.onLongPress,
|
||||||
|
tileColor: tileColor ?? this.tileColor,
|
||||||
|
visualDensity: visualDensity ?? this.visualDensity,
|
||||||
|
contentPadding: contentPadding ?? this.contentPadding,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
|
|
||||||
|
final leading = this.leading ??
|
||||||
|
StreamUserAvatar(
|
||||||
|
user: user,
|
||||||
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final title = this.title ??
|
||||||
|
Text(
|
||||||
|
user.name,
|
||||||
|
style: chatThemeData.textTheme.bodyBold,
|
||||||
|
);
|
||||||
|
|
||||||
|
final subtitle = this.subtitle ??
|
||||||
|
UserLastActive(
|
||||||
|
user: user,
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedWidget = this.selectedWidget ??
|
||||||
|
StreamSvgIcon.checkSend(
|
||||||
|
color: chatThemeData.colorTheme.accentPrimary,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
leading: leading,
|
||||||
|
trailing: selected ? selectedWidget : null,
|
||||||
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that displays a user's last active time.
|
||||||
|
class UserLastActive extends StatelessWidget {
|
||||||
|
/// Creates a new instance of the [UserLastActive] widget.
|
||||||
|
const UserLastActive({
|
||||||
|
Key? key,
|
||||||
|
required this.user,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The user whose last active time is displayed.
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final chatTheme = StreamChatTheme.of(context);
|
||||||
|
return Text(
|
||||||
|
user.online
|
||||||
|
? context.translations.userOnlineText
|
||||||
|
: '${context.translations.userLastOnlineText} '
|
||||||
|
'${Jiffy(user.lastActive).fromNow()}',
|
||||||
|
style: chatTheme.textTheme.footnote.copyWith(
|
||||||
|
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/v4/stream_list_view_indexed_widget_builder.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/v4/user_list_view/stream_user_list_tile.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// Default separator builder for [StreamUserListView].
|
||||||
|
Widget defaultUserListViewSeparatorBuilder(
|
||||||
|
BuildContext context,
|
||||||
|
List<User> users,
|
||||||
|
int index,
|
||||||
|
) =>
|
||||||
|
const StreamUserListSeparator();
|
||||||
|
|
||||||
|
/// Signature for the item builder that creates the children of the
|
||||||
|
/// [StreamUserListView].
|
||||||
|
typedef StreamUserListViewIndexedWidgetBuilder
|
||||||
|
= StreamListViewIndexedWidgetBuilder<User, StreamUserListTile>;
|
||||||
|
|
||||||
|
/// A [ListView] that shows a list of [User]s,
|
||||||
|
/// it uses [StreamUserListTile] as a default item.
|
||||||
|
///
|
||||||
|
/// This is the new version of [UserListView] that uses
|
||||||
|
/// [StreamUserListController].
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// StreamUserListView(
|
||||||
|
/// controller: controller,
|
||||||
|
/// onUserTap: (user) {
|
||||||
|
/// // Handle user tap event
|
||||||
|
/// },
|
||||||
|
/// onUserLongPress: (user) {
|
||||||
|
/// // Handle user long press event
|
||||||
|
/// },
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [StreamUserListTile]
|
||||||
|
/// * [StreamUserListController]
|
||||||
|
class StreamUserListView extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamUserListView].
|
||||||
|
const StreamUserListView({
|
||||||
|
Key? key,
|
||||||
|
required this.controller,
|
||||||
|
this.itemBuilder,
|
||||||
|
this.separatorBuilder = defaultUserListViewSeparatorBuilder,
|
||||||
|
this.emptyBuilder,
|
||||||
|
this.loadingBuilder,
|
||||||
|
this.errorBuilder,
|
||||||
|
this.onUserTap,
|
||||||
|
this.onUserLongPress,
|
||||||
|
this.loadMoreTriggerIndex = 3,
|
||||||
|
this.padding,
|
||||||
|
this.physics,
|
||||||
|
this.reverse = false,
|
||||||
|
this.scrollController,
|
||||||
|
this.primary,
|
||||||
|
this.scrollBehavior,
|
||||||
|
this.shrinkWrap = false,
|
||||||
|
this.cacheExtent,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
|
this.restorationId,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The [StreamUserListController] used to control the list of users.
|
||||||
|
final StreamUserListController controller;
|
||||||
|
|
||||||
|
/// A builder that is called to build items in the [ListView].
|
||||||
|
///
|
||||||
|
/// The `user` parameter is the [User] at this position in the list
|
||||||
|
/// and the `defaultWidget` is the default widget used
|
||||||
|
/// i.e: [StreamUserListTile].
|
||||||
|
final StreamUserListViewIndexedWidgetBuilder? itemBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the list separator.
|
||||||
|
final PagedValueListViewIndexedWidgetBuilder<User> separatorBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the empty state of the list.
|
||||||
|
///
|
||||||
|
/// If not provided, [StreamUserListEmptyWidget] will be used.
|
||||||
|
final WidgetBuilder? emptyBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the loading state of the list.
|
||||||
|
///
|
||||||
|
/// If not provided, [StreamUserListLoadingTile] will be used.
|
||||||
|
final WidgetBuilder? loadingBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the error state of the list.
|
||||||
|
///
|
||||||
|
/// If not provided, [StreamUserListErrorWidget] will be used.
|
||||||
|
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
|
||||||
|
|
||||||
|
/// Called when the user taps this list tile.
|
||||||
|
final void Function(User)? onUserTap;
|
||||||
|
|
||||||
|
/// Called when the user long-presses on this list tile.
|
||||||
|
final void Function(User)? onUserLongPress;
|
||||||
|
|
||||||
|
/// The index to take into account when triggering [controller.loadMore].
|
||||||
|
final int loadMoreTriggerIndex;
|
||||||
|
|
||||||
|
/// The amount of space by which to inset the children.
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.reverse}
|
||||||
|
/// Whether the scroll view scrolls in the reading direction.
|
||||||
|
///
|
||||||
|
/// For example, if [scrollDirection] is [Axis.vertical], then the scroll view
|
||||||
|
/// scrolls from top to bottom when [reverse] is false and from bottom to top
|
||||||
|
/// when [reverse] is true.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.controller}
|
||||||
|
/// An object that can be used to control the position to which this scroll
|
||||||
|
/// view is scrolled.
|
||||||
|
///
|
||||||
|
/// Must be null if [primary] is true.
|
||||||
|
///
|
||||||
|
/// A [ScrollController] serves several purposes. It can be used to control
|
||||||
|
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
|
||||||
|
/// It can be used to control whether the scroll view should automatically
|
||||||
|
/// save and restore its scroll position in the [PageStorage] (see
|
||||||
|
/// [ScrollController.keepScrollOffset]). It can be used to read the current
|
||||||
|
/// scroll position (see [ScrollController.offset]), or change it (see
|
||||||
|
/// [ScrollController.animateTo]).
|
||||||
|
/// {@endtemplate}
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.primary}
|
||||||
|
/// Whether this is the primary scroll view associated with the parent
|
||||||
|
/// [PrimaryScrollController].
|
||||||
|
///
|
||||||
|
/// When this is true, the scroll view is scrollable even if it does not have
|
||||||
|
/// sufficient content to actually scroll. Otherwise, by default the user can
|
||||||
|
/// only scroll the view if it has sufficient content. See [physics].
|
||||||
|
///
|
||||||
|
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
|
||||||
|
/// ScrollAction is not handled by an otherwise focused part of the
|
||||||
|
/// application, the ScrollAction will be evaluated using this scroll view,
|
||||||
|
/// for example, when executing [Shortcuts] key events like page up and down.
|
||||||
|
///
|
||||||
|
/// On iOS, this also identifies the scroll view that will scroll to top in
|
||||||
|
/// response to a tap in the status bar.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// Defaults to true when [scrollController] is null.
|
||||||
|
final bool? primary;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.shadow.scrollBehavior}
|
||||||
|
///
|
||||||
|
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
|
||||||
|
/// [ScrollPhysics] is provided in [physics], it will take precedence,
|
||||||
|
/// followed by [scrollBehavior], and then the inherited ancestor
|
||||||
|
/// [ScrollBehavior].
|
||||||
|
final ScrollBehavior? scrollBehavior;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.shrinkWrap}
|
||||||
|
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||||
|
/// determined by the contents being viewed.
|
||||||
|
///
|
||||||
|
/// If the scroll view does not shrink wrap, then the scroll view will expand
|
||||||
|
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
|
||||||
|
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
|
||||||
|
/// be true.
|
||||||
|
///
|
||||||
|
/// Shrink wrapping the content of the scroll view is significantly more
|
||||||
|
/// expensive than expanding to the maximum allowed size because the content
|
||||||
|
/// can expand and contract during scrolling, which means the size of the
|
||||||
|
/// scroll view needs to be recomputed whenever the scroll position changes.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.physics}
|
||||||
|
/// How the scroll view should respond to user input.
|
||||||
|
///
|
||||||
|
/// For example, determines how the scroll view continues to animate after the
|
||||||
|
/// user stops dragging the scroll view.
|
||||||
|
///
|
||||||
|
/// Defaults to matching platform conventions. Furthermore, if [primary] is
|
||||||
|
/// false, then the user cannot scroll if there is insufficient content to
|
||||||
|
/// scroll, while if [primary] is true, they can always attempt to scroll.
|
||||||
|
///
|
||||||
|
/// To force the scroll view to always be scrollable even if there is
|
||||||
|
/// insufficient content, as if [primary] was true but without necessarily
|
||||||
|
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
|
||||||
|
/// object, as in:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// To force the scroll view to use the default platform conventions and not
|
||||||
|
/// be scrollable if there is insufficient content, regardless of the value of
|
||||||
|
/// [primary], provide an explicit [ScrollPhysics] object, as in:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// physics: const ScrollPhysics(),
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The physics can be changed dynamically (by providing a new object in a
|
||||||
|
/// subsequent build), but new physics will only take effect if the _class_ of
|
||||||
|
/// the provided object changes. Merely constructing a new instance with a
|
||||||
|
/// different configuration is insufficient to cause the physics to be
|
||||||
|
/// reapplied. (This is because the final object used is generated
|
||||||
|
/// dynamically, which can be relatively expensive, and it would be
|
||||||
|
/// inefficient to speculatively create this object each frame to see if the
|
||||||
|
/// physics should be updated.)
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
|
||||||
|
/// [ScrollPhysics] provided by that behavior will take precedence after
|
||||||
|
/// [physics].
|
||||||
|
final ScrollPhysics? physics;
|
||||||
|
|
||||||
|
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
|
||||||
|
final double? cacheExtent;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.restorationId}
|
||||||
|
final String? restorationId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => PagedValueListView<int, User>(
|
||||||
|
padding: padding,
|
||||||
|
physics: physics,
|
||||||
|
reverse: reverse,
|
||||||
|
controller: controller,
|
||||||
|
primary: primary,
|
||||||
|
shrinkWrap: shrinkWrap,
|
||||||
|
keyboardDismissBehavior: keyboardDismissBehavior,
|
||||||
|
restorationId: restorationId,
|
||||||
|
dragStartBehavior: dragStartBehavior,
|
||||||
|
cacheExtent: cacheExtent,
|
||||||
|
loadMoreTriggerIndex: loadMoreTriggerIndex,
|
||||||
|
separatorBuilder: separatorBuilder,
|
||||||
|
itemBuilder: (context, users, index) {
|
||||||
|
final user = users[index];
|
||||||
|
final onTap = onUserTap;
|
||||||
|
final onLongPress = onUserLongPress;
|
||||||
|
|
||||||
|
final streamUserListTile = StreamUserListTile(
|
||||||
|
user: user,
|
||||||
|
onTap: onTap == null ? null : () => onTap(user),
|
||||||
|
onLongPress: onLongPress == null ? null : () => onLongPress(user),
|
||||||
|
);
|
||||||
|
|
||||||
|
return itemBuilder?.call(
|
||||||
|
context,
|
||||||
|
users,
|
||||||
|
index,
|
||||||
|
streamUserListTile,
|
||||||
|
) ??
|
||||||
|
streamUserListTile;
|
||||||
|
},
|
||||||
|
loadMoreErrorBuilder: (context, error) =>
|
||||||
|
StreamUserListLoadMoreError(onTap: controller.retry),
|
||||||
|
loadMoreIndicatorBuilder: (context) => const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: StreamUserListLoadMoreIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
emptyBuilder: (context) =>
|
||||||
|
emptyBuilder?.call(context) ??
|
||||||
|
const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: StreamUserListEmptyWidget(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loadingBuilder: (context) =>
|
||||||
|
loadingBuilder?.call(context) ??
|
||||||
|
ListView.separated(
|
||||||
|
padding: padding,
|
||||||
|
physics: physics,
|
||||||
|
reverse: reverse,
|
||||||
|
itemCount: 25,
|
||||||
|
separatorBuilder: (_, __) => const StreamUserListSeparator(),
|
||||||
|
itemBuilder: (_, __) => const StreamChannelListLoadingTile(),
|
||||||
|
),
|
||||||
|
errorBuilder: (context, error) =>
|
||||||
|
errorBuilder?.call(context, error) ??
|
||||||
|
Center(
|
||||||
|
child: StreamUserListErrorWidget(
|
||||||
|
onPressed: controller.refresh,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [StreamUserListTile] that can be used in a [ListView] to show a
|
||||||
|
/// loading tile while waiting for the [StreamUserListController] to load
|
||||||
|
/// more channels.
|
||||||
|
class StreamUserListLoadMoreIndicator extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamUserListLoadMoreIndicator].
|
||||||
|
const StreamUserListLoadMoreIndicator({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [StreamUserListTile] that is used to display the error indicator when
|
||||||
|
/// loading more users fails.
|
||||||
|
class StreamUserListLoadMoreError extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamUserListLoadMoreError].
|
||||||
|
const StreamUserListLoadMoreError({
|
||||||
|
Key? key,
|
||||||
|
this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The callback to invoke when the user taps on the error indicator.
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
color: theme.colorTheme.textLowEmphasis.withOpacity(0.9),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.translations.loadingChannelsError,
|
||||||
|
style: theme.textTheme.body.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamSvgIcon.retry(color: Colors.white),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that is used to display a separator between
|
||||||
|
/// [StreamUserListTile] items.
|
||||||
|
class StreamUserListSeparator extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamUserListSeparator].
|
||||||
|
const StreamUserListSeparator({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
|
||||||
|
return Container(
|
||||||
|
height: 1,
|
||||||
|
color: effect.color!.withOpacity(effect.alpha ?? 1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that is used to display an error screen
|
||||||
|
/// when [StreamUserListController] fails to load initial users.
|
||||||
|
class StreamUserListErrorWidget extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamUserListErrorWidget] widget.
|
||||||
|
const StreamUserListErrorWidget({
|
||||||
|
Key? key,
|
||||||
|
this.onPressed,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The callback to invoke when the user taps on the retry button.
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
const WidgetSpan(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(right: 2),
|
||||||
|
child: Icon(Icons.error_outline),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: context.translations.loadingChannelsError),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Text(context.translations.retryLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that is used to display an empty state when
|
||||||
|
/// [StreamUserListController] loads zero users.
|
||||||
|
class StreamUserListEmptyWidget extends StatelessWidget {
|
||||||
|
/// Creates a new instance of [StreamUserListEmptyWidget] widget.
|
||||||
|
const StreamUserListEmptyWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
StreamSvgIcon.message(
|
||||||
|
size: 148,
|
||||||
|
color: chatThemeData.colorTheme.disabled,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
Text(
|
||||||
|
context.translations.letsStartChattingLabel,
|
||||||
|
style: chatThemeData.textTheme.headline,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ export 'src/user_item.dart';
|
|||||||
export 'src/user_list_view.dart';
|
export 'src/user_list_view.dart';
|
||||||
export 'src/user_mention_tile.dart';
|
export 'src/user_mention_tile.dart';
|
||||||
export 'src/utils.dart';
|
export 'src/utils.dart';
|
||||||
|
|
||||||
// v4
|
// v4
|
||||||
export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart';
|
export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart';
|
||||||
export 'src/v4/channel_list_view/stream_channel_list_tile.dart';
|
export 'src/v4/channel_list_view/stream_channel_list_tile.dart';
|
||||||
@@ -57,7 +58,13 @@ export 'src/v4/message_input/stream_attachment_picker.dart';
|
|||||||
export 'src/v4/message_input/stream_message_input.dart';
|
export 'src/v4/message_input/stream_message_input.dart';
|
||||||
export 'src/v4/message_input/stream_message_send_button.dart';
|
export 'src/v4/message_input/stream_message_send_button.dart';
|
||||||
export 'src/v4/message_input/stream_message_text_field.dart';
|
export 'src/v4/message_input/stream_message_text_field.dart';
|
||||||
|
export 'src/v4/message_search_list_view/stream_message_search_list_tile.dart';
|
||||||
|
export 'src/v4/message_search_list_view/stream_message_search_list_view.dart';
|
||||||
export 'src/v4/stream_channel_avatar.dart';
|
export 'src/v4/stream_channel_avatar.dart';
|
||||||
export 'src/v4/stream_channel_info_bottom_sheet.dart';
|
export 'src/v4/stream_channel_info_bottom_sheet.dart';
|
||||||
export 'src/v4/stream_channel_name.dart';
|
export 'src/v4/stream_channel_name.dart';
|
||||||
|
export 'src/v4/stream_list_view_indexed_widget_builder.dart';
|
||||||
|
export 'src/v4/stream_message_preview_text.dart';
|
||||||
|
export 'src/v4/user_list_view/stream_user_list_tile.dart';
|
||||||
|
export 'src/v4/user_list_view/stream_user_list_view.dart';
|
||||||
export 'src/visible_footnote.dart';
|
export 'src/visible_footnote.dart';
|
||||||
|
|||||||
+2
-2
@@ -67,7 +67,7 @@ void main() {
|
|||||||
_context = context;
|
_context = context;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: MessageSearchBloc(
|
body: MessageSearchBloc(
|
||||||
child: StreamMessageSearchListView(
|
child: MessageSearchListView(
|
||||||
filters: Filter.in_('members', const ['test_id']),
|
filters: Filter.in_('members', const ['test_id']),
|
||||||
messageQuery: 'test query',
|
messageQuery: 'test query',
|
||||||
),
|
),
|
||||||
@@ -100,7 +100,7 @@ void main() {
|
|||||||
_context = context;
|
_context = context;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: MessageSearchBloc(
|
body: MessageSearchBloc(
|
||||||
child: StreamMessageSearchListView(
|
child: MessageSearchListView(
|
||||||
filters: Filter.in_('members', const ['test_id']),
|
filters: Filter.in_('members', const ['test_id']),
|
||||||
messageQuery: 'test query',
|
messageQuery: 'test query',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ void main() {
|
|||||||
_context = context;
|
_context = context;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: UsersBloc(
|
body: UsersBloc(
|
||||||
child: StreamUserListView(),
|
child: UserListView(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -89,7 +89,7 @@ void main() {
|
|||||||
_context = context;
|
_context = context;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: UsersBloc(
|
body: UsersBloc(
|
||||||
child: StreamUserListView(),
|
child: UserListView(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart';
|
||||||
|
|
||||||
|
/// Signature for a function that creates a widget for a given index, e.g., in a
|
||||||
|
/// [PagedValueListView].
|
||||||
|
typedef PagedValueListViewIndexedWidgetBuilder<T> = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
List<T> values,
|
||||||
|
int index,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Signature for the item builder that creates the children of the
|
||||||
|
/// [PagedValueListView].
|
||||||
|
typedef PagedValueListViewLoadMoreErrorBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
StreamChatError error,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A [ListView] that loads more pages when the user scrolls to the end of the
|
||||||
|
/// list.
|
||||||
|
///
|
||||||
|
/// Use [loadMoreTriggerIndex] to set the index of the item that triggers the
|
||||||
|
/// loading of the next page.
|
||||||
|
class PagedValueListView<K, V> extends StatefulWidget {
|
||||||
|
/// Creates a new instance of [PagedValueListView] widget.
|
||||||
|
const PagedValueListView({
|
||||||
|
Key? key,
|
||||||
|
required this.controller,
|
||||||
|
required this.itemBuilder,
|
||||||
|
required this.separatorBuilder,
|
||||||
|
required this.emptyBuilder,
|
||||||
|
required this.loadMoreErrorBuilder,
|
||||||
|
required this.loadMoreIndicatorBuilder,
|
||||||
|
required this.loadingBuilder,
|
||||||
|
required this.errorBuilder,
|
||||||
|
this.loadMoreTriggerIndex = 3,
|
||||||
|
this.padding,
|
||||||
|
this.physics,
|
||||||
|
this.reverse = false,
|
||||||
|
this.scrollController,
|
||||||
|
this.primary,
|
||||||
|
this.scrollBehavior,
|
||||||
|
this.shrinkWrap = false,
|
||||||
|
this.cacheExtent,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
|
this.restorationId,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The [PagedValueNotifier] used to control the list of items.
|
||||||
|
final PagedValueNotifier<K, V> controller;
|
||||||
|
|
||||||
|
/// A builder that is called to build items in the [ListView].
|
||||||
|
///
|
||||||
|
/// The `value` parameter is the [V] at this position in the list.
|
||||||
|
final PagedValueListViewIndexedWidgetBuilder<V> itemBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the list separator.
|
||||||
|
final PagedValueListViewIndexedWidgetBuilder<V> separatorBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the empty state of the list.
|
||||||
|
final WidgetBuilder emptyBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the load more error state of the list.
|
||||||
|
final PagedValueListViewLoadMoreErrorBuilder loadMoreErrorBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the load more indicator of the list.
|
||||||
|
final WidgetBuilder loadMoreIndicatorBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the loading state of the list.
|
||||||
|
final WidgetBuilder loadingBuilder;
|
||||||
|
|
||||||
|
/// A builder that is called to build the error state of the list.
|
||||||
|
final Widget Function(BuildContext, StreamChatError) errorBuilder;
|
||||||
|
|
||||||
|
/// The index to take into account when triggering [controller.loadMore].
|
||||||
|
final int loadMoreTriggerIndex;
|
||||||
|
|
||||||
|
/// The amount of space by which to inset the children.
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.reverse}
|
||||||
|
/// Whether the scroll view scrolls in the reading direction.
|
||||||
|
///
|
||||||
|
/// For example, if [scrollDirection] is [Axis.vertical], then the scroll view
|
||||||
|
/// scrolls from top to bottom when [reverse] is false and from bottom to top
|
||||||
|
/// when [reverse] is true.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.controller}
|
||||||
|
/// An object that can be used to control the position to which this scroll
|
||||||
|
/// view is scrolled.
|
||||||
|
///
|
||||||
|
/// Must be null if [primary] is true.
|
||||||
|
///
|
||||||
|
/// A [ScrollController] serves several purposes. It can be used to control
|
||||||
|
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
|
||||||
|
/// It can be used to control whether the scroll view should automatically
|
||||||
|
/// save and restore its scroll position in the [PageStorage] (see
|
||||||
|
/// [ScrollController.keepScrollOffset]). It can be used to read the current
|
||||||
|
/// scroll position (see [ScrollController.offset]), or change it (see
|
||||||
|
/// [ScrollController.animateTo]).
|
||||||
|
/// {@endtemplate}
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.primary}
|
||||||
|
/// Whether this is the primary scroll view associated with the parent
|
||||||
|
/// [PrimaryScrollController].
|
||||||
|
///
|
||||||
|
/// When this is true, the scroll view is scrollable even if it does not have
|
||||||
|
/// sufficient content to actually scroll. Otherwise, by default the user can
|
||||||
|
/// only scroll the view if it has sufficient content. See [physics].
|
||||||
|
///
|
||||||
|
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
|
||||||
|
/// ScrollAction is not handled by an otherwise focused part of the
|
||||||
|
/// application, the ScrollAction will be evaluated using this scroll view,
|
||||||
|
/// for example, when executing [Shortcuts] key events like page up and down.
|
||||||
|
///
|
||||||
|
/// On iOS, this also identifies the scroll view that will scroll to top in
|
||||||
|
/// response to a tap in the status bar.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// Defaults to true when [scrollController] is null.
|
||||||
|
final bool? primary;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.shadow.scrollBehavior}
|
||||||
|
///
|
||||||
|
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
|
||||||
|
/// [ScrollPhysics] is provided in [physics], it will take precedence,
|
||||||
|
/// followed by [scrollBehavior], and then the inherited ancestor
|
||||||
|
/// [ScrollBehavior].
|
||||||
|
final ScrollBehavior? scrollBehavior;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.shrinkWrap}
|
||||||
|
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||||
|
/// determined by the contents being viewed.
|
||||||
|
///
|
||||||
|
/// If the scroll view does not shrink wrap, then the scroll view will expand
|
||||||
|
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
|
||||||
|
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
|
||||||
|
/// be true.
|
||||||
|
///
|
||||||
|
/// Shrink wrapping the content of the scroll view is significantly more
|
||||||
|
/// expensive than expanding to the maximum allowed size because the content
|
||||||
|
/// can expand and contract during scrolling, which means the size of the
|
||||||
|
/// scroll view needs to be recomputed whenever the scroll position changes.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.physics}
|
||||||
|
/// How the scroll view should respond to user input.
|
||||||
|
///
|
||||||
|
/// For example, determines how the scroll view continues to animate after the
|
||||||
|
/// user stops dragging the scroll view.
|
||||||
|
///
|
||||||
|
/// Defaults to matching platform conventions. Furthermore, if [primary] is
|
||||||
|
/// false, then the user cannot scroll if there is insufficient content to
|
||||||
|
/// scroll, while if [primary] is true, they can always attempt to scroll.
|
||||||
|
///
|
||||||
|
/// To force the scroll view to always be scrollable even if there is
|
||||||
|
/// insufficient content, as if [primary] was true but without necessarily
|
||||||
|
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
|
||||||
|
/// object, as in:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// To force the scroll view to use the default platform conventions and not
|
||||||
|
/// be scrollable if there is insufficient content, regardless of the value of
|
||||||
|
/// [primary], provide an explicit [ScrollPhysics] object, as in:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// physics: const ScrollPhysics(),
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The physics can be changed dynamically (by providing a new object in a
|
||||||
|
/// subsequent build), but new physics will only take effect if the _class_ of
|
||||||
|
/// the provided object changes. Merely constructing a new instance with a
|
||||||
|
/// different configuration is insufficient to cause the physics to be
|
||||||
|
/// reapplied. (This is because the final object used is generated
|
||||||
|
/// dynamically, which can be relatively expensive, and it would be
|
||||||
|
/// inefficient to speculatively create this object each frame to see if the
|
||||||
|
/// physics should be updated.)
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
|
||||||
|
/// [ScrollPhysics] provided by that behavior will take precedence after
|
||||||
|
/// [physics].
|
||||||
|
final ScrollPhysics? physics;
|
||||||
|
|
||||||
|
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
|
||||||
|
final double? cacheExtent;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
|
||||||
|
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
|
||||||
|
/// dismiss the keyboard automatically.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.restorationId}
|
||||||
|
final String? restorationId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PagedValueListView<K, V>> createState() =>
|
||||||
|
_PagedValueListViewState<K, V>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PagedValueListViewState<K, V> extends State<PagedValueListView<K, V>> {
|
||||||
|
PagedValueNotifier<K, V> get _controller => widget.controller;
|
||||||
|
|
||||||
|
// Avoids duplicate requests on rebuilds.
|
||||||
|
bool _hasRequestedNextPage = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.doInitialLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant PagedValueListView<K, V> oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (_controller != oldWidget.controller) {
|
||||||
|
// reset duplicate requests flag
|
||||||
|
_hasRequestedNextPage = false;
|
||||||
|
_controller.doInitialLoad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => PagedValueListenableBuilder<K, V>(
|
||||||
|
valueListenable: _controller,
|
||||||
|
builder: (context, value, _) => value.when(
|
||||||
|
(items, nextPageKey, error) {
|
||||||
|
if (items.isEmpty) {
|
||||||
|
return widget.emptyBuilder(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
padding: widget.padding,
|
||||||
|
physics: widget.physics,
|
||||||
|
reverse: widget.reverse,
|
||||||
|
controller: widget.scrollController,
|
||||||
|
primary: widget.primary,
|
||||||
|
shrinkWrap: widget.shrinkWrap,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
|
restorationId: widget.restorationId,
|
||||||
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
|
cacheExtent: widget.cacheExtent,
|
||||||
|
itemCount: value.itemCount,
|
||||||
|
separatorBuilder: (context, index) =>
|
||||||
|
widget.separatorBuilder(context, items, index),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (!_hasRequestedNextPage) {
|
||||||
|
final newPageRequestTriggerIndex =
|
||||||
|
items.length - widget.loadMoreTriggerIndex;
|
||||||
|
final isBuildingTriggerIndexItem =
|
||||||
|
index == newPageRequestTriggerIndex;
|
||||||
|
if (nextPageKey != null && isBuildingTriggerIndexItem) {
|
||||||
|
// Schedules the request for the end of this frame.
|
||||||
|
WidgetsBinding.instance?.addPostFrameCallback((_) async {
|
||||||
|
if (error == null) {
|
||||||
|
await _controller.loadMore(nextPageKey);
|
||||||
|
}
|
||||||
|
_hasRequestedNextPage = false;
|
||||||
|
});
|
||||||
|
_hasRequestedNextPage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == items.length) {
|
||||||
|
if (error != null) {
|
||||||
|
return widget.loadMoreErrorBuilder(context, error);
|
||||||
|
}
|
||||||
|
return widget.loadMoreIndicatorBuilder(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.itemBuilder(context, items, index);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => widget.loadingBuilder(context),
|
||||||
|
error: (error) => widget.errorBuilder(context, error),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:stream_chat/stream_chat.dart' hide Success;
|
||||||
|
import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart';
|
||||||
|
|
||||||
|
/// The default channel page limit to load.
|
||||||
|
const defaultMessageSearchPagedLimit = 10;
|
||||||
|
|
||||||
|
const _kDefaultBackendPaginationLimit = 30;
|
||||||
|
|
||||||
|
/// A controller for a user list.
|
||||||
|
///
|
||||||
|
/// This class lets you perform tasks such as:
|
||||||
|
/// * Load initial data.
|
||||||
|
/// * Load more data using [loadMore].
|
||||||
|
/// * Replace the previously loaded users.
|
||||||
|
class StreamMessageSearchListController
|
||||||
|
extends PagedValueNotifier<String, GetMessageResponse> {
|
||||||
|
/// Creates a Stream user list controller.
|
||||||
|
///
|
||||||
|
/// * `client` is the Stream chat client to use for the channels list.
|
||||||
|
///
|
||||||
|
/// * `filter` is the query filters to use.
|
||||||
|
///
|
||||||
|
/// * `sort` is the sorting used for the users matching the filters.
|
||||||
|
///
|
||||||
|
/// * `presence` sets whether you'll receive user presence updates via the
|
||||||
|
/// websocket events.
|
||||||
|
///
|
||||||
|
/// * `limit` is the limit to apply to the user list.
|
||||||
|
StreamMessageSearchListController({
|
||||||
|
required this.client,
|
||||||
|
required this.filter,
|
||||||
|
this.messageFilter,
|
||||||
|
this.searchQuery,
|
||||||
|
this.sort,
|
||||||
|
this.limit = defaultMessageSearchPagedLimit,
|
||||||
|
}) : assert(
|
||||||
|
messageFilter != null || searchQuery != null,
|
||||||
|
'Either messageFilter or searchQuery must be provided',
|
||||||
|
),
|
||||||
|
assert(
|
||||||
|
messageFilter == null || searchQuery == null,
|
||||||
|
'Only one of messageFilter or searchQuery can be provided',
|
||||||
|
),
|
||||||
|
_activeFilter = filter,
|
||||||
|
_activeMessageFilter = messageFilter,
|
||||||
|
_activeSearchQuery = searchQuery,
|
||||||
|
_activeSort = sort,
|
||||||
|
super(const PagedValue.loading());
|
||||||
|
|
||||||
|
/// Creates a [StreamUserListController] from the passed [value].
|
||||||
|
StreamMessageSearchListController.fromValue(
|
||||||
|
PagedValue<String, GetMessageResponse> value, {
|
||||||
|
required this.client,
|
||||||
|
required this.filter,
|
||||||
|
this.messageFilter,
|
||||||
|
this.searchQuery,
|
||||||
|
this.sort,
|
||||||
|
this.limit = defaultMessageSearchPagedLimit,
|
||||||
|
}) : assert(
|
||||||
|
messageFilter != null || searchQuery != null,
|
||||||
|
'Either messageFilter or searchQuery must be provided',
|
||||||
|
),
|
||||||
|
assert(
|
||||||
|
messageFilter == null || searchQuery == null,
|
||||||
|
'Only one of messageFilter or searchQuery can be provided',
|
||||||
|
),
|
||||||
|
_activeFilter = filter,
|
||||||
|
_activeMessageFilter = messageFilter,
|
||||||
|
_activeSearchQuery = searchQuery,
|
||||||
|
_activeSort = sort,
|
||||||
|
super(value);
|
||||||
|
|
||||||
|
/// The client to use for the channels list.
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
/// The query filters to use.
|
||||||
|
///
|
||||||
|
/// You can query on any of the custom fields you've defined on the [User].
|
||||||
|
///
|
||||||
|
/// You can also filter other built-in channel fields.
|
||||||
|
final Filter filter;
|
||||||
|
Filter _activeFilter;
|
||||||
|
|
||||||
|
/// The message query filters to use.
|
||||||
|
///
|
||||||
|
/// You can query on any of the custom fields you've defined on the [Channel].
|
||||||
|
///
|
||||||
|
/// You can also filter other built-in channel fields.
|
||||||
|
final Filter? messageFilter;
|
||||||
|
Filter? _activeMessageFilter;
|
||||||
|
|
||||||
|
/// Message String to search on.
|
||||||
|
final String? searchQuery;
|
||||||
|
String? _activeSearchQuery;
|
||||||
|
|
||||||
|
/// The sorting used for the users matching the filters.
|
||||||
|
///
|
||||||
|
/// Sorting is based on field and direction, multiple sorting options
|
||||||
|
/// can be provided.
|
||||||
|
///
|
||||||
|
/// Direction can be ascending or descending.
|
||||||
|
final List<SortOption>? sort;
|
||||||
|
List<SortOption>? _activeSort;
|
||||||
|
|
||||||
|
/// The limit to apply to the user list. The default is set to
|
||||||
|
/// [defaultUserPagedLimit].
|
||||||
|
final int limit;
|
||||||
|
|
||||||
|
/// Allows for the change of filters used for user queries.
|
||||||
|
///
|
||||||
|
/// Use this if you need to support runtime filter changes,
|
||||||
|
/// through custom filters UI.
|
||||||
|
set filter(Filter value) => _activeFilter = value;
|
||||||
|
|
||||||
|
/// Allows for the change of message filters used for user queries.
|
||||||
|
///
|
||||||
|
/// Use this if you need to support runtime filter changes,
|
||||||
|
/// through custom filters UI.
|
||||||
|
set messageFilter(Filter? value) => _activeMessageFilter = value;
|
||||||
|
|
||||||
|
/// Allows for the change of filters used for user queries.
|
||||||
|
///
|
||||||
|
/// Use this if you need to support runtime filter changes,
|
||||||
|
/// through custom filters UI.
|
||||||
|
set searchQuery(String? value) => _activeSearchQuery = value;
|
||||||
|
|
||||||
|
/// Allows for the change of the query sort used for user queries.
|
||||||
|
///
|
||||||
|
/// Use this if you need to support runtime sort changes,
|
||||||
|
/// through custom sort UI.
|
||||||
|
set sort(List<SortOption>? value) => _activeSort = value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> doInitialLoad() async {
|
||||||
|
final limit = min(
|
||||||
|
this.limit * defaultInitialPagedLimitMultiplier,
|
||||||
|
_kDefaultBackendPaginationLimit,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
final response = await client.search(
|
||||||
|
_activeFilter,
|
||||||
|
sort: _activeSort,
|
||||||
|
query: _activeSearchQuery,
|
||||||
|
messageFilters: _activeMessageFilter,
|
||||||
|
paginationParams: PaginationParams(limit: limit),
|
||||||
|
);
|
||||||
|
|
||||||
|
final results = response.results;
|
||||||
|
final nextKey = response.next;
|
||||||
|
value = PagedValue(
|
||||||
|
items: results,
|
||||||
|
nextPageKey: nextKey,
|
||||||
|
);
|
||||||
|
} on StreamChatError catch (error) {
|
||||||
|
value = PagedValue.error(error);
|
||||||
|
} catch (error) {
|
||||||
|
final chatError = StreamChatError(error.toString());
|
||||||
|
value = PagedValue.error(chatError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadMore(String nextPageKey) async {
|
||||||
|
final previousValue = value.asSuccess;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await client.search(
|
||||||
|
_activeFilter,
|
||||||
|
sort: _activeSort,
|
||||||
|
query: _activeSearchQuery,
|
||||||
|
messageFilters: _activeMessageFilter,
|
||||||
|
paginationParams: PaginationParams(limit: limit, next: nextPageKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
final results = response.results;
|
||||||
|
final previousItems = previousValue.items;
|
||||||
|
final newItems = previousItems + results;
|
||||||
|
final next = response.next;
|
||||||
|
final nextKey = next != null && next.isNotEmpty ? next : null;
|
||||||
|
value = PagedValue(
|
||||||
|
items: newItems,
|
||||||
|
nextPageKey: nextKey,
|
||||||
|
);
|
||||||
|
} on StreamChatError catch (error) {
|
||||||
|
value = previousValue.copyWith(error: error);
|
||||||
|
} catch (error) {
|
||||||
|
final chatError = StreamChatError(error.toString());
|
||||||
|
value = previousValue.copyWith(error: chatError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> refresh({bool resetValue = true}) {
|
||||||
|
if (resetValue) {
|
||||||
|
_activeFilter = filter;
|
||||||
|
_activeMessageFilter = messageFilter;
|
||||||
|
_activeSearchQuery = searchQuery;
|
||||||
|
_activeSort = sort;
|
||||||
|
}
|
||||||
|
return super.refresh(resetValue: resetValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:stream_chat/stream_chat.dart' hide Success;
|
||||||
|
import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart';
|
||||||
|
|
||||||
|
/// The default channel page limit to load.
|
||||||
|
const defaultUserPagedLimit = 10;
|
||||||
|
|
||||||
|
const _kDefaultBackendPaginationLimit = 30;
|
||||||
|
|
||||||
|
/// A controller for a user list.
|
||||||
|
///
|
||||||
|
/// This class lets you perform tasks such as:
|
||||||
|
/// * Load initial data.
|
||||||
|
/// * Load more data using [loadMore].
|
||||||
|
/// * Replace the previously loaded users.
|
||||||
|
class StreamUserListController extends PagedValueNotifier<int, User> {
|
||||||
|
/// Creates a Stream user list controller.
|
||||||
|
///
|
||||||
|
/// * `client` is the Stream chat client to use for the channels list.
|
||||||
|
///
|
||||||
|
/// * `filter` is the query filters to use.
|
||||||
|
///
|
||||||
|
/// * `sort` is the sorting used for the users matching the filters.
|
||||||
|
///
|
||||||
|
/// * `presence` sets whether you'll receive user presence updates via the
|
||||||
|
/// websocket events.
|
||||||
|
///
|
||||||
|
/// * `limit` is the limit to apply to the user list.
|
||||||
|
StreamUserListController({
|
||||||
|
required this.client,
|
||||||
|
this.filter,
|
||||||
|
this.sort,
|
||||||
|
this.presence = true,
|
||||||
|
this.limit = defaultUserPagedLimit,
|
||||||
|
}) : _activeFilter = filter,
|
||||||
|
_activeSort = sort,
|
||||||
|
super(const PagedValue.loading());
|
||||||
|
|
||||||
|
/// Creates a [StreamUserListController] from the passed [value].
|
||||||
|
StreamUserListController.fromValue(
|
||||||
|
PagedValue<int, User> value, {
|
||||||
|
required this.client,
|
||||||
|
this.filter,
|
||||||
|
this.sort,
|
||||||
|
this.presence = true,
|
||||||
|
this.limit = defaultUserPagedLimit,
|
||||||
|
}) : _activeFilter = filter,
|
||||||
|
_activeSort = sort,
|
||||||
|
super(value);
|
||||||
|
|
||||||
|
/// The client to use for the channels list.
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
/// The query filters to use.
|
||||||
|
///
|
||||||
|
/// You can query on any of the custom fields you've defined on the [User].
|
||||||
|
///
|
||||||
|
/// You can also filter other built-in channel fields.
|
||||||
|
final Filter? filter;
|
||||||
|
Filter? _activeFilter;
|
||||||
|
|
||||||
|
/// The sorting used for the users matching the filters.
|
||||||
|
///
|
||||||
|
/// Sorting is based on field and direction, multiple sorting options
|
||||||
|
/// can be provided.
|
||||||
|
///
|
||||||
|
/// Direction can be ascending or descending.
|
||||||
|
final List<SortOption>? sort;
|
||||||
|
List<SortOption>? _activeSort;
|
||||||
|
|
||||||
|
/// If true you’ll receive user presence updates via the websocket events
|
||||||
|
final bool presence;
|
||||||
|
|
||||||
|
/// The limit to apply to the user list. The default is set to
|
||||||
|
/// [defaultUserPagedLimit].
|
||||||
|
final int limit;
|
||||||
|
|
||||||
|
/// Allows for the change of filters used for user queries.
|
||||||
|
///
|
||||||
|
/// Use this if you need to support runtime filter changes,
|
||||||
|
/// through custom filters UI.
|
||||||
|
set filter(Filter? value) => _activeFilter = value;
|
||||||
|
|
||||||
|
/// Allows for the change of the query sort used for user queries.
|
||||||
|
///
|
||||||
|
/// Use this if you need to support runtime sort changes,
|
||||||
|
/// through custom sort UI.
|
||||||
|
set sort(List<SortOption>? value) => _activeSort = value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> doInitialLoad() async {
|
||||||
|
final limit = min(
|
||||||
|
this.limit * defaultInitialPagedLimitMultiplier,
|
||||||
|
_kDefaultBackendPaginationLimit,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
final userResponse = await client.queryUsers(
|
||||||
|
filter: _activeFilter,
|
||||||
|
sort: _activeSort,
|
||||||
|
presence: presence,
|
||||||
|
pagination: PaginationParams(limit: limit),
|
||||||
|
);
|
||||||
|
|
||||||
|
final users = userResponse.users;
|
||||||
|
final nextKey = users.length < limit ? null : users.length;
|
||||||
|
value = PagedValue(
|
||||||
|
items: users,
|
||||||
|
nextPageKey: nextKey,
|
||||||
|
);
|
||||||
|
} on StreamChatError catch (error) {
|
||||||
|
value = PagedValue.error(error);
|
||||||
|
} catch (error) {
|
||||||
|
final chatError = StreamChatError(error.toString());
|
||||||
|
value = PagedValue.error(chatError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loadMore(int nextPageKey) async {
|
||||||
|
final previousValue = value.asSuccess;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final userResponse = await client.queryUsers(
|
||||||
|
filter: _activeFilter,
|
||||||
|
sort: _activeSort,
|
||||||
|
presence: presence,
|
||||||
|
pagination: PaginationParams(limit: limit, offset: nextPageKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
final users = userResponse.users;
|
||||||
|
final previousItems = previousValue.items;
|
||||||
|
final newItems = previousItems + users;
|
||||||
|
final nextKey = users.length < limit ? null : newItems.length;
|
||||||
|
value = PagedValue(
|
||||||
|
items: newItems,
|
||||||
|
nextPageKey: nextKey,
|
||||||
|
);
|
||||||
|
} on StreamChatError catch (error) {
|
||||||
|
value = previousValue.copyWith(error: error);
|
||||||
|
} catch (error) {
|
||||||
|
final chatError = StreamChatError(error.toString());
|
||||||
|
value = previousValue.copyWith(error: chatError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> refresh({bool resetValue = true}) {
|
||||||
|
if (resetValue) {
|
||||||
|
_activeFilter = filter;
|
||||||
|
_activeSort = sort;
|
||||||
|
}
|
||||||
|
return super.refresh(resetValue: resetValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the previously loaded users with [users] and updates
|
||||||
|
/// the nextPageKey.
|
||||||
|
set users(List<User> users) {
|
||||||
|
value = PagedValue(
|
||||||
|
items: users,
|
||||||
|
nextPageKey: users.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,11 +12,14 @@ export 'src/message_list_core.dart' hide MessageListCoreState;
|
|||||||
export 'src/message_search_bloc.dart';
|
export 'src/message_search_bloc.dart';
|
||||||
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
|
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
|
||||||
export 'src/message_text_field_controller.dart';
|
export 'src/message_text_field_controller.dart';
|
||||||
|
export 'src/paged_value_list_view.dart';
|
||||||
export 'src/paged_value_notifier.dart' show PagedValueListenableBuilder;
|
export 'src/paged_value_notifier.dart' show PagedValueListenableBuilder;
|
||||||
export 'src/stream_channel.dart';
|
export 'src/stream_channel.dart';
|
||||||
export 'src/stream_channel_list_controller.dart';
|
export 'src/stream_channel_list_controller.dart';
|
||||||
export 'src/stream_channel_list_event_handler.dart';
|
export 'src/stream_channel_list_event_handler.dart';
|
||||||
export 'src/stream_chat_core.dart';
|
export 'src/stream_chat_core.dart';
|
||||||
|
export 'src/stream_message_search_list_controller.dart';
|
||||||
|
export 'src/stream_user_list_controller.dart';
|
||||||
export 'src/typedef.dart';
|
export 'src/typedef.dart';
|
||||||
export 'src/user_list_core.dart' hide UserListCoreState;
|
export 'src/user_list_core.dart' hide UserListCoreState;
|
||||||
export 'src/users_bloc.dart';
|
export 'src/users_bloc.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user