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_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index 44ac9b3f..d9d5933d 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,9 @@ class MessageSearchBlocState extends State with AutomaticKeepAliveClientMixin { late StreamChatCoreState _streamChatCoreState; + String? nextId; + String? previousId; + /// The current messages list List? get messageResponses => _messageResponses.valueOrNull; @@ -76,11 +79,17 @@ class MessageSearchBlocState extends State _queryMessagesLoadingController.add(true); } try { - final clear = pagination == null || pagination.offset == 0; + var clear = pagination == null; + if (sort != null) { + clear |= pagination?.next == null; + } else { + final offset = pagination?.offset; + clear |= offset == null || offset == 0; + } final oldMessages = List.from(messageResponses ?? []); - final messages = await client.search( + final response = await client.search( filter, sort: sort, query: query, @@ -88,10 +97,20 @@ 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; + if (clear) { - _messageResponses.add(messages.results); + _messageResponses.add(response.results); } else { - final temp = oldMessages + messages.results; + final temp = oldMessages + response.results; _messageResponses.add(temp); } if (_messageResponses.hasValue && _queryMessagesLoadingController.value) { 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 4901c332..2e9f6c61 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 @@ -37,7 +37,7 @@ class MessageSearchListCore extends StatefulWidget { /// * [errorBuilder] /// * [loadingBuilder] /// * [childBuilder] - const MessageSearchListCore({ + MessageSearchListCore({ Key? key, required this.emptyBuilder, required this.errorBuilder, @@ -49,7 +49,21 @@ class MessageSearchListCore extends StatefulWidget { this.paginationParams, 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", + ), + assert( + paginationParams?.offset == null || + paginationParams?.offset == 0 || + sortOptions == null, + 'Cannot specify `offset` with `sortOptions` parameter', + ), + super(key: key); /// A [MessageSearchListController] allows reloading and pagination. /// Use [MessageSearchListController.loadData] and @@ -73,9 +87,8 @@ 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; /// The message query filters to use. @@ -159,15 +172,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) { 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 9100e76d..c1781732 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 @@ -113,8 +113,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 c2e1cde2..fcedc157 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;