Merge pull request #660 from GetStream/fix/message-search-pagination

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