Merge pull request #406 from GetStream/feat/type-safe-filter

[CDS-191] feat(llc): add support for type safe filters
This commit is contained in:
Salvatore Giordano
2021-04-30 11:30:03 +02:00
committed by GitHub
35 changed files with 628 additions and 242 deletions
+46 -39
View File
@@ -19,19 +19,24 @@ class Channel {
/// Create a channel client instance. /// Create a channel client instance.
Channel( Channel(
this._client, this._client,
this.type, this._type,
this._id, this._id, {
this._extraData, Map<String, Object> extraData = const {},
) : _cid = _id != null ? '$type:$_id' : null { }) : _cid = _id != null ? '$_type:$_id' : null,
_extraData = extraData {
_client.logger.info('New Channel instance not initialized created'); _client.logger.info('New Channel instance not initialized created');
} }
/// Create a channel client instance from a [ChannelState] object /// Create a channel client instance from a [ChannelState] object
Channel.fromState(this._client, ChannelState channelState) : _extraData = {} { Channel.fromState(this._client, ChannelState channelState)
_cid = channelState.channel!.cid; : assert(
_id = channelState.channel!.id; channelState.channel != null,
type = channelState.channel!.type; 'No channel found inside channel state',
),
_id = channelState.channel!.id,
_type = channelState.channel!.type,
_cid = channelState.channel!.cid,
_extraData = channelState.channel!.extraData ?? {} {
state = ChannelClientState(this, channelState); state = ChannelClientState(this, channelState);
_initializedCompleter.complete(true); _initializedCompleter.complete(true);
_client.logger.info('New Channel instance initialized created'); _client.logger.info('New Channel instance initialized created');
@@ -41,19 +46,20 @@ class Channel {
ChannelClientState? state; ChannelClientState? state;
/// The channel type /// The channel type
String? type; final String _type;
String? _id; String? _id;
String? _cid; String? _cid;
Map<String, dynamic> _extraData; final Map<String, dynamic> _extraData;
set extraData(Map<String, dynamic> extraData) { set extraData(Map<String, dynamic> extraData) {
if (_initializedCompleter.isCompleted) { if (_initializedCompleter.isCompleted) {
throw Exception( throw StateError(
'Once the channel is initialized you should use channel.update ' 'Once the channel is initialized you should use channel.update '
'to update channel data'); 'to update channel data',
);
} }
_extraData = extraData; _extraData.addAll(extraData);
} }
/// Returns true if the channel is muted /// Returns true if the channel is muted
@@ -179,6 +185,9 @@ class Channel {
/// Channel id /// Channel id
String? get id => state?._channelState.channel?.id ?? _id; String? get id => state?._channelState.channel?.id ?? _id;
/// Channel type
String get type => state?._channelState.channel?.type ?? _type;
/// Channel cid /// Channel cid
String? get cid => state?._channelState.channel?.cid ?? _cid; String? get cid => state?._channelState.channel?.cid ?? _cid;
@@ -193,9 +202,11 @@ class Channel {
state?._channelState.channel?.extraData ?? _extraData; state?._channelState.channel?.extraData ?? _extraData;
/// Channel extra data as a stream /// Channel extra data as a stream
Stream<Map<String, dynamic>?>? get extraDataStream { Stream<Map<String, dynamic>> get extraDataStream {
_checkInitialized(); _checkInitialized();
return state?.channelStateStream.map((cs) => cs.channel?.extraData); return state!.channelStateStream.map(
(cs) => cs.channel?.extraData ?? _extraData,
);
} }
/// The main Stream chat client /// The main Stream chat client
@@ -384,7 +395,7 @@ class Channel {
message = await attachmentsUploadCompleter.future; message = await attachmentsUploadCompleter.future;
} }
final response = await _client.sendMessage(message, id!, type!); final response = await _client.sendMessage(message, id!, type);
state!.addMessage(response.message); state!.addMessage(response.message);
return response; return response;
} catch (error) { } catch (error) {
@@ -537,7 +548,7 @@ class Channel {
return _client.sendFile( return _client.sendFile(
file, file,
id!, id!,
type!, type,
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
cancelToken: cancelToken, cancelToken: cancelToken,
); );
@@ -553,7 +564,7 @@ class Channel {
return _client.sendImage( return _client.sendImage(
file, file,
id!, id!,
type!, type,
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
cancelToken: cancelToken, cancelToken: cancelToken,
); );
@@ -565,18 +576,16 @@ class Channel {
Map<String, dynamic>? messageFilters, Map<String, dynamic>? messageFilters,
List<SortOption>? sort, List<SortOption>? sort,
PaginationParams? paginationParams, PaginationParams? paginationParams,
}) => }) {
_client.search( _checkInitialized();
{ return _client.search(
'cid': { Filter.in_('cid', [cid!]),
r'$in': [cid], sort: sort,
}, query: query,
}, paginationParams: paginationParams,
sort: sort, messageFilters: messageFilters,
query: query, );
paginationParams: paginationParams, }
messageFilters: messageFilters,
);
/// Delete a file from this channel /// Delete a file from this channel
Future<EmptyResponse> deleteFile( Future<EmptyResponse> deleteFile(
@@ -587,7 +596,7 @@ class Channel {
return _client.deleteFile( return _client.deleteFile(
url, url,
id!, id!,
type!, type,
cancelToken: cancelToken, cancelToken: cancelToken,
); );
} }
@@ -601,7 +610,7 @@ class Channel {
return _client.deleteImage( return _client.deleteImage(
url, url,
id!, id!,
type!, type,
cancelToken: cancelToken, cancelToken: cancelToken,
); );
} }
@@ -903,7 +912,7 @@ class Channel {
void _initState(ChannelState channelState) { void _initState(ChannelState channelState) {
state = ChannelClientState(this, channelState); state = ChannelClientState(this, channelState);
client.state.channels![cid] = this; client.state.channels[cid!] = this;
if (!_initializedCompleter.isCompleted) { if (!_initializedCompleter.isCompleted) {
_initializedCompleter.complete(true); _initializedCompleter.complete(true);
} }
@@ -1018,9 +1027,7 @@ class Channel {
bool preferOffline = false, bool preferOffline = false,
}) async { }) async {
var path = '/channels/$type'; var path = '/channels/$type';
if (id != null) { if (id != null) path = '$path/$id';
path = '$path/$id';
}
path = '$path/query'; path = '$path/query';
final payload = Map<String, dynamic>.from({ final payload = Map<String, dynamic>.from({
@@ -1082,7 +1089,7 @@ class Channel {
/// Query channel members /// Query channel members
Future<QueryMembersResponse> queryMembers({ Future<QueryMembersResponse> queryMembers({
Map<String, dynamic>? filter, Filter? filter,
List<SortOption>? sort, List<SortOption>? sort,
PaginationParams? pagination, PaginationParams? pagination,
}) async { }) async {
+29 -39
View File
@@ -29,6 +29,8 @@ import 'package:stream_chat/src/platform_detector/platform_detector.dart';
import 'package:stream_chat/version.dart'; import 'package:stream_chat/version.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:stream_chat/src/models/filter.dart';
/// Handler function used for logging records. Function requires a single /// Handler function used for logging records. Function requires a single
/// [LogRecord] as the only parameter. /// [LogRecord] as the only parameter.
typedef LogHandlerFunction = void Function(LogRecord record); typedef LogHandlerFunction = void Function(LogRecord record);
@@ -552,13 +554,11 @@ class StreamChatClient {
type: EventType.connectionRecovered, type: EventType.connectionRecovered,
online: true, online: true,
)); ));
if (state.channels?.isNotEmpty == true) { if (state.channels.isNotEmpty == true) {
// ignore: unawaited_futures // ignore: unawaited_futures
queryChannelsOnline(filter: { queryChannelsOnline(
'cid': { filter: Filter.in_('cid', state.channels.keys.toList()),
'\$in': state.channels!.keys.toList(), ).then(
},
}).then(
(_) async { (_) async {
await resync(); await resync();
}, },
@@ -641,7 +641,7 @@ class StreamChatClient {
/// Requests channels with a given query. /// Requests channels with a given query.
Stream<List<Channel>> queryChannels({ Stream<List<Channel>> queryChannels({
Map<String, dynamic>? filter, Filter? filter,
List<SortOption<ChannelModel>>? sort, List<SortOption<ChannelModel>>? sort,
Map<String, dynamic>? options, Map<String, dynamic>? options,
PaginationParams paginationParams = const PaginationParams(), PaginationParams paginationParams = const PaginationParams(),
@@ -684,7 +684,7 @@ class StreamChatClient {
/// Requests channels with a given query from the API. /// Requests channels with a given query from the API.
Future<List<Channel>> queryChannelsOnline({ Future<List<Channel>> queryChannelsOnline({
required Map<String, dynamic>? filter, Filter? filter,
List<SortOption<ChannelModel>>? sort, List<SortOption<ChannelModel>>? sort,
Map<String, dynamic>? options, Map<String, dynamic>? options,
int? messageLimit, int? messageLimit,
@@ -764,7 +764,7 @@ class StreamChatClient {
final updateData = _mapChannelStateToChannel(channels); final updateData = _mapChannelStateToChannel(channels);
await _chatPersistenceClient?.updateChannelQueries( await _chatPersistenceClient?.updateChannelQueries(
filter ?? {}, filter,
channels.map((c) => c.channel!.cid).toList(), channels.map((c) => c.channel!.cid).toList(),
clearQueryCache: paginationParams.offset == 0, clearQueryCache: paginationParams.offset == 0,
); );
@@ -775,8 +775,8 @@ class StreamChatClient {
/// Requests channels with a given query from the Persistence client. /// Requests channels with a given query from the Persistence client.
Future<List<Channel>> queryChannelsOffline({ Future<List<Channel>> queryChannelsOffline({
required Map<String, dynamic>? filter, Filter? filter,
required List<SortOption<ChannelModel>>? sort, List<SortOption<ChannelModel>>? sort,
PaginationParams paginationParams = const PaginationParams(), PaginationParams paginationParams = const PaginationParams(),
}) async { }) async {
final offlineChannels = (await _chatPersistenceClient?.getChannelStates( final offlineChannels = (await _chatPersistenceClient?.getChannelStates(
@@ -790,10 +790,10 @@ class StreamChatClient {
return updatedData.value; return updatedData.value;
} }
MapEntry<Map<String?, Channel>, List<Channel>> _mapChannelStateToChannel( MapEntry<Map<String, Channel>, List<Channel>> _mapChannelStateToChannel(
List<ChannelState> channelStates, List<ChannelState> channelStates,
) { ) {
final channels = {...state.channels ?? {}}; final channels = {...state.channels};
final newChannels = <Channel>[]; final newChannels = <Channel>[];
for (final channelState in channelStates) { for (final channelState in channelStates) {
final channel = channels[channelState.channel!.cid]; final channel = channels[channelState.channel!.cid];
@@ -802,7 +802,7 @@ class StreamChatClient {
newChannels.add(channel); newChannels.add(channel);
} else { } else {
final newChannel = Channel.fromState(this, channelState); final newChannel = Channel.fromState(this, channelState);
channels[newChannel.cid] = newChannel; channels[newChannel.cid!] = newChannel;
newChannels.add(newChannel); newChannels.add(newChannel);
} }
} }
@@ -1020,7 +1020,7 @@ class StreamChatClient {
/// Requests users with a given query. /// Requests users with a given query.
Future<QueryUsersResponse> queryUsers({ Future<QueryUsersResponse> queryUsers({
Map<String, dynamic>? filter, Filter? filter,
List<SortOption>? sort, List<SortOption>? sort,
Map<String, dynamic>? options, Map<String, dynamic>? options,
PaginationParams? pagination, PaginationParams? pagination,
@@ -1030,7 +1030,7 @@ class StreamChatClient {
}; };
final payload = <String, dynamic>{ final payload = <String, dynamic>{
'filter_conditions': filter ?? {}, 'filter_conditions': filter,
'sort': sort, 'sort': sort,
}..addAll(defaultOptions); }..addAll(defaultOptions);
@@ -1061,16 +1061,13 @@ class StreamChatClient {
/// A message search. /// A message search.
Future<SearchMessagesResponse> search( Future<SearchMessagesResponse> search(
Map<String, dynamic> filters, { Filter filter, {
String? query, String? query,
List<SortOption>? sort, List<SortOption>? sort,
PaginationParams? paginationParams, PaginationParams? paginationParams,
Map<String, dynamic>? messageFilters, Map<String, dynamic>? messageFilters,
}) async { }) async {
assert(() { assert(() {
if (filters.isEmpty) {
throw ArgumentError('`filters` cannot be set as null or empty');
}
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`');
} }
@@ -1083,7 +1080,7 @@ class StreamChatClient {
}(), 'Check incoming params.'); }(), 'Check incoming params.');
final payload = { final payload = {
'filter_conditions': filters, 'filter_conditions': filter,
'message_filter_conditions': messageFilters, 'message_filter_conditions': messageFilters,
'query': query, 'query': query,
'sort': sort, 'sort': sort,
@@ -1194,15 +1191,12 @@ class StreamChatClient {
Channel channel( Channel channel(
String type, { String type, {
String? id, String? id,
Map<String, dynamic> extraData = const {}, Map<String, Object> extraData = const {},
}) { }) {
if (id != null && state.channels?.containsKey('$type:$id') == true) { if (id != null && state.channels.containsKey('$type:$id')) {
if (state.channels!['$type:$id'] != null) { return state.channels['$type:$id']!;
return state.channels!['$type:$id'] as Channel;
}
} }
return Channel(this, type, id, extraData: extraData);
return Channel(this, type, id, extraData);
} }
/// Update or Create the given user object. /// Update or Create the given user object.
@@ -1443,9 +1437,7 @@ class ClientState {
if (cid != null) { if (cid != null) {
_client.chatPersistenceClient?.deleteChannels([cid]); _client.chatPersistenceClient?.deleteChannels([cid]);
} }
if (channels != null) { channels = channels..removeWhere((cid, ch) => cid == event.cid);
channels = channels?..removeWhere((cid, ch) => cid == event.cid);
}
})); }));
} }
@@ -1468,9 +1460,7 @@ class ClientState {
.listen((Event event) async { .listen((Event event) async {
final eventChannel = event.channel!; final eventChannel = event.channel!;
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
if (channels != null) { channels = channels..remove(eventChannel.cid);
channels = channels?..remove(eventChannel.cid);
}
})); }));
} }
@@ -1521,13 +1511,13 @@ class ClientState {
_channelsController.stream; _channelsController.stream;
/// The current list of channels in memory /// The current list of channels in memory
Map<String?, Channel>? get channels => _channelsController.value; Map<String, Channel> get channels => _channelsController.value!;
set channels(Map<String?, Channel>? v) { set channels(Map<String, Channel>? v) {
_channelsController.add(v); if (v != null) _channelsController.add(v);
} }
final BehaviorSubject<Map<String?, Channel>?> _channelsController = final BehaviorSubject<Map<String, Channel>> _channelsController =
BehaviorSubject.seeded({}); BehaviorSubject.seeded({});
final BehaviorSubject<OwnUser?> _userController = BehaviorSubject(); final BehaviorSubject<OwnUser?> _userController = BehaviorSubject();
final BehaviorSubject<Map<String?, User?>> _usersController = final BehaviorSubject<Map<String?, User?>> _usersController =
@@ -1541,7 +1531,7 @@ class ClientState {
_userController.close(); _userController.close();
_unreadChannelsController.close(); _unreadChannelsController.close();
_totalUnreadCountController.close(); _totalUnreadCountController.close();
channels!.values.forEach((c) => c.dispose()); channels.values.forEach((c) => c.dispose());
_channelsController.close(); _channelsController.close();
} }
} }
@@ -2,6 +2,7 @@ import 'package:stream_chat/src/api/requests.dart';
import 'package:stream_chat/src/models/channel_model.dart'; import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/channel_state.dart'; import 'package:stream_chat/src/models/channel_state.dart';
import 'package:stream_chat/src/models/event.dart'; import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/filter.dart';
import 'package:stream_chat/src/models/member.dart'; import 'package:stream_chat/src/models/member.dart';
import 'package:stream_chat/src/models/message.dart'; import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/reaction.dart'; import 'package:stream_chat/src/models/reaction.dart';
@@ -90,7 +91,7 @@ abstract class ChatPersistenceClient {
/// Optionally, pass [filter], [sort], [paginationParams] /// Optionally, pass [filter], [sort], [paginationParams]
/// for filtering out states. /// for filtering out states.
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelState>> getChannelStates({
Map<String, dynamic>? filter, Filter? filter,
List<SortOption<ChannelModel>>? sort, List<SortOption<ChannelModel>>? sort,
PaginationParams? paginationParams, PaginationParams? paginationParams,
}); });
@@ -100,7 +101,7 @@ abstract class ChatPersistenceClient {
/// If [clearQueryCache] is true before the insert /// If [clearQueryCache] is true before the insert
/// the list of matching rows will be deleted /// the list of matching rows will be deleted
Future<void> updateChannelQueries( Future<void> updateChannelQueries(
Map<String, dynamic> filter, Filter? filter,
List<String> cids, { List<String> cids, {
bool clearQueryCache = false, bool clearQueryCache = false,
}); });
@@ -23,16 +23,16 @@ class ChannelModel {
this.memberCount = 0, this.memberCount = 0,
this.extraData, this.extraData,
this.team, this.team,
}) : config = config ?? ChannelConfig(), }) : assert(
createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now(),
assert(
(cid != null && cid.contains(':')) || (id != null && type != null), (cid != null && cid.contains(':')) || (id != null && type != null),
'provide either a cid or an id and type', 'provide either a cid or an id and type',
), ),
id = id ?? cid!.split(':')[1], id = id ?? cid!.split(':')[1],
type = type ?? cid!.split(':')[0], type = type ?? cid!.split(':')[0],
cid = cid ?? '$type:$id'; cid = cid ?? '$type:$id',
config = config ?? ChannelConfig(),
createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now();
/// Create a new instance from a json /// Create a new instance from a json
factory ChannelModel.fromJson(Map<String, dynamic> json) => factory ChannelModel.fromJson(Map<String, dynamic> json) =>
@@ -0,0 +1,208 @@
// ignore_for_file: non_constant_identifier_names, constant_identifier_names
import 'package:equatable/equatable.dart';
const _groupOperators = [
FilterOperator.and,
FilterOperator.or,
FilterOperator.nor,
];
/// Possible operators to use in filters.
enum FilterOperator {
/// Matches values that are equal to a specified value.
equal,
/// Matches all values that are not equal to a specified value.
notEqual,
/// Matches values that are greater than a specified value.
greater,
/// Matches values that are greater than a specified value.
greaterOrEqual,
/// Matches values that are less than a specified value.
less,
/// Matches values that are less than or equal to a specified value.
lessOrEqual,
/// Matches any of the values specified in an array.
in_,
/// Matches none of the values specified in an array.
notIn,
/// Matches values by performing text search with the specified value.
query,
/// Matches values with the specified prefix.
autoComplete,
/// Matches values that exist/don't exist based on the specified boolean value.
exists,
/// Matches all the values specified in an array.
and,
/// Matches at least one of the values specified in an array.
or,
/// Matches none of the values specified in an array.
nor,
}
/// Helper extension for [FilterOperator]
extension FilterOperatorX on FilterOperator {
/// Converts [FilterOperator] into rew values
String get rawValue => {
FilterOperator.equal: '\$eq',
FilterOperator.notEqual: '\$ne',
FilterOperator.greater: '\$gt',
FilterOperator.greaterOrEqual: '\$gte',
FilterOperator.less: '\$lt',
FilterOperator.lessOrEqual: '\$lte',
FilterOperator.in_: '\$in',
FilterOperator.notIn: '\$nin',
FilterOperator.query: '\$q',
FilterOperator.autoComplete: '\$autocomplete',
FilterOperator.exists: '\$exists',
FilterOperator.and: '\$and',
FilterOperator.or: '\$or',
FilterOperator.nor: '\$nor',
}[this]!;
}
/// Stream supports a limited set of filters for querying channels,
/// users and members. The example below shows how to filter for channels
/// of type messaging where the current user is a member
///
/// ```dart
/// final filter = Filter.and(
/// Filter.equal('type', 'messaging'),
/// Filter.in_('members', [user.id])
/// )
/// ```
/// See <a href="https://getstream.io/chat/docs/query_channels/?language=dart" target="_top">Query Channels Documentation</a>
class Filter extends Equatable {
const Filter.__({
required this.operator,
required this.value,
this.key,
});
Filter._({
required FilterOperator operator,
required this.value,
this.key,
}) : operator = operator.rawValue;
/// Combines the provided filters and matches the values
/// matched by all filters.
factory Filter.and(List<Filter> filters) =>
Filter._(operator: FilterOperator.and, value: filters);
/// Combines the provided filters and matches the values
/// matched by at least one of the filters.
factory Filter.or(List<Filter> filters) =>
Filter._(operator: FilterOperator.or, value: filters);
/// Combines the provided filters and matches the values
/// not matched by all the filters.
factory Filter.nor(List<Filter> filters) =>
Filter._(operator: FilterOperator.nor, value: filters);
/// Matches values that are equal to a specified value.
factory Filter.equal(String key, Object value) =>
Filter._(operator: FilterOperator.equal, key: key, value: value);
/// Matches all values that are not equal to a specified value.
factory Filter.notEqual(String key, Object value) =>
Filter._(operator: FilterOperator.notEqual, key: key, value: value);
/// Matches values that are greater than a specified value.
factory Filter.greater(String key, Object value) =>
Filter._(operator: FilterOperator.greater, key: key, value: value);
/// Matches values that are greater than a specified value.
factory Filter.greaterOrEqual(String key, Object value) =>
Filter._(operator: FilterOperator.greaterOrEqual, key: key, value: value);
/// Matches values that are less than a specified value.
factory Filter.less(String key, Object value) =>
Filter._(operator: FilterOperator.less, key: key, value: value);
/// Matches values that are less than or equal to a specified value.
factory Filter.lessOrEqual(String key, Object value) =>
Filter._(operator: FilterOperator.lessOrEqual, key: key, value: value);
/// Matches any of the values specified in an array.
factory Filter.in_(String key, List<Object> values) =>
Filter._(operator: FilterOperator.in_, key: key, value: values);
/// Matches none of the values specified in an array.
factory Filter.notIn(String key, List<Object> values) =>
Filter._(operator: FilterOperator.notIn, key: key, value: values);
/// Matches values by performing text search with the specified value.
factory Filter.query(String key, String text) =>
Filter._(operator: FilterOperator.query, key: key, value: text);
/// Matches values with the specified prefix.
factory Filter.autoComplete(String key, String text) =>
Filter._(operator: FilterOperator.autoComplete, key: key, value: text);
/// Matches values that exist/don't exist based on the specified boolean value.
factory Filter.exists(String key, {bool exists = true}) =>
Filter._(operator: FilterOperator.exists, key: key, value: exists);
/// Creates a custom [Filter] if there isn't one already available.
const factory Filter.custom({
required String operator,
required Object value,
String? key,
}) = Filter.__;
/// An operator used for the filter. The operator string must start with `$`
final String operator;
/// The "left-hand" side of the filter.
/// Specifies the name of the field the filter should match.
///
/// Some operators like `and` or `or`,
/// don't require the key value to be present.
/// see-more : [_groupOperators]
final String? key;
/// The "right-hand" side of the filter.
/// Specifies the [value] the filter should match.
final Object /*List<Object>|List<Filter>|String*/ value;
@override
List<Object?> get props => [operator, key, value];
/// Serializes to json object
Map<String, Object> toJson() {
final json = <String, Object>{};
final groupOperators = _groupOperators.map((it) => it.rawValue);
assert(
groupOperators.contains(operator) || key != null,
'Filter must contain the `key` when the operator is not a '
'group operator.',
);
if (groupOperators.contains(operator)) {
// Filters with group operators are encoded in the following form:
// { $<operator>: [ <filter 1>, <filter 2> ] }
json[operator] = value;
} else {
// Normal filters are encoded in the following form:
// { key: { $<operator>: <value> } }
json[key!] = {operator: value};
}
return json;
}
}
@@ -28,6 +28,7 @@ export './src/models/channel_state.dart';
export './src/models/command.dart'; export './src/models/command.dart';
export './src/models/device.dart'; export './src/models/device.dart';
export './src/models/event.dart'; export './src/models/event.dart';
export './src/models/filter.dart' show Filter;
export './src/models/member.dart'; export './src/models/member.dart';
export './src/models/message.dart'; export './src/models/message.dart';
export './src/models/mute.dart'; export './src/models/mute.dart';
@@ -516,7 +516,7 @@ void main() {
); );
await channelClient.watch(); await channelClient.watch();
final event = const Event(type: EventType.any); const event = Event(type: EventType.any);
when( when(
() => mockDio.post<String>( () => mockDio.post<String>(
@@ -564,7 +564,7 @@ void main() {
); );
await channelClient.watch(); await channelClient.watch();
final event = const Event(type: EventType.typingStart); const event = Event(type: EventType.typingStart);
when( when(
() => mockDio.post<String>( () => mockDio.post<String>(
@@ -610,7 +610,7 @@ void main() {
); );
await channelClient.watch(); await channelClient.watch();
final event = const Event(type: EventType.typingStop); const event = Event(type: EventType.typingStop);
when( when(
() => mockDio.post<String>( () => mockDio.post<String>(
+13 -25
View File
@@ -10,6 +10,7 @@ import 'package:stream_chat/src/api/requests.dart';
import 'package:stream_chat/src/client.dart'; import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/exceptions.dart'; import 'package:stream_chat/src/exceptions.dart';
import 'package:stream_chat/src/models/channel_model.dart'; import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/filter.dart';
import 'package:stream_chat/src/models/message.dart'; import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/user.dart'; import 'package:stream_chat/src/models/user.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@@ -89,7 +90,7 @@ void main() {
test('Channel', () { test('Channel', () {
final client = StreamChatClient('test'); final client = StreamChatClient('test');
final data = <String, dynamic>{'test': 1}; final data = <String, Object>{'test': 1};
final channelClient = client.channel('type', id: 'id', extraData: data); final channelClient = client.channel('type', id: 'id', extraData: data);
expect(channelClient.type, 'type'); expect(channelClient.type, 'type');
expect(channelClient.id, 'id'); expect(channelClient.id, 'id');
@@ -126,7 +127,7 @@ void main() {
), ),
); );
await client.queryChannelsOnline(filter: null, waitForConnect: false); await client.queryChannelsOnline(waitForConnect: false);
verify(() => verify(() =>
mockDio.get<String>('/channels', queryParameters: queryParams)) mockDio.get<String>('/channels', queryParameters: queryParams))
@@ -140,11 +141,7 @@ void main() {
when(() => mockDio.interceptors).thenReturn(Interceptors()); when(() => mockDio.interceptors).thenReturn(Interceptors());
final client = StreamChatClient('api-key', httpClient: mockDio); final client = StreamChatClient('api-key', httpClient: mockDio);
final queryFilter = <String, dynamic>{ final queryFilter = Filter.in_('id', const ['test']);
'id': {
'\$in': ['test'],
},
};
final sortOptions = <SortOption<ChannelModel>>[]; final sortOptions = <SortOption<ChannelModel>>[];
final options = {'state': false, 'watch': false, 'presence': true}; final options = {'state': false, 'watch': false, 'presence': true};
const paginationParams = PaginationParams(offset: 2); const paginationParams = PaginationParams(offset: 2);
@@ -192,11 +189,7 @@ void main() {
when(() => mockDio.interceptors).thenReturn(Interceptors()); when(() => mockDio.interceptors).thenReturn(Interceptors());
final client = StreamChatClient('api-key', httpClient: mockDio); final client = StreamChatClient('api-key', httpClient: mockDio);
final filter = { final filter = Filter.in_('cid', const ['messaging:testId']);
'cid': {
r'$in': ['messaging:testId']
}
};
const query = 'hello'; const query = 'hello';
final queryParams = { final queryParams = {
'payload': json.encode({ 'payload': json.encode({
@@ -229,11 +222,7 @@ void main() {
when(() => mockDio.interceptors).thenReturn(Interceptors()); when(() => mockDio.interceptors).thenReturn(Interceptors());
final client = StreamChatClient('api-key', httpClient: mockDio); final client = StreamChatClient('api-key', httpClient: mockDio);
final filters = { final filters = Filter.in_('id', const ['test']);
'id': {
'\$in': ['test'],
},
};
const sortOptions = [SortOption('name')]; const sortOptions = [SortOption('name')];
const query = 'query'; const query = 'query';
final queryParams = { final queryParams = {
@@ -374,7 +363,7 @@ void main() {
final client = StreamChatClient('api-key', httpClient: mockDio); final client = StreamChatClient('api-key', httpClient: mockDio);
final queryParams = { final queryParams = {
'payload': json.encode({ 'payload': json.encode({
'filter_conditions': {}, 'filter_conditions': null,
'sort': null, 'sort': null,
'presence': false, 'presence': false,
}), }),
@@ -407,11 +396,7 @@ void main() {
when(() => mockDio.interceptors).thenReturn(Interceptors()); when(() => mockDio.interceptors).thenReturn(Interceptors());
final client = StreamChatClient('api-key', httpClient: mockDio); final client = StreamChatClient('api-key', httpClient: mockDio);
final queryFilter = <String, dynamic>{ final queryFilter = Filter.in_('id', const ['test']);
'id': {
'\$in': ['test'],
},
};
const sortOptions = <SortOption>[]; const sortOptions = <SortOption>[];
final options = {'presence': true}; final options = {'presence': true};
final queryParams = <String, dynamic>{ final queryParams = <String, dynamic>{
@@ -1163,8 +1148,11 @@ void main() {
httpClient: mockDio, httpClient: mockDio,
); );
final channelClient = final channelClient = client.channel(
client.channel('type', id: 'id', extraData: {'name': 'init'}); 'type',
id: 'id',
extraData: {'name': 'init'},
);
const update = { const update = {
'set': {'name': 'demo'} 'set': {'name': 'demo'}
@@ -0,0 +1,212 @@
import 'dart:convert';
import 'package:test/test.dart';
import 'package:stream_chat/src/models/filter.dart';
void main() {
group('operators', () {
test('equal', () {
const key = 'testKey';
const value = 'testValue';
final filter = Filter.equal(key, value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, FilterOperator.equal.rawValue);
});
test('notEqual', () {
const key = 'testKey';
const value = 'testValue';
final filter = Filter.notEqual(key, value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, FilterOperator.notEqual.rawValue);
});
test('greater', () {
const key = 'testKey';
const value = 'testValue';
final filter = Filter.greater(key, value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, FilterOperator.greater.rawValue);
});
test('greaterOrEqual', () {
const key = 'testKey';
const value = 'testValue';
final filter = Filter.greaterOrEqual(key, value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, FilterOperator.greaterOrEqual.rawValue);
});
test('less', () {
const key = 'testKey';
const value = 'testValue';
final filter = Filter.less(key, value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, FilterOperator.less.rawValue);
});
test('lessOrEqual', () {
const key = 'testKey';
const value = 'testValue';
final filter = Filter.lessOrEqual(key, value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, FilterOperator.lessOrEqual.rawValue);
});
test('in', () {
const key = 'testKey';
const values = ['testValue'];
final filter = Filter.in_(key, values);
expect(filter.key, key);
expect(filter.value, values);
expect(filter.operator, FilterOperator.in_.rawValue);
});
test('in', () {
const key = 'testKey';
const values = ['testValue'];
final filter = Filter.in_(key, values);
expect(filter.key, key);
expect(filter.value, values);
expect(filter.operator, FilterOperator.in_.rawValue);
});
test('notIn', () {
const key = 'testKey';
const values = ['testValue'];
final filter = Filter.notIn(key, values);
expect(filter.key, key);
expect(filter.value, values);
expect(filter.operator, FilterOperator.notIn.rawValue);
});
test('query', () {
const key = 'testKey';
const value = 'testQuery';
final filter = Filter.query(key, value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, FilterOperator.query.rawValue);
});
test('autoComplete', () {
const key = 'testKey';
const value = 'testQuery';
final filter = Filter.autoComplete(key, value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, FilterOperator.autoComplete.rawValue);
});
test('exists', () {
const key = 'testKey';
final filter = Filter.exists(key);
expect(filter.key, key);
expect(filter.value, isTrue);
expect(filter.operator, FilterOperator.exists.rawValue);
});
test('notExists', () {
const key = 'testKey';
final filter = Filter.exists(key, exists: false);
expect(filter.key, key);
expect(filter.value, isFalse);
expect(filter.operator, FilterOperator.exists.rawValue);
});
test('custom', () {
const key = 'testKey';
const value = 'testValue';
const operator = '\$customOperator';
const filter = Filter.custom(operator: operator, key: key, value: value);
expect(filter.key, key);
expect(filter.value, value);
expect(filter.operator, operator);
});
group('groupedOperator', () {
final filter1 = Filter.equal('testKey', 'testValue');
final filter2 = Filter.in_('testKey', const ['testValue']);
final filters = [filter1, filter2];
test('and', () {
final filter = Filter.and(filters);
expect(filter.key, isNull);
expect(filter.value, filters);
expect(filter.operator, FilterOperator.and.rawValue);
});
test('or', () {
final filter = Filter.or(filters);
expect(filter.key, isNull);
expect(filter.value, filters);
expect(filter.operator, FilterOperator.or.rawValue);
});
test('nor', () {
final filter = Filter.nor(filters);
expect(filter.key, isNull);
expect(filter.value, filters);
expect(filter.operator, FilterOperator.nor.rawValue);
});
});
});
group('encoding', () {
group('nonGroupedFilter', () {
test('simpleValue', () {
const key = 'testKey';
const value = 'testValue';
final filter = Filter.equal(key, value);
final encoded = json.encode(filter);
expect(
encoded,
'{"$key":{"${FilterOperator.equal.rawValue}":${json.encode(value)}}}',
);
});
test('listValue', () {
const key = 'testKey';
const values = ['testValue'];
final filter = Filter.in_(key, values);
final encoded = json.encode(filter);
expect(
encoded,
'{"$key":{"${FilterOperator.in_.rawValue}":${json.encode(values)}}}',
);
});
});
test('groupedFilter', () {
final filter1 = Filter.equal('testKey', 'testValue');
final filter2 = Filter.in_('testKey', const ['testValue']);
final filters = [filter1, filter2];
final filter = Filter.and(filters);
final encoded = json.encode(filter);
expect(
encoded,
'{"${FilterOperator.and.rawValue}":${json.encode(filters)}}',
);
});
group('equality', () {
test('simpleFilter', () {
final filter1 = Filter.equal('testKey', 'testValue');
final filter2 = Filter.equal('testKey', 'testValue');
expect(filter1, filter2);
});
test('groupedFilter', () {
final filter1 = Filter.and([Filter.equal('testKey', 'testValue')]);
final filter2 = Filter.and([Filter.equal('testKey', 'testValue')]);
expect(filter1, filter2);
});
});
});
}
@@ -93,11 +93,10 @@ class ChannelListPage extends StatelessWidget {
onTap(channel); onTap(channel);
} }
: null, : null,
filter: { filter: Filter.in_(
'members': { 'members',
'\$in': [StreamChat.of(context).user.id], [StreamChat.of(context).user.id],
} ),
},
sort: [SortOption('last_message_at')], sort: [SortOption('last_message_at')],
pagination: PaginationParams( pagination: PaginationParams(
limit: 20, limit: 20,
@@ -56,11 +56,10 @@ class ChannelListPage extends StatelessWidget {
return Scaffold( return Scaffold(
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListView( child: ChannelListView(
filter: { filter: Filter.in_(
// 'members': { 'members',
// '\$in': [StreamChat.of(context).user.id], [StreamChat.of(context).user.id],
// } ),
},
sort: [SortOption('last_message_at')], sort: [SortOption('last_message_at')],
pagination: PaginationParams( pagination: PaginationParams(
limit: 20, limit: 20,
@@ -57,11 +57,10 @@ class ChannelListPage extends StatelessWidget {
return Scaffold( return Scaffold(
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListView( child: ChannelListView(
filter: { filter: Filter.in_(
'members': { 'members',
'\$in': [StreamChat.of(context).user.id], [StreamChat.of(context).user.id],
} ),
},
channelPreviewBuilder: _channelPreviewBuilder, channelPreviewBuilder: _channelPreviewBuilder,
// sort: [SortOption('last_message_at')], // sort: [SortOption('last_message_at')],
pagination: PaginationParams( pagination: PaginationParams(
@@ -49,11 +49,10 @@ class ChannelListPage extends StatelessWidget {
return Scaffold( return Scaffold(
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListView( child: ChannelListView(
filter: { filter: Filter.in_(
'members': { 'members',
'\$in': [StreamChat.of(context).user.id], [StreamChat.of(context).user.id],
} ),
},
sort: [SortOption('last_message_at')], sort: [SortOption('last_message_at')],
pagination: PaginationParams( pagination: PaginationParams(
limit: 20, limit: 20,
@@ -52,11 +52,10 @@ class ChannelListPage extends StatelessWidget {
return Scaffold( return Scaffold(
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListView( child: ChannelListView(
filter: { filter: Filter.in_(
'members': { 'members',
'\$in': [StreamChat.of(context).user.id], [StreamChat.of(context).user.id],
} ),
},
sort: [SortOption('last_message_at')], sort: [SortOption('last_message_at')],
pagination: PaginationParams( pagination: PaginationParams(
limit: 20, limit: 20,
@@ -80,11 +80,10 @@ class ChannelListPage extends StatelessWidget {
return Scaffold( return Scaffold(
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListView( child: ChannelListView(
filter: { filter: Filter.in_(
'members': { 'members',
'\$in': [StreamChat.of(context).user.id], [StreamChat.of(context).user.id],
} ),
},
sort: [SortOption('last_message_at')], sort: [SortOption('last_message_at')],
pagination: PaginationParams( pagination: PaginationParams(
limit: 20, limit: 20,
@@ -84,7 +84,7 @@ class ChannelListView extends StatefulWidget {
/// The query filters to use. /// The 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].
/// You can also filter other built-in channel fields. /// You can also filter other built-in channel fields.
final Map<String, dynamic> filter; final Filter filter;
/// Query channels options. /// Query channels options.
/// ///
@@ -1243,11 +1243,10 @@ class MessageInputState extends State<MessageInput> {
Future<List<Member>> queryMembers; Future<List<Member>> queryMembers;
if (query.isNotEmpty) { if (query.isNotEmpty) {
queryMembers = StreamChannel.of(context).channel.queryMembers(filter: { queryMembers = StreamChannel.of(context)
'name': { .channel
'\$autocomplete': query, .queryMembers(filter: Filter.autoComplete('name', query))
}, .then((res) => res.members);
}).then((res) => res.members);
} }
final members = StreamChannel.of(context).channel.state.members?.where((m) { final members = StreamChannel.of(context).channel.state.members?.where((m) {
@@ -2096,7 +2095,7 @@ class MessageInputState extends State<MessageInput> {
} }
return sendingFuture.then((resp) { return sendingFuture.then((resp) {
if (resp.message?.type == 'error') { if (resp.message?._type == 'error') {
_parseExistingMessage(message); _parseExistingMessage(message);
} }
if (widget.onMessageSent != null) { if (widget.onMessageSent != null) {
@@ -71,7 +71,7 @@ class MessageSearchListView extends StatefulWidget {
/// The query filters to use. /// The 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].
/// You can also filter other built-in channel fields. /// You can also filter other built-in channel fields.
final Map<String, dynamic> filters; final Filter filters;
/// The sorting used for the channels matching the filters. /// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options can be provided.
@@ -72,7 +72,7 @@ class UserListView extends StatefulWidget {
/// The query filters to use. /// The 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].
/// You can also filter other built-in channel fields. /// You can also filter other built-in channel fields.
final Map<String, dynamic> filter; final Filter filter;
/// Query channels options. /// Query channels options.
/// ///
@@ -4,6 +4,7 @@ analyzer:
- lib/**/*.freezed.dart - lib/**/*.freezed.dart
- example/* - example/*
- test/* - test/*
linter: linter:
rules: rules:
- always_use_package_imports - always_use_package_imports
@@ -78,14 +78,12 @@ class HomeScreen extends StatelessWidget {
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListCore( child: ChannelListCore(
channelListController: channelListController, channelListController: channelListController,
filter: { filter: Filter.and([
'type': 'messaging', Filter.equal('type', 'messaging'),
'members': { Filter.in_('members', [
r'$in': [ StreamChatCore.of(context).user!.id,
StreamChatCore.of(context).user!.id, ])
] ]),
}
},
emptyBuilder: (BuildContext context) { emptyBuilder: (BuildContext context) {
return Center( return Center(
child: Text('Looks like you are not in any channels'), child: Text('Looks like you are not in any channels'),
@@ -92,7 +92,7 @@ class ChannelListCore extends StatefulWidget {
/// The query filters to use. /// The 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].
/// You can also filter other built-in channel fields. /// You can also filter other built-in channel fields.
final Map<String, dynamic>? filter; final Filter? filter;
/// Query channels options. /// Query channels options.
/// ///
@@ -88,7 +88,7 @@ class ChannelsBlocState extends State<ChannelsBloc>
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream /// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({ Future<void> queryChannels({
Map<String, dynamic>? filter, Filter? filter,
List<SortOption<ChannelModel>>? sortOptions, List<SortOption<ChannelModel>>? sortOptions,
PaginationParams paginationParams = const PaginationParams(limit: 30), PaginationParams paginationParams = const PaginationParams(limit: 30),
Map<String, dynamic>? options, Map<String, dynamic>? options,
@@ -160,9 +160,8 @@ class ChannelsBlocState extends State<ChannelsBloc>
newChannels.insert(0, _hiddenChannels[hiddenIndex]); newChannels.insert(0, _hiddenChannels[hiddenIndex]);
_hiddenChannels.removeAt(hiddenIndex); _hiddenChannels.removeAt(hiddenIndex);
} else { } else {
if (client.state.channels != null && if (client.state.channels[e.cid] != null) {
client.state.channels?[e.cid] != null) { newChannels.insert(0, client.state.channels[e.cid]!);
newChannels.insert(0, client.state.channels![e.cid]!);
} }
} }
} }
@@ -9,7 +9,7 @@ import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
/// [MessageSearchBloc] can be access at anytime by using the static [of] method /// [MessageSearchBloc] can be access at anytime by using the static [of] method
/// using Flutter's [BuildContext]. /// using Flutter's [BuildContext].
/// ///
// API docs: https://getstream.io/chat/docs/flutter-dart/send_message/ /// API docs: https://getstream.io/chat/docs/flutter-dart/send_message/
class MessageSearchBloc extends StatefulWidget { class MessageSearchBloc extends StatefulWidget {
/// Instantiate a new MessageSearchBloc /// Instantiate a new MessageSearchBloc
const MessageSearchBloc({ const MessageSearchBloc({
@@ -58,7 +58,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
/// Calls [StreamChatClient.search] updating /// Calls [StreamChatClient.search] updating
/// [messagesStream] and [queryMessagesLoading] stream /// [messagesStream] and [queryMessagesLoading] stream
Future<void> search({ Future<void> search({
required Map<String, dynamic> filter, required Filter filter,
Map<String, dynamic>? messageFilter, Map<String, dynamic>? messageFilter,
List<SortOption>? sort, List<SortOption>? sort,
String? query, String? query,
@@ -67,7 +67,7 @@ class MessageSearchListCore extends StatefulWidget {
/// The query filters to use. /// The 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].
/// You can also filter other built-in channel fields. /// You can also filter other built-in channel fields.
final Map<String, dynamic> filters; final Filter filters;
/// The sorting used for the channels matching the filters. /// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be /// Sorting is based on field and direction, multiple sorting options can be
@@ -90,7 +90,7 @@ class UserListCore extends StatefulWidget {
/// The query filters to use. /// The 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].
/// You can also filter other built-in channel fields. /// You can also filter other built-in channel fields.
final Map<String, dynamic>? filter; final Filter? filter;
/// Query channels options. /// Query channels options.
/// ///
@@ -58,7 +58,7 @@ class UsersBlocState extends State<UsersBloc>
/// 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)
Future<void> queryUsers({ Future<void> queryUsers({
Map<String, dynamic>? filter, Filter? filter,
List<SortOption>? sort, List<SortOption>? sort,
Map<String, dynamic>? options, Map<String, dynamic>? options,
PaginationParams? pagination, PaginationParams? pagination,
@@ -24,7 +24,7 @@ void main() {
client, client,
'testType$index', 'testType$index',
'testId$index', 'testId$index',
{'extra_data_key': 'extra_data_value_$index'}, extraData: {'extra_data_key': 'extra_data_value_$index'},
); );
}, },
); );
@@ -26,7 +26,7 @@ void main() {
client, client,
'testType$index', 'testType$index',
'testId$index', 'testId$index',
{'extra_data_key': 'extra_data_value_$index'}, extraData: {'extra_data_key': 'extra_data_value_$index'},
); );
}, },
); );
@@ -703,7 +703,7 @@ void main() {
final mockClient = MockClient(); final mockClient = MockClient();
final channels = _generateChannels(mockClient); final channels = _generateChannels(mockClient);
final stateChannels = { final stateChannels = {
for (var c in _generateChannels(mockClient, offset: 5)) c.cid: c for (var c in _generateChannels(mockClient, offset: 5)) c.cid!: c
}; };
const channelsBlocKey = Key('channelsBloc'); const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc( final channelsBloc = ChannelsBloc(
@@ -8,6 +8,8 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/get_message_response_matcher.dart'; import 'matchers/get_message_response_matcher.dart';
import 'mocks.dart'; import 'mocks.dart';
final testFilter = Filter.custom(operator: '\$test', value: 'testValue');
void main() { void main() {
List<GetMessageResponse> _generateMessages({ List<GetMessageResponse> _generateMessages({
int count = 3, int count = 3,
@@ -50,7 +52,7 @@ void main() {
); );
try { try {
await usersBlocState.search(filter: {}); await usersBlocState.search(filter: testFilter);
} catch (e) { } catch (e) {
expect(e, isInstanceOf<Exception>()); expect(e, isInstanceOf<Exception>());
} }
@@ -82,7 +84,7 @@ void main() {
final messageResponseList = _generateMessages(); final messageResponseList = _generateMessages();
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -91,7 +93,7 @@ void main() {
(_) async => SearchMessagesResponse()..results = messageResponseList, (_) async => SearchMessagesResponse()..results = messageResponseList,
); );
messageSearchBlocState.search(filter: {}); messageSearchBlocState.search(filter: testFilter);
await expectLater( await expectLater(
messageSearchBlocState.messagesStream, messageSearchBlocState.messagesStream,
@@ -99,7 +101,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -133,14 +135,14 @@ void main() {
const error = 'Error! Error! Error!'; const error = 'Error! Error! Error!';
when(() => mockClient.search( when(() => mockClient.search(
any(), 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: any(named: 'paginationParams'),
)).thenThrow(error); )).thenThrow(error);
messageSearchBlocState.search(filter: {}); messageSearchBlocState.search(filter: testFilter);
await expectLater( await expectLater(
messageSearchBlocState.messagesStream, messageSearchBlocState.messagesStream,
@@ -148,7 +150,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -185,7 +187,7 @@ void main() {
final messageResponseList = _generateMessages(); final messageResponseList = _generateMessages();
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -194,7 +196,7 @@ void main() {
(_) async => SearchMessagesResponse()..results = messageResponseList, (_) async => SearchMessagesResponse()..results = messageResponseList,
); );
messageSearchBlocState.search(filter: {}); messageSearchBlocState.search(filter: testFilter);
await expectLater( await expectLater(
messageSearchBlocState.messagesStream, messageSearchBlocState.messagesStream,
@@ -202,7 +204,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -214,7 +216,7 @@ void main() {
final pagination = PaginationParams(offset: offset); final pagination = PaginationParams(offset: offset);
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -224,7 +226,7 @@ void main() {
SearchMessagesResponse()..results = paginatedMessageResponseList, SearchMessagesResponse()..results = paginatedMessageResponseList,
); );
messageSearchBlocState.search(pagination: pagination, filter: {}); messageSearchBlocState.search(pagination: pagination, filter: testFilter);
await Future.wait([ await Future.wait([
expectLater( expectLater(
@@ -240,7 +242,7 @@ void main() {
]); ]);
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -277,7 +279,7 @@ void main() {
final messageResponseList = _generateMessages(); final messageResponseList = _generateMessages();
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -286,7 +288,7 @@ void main() {
(_) async => SearchMessagesResponse()..results = messageResponseList, (_) async => SearchMessagesResponse()..results = messageResponseList,
); );
messageSearchBlocState.search(filter: {}); messageSearchBlocState.search(filter: testFilter);
await expectLater( await expectLater(
messageSearchBlocState.messagesStream, messageSearchBlocState.messagesStream,
@@ -294,7 +296,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -306,14 +308,14 @@ void main() {
const error = 'Error! Error! Error!'; const error = 'Error! Error! Error!';
when(() => mockClient.search( when(() => mockClient.search(
any(), 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: pagination,
)).thenThrow(error); )).thenThrow(error);
messageSearchBlocState.search(pagination: pagination, filter: {}); messageSearchBlocState.search(pagination: pagination, filter: testFilter);
await expectLater( await expectLater(
messageSearchBlocState.queryMessagesLoading, messageSearchBlocState.queryMessagesLoading,
@@ -321,7 +323,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -6,6 +6,8 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart'; import 'mocks.dart';
final testFilter = Filter.custom(operator: '\$test', value: 'testValue');
void main() { void main() {
List<GetMessageResponse> _generateMessages({ List<GetMessageResponse> _generateMessages({
int count = 3, int count = 3,
@@ -37,7 +39,7 @@ void main() {
loadingBuilder: (BuildContext context) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(),
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: const {}, filters: testFilter,
); );
await tester.pumpWidget(messageSearchListCore); await tester.pumpWidget(messageSearchListCore);
@@ -58,7 +60,7 @@ void main() {
loadingBuilder: (BuildContext context) => Offstage(), loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object? error) => Offstage(), errorBuilder: (BuildContext context, Object? error) => Offstage(),
filters: {}, filters: testFilter,
); );
final mockClient = MockClient(); final mockClient = MockClient();
@@ -89,7 +91,7 @@ void main() {
emptyBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(), errorBuilder: (BuildContext context, Object error) => Offstage(),
messageSearchListController: controller, messageSearchListController: controller,
filters: {}, filters: testFilter,
); );
expect(controller.loadData, isNull); expect(controller.loadData, isNull);
@@ -125,14 +127,14 @@ void main() {
errorBuilder: (BuildContext context, Object error) => Offstage( errorBuilder: (BuildContext context, Object error) => Offstage(
key: errorWidgetKey, key: errorWidgetKey,
), ),
filters: {}, filters: testFilter,
); );
final mockClient = MockClient(); final mockClient = MockClient();
const error = 'Error! Error! Error!'; const error = 'Error! Error! Error!';
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -153,7 +155,7 @@ void main() {
expect(find.byKey(errorWidgetKey), findsOneWidget); expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -174,14 +176,14 @@ void main() {
loadingBuilder: (BuildContext context) => Offstage(), loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey), emptyBuilder: (BuildContext context) => Offstage(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => Offstage(), errorBuilder: (BuildContext context, Object error) => Offstage(),
filters: {}, filters: testFilter,
); );
final mockClient = MockClient(); final mockClient = MockClient();
final messageResponseList = <GetMessageResponse>[]; final messageResponseList = <GetMessageResponse>[];
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -204,7 +206,7 @@ void main() {
expect(find.byKey(emptyWidgetKey), findsOneWidget); expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -226,14 +228,14 @@ void main() {
loadingBuilder: (BuildContext context) => Offstage(), loadingBuilder: (BuildContext context) => Offstage(),
emptyBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(), errorBuilder: (BuildContext context, Object error) => Offstage(),
filters: {}, filters: testFilter,
); );
final mockClient = MockClient(); final mockClient = MockClient();
final messageResponseList = _generateMessages(); final messageResponseList = _generateMessages();
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -256,7 +258,7 @@ void main() {
expect(find.byKey(childWidgetKey), findsOneWidget); expect(find.byKey(childWidgetKey), findsOneWidget);
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -284,14 +286,14 @@ void main() {
emptyBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(), errorBuilder: (BuildContext context, Object error) => Offstage(),
paginationParams: pagination, paginationParams: pagination,
filters: {}, filters: testFilter,
); );
final mockClient = MockClient(); final mockClient = MockClient();
final messageResponseList = _generateMessages(); final messageResponseList = _generateMessages();
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -325,7 +327,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -341,7 +343,7 @@ void main() {
final paginatedMessageResponseList = _generateMessages(offset: offset); final paginatedMessageResponseList = _generateMessages(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset); final updatedPagination = pagination.copyWith(offset: offset);
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -365,7 +367,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -400,14 +402,14 @@ void main() {
emptyBuilder: (BuildContext context) => Offstage(), emptyBuilder: (BuildContext context) => Offstage(),
errorBuilder: (BuildContext context, Object error) => Offstage(), errorBuilder: (BuildContext context, Object error) => Offstage(),
paginationParams: pagination.copyWith(limit: limit), paginationParams: pagination.copyWith(limit: limit),
filters: {}, filters: testFilter,
); );
final mockClient = MockClient(); final mockClient = MockClient();
final messageResponseList = _generateMessages(); final messageResponseList = _generateMessages();
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -445,7 +447,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -458,7 +460,7 @@ void main() {
final updatedMessageResponseList = _generateMessages(count: limit); final updatedMessageResponseList = _generateMessages(count: limit);
final updatedPagination = pagination.copyWith(limit: limit); final updatedPagination = pagination.copyWith(limit: limit);
when(() => mockClient.search( when(() => mockClient.search(
any(), 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'),
@@ -479,7 +481,7 @@ void main() {
); );
verify(() => mockClient.search( verify(() => mockClient.search(
any(), 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'),
@@ -18,7 +18,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
/// Creates a new channel query dao instance /// Creates a new channel query dao instance
ChannelQueryDao(MoorChatDatabase db) : super(db); ChannelQueryDao(MoorChatDatabase db) : super(db);
String _computeHash(Map<String, dynamic>? filter) { String _computeHash(Filter? filter) {
if (filter == null) { if (filter == null) {
return 'allchannels'; return 'allchannels';
} }
@@ -30,7 +30,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
/// If [clearQueryCache] is true before the insert /// If [clearQueryCache] is true before the insert
/// the list of matching rows will be deleted /// the list of matching rows will be deleted
Future<void> updateChannelQueries( Future<void> updateChannelQueries(
Map<String, dynamic> filter, Filter? filter,
List<String> cids, { List<String> cids, {
bool clearQueryCache = false, bool clearQueryCache = false,
}) async => }) async =>
@@ -58,7 +58,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
}); });
/// ///
Future<List<String>> getCachedChannelCids(Map<String, dynamic>? filter) { Future<List<String>> getCachedChannelCids(Filter? filter) {
final hash = _computeHash(filter); final hash = _computeHash(filter);
return (select(channelQueries)..where((c) => c.queryHash.equals(hash))) return (select(channelQueries)..where((c) => c.queryHash.equals(hash)))
.map((c) => c.channelCid) .map((c) => c.channelCid)
@@ -67,7 +67,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
/// Get list of channels by filter, sort and paginationParams /// Get list of channels by filter, sort and paginationParams
Future<List<ChannelModel>> getChannels({ Future<List<ChannelModel>> getChannels({
Map<String, dynamic>? filter, Filter? filter,
List<SortOption<ChannelModel>>? sort, List<SortOption<ChannelModel>>? sort,
PaginationParams? paginationParams, PaginationParams? paginationParams,
}) async { }) async {
@@ -253,7 +253,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
@override @override
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelState>> getChannelStates({
Map<String, dynamic>? filter, Filter? filter,
List<SortOption<ChannelModel>>? sort, List<SortOption<ChannelModel>>? sort,
PaginationParams? paginationParams, PaginationParams? paginationParams,
}) { }) {
@@ -273,7 +273,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
@override @override
Future<void> updateChannelQueries( Future<void> updateChannelQueries(
Map<String, dynamic> filter, Filter? filter,
List<String> cids, { List<String> cids, {
bool clearQueryCache = false, bool clearQueryCache = false,
}) { }) {
@@ -17,11 +17,7 @@ void main() {
}); });
test('updateChannelQueries', () async { test('updateChannelQueries', () async {
const filter = { final filter = Filter.in_('members', const ['testUserId']);
'members': {
r'$in': ['testUserId'],
},
};
const cids = ['testCid1', 'testCid2', 'testCid3']; const cids = ['testCid1', 'testCid2', 'testCid3'];
@@ -36,11 +32,7 @@ void main() {
}); });
test('clear queryCache before updateChannelQueries', () async { test('clear queryCache before updateChannelQueries', () async {
const filter = { final filter = Filter.in_('members', const ['testUserId']);
'members': {
r'$in': ['testUserId'],
},
};
const cids = ['testCid1', 'testCid2', 'testCid3']; const cids = ['testCid1', 'testCid2', 'testCid3'];
@@ -59,11 +51,7 @@ void main() {
}); });
test('getCachedChannelCids', () async { test('getCachedChannelCids', () async {
const filter = { final filter = Filter.in_('members', const ['testUserId']);
'members': {
r'$in': ['testUserId'],
},
};
const cids = ['testCid1', 'testCid2', 'testCid3']; const cids = ['testCid1', 'testCid2', 'testCid3'];
@@ -78,7 +66,7 @@ void main() {
}); });
Future<List<ChannelModel>> _insertTestDataForGetChannel( Future<List<ChannelModel>> _insertTestDataForGetChannel(
Map<String, Object> filter, { Filter filter, {
int count = 3, int count = 3,
}) async { }) async {
final now = DateTime.now(); final now = DateTime.now();
@@ -110,11 +98,7 @@ void main() {
} }
group('getChannels', () { group('getChannels', () {
const filter = { final filter = Filter.in_('members', const ['testUserId']);
'members': {
r'$in': ['testUserId'],
},
};
test('should return empty list of channels', () async { test('should return empty list of channels', () async {
final channels = await channelQueryDao.getChannels(filter: filter); final channels = await channelQueryDao.getChannels(filter: filter);
@@ -292,7 +292,7 @@ void main() {
}); });
test('updateChannelQueries', () async { test('updateChannelQueries', () async {
const filter = <String, dynamic>{}; final filter = Filter.in_('members', const ['testUserId']);
const cids = <String>[]; const cids = <String>[];
when(() => when(() =>
mockDatabase.channelQueryDao.updateChannelQueries(filter, cids)) mockDatabase.channelQueryDao.updateChannelQueries(filter, cids))