feat: add pagination controls to ChatOptions

This commit is contained in:
Freek van de Ven 2025-02-14 15:45:58 +01:00 committed by FlutterJoey
parent ab8a9d9e6f
commit f57ba9a736
5 changed files with 69 additions and 28 deletions

View file

@ -13,6 +13,7 @@
- Changed baseScreenBuilder to include a chatTitle that can be used to show provide the title logic to apps that use the baseScreenBuilder
- Added loadNewMessagesAfter, loadOldMessagesBefore and removed pagination from getMessages in the ChatRepositoryInterface to change pagination behavior to rely on the stream and two methods indicating that more messages should be added to the stream
- Added chatTitleResolver that can be used to resolve the chat title from the chat model or return null to allow for default behavior
- Added ChatPaginationControls to the ChatOptions to allow for more control over the pagination
## 4.0.0
- Move to the new user story architecture

View file

@ -22,6 +22,7 @@ class ChatBuilders {
this.chatMessageBuilder = DefaultChatMessageBuilder.builder,
this.usernameBuilder,
this.loadingWidgetBuilder = DefaultChatLoadingOverlay.builder,
this.loadingChatMessageBuilder = DefaultChatMessageLoader.builder,
});
/// The base screen builder
@ -75,7 +76,14 @@ class ChatBuilders {
final ImagePickerContainerBuilder? imagePickerContainerBuilder;
/// The loading widget builder
final Widget Function(BuildContext context) loadingWidgetBuilder;
/// This is used to build the loading widget that is displayed on the chat
/// screen when loading the chat
final WidgetBuilder loadingWidgetBuilder;
/// The loading widget builder for chat messages
/// This is displayed in the list of chat messages when loading more messages
/// can be above and below the list
final WidgetBuilder loadingChatMessageBuilder;
}
/// The button builder

View file

@ -14,6 +14,7 @@ class ChatOptions {
this.translations = const ChatTranslations.empty(),
this.builders = const ChatBuilders(),
this.spacing = const ChatSpacing(),
this.paginationControls = const ChatPaginationControls(),
this.messageTheme,
this.messageThemeResolver = _defaultMessageThemeResolver,
this.chatTitleResolver,
@ -45,6 +46,9 @@ class ChatOptions {
//// The spacing between elements of the chat
final ChatSpacing spacing;
/// The pagination settings for the chat
final ChatPaginationControls paginationControls;
/// [groupChatEnabled] is a boolean that indicates if group chat is enabled.
final bool groupChatEnabled;
@ -230,3 +234,24 @@ class ChatSpacing {
/// sender.
final double chatBetweenMessagesPadding;
}
/// The chat pagination controls
/// Use this to define how sensitive the chat pagination should be.
class ChatPaginationControls {
/// The chat pagination controls constructor
const ChatPaginationControls({
this.scrollOffset = 50.0,
this.loadingIndicatorForNewMessages = true,
this.loadingIndicatorForOldMessages = true,
});
/// The minimum scroll offset to trigger the pagination to call for more pages
/// on both sides of the chat. Defaults to 50.0
final double scrollOffset;
/// Whether to show a loading indicator for new messages loading
final bool loadingIndicatorForNewMessages;
/// Whether to show a loading indicator for old messages loading
final bool loadingIndicatorForOldMessages;
}

View file

@ -259,7 +259,7 @@ class _ChatBody extends HookWidget {
var oldCount = messages.length;
try {
debugPrint("loading from message: ${oldestMsg.id}");
debugPrint("loading old messages from message: ${oldestMsg.id}");
await service.loadOldMessagesBefore(firstMessage: oldestMsg);
} finally {
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -284,7 +284,7 @@ class _ChatBody extends HookWidget {
var newestMsg = messages.last;
try {
debugPrint("loading from message: ${newestMsg.id}");
debugPrint("loading new messages from message: ${newestMsg.id}");
await service.loadNewMessagesAfter(lastMessage: newestMsg);
} finally {
isLoadingNewer.value = false;
@ -298,11 +298,13 @@ class _ChatBody extends HookWidget {
var offset = scrollController.offset;
var maxScroll = scrollController.position.maxScrollExtent;
if ((maxScroll - offset) <= 50 && !isLoadingOlder.value) {
if ((maxScroll - offset) <= options.paginationControls.scrollOffset &&
!isLoadingOlder.value) {
unawaited(loadOlderMessages());
}
if (offset <= 50 && !isLoadingNewer.value) {
if (offset <= options.paginationControls.scrollOffset &&
!isLoadingNewer.value) {
unawaited(loadNewerMessages());
}
}
@ -326,12 +328,14 @@ class _ChatBody extends HookWidget {
userMap[u.id] = u;
}
var topSpinner = (isLoadingOlder.value && options.enableLoadingIndicator)
? const _LoaderItem()
var topSpinner = (isLoadingOlder.value &&
options.paginationControls.loadingIndicatorForOldMessages)
? options.builders.loadingChatMessageBuilder.call(context)
: const SizedBox.shrink();
var bottomSpinner = (isLoadingNewer.value && options.enableLoadingIndicator)
? const _LoaderItem()
var bottomSpinner = (isLoadingNewer.value &&
options.paginationControls.loadingIndicatorForNewMessages)
? options.builders.loadingChatMessageBuilder.call(context)
: const SizedBox.shrink();
var reversedMessages = messages.reversed.toList();
@ -348,7 +352,6 @@ class _ChatBody extends HookWidget {
bubbleChildren.add(
ChatBubble(
key: ValueKey(msg.id),
message: msg,
previousMessage: prevMsg,
sender: userMap[msg.senderId],
@ -370,7 +373,6 @@ class _ChatBody extends HookWidget {
child: Align(
alignment: options.chatAlignment ?? Alignment.bottomCenter,
child: ListView(
shrinkWrap: true,
reverse: true,
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
@ -392,20 +394,3 @@ class _ChatBody extends HookWidget {
);
}
}
/// A small row spinner item to show partial loading
class _LoaderItem extends StatelessWidget {
const _LoaderItem();
@override
Widget build(BuildContext context) => const Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}

View file

@ -19,3 +19,25 @@ class DefaultChatLoadingOverlay extends StatelessWidget {
],
);
}
/// A small row spinner item to show partial loading
class DefaultChatMessageLoader extends StatelessWidget {
/// Creates a new default chat message loader
const DefaultChatMessageLoader({super.key});
/// Builds the default chat message loader
static Widget builder(BuildContext context) =>
const DefaultChatMessageLoader();
@override
Widget build(BuildContext context) => const Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}