diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 61f00005..9ec385ab 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -13,6 +13,8 @@ ✅ Added - Added `Filter.contains` and `Filter.empty` +- Added support for `next`, `previous` value pagination in `client.search` + , [read more.](https://getstream.io/chat/docs/other-rest/search/#pagination) ## 2.2.1 diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index c6243148..fa08a475 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -36,6 +36,10 @@ class GeneralApi { PaginationParams? pagination, Filter? messageFilters, }) async { + assert( + pagination?.offset == null || pagination?.offset == 0 || sort == null, + 'Cannot specify `offset` with `sort` parameter', + ); assert(() { if (query == null && messageFilters == null) { throw ArgumentError('Provide at least `query` or `messageFilters`'); diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 29112d72..0bc49fc7 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -60,12 +60,16 @@ class PaginationParams extends Equatable { /// ``` const PaginationParams({ this.limit = 10, - this.offset = 0, + this.offset, + this.next, this.greaterThan, this.greaterThanOrEqual, this.lessThan, this.lessThanOrEqual, - }); + }) : assert( + offset == null || offset == 0 || next == null, + 'Cannot specify non-zero `offset` with `next` parameter', + ); /// Create a new instance from a json factory PaginationParams.fromJson(Map json) => @@ -75,7 +79,10 @@ class PaginationParams extends Equatable { final int limit; /// The offset of requesting items. - final int offset; + final int? offset; + + /// A key used to paginate. + final String? next; /// Filter on ids greater than the given value. @JsonKey(name: 'id_gt') @@ -100,6 +107,7 @@ class PaginationParams extends Equatable { PaginationParams copyWith({ int? limit, int? offset, + String? next, String? greaterThan, String? greaterThanOrEqual, String? lessThan, @@ -108,6 +116,7 @@ class PaginationParams extends Equatable { PaginationParams( limit: limit ?? this.limit, offset: offset ?? this.offset, + next: next ?? this.next, greaterThan: greaterThan ?? this.greaterThan, greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, lessThan: lessThan ?? this.lessThan, @@ -118,6 +127,7 @@ class PaginationParams extends Equatable { List get props => [ limit, offset, + next, greaterThan, greaterThanOrEqual, lessThan, diff --git a/packages/stream_chat/lib/src/core/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart index 6cd935b8..fd0ac624 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -23,6 +23,7 @@ PaginationParams _$PaginationParamsFromJson(Map json) { return PaginationParams( limit: json['limit'] as int, offset: json['offset'] as int, + next: json['next'] as String?, greaterThan: json['id_gt'] as String?, greaterThanOrEqual: json['id_gte'] as String?, lessThan: json['id_lt'] as String?, @@ -42,6 +43,7 @@ Map _$PaginationParamsToJson(PaginationParams instance) { } } + writeNotNull('next', instance.next); writeNotNull('id_gt', instance.greaterThan); writeNotNull('id_gte', instance.greaterThanOrEqual); writeNotNull('id_lt', instance.lessThan); diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 033e91b2..5b52b65d 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -253,6 +253,12 @@ class SearchMessagesResponse extends _BaseResponse { @JsonKey(defaultValue: []) late List results; + /// Message id of where to start searching from for next [results] + late String? next; + + /// Message id of where to start searching from for previous [results] + late String? previous; + /// Create a new instance from a json static SearchMessagesResponse fromJson(Map json) => _$SearchMessagesResponseFromJson(json); diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 15b64a05..10d96682 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -161,7 +161,9 @@ SearchMessagesResponse _$SearchMessagesResponseFromJson( ..results = (json['results'] as List?) ?.map((e) => GetMessageResponse.fromJson(e as Map)) .toList() ?? - []; + [] + ..next = json['next'] as String? + ..previous = json['previous'] as String?; } GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson( diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index afb3f6aa..570d7c77 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -86,6 +86,24 @@ void main() { }, ); + test( + 'should throw if `pagination.offset` and `sort` both are provided', + () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const sort = [SortOption('test-field')]; + const pagination = PaginationParams(offset: 10); + try { + await generalApi.searchMessages( + filter, + sort: sort, + pagination: pagination, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + test('should run successfully with `query`', () async { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); const query = 'test-query'; diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 76253403..876a3f61 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -9,11 +9,23 @@ void main() { expect(j, {'field': 'name', 'direction': -1}); }); - test('PaginationParams', () { - const option = PaginationParams(); - final j = option.toJson(); - expect(j, containsPair('limit', 10)); - expect(j, containsPair('offset', 0)); + group('PaginationParams', () { + test('default', () { + const option = PaginationParams(); + final j = option.toJson(); + expect(j, containsPair('limit', 10)); + }); + + test( + 'should throw if non-zero `offset` and `next` both are provided', + () { + try { + PaginationParams(offset: 10, next: 'next-message-id'); + } catch (e) { + expect(e, isA()); + } + }, + ); }); }); } diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 02e2b214..ed8f5e06 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,5 +1,22 @@ ## 2.2.1 +🛑️ Breaking Changes from `2.2.1` + +- `MessageSearchListView` paginationParams property is now non-nullable with a default value. + ```dart + paginationParams = const PaginationParams(limit: 30) + ``` +- `UserListView` pagination property is now non-nullable with a default value. + ```dart + pagination = const PaginationParams(limit: 30) + ``` + +🐞 Fixed + +- Fixed `MessageSearchListView` pagination. + +## 2.2.1 + - Updated `stream_chat_flutter_core` dependency to 2.2.1 ## 2.2.0 @@ -7,14 +24,13 @@ ✅ Added - [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): - Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image - is loading + Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image is loading - Added a `backgroundColor` property to the following widgets: - - `ChannelHeader` - - `ChannelListHeader` - - `GalleryHeader` - - `GalleryFooter` - - `ThreadHeader` + - `ChannelHeader` + - `ChannelListHeader` + - `GalleryHeader` + - `GalleryFooter` + - `ThreadHeader` - Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message. - Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded. This will override the default error alert behaviour. @@ -34,9 +50,8 @@ You can call `.copyWith` to customize just a subset of properties. 🔄 Changed -Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with -them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming -breakdown: +Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with them, and have been +upgraded with some goodies like `lerp` functions. Here's the full naming breakdown: * `AvatarTheme` is now `AvatarThemeData` * `ChannelHeaderTheme` is now `ChannelHeaderThemeData` @@ -53,18 +68,18 @@ breakdown: 🐞 Fixed -- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the - camera is null. +- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. - Fixed date dividers position/alignment in non reversed `MessageListView`. - Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set. -- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text. +- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when + sending a message with no text. ## 2.1.2 🐞 Fixed -- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no - members when sending message +- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending + message ## 2.1.1 @@ -82,8 +97,7 @@ breakdown: 🔄 Changed - `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`. -- `StreamChat.of(context).userStream` is now deprecated in favor - of `StreamChat.of(context).currentUserStream`. +- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`. 🐞 Fixed @@ -136,8 +150,7 @@ You can call `.copyWith` to customize just a subset of properties - Added video compress options (frame and quality) to `MessageInput` - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView - header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer - `MessageWidget` accepts a `userAvatarBuilder` - Added pinMessage ui support - Added `MessageListView.threadSeparatorBuilder` property @@ -146,12 +159,10 @@ You can call `.copyWith` to customize just a subset of properties 🐞 Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text - box when editing message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator - use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without - a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing + message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload - `MessageListView` not rendering if the user is not a member of the channel - Fix `MessageInput` overflow when there are no actions - Minor fixes and improvements @@ -204,18 +215,15 @@ You can call `.copyWith` to customize just a subset of properties. ✅ Added - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView - header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer - `MessageWidget` accepts a `userAvatarBuilder` 🐞 Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text - box when editing message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator - use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without - a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing + message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload - `MessageListView` not rendering if the user is not a member of the channel ## 2.0.0-nullsafety.7 @@ -285,8 +293,7 @@ You can call `.copyWith` to customize just a subset of properties. - Show error messages as system and keep them in the message input - Remove notification badge logic - Use shimmer while loading images -- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated - to `MessageInput` +- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput` - Add possibility to specify custom message actions using `MessageWidget.customActions` - Added `MessageListView.onAttachmentTap` callback - Fixed message newline issue @@ -343,8 +350,7 @@ You can call `.copyWith` to customize just a subset of properties. - Improved api documentation - Updated `stream_chat` dependency to `^1.0.0-beta` - Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples) -- Reimplemented existing widgets - using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) +- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) ## 0.2.21 @@ -361,8 +367,8 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.20+2 -- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the - list when a new message arrives +- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message + arrives ## 0.2.20+1 @@ -396,8 +402,7 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.16 -- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress - implementation +- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation - Make public autofocus field of the TextField of message_input ## 0.2.15 @@ -582,11 +587,10 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.1-alpha+1 -- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have - the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to - your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in - the usual way, but you need to add a `StreamChat` widget to every route of your app. - Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more +- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget + as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of + your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to + every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more information. ```dart @@ -688,8 +692,8 @@ Widget build(BuildContext context) { - Add gesture (vertical drag down) to close the keyboard -- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will - even close the keyboard) +- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the + keyboard) The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261 diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 0fd45ea1..f80c688f 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -58,7 +58,7 @@ class MessageSearchListView extends StatefulWidget { required this.filters, this.messageQuery, this.sortOptions, - this.paginationParams, + this.paginationParams = const PaginationParams(limit: 30), this.messageFilters, this.separatorBuilder, this.itemBuilder, @@ -93,7 +93,7 @@ class MessageSearchListView extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams? paginationParams; + final PaginationParams paginationParams; /// The message query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index eb45f918..4d650192 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -51,7 +51,7 @@ class UserListView extends StatefulWidget { this.filter, this.sort, this.presence, - this.pagination, + this.pagination = const PaginationParams(limit: 30), this.onUserTap, this.onUserLongPress, this.userWidget, @@ -93,7 +93,7 @@ class UserListView extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams? pagination; + final PaginationParams pagination; /// Function called when tapping on a channel /// By default it calls [Navigator.push] building a [MaterialPageRoute] diff --git a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart index 4da1c7dc..69c63abe 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart @@ -69,6 +69,7 @@ void main() { body: MessageSearchBloc( child: MessageSearchListView( filters: Filter.in_('members', const ['test_id']), + messageQuery: 'test query', ), ), ); @@ -100,6 +101,7 @@ void main() { body: MessageSearchBloc( child: MessageSearchListView( filters: Filter.in_('members', const ['test_id']), + messageQuery: 'test query', ), ), ); diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 1378ced0..d0d89a6b 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,20 @@ +## Upcoming + +🛑️ Breaking Changes from `2.2.1` + +- `MessageSearchListViewCore` paginationParams property is now non-nullable with a default value. + ```dart + paginationParams = const PaginationParams(limit: 30) + ``` +- `UserListViewCore` pagination property is now non-nullable with a default value. + ```dart + pagination = const PaginationParams(limit: 30) + ``` + +🐞 Fixed + +- Fixed `MessageSearchBloc` pagination. + ## 2.2.1 - Updated `stream_chat` dependency to 2.2.1 @@ -5,13 +22,17 @@ ## 2.2.0 🛑️ Breaking Changes from `2.1.1` + - Renamed `BetterStreamBuilder.loadingBuilder` to `.noDataBuilder` 🔄 Changed + - `BetterStreamBuilder.initialData` is now nullable/not-required. 🐞 Fixed -- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh + +- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after + refresh ## 2.1.1 @@ -20,12 +41,15 @@ ## 2.1.0 🛑️ Breaking Changes from `2.0.0` + - Changed default message filter of `MessageListCore` ✅ Added + - Added `MessageListCore.paginationLimit` 🔄 Changed + - `StreamChatCore.of(context).user` is now deprecated in favor of `StreamChatCore.of(context).currentUser`. - `StreamChatCore.of(context).userStream` is now deprecated in favor of `StreamChatCore.of(context).currentUserStream`. @@ -34,7 +58,8 @@ 🛑️ Breaking Changes from `1.5.3` - migrate this package to null safety -- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual + params/properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence @@ -51,6 +76,7 @@ - Performance improvements ## 2.0.0-nullsafety.9 + - Update llc dependency ## 2.0.0-nullsafety.8 diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 6687eef5..1aed7198 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -105,7 +105,8 @@ class ChannelsBlocState extends State }) async { final client = _streamChatCoreState!.client; - final clear = paginationParams.offset == 0; + final offset = paginationParams.offset; + final clear = offset == null || offset == 0; if (clear && _paginationEnded) { _paginationEnded = false; } diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index 44ac9b3f..6477dfcd 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart @@ -43,6 +43,12 @@ class MessageSearchBlocState extends State with AutomaticKeepAliveClientMixin { late StreamChatCoreState _streamChatCoreState; + /// The key used to paginate next items. + String? nextId; + + /// The key used to paginate previous items. + String? previousId; + /// The current messages list List? get messageResponses => _messageResponses.valueOrNull; @@ -59,6 +65,8 @@ class MessageSearchBlocState extends State Stream get queryMessagesLoading => _queryMessagesLoadingController.stream; + bool _paginationEnded = false; + /// Calls [StreamChatClient.search] updating /// [messagesStream] and [queryMessagesLoading] stream Future search({ @@ -66,21 +74,34 @@ class MessageSearchBlocState extends State Filter? messageFilter, List? sort, String? query, - PaginationParams? pagination, + PaginationParams pagination = const PaginationParams(limit: 30), }) async { final client = _streamChatCoreState.client; - if (_queryMessagesLoadingController.value == true) return; + var clear = false; + if (sort != null) { + clear |= pagination.next == null; + } else { + final offset = pagination.offset; + clear |= offset == null || offset == 0; + } + + if (clear && _paginationEnded) { + _paginationEnded = false; + } + + if ((!clear && _paginationEnded) || + _queryMessagesLoadingController.value == true) { + return; + } if (_messageResponses.hasValue) { _queryMessagesLoadingController.add(true); } try { - final clear = pagination == null || pagination.offset == 0; - final oldMessages = List.from(messageResponses ?? []); - final messages = await client.search( + final response = await client.search( filter, sort: sort, query: query, @@ -88,15 +109,29 @@ class MessageSearchBlocState extends State messageFilters: messageFilter, ); + final next = response.next; + final previous = response.previous; + + nextId = next != null && next.isNotEmpty + ? next + : /*reset nextId if we get nothing*/ null; + previousId = previous != null && previous.isNotEmpty + ? previous + : /*reset previousId if we get nothing*/ null; + + final newMessages = response.results; if (clear) { - _messageResponses.add(messages.results); + _messageResponses.add(newMessages); } else { - final temp = oldMessages + messages.results; + final temp = oldMessages + newMessages; _messageResponses.add(temp); } if (_messageResponses.hasValue && _queryMessagesLoadingController.value) { _queryMessagesLoadingController.add(false); } + if (newMessages.isEmpty || newMessages.length < pagination.limit) { + _paginationEnded = true; + } } catch (e, stk) { // reset loading controller _queryMessagesLoadingController.add(false); diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index 234b5c9a..28e092a7 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -47,10 +47,18 @@ class MessageSearchListCore extends StatefulWidget { required this.filters, this.messageQuery, this.sortOptions, - this.paginationParams, + this.paginationParams = const PaginationParams(limit: 30), this.messageFilters, this.messageSearchListController, - }) : super(key: key); + }) : assert( + messageQuery != null || messageFilters != null, + 'Provide at least `query` or `messageFilters`', + ), + assert( + messageQuery == null || messageFilters == null, + "Can't provide both `query` and `messageFilters` at the same time", + ), + super(key: key); /// A [MessageSearchListController] allows reloading and pagination. /// Use [MessageSearchListController.loadData] and @@ -74,10 +82,9 @@ class MessageSearchListCore extends StatefulWidget { final List? sortOptions; /// Pagination parameters - /// limit: the number of users to return (max is 30) + /// limit: the number of messages to return (max is 30) /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - final PaginationParams? paginationParams; + final PaginationParams paginationParams; /// The message query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. @@ -155,15 +162,25 @@ class MessageSearchListCoreState extends State { ); /// Fetches more messages with updated pagination and updates the widget - Future paginateData() => _messageSearchBloc!.search( - filter: widget.filters, - sort: widget.sortOptions, - pagination: widget.paginationParams!.copyWith( - offset: _messageSearchBloc!.messageResponses?.length ?? 0, - ), - query: widget.messageQuery, - messageFilter: widget.messageFilters, + Future paginateData() { + PaginationParams pagination; + if (widget.sortOptions != null) { + pagination = widget.paginationParams.copyWith( + next: _messageSearchBloc?.nextId, ); + } else { + pagination = widget.paginationParams.copyWith( + offset: _messageSearchBloc?.messageResponses?.length, + ); + } + return _messageSearchBloc!.search( + filter: widget.filters, + sort: widget.sortOptions, + pagination: pagination, + query: widget.messageQuery, + messageFilter: widget.messageFilters, + ); + } @override void didUpdateWidget(MessageSearchListCore oldWidget) { @@ -173,8 +190,8 @@ class MessageSearchListCoreState extends State { widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() || widget.messageFilters?.toString() != oldWidget.messageFilters?.toString() || - widget.paginationParams?.toJson().toString() != - oldWidget.paginationParams?.toJson().toString()) { + widget.paginationParams.toJson().toString() != + oldWidget.paginationParams.toJson().toString()) { loadData(); } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index 57d61fe6..e403be4e 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -66,7 +66,7 @@ class UserListCore extends StatefulWidget { this.filter, this.sort, this.presence, - this.pagination, + this.pagination = const PaginationParams(limit: 30), this.groupAlphabetically = false, this.userListController, }) : super(key: key); @@ -106,7 +106,7 @@ class UserListCore extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams? pagination; + final PaginationParams pagination; /// Set it to true to group users by their first character /// @@ -201,7 +201,7 @@ class UserListCoreState extends State filter: widget.filter, sort: widget.sort, presence: widget.presence, - pagination: widget.pagination!.copyWith( + pagination: widget.pagination.copyWith( offset: _usersBloc!.users?.length ?? 0, ), ); @@ -212,8 +212,8 @@ class UserListCoreState extends State if (widget.filter?.toString() != oldWidget.filter?.toString() || jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || widget.presence != oldWidget.presence || - widget.pagination?.toJson().toString() != - oldWidget.pagination?.toJson().toString()) { + widget.pagination.toJson().toString() != + oldWidget.pagination.toJson().toString()) { loadData(); } diff --git a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart index e7916478..d112961d 100644 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -57,6 +57,8 @@ class UsersBlocState extends State late StreamChatCoreState _streamChatCore; + bool _paginationEnded = false; + /// The Query Users method allows you to search for users and see if they are /// online/offline. /// [API Reference](https://getstream.io/chat/docs/flutter-dart/query_users/?language=dart) @@ -64,19 +66,27 @@ class UsersBlocState extends State Filter? filter, List? sort, bool? presence, - PaginationParams? pagination, + PaginationParams pagination = const PaginationParams(limit: 30), }) async { final client = _streamChatCore.client; - if (_queryUsersLoadingController.value == true) return; + final offset = pagination.offset; + final clear = offset == null || offset == 0; + + if (clear && _paginationEnded) { + _paginationEnded = false; + } + + if ((!clear && _paginationEnded) || + _queryUsersLoadingController.value == true) { + return; + } if (_usersController.hasValue) { _queryUsersLoadingController.add(true); } try { - final clear = pagination == null || pagination.offset == 0; - final oldUsers = List.from(users ?? []); final usersResponse = await client.queryUsers( @@ -86,6 +96,7 @@ class UsersBlocState extends State pagination: pagination, ); + final newUsers = usersResponse.users; if (clear) { _usersController.add(usersResponse.users); } else { @@ -95,6 +106,9 @@ class UsersBlocState extends State if (_usersController.hasValue && _queryUsersLoadingController.value) { _queryUsersLoadingController.add(false); } + if (newUsers.isEmpty || newUsers.length < pagination.limit) { + _paginationEnded = true; + } } catch (e, stk) { // reset loading controller _queryUsersLoadingController.add(false); diff --git a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart index 979ca5b2..c689d90c 100644 --- a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart @@ -31,8 +31,7 @@ void main() { ); testWidgets( - 'messageSearchBlocState.search() should throw if used where ' - 'StreamChat is not present in the widget tree', + '''messageSearchBlocState.search() should throw if used where StreamChat is not present in the widget tree''', (tester) async { const messageSearchBloc = MessageSearchBloc( child: Offstage(), @@ -74,7 +73,10 @@ void main() { messageFilters: any(named: 'messageFilters'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(filter: testFilter); @@ -95,8 +97,7 @@ void main() { ); testWidgets( - 'messageSearchBlocState.messagesStream should emit error ' - 'if client.search() throws', + '''messageSearchBlocState.messagesStream should emit error if client.search() throws''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -144,9 +145,7 @@ void main() { ); testWidgets( - 'calling messageSearchBlocState.search() again with an offset ' - 'should emit new data through messagesStream and also emit loading state ' - 'through queryMessagesLoading', + '''calling messageSearchBlocState.search() again with an offset should emit new data through messagesStream and also emit loading state through queryMessagesLoading''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -168,19 +167,23 @@ void main() { find.byKey(messageSearchBlocKey), ); - final messageResponseList = _generateMessages(); + const pagination = PaginationParams(limit: 25); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); - messageSearchBlocState.search(filter: testFilter); + messageSearchBlocState.search(pagination: pagination, filter: testFilter); await expectLater( messageSearchBlocState.messagesStream, @@ -192,22 +195,24 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).called(1); final offset = messageResponseList.length; final paginatedMessageResponseList = _generateMessages(offset: offset); - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = paginatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = paginatedMessageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(pagination: pagination, filter: testFilter); @@ -236,9 +241,7 @@ void main() { ); testWidgets( - 'calling messageSearchBlocState.search() again with an offset ' - 'should emit error through queryUsersLoading if ' - 'client.search() throws', + '''calling messageSearchBlocState.search() again with an offset should emit error through queryUsersLoading if client.search() throws''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -260,19 +263,23 @@ void main() { find.byKey(messageSearchBlocKey), ); - final messageResponseList = _generateMessages(); + const pagination = PaginationParams(limit: 25); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); - messageSearchBlocState.search(filter: testFilter); + messageSearchBlocState.search(pagination: pagination, filter: testFilter); await expectLater( messageSearchBlocState.messagesStream, @@ -284,11 +291,11 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).called(1); final offset = messageResponseList.length; - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); const error = 'Error! Error! Error!'; when(() => mockClient.search( @@ -296,10 +303,13 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).thenThrow(error); - messageSearchBlocState.search(pagination: pagination, filter: testFilter); + messageSearchBlocState.search( + pagination: newPagination, + filter: testFilter, + ); await expectLater( messageSearchBlocState.queryMessagesLoading, @@ -311,8 +321,80 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).called(1); }, ); + + testWidgets( + '''calling messageSearchBlocState.search() again with an offset should do nothing and return if pagination is completed''', + (tester) async { + const messageSearchBlocKey = Key('messageSearchBloc'); + const childKey = Key('child'); + const messageSearchBloc = MessageSearchBloc( + key: messageSearchBlocKey, + child: Offstage(key: childKey), + ); + + final mockClient = MockClient(); + + await tester.pumpWidget( + StreamChatCore( + client: mockClient, + child: messageSearchBloc, + ), + ); + + final messageSearchBlocState = tester.state( + find.byKey(messageSearchBlocKey), + ); + + const pagination = PaginationParams(limit: 25); + + final messageResponseList = _generateMessages(count: 20); + + when(() => mockClient.search( + testFilter, + query: any(named: 'query'), + sort: any(named: 'sort'), + messageFilters: any(named: 'messageFilters'), + paginationParams: pagination, + )).thenAnswer( + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, + ); + + messageSearchBlocState.search(pagination: pagination, filter: testFilter); + + await expectLater( + messageSearchBlocState.messagesStream, + emits(isSameMessageResponseListAs(messageResponseList)), + ); + + verify(() => mockClient.search( + testFilter, + query: any(named: 'query'), + sort: any(named: 'sort'), + messageFilters: any(named: 'messageFilters'), + paginationParams: pagination, + )).called(1); + + final offset = messageResponseList.length; + final newPagination = pagination.copyWith(offset: offset); + + messageSearchBlocState.search( + filter: testFilter, + pagination: newPagination, + ); + + // should emit nothing. + await expectLater( + // skipping the initial data (behaviorSubject). + messageSearchBlocState.messagesStream.skip(1), + emitsInOrder([]), + ); + }, + ); } diff --git a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart index 6b700c76..03396513 100644 --- a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart @@ -7,6 +7,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'mocks.dart'; const testFilter = Filter.custom(operator: '\$test', value: 'testValue'); +const testMessageFilter = Filter.custom(operator: '\$test', value: 'testValue'); void main() { List _generateMessages({ @@ -28,6 +29,40 @@ void main() { }, ); + testWidgets( + 'should throw if both `messageQuery` and `messageFilters` are provided', + (tester) async { + expect( + () => MessageSearchListCore( + childBuilder: (_) => const Offstage(), + loadingBuilder: (_) => const Offstage(), + emptyBuilder: (_) => const Offstage(), + errorBuilder: (_, __) => const Offstage(), + filters: testFilter, + messageFilters: testMessageFilter, + messageQuery: 'test', + ), + throwsAssertionError, + ); + }, + ); + + testWidgets( + 'should throw if both `messageQuery` and `messageFilters` are not provided', + (tester) async { + expect( + () => MessageSearchListCore( + childBuilder: (_) => const Offstage(), + loadingBuilder: (_) => const Offstage(), + emptyBuilder: (_) => const Offstage(), + errorBuilder: (_, __) => const Offstage(), + filters: testFilter, + ), + throwsAssertionError, + ); + }, + ); + testWidgets( 'should throw if MessageSearchListCore is used where MessageSearchBloc ' 'is not present in the widget tree', @@ -40,6 +75,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object? error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); await tester.pumpWidget(messageSearchListCore); @@ -61,6 +97,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object? error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -92,6 +129,7 @@ void main() { errorBuilder: (BuildContext context, Object error) => const Offstage(), messageSearchListController: controller, filters: testFilter, + messageFilters: testMessageFilter, ); expect(controller.loadData, isNull); @@ -129,6 +167,7 @@ void main() { key: errorWidgetKey, ), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -138,7 +177,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenThrow(error); @@ -159,7 +198,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -179,6 +218,7 @@ void main() { const Offstage(key: emptyWidgetKey), errorBuilder: (BuildContext context, Object error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -188,10 +228,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -211,7 +254,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -231,6 +274,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -240,10 +284,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -263,7 +310,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -275,7 +322,7 @@ void main() { (tester) async { const messageSearchListCoreKey = Key('messageSearchListCore'); const childWidgetKey = Key('childWidget'); - const pagination = PaginationParams(); + const pagination = PaginationParams(limit: 25); final messageSearchListCore = MessageSearchListCore( key: messageSearchListCoreKey, childBuilder: (List messages) => Container( @@ -289,19 +336,23 @@ void main() { errorBuilder: (BuildContext context, Object error) => const Offstage(), paginationParams: pagination, filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); - final messageResponseList = _generateMessages(); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -332,7 +383,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).called(1); @@ -348,11 +399,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = paginatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = paginatedMessageResponseList + ..next = null + ..previous = null, ); await messageSearchListCoreState.paginateData(); @@ -372,7 +425,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).called(1); }, @@ -406,6 +459,7 @@ void main() { const Offstage(), paginationParams: pagination.copyWith(limit: limit), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -415,10 +469,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -453,7 +510,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).called(1); @@ -466,11 +523,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = updatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = updatedMessageResponseList + ..next = null + ..previous = null, ); await tester.pumpAndSettle(); @@ -487,7 +546,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).called(1); }, diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart index eac8dc71..77977bb8 100644 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/user_list_core_test.dart @@ -321,7 +321,7 @@ void main() { (tester) async { const userListCoreKey = Key('userListCore'); const listWidgetKey = Key('listWidget'); - const pagination = PaginationParams(); + const pagination = PaginationParams(limit: 15); final userListCore = UserListCore( key: userListCoreKey, listBuilder: (_, items) => Container( @@ -347,7 +347,7 @@ void main() { final mockClient = MockClient(); - final users = _generateUsers(); + final users = _generateUsers(count: 15); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), diff --git a/packages/stream_chat_flutter_core/test/users_bloc_test.dart b/packages/stream_chat_flutter_core/test/users_bloc_test.dart index af088a76..549f9291 100644 --- a/packages/stream_chat_flutter_core/test/users_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/users_bloc_test.dart @@ -164,16 +164,17 @@ void main() { find.byKey(usersBlocKey), ); - final users = _generateUsers(); + const pagination = PaginationParams(limit: 25); + final users = _generateUsers(count: 25); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, )).thenAnswer((_) async => QueryUsersResponse()..users = users); - usersBlocState.queryUsers(); + usersBlocState.queryUsers(pagination: pagination); await expectLater( usersBlocState.usersStream, @@ -184,23 +185,23 @@ void main() { filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, )).called(1); final offset = users.length; final paginatedUsers = _generateUsers(offset: offset); - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )) - .thenAnswer( - (_) async => QueryUsersResponse()..users = paginatedUsers); + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).thenAnswer( + (_) async => QueryUsersResponse()..users = paginatedUsers, + ); - usersBlocState.queryUsers(pagination: pagination); + usersBlocState.queryUsers(pagination: newPagination); await Future.wait([ expectLater( @@ -217,7 +218,7 @@ void main() { filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: pagination, + pagination: newPagination, )).called(1); }, ); @@ -247,13 +248,89 @@ void main() { find.byKey(usersBlocKey), ); - final users = _generateUsers(); + const pagination = PaginationParams(limit: 25); + final users = _generateUsers(count: 25); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, + )).thenAnswer((_) async => QueryUsersResponse()..users = users); + + usersBlocState.queryUsers(pagination: pagination); + + await expectLater( + usersBlocState.usersStream, + emits(isSameUserListAs(users)), + ); + + verify(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: pagination, + )).called(1); + + final offset = users.length; + final newPagination = pagination.copyWith(offset: offset); + + const error = 'Error! Error! Error!'; + + when(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).thenThrow(error); + + usersBlocState.queryUsers(pagination: newPagination); + + await expectLater( + usersBlocState.queryUsersLoading, + emitsError(error), + ); + + verify(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).called(1); + }, + ); + + testWidgets( + '''calling usersBlocState.queryUsers() again with an offset should do nothing and return if pagination is completed''', + (tester) async { + const usersBlocKey = Key('usersBloc'); + const childKey = Key('child'); + const usersBloc = UsersBloc( + key: usersBlocKey, + child: Offstage(key: childKey), + ); + + final mockClient = MockClient(); + + await tester.pumpWidget( + StreamChatCore( + client: mockClient, + child: usersBloc, + ), + ); + + final usersBlocState = tester.state( + find.byKey(usersBlocKey), + ); + + const pagination = PaginationParams(limit: 30); + final users = _generateUsers(count: 25); + + when(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: pagination, )).thenAnswer((_) async => QueryUsersResponse()..users = users); usersBlocState.queryUsers(); @@ -271,30 +348,16 @@ void main() { )).called(1); final offset = users.length; - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); - const error = 'Error! Error! Error!'; - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).thenThrow(error); - - usersBlocState.queryUsers(pagination: pagination); + usersBlocState.queryUsers(pagination: newPagination); + // should emit nothing. await expectLater( - usersBlocState.queryUsersLoading, - emitsError(error), + // skipping the initial data (behaviorSubject). + usersBlocState.usersStream, + emitsInOrder([]), ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).called(1); }, ); } diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index c87b94d4..d1ed0925 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -112,8 +112,9 @@ class ChannelQueryDao extends DatabaseAccessor cachedChannels.sort(chainedComparator); - if (paginationParams?.offset != null && cachedChannels.isNotEmpty) { - cachedChannels.removeRange(0, paginationParams!.offset); + final offset = paginationParams?.offset; + if (offset != null && offset > 0 && cachedChannels.isNotEmpty) { + cachedChannels.removeRange(0, offset); } if (paginationParams?.limit != null) { diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index b7d9a5db..ef140239 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -117,8 +117,9 @@ class MessageDao extends DatabaseAccessor msgList.removeRange(0, greaterThanIndex); } } - if (options?.limit != null) { - return msgList.take(options!.limit).toList(); + final limit = options?.limit; + if (limit != null && limit > 0) { + return msgList.take(limit).toList(); } } return msgList;