Merge branch 'develop' into overlay-alt

This commit is contained in:
Deven Joshi
2021-09-10 18:14:39 +05:30
committed by GitHub
66 changed files with 2029 additions and 514 deletions
+36 -7
View File
@@ -1,3 +1,25 @@
## Upcoming
🛑️ Breaking Changes from `2.2.1`
- Added 6 new methods in `ChatPersistenceClient`.
- `bulkUpdateMessages`
- `bulkUpdatePinnedMessages`
- `bulkUpdateMembers`
- `bulkUpdateReads`
- `updatePinnedMessageReactions`
- `deletePinnedMessageReactionsByMessageId`
✅ Added
- Added `Filter.contains` and `Filter.empty`
- Added support for `next`, `previous` value pagination in `client.search`
, [read more.](https://getstream.io/chat/docs/other-rest/search/#pagination)
🐞 Fixed
- [[#659]](https://github.com/GetStream/stream-chat-flutter/issues/659) Fixed unread count not updating correctly.
## 2.2.1
🐞 Fixed
@@ -13,11 +35,16 @@
✅ Added
- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same.
- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same.
- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized.
- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a channel has been initialized.
- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key
on `extraData`, so `user.image` and `user.extraData['image']` is the same.
- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`,
so `user.name` and `user.extraData['name']` is the same.
- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a
channel has been initialized.
- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a
channel has been initialized.
- Added slow mode which allows a cooldown period after a user sends a message.
## 2.1.1
🐞 Fixed
@@ -42,7 +69,7 @@
🐞 Fixed
- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working
- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working
- [#575](https://github.com/GetStream/stream-chat-flutter/issues/575): Wrong `OwnUser.*`
## 2.0.0
@@ -70,11 +97,11 @@
🐞 Fixed
- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection
- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet
connection
- several minor fixes
- performance improvements
✅ Added
- New `Location` enum is introduced for easily changing the client location/baseUrl.
@@ -89,6 +116,7 @@
## 2.0.0-nullsafety.8
🐞 Fixed
- Export `PushProvider` enum
## 2.0.0-nullsafety.7
@@ -124,6 +152,7 @@
- Fix thread reply not working with attachments
- Minor fixes
## 2.0.0-nullsafety.5
- Minor fixes
+20 -12
View File
@@ -1366,26 +1366,21 @@ class ClientState {
.where((event) =>
event.me != null && event.type != EventType.healthCheck)
.map((e) => e.me!)
.listen((user) {
currentUser = currentUser?.merge(user) ?? user;
final totalUnreadCount = user.totalUnreadCount;
_totalUnreadCountController.add(totalUnreadCount);
final unreadChannels = user.unreadChannels;
if (unreadChannels != null) {
_unreadChannelsController.add(unreadChannels);
}
}),
.listen((user) => currentUser = currentUser?.merge(user) ?? user),
_client
.on()
.map((event) => event.unreadChannels)
.whereType<int>()
.listen(_unreadChannelsController.add),
.listen((count) {
currentUser = currentUser?.copyWith(unreadChannels: count);
}),
_client
.on()
.map((event) => event.totalUnreadCount)
.whereType<int>()
.listen(_totalUnreadCountController.add),
.listen((count) {
currentUser = currentUser?.copyWith(totalUnreadCount: count);
}),
]);
_listenChannelDeleted();
@@ -1441,6 +1436,7 @@ class ClientState {
/// Sets the user currently interacting with the client
/// note: this fully overrides the [currentUser]
set currentUser(OwnUser? user) {
_computeUnreadCounts(user);
_currentUserController.add(user);
}
@@ -1506,6 +1502,18 @@ class ClientState {
_channelsController.add(newChannels);
}
void _computeUnreadCounts(OwnUser? user) {
final totalUnreadCount = user?.totalUnreadCount;
if (totalUnreadCount != null) {
_totalUnreadCountController.add(totalUnreadCount);
}
final unreadChannels = user?.unreadChannels;
if (unreadChannels != null) {
_unreadChannelsController.add(unreadChannels);
}
}
final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({});
final _currentUserController = BehaviorSubject<OwnUser?>();
final _usersController = BehaviorSubject<Map<String, User>>.seeded({});
@@ -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`');
@@ -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<String, dynamic> 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<Object?> get props => [
limit,
offset,
next,
greaterThan,
greaterThanOrEqual,
lessThan,
@@ -23,6 +23,7 @@ PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> 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<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
}
}
writeNotNull('next', instance.next);
writeNotNull('id_gt', instance.greaterThan);
writeNotNull('id_gte', instance.greaterThanOrEqual);
writeNotNull('id_lt', instance.lessThan);
@@ -253,6 +253,12 @@ class SearchMessagesResponse extends _BaseResponse {
@JsonKey(defaultValue: [])
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
static SearchMessagesResponse fromJson(Map<String, dynamic> json) =>
_$SearchMessagesResponseFromJson(json);
@@ -161,7 +161,9 @@ SearchMessagesResponse _$SearchMessagesResponseFromJson(
..results = (json['results'] as List<dynamic>?)
?.map((e) => GetMessageResponse.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
[]
..next = json['next'] as String?
..previous = json['previous'] as String?;
}
GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson(
@@ -51,6 +51,9 @@ enum FilterOperator {
/// Matches none of the values specified in an array.
nor,
/// Matches any list that contains the specified value
contains,
}
/// Helper extension for [FilterOperator]
@@ -71,6 +74,7 @@ extension FilterOperatorX on FilterOperator {
FilterOperator.and: '\$and',
FilterOperator.or: '\$or',
FilterOperator.nor: '\$nor',
FilterOperator.contains: '\$contains',
}[this]!;
}
@@ -157,6 +161,10 @@ class Filter extends Equatable {
factory Filter.exists(String key, {bool exists = true}) =>
Filter._(operator: FilterOperator.exists, key: key, value: exists);
/// Matches any list that contains the specified values
factory Filter.contains(String key, Object value) =>
Filter._(operator: FilterOperator.contains, key: key, value: value);
/// Creates a custom [Filter] if there isn't one already available.
const factory Filter.custom({
required Object value,
@@ -164,6 +172,9 @@ class Filter extends Equatable {
String? key,
}) = Filter.__;
/// An empty filter
factory Filter.empty() => const Filter.raw(value: {});
/// Creates a custom [Filter] from a raw map value
///
/// ```dart
@@ -17,7 +17,7 @@ class OwnUser extends User {
this.devices = const [],
this.mutes = const [],
this.totalUnreadCount = 0,
this.unreadChannels,
this.unreadChannels = 0,
this.channelMutes = const [],
required String id,
String? role,
@@ -151,8 +151,8 @@ class OwnUser extends User {
final int totalUnreadCount;
/// Total unread channels by the user.
@JsonKey(includeIfNull: false)
final int? unreadChannels;
@JsonKey(includeIfNull: false, defaultValue: 0)
final int unreadChannels;
/// Known top level fields.
///
@@ -17,7 +17,7 @@ OwnUser _$OwnUserFromJson(Map<String, dynamic> json) {
.toList() ??
[],
totalUnreadCount: json['total_unread_count'] as int? ?? 0,
unreadChannels: json['unread_channels'] as int?,
unreadChannels: json['unread_channels'] as int? ?? 0,
channelMutes: (json['channel_mutes'] as List<dynamic>?)
?.map((e) => Mute.fromJson(e as Map<String, dynamic>))
.toList() ??
@@ -143,11 +143,19 @@ abstract class ChatPersistenceClient {
/// Updates the message data of a particular channel [cid] with
/// the new [messages] data
Future<void> updateMessages(String cid, List<Message> messages);
Future<void> updateMessages(String cid, List<Message> messages) =>
bulkUpdateMessages({cid: messages});
/// Bulk updates the message data of multiple channels.
Future<void> bulkUpdateMessages(Map<String, List<Message>> messages);
/// Updates the pinned message data of a particular channel [cid] with
/// the new [messages] data
Future<void> updatePinnedMessages(String cid, List<Message> messages);
Future<void> updatePinnedMessages(String cid, List<Message> messages) =>
bulkUpdatePinnedMessages({cid: messages});
/// Bulk updates the message data of multiple channels.
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>> messages);
/// Returns all the threads by parent message of a particular channel by
/// providing channel [cid]
@@ -158,11 +166,19 @@ abstract class ChatPersistenceClient {
/// Updates all the members of a particular channle [cid]
/// with the new [members] data
Future<void> updateMembers(String cid, List<Member> members);
Future<void> updateMembers(String cid, List<Member> members) =>
bulkUpdateMembers({cid: members});
/// Bulk updates the members data of multiple channels.
Future<void> bulkUpdateMembers(Map<String, List<Member>> members);
/// Updates the read data of a particular channel [cid] with
/// the new [reads] data
Future<void> updateReads(String cid, List<Read> reads);
Future<void> updateReads(String cid, List<Read> reads) =>
bulkUpdateReads({cid: reads});
/// Bulk updates the read data of multiple channels.
Future<void> bulkUpdateReads(Map<String, List<Read>> reads);
/// Updates the users data with the new [users] data
Future<void> updateUsers(List<User> users);
@@ -170,9 +186,15 @@ abstract class ChatPersistenceClient {
/// Updates the reactions data with the new [reactions] data
Future<void> updateReactions(List<Reaction> reactions);
/// Updates the pinned message reactions data with the new [reactions] data
Future<void> updatePinnedMessageReactions(List<Reaction> reactions);
/// Deletes all the reactions by [messageIds]
Future<void> deleteReactionsByMessageId(List<String> messageIds);
/// Deletes all the pinned messages reactions by [messageIds]
Future<void> deletePinnedMessageReactionsByMessageId(List<String> messageIds);
/// Deletes all the members by channel [cids]
Future<void> deleteMembersByCids(List<String> cids);
@@ -182,85 +204,92 @@ abstract class ChatPersistenceClient {
/// Update list of channel states
Future<void> updateChannelStates(List<ChannelState> channelStates) async {
final deleteReactions = deleteReactionsByMessageId(channelStates
.expand((it) => it.messages)
.map((m) => m.id)
.toList(growable: false));
final reactionsToDelete = <String>[];
final pinnedReactionsToDelete = <String>[];
final membersToDelete = <String>[];
final cleanedChannelStates =
channelStates.where((it) => it.channel != null);
final channels = <ChannelModel>[];
final channelWithMessages = <String, List<Message>>{};
final channelWithPinnedMessages = <String, List<Message>>{};
final channelWithReads = <String, List<Read>>{};
final channelWithMembers = <String, List<Member>>{};
final deleteMembers = deleteMembersByCids(
cleanedChannelStates.map((it) => it.channel!.cid).toList(growable: false),
);
final users = <User>[];
final reactions = <Reaction>[];
final pinnedReactions = <Reaction>[];
for (final state in channelStates) {
final channel = state.channel;
if (channel != null) {
channels.add(channel);
final cid = channel.cid;
final reads = state.read;
final members = state.members;
final messages = state.messages;
final pinnedMessages = state.pinnedMessages;
// Preparing deletion data
membersToDelete.add(cid);
reactionsToDelete.addAll(state.messages.map((it) => it.id));
pinnedReactionsToDelete.addAll(state.pinnedMessages.map((it) => it.id));
// preparing addition data
channelWithReads[cid] = reads;
channelWithMembers[cid] = members;
channelWithMessages[cid] = messages;
channelWithPinnedMessages[cid] = pinnedMessages;
List<Reaction> expandReactions(Message message) {
final own = message.ownReactions;
final latest = message.latestReactions;
return [
if (own != null) ...own.where((r) => r.userId != null),
if (latest != null) ...latest.where((r) => r.userId != null),
];
}
reactions.addAll(messages.expand(expandReactions));
pinnedReactions.addAll(pinnedMessages.expand(expandReactions));
users.addAll([
channel.createdBy,
...reads.map((it) => it.user),
...members.map((it) => it.user),
...reactions.map((it) => it.user),
...pinnedReactions.map((it) => it.user),
].withNullifyer);
}
}
// Removing old members and reactions data as they may have
// changes over the time.
await Future.wait([
deleteReactions,
deleteMembers,
deleteMembersByCids(membersToDelete),
deleteReactionsByMessageId(reactionsToDelete),
deletePinnedMessageReactionsByMessageId(pinnedReactionsToDelete),
]);
final channels = cleanedChannelStates.map((it) => it.channel).withNullifyer;
final reactions = cleanedChannelStates
.expand((it) => it.messages)
.expand((it) => [
if (it.ownReactions != null)
...it.ownReactions!.where((r) => r.userId != null),
if (it.latestReactions != null)
...it.latestReactions!.where((r) => r.userId != null),
])
.withNullifyer;
final users = cleanedChannelStates
.map((cs) => [
cs.channel?.createdBy,
...cs.messages
.map((m) => [
m.user,
if (m.latestReactions != null)
...m.latestReactions!.map((r) => r.user),
if (m.ownReactions != null)
...m.ownReactions!.map((r) => r.user),
])
.expand((v) => v),
...cs.read.map((r) => r.user),
...cs.members.map((m) => m.user),
])
.expand((it) => it)
.withNullifyer;
final updateMessagesFuture = cleanedChannelStates.map((it) {
final cid = it.channel!.cid;
final messages = it.messages;
return updateMessages(cid, messages.toList(growable: false));
}).toList(growable: false);
final updatePinnedMessagesFuture = cleanedChannelStates.map((it) {
final cid = it.channel!.cid;
final messages = it.pinnedMessages;
return updatePinnedMessages(cid, messages.toList(growable: false));
}).toList(growable: false);
final updateReadsFuture = cleanedChannelStates.map((it) {
final cid = it.channel!.cid;
final reads = it.read;
return updateReads(cid, reads.toList(growable: false));
}).toList(growable: false);
final updateMembersFuture = cleanedChannelStates.map((it) {
final cid = it.channel!.cid;
final members = it.members;
return updateMembers(cid, members.toList(growable: false));
}).toList(growable: false);
// Updating first as does not depend on any other table.
await Future.wait([
...updateMessagesFuture,
...updatePinnedMessagesFuture,
...updateReadsFuture,
...updateMembersFuture,
updateUsers(users.toList(growable: false)),
updateChannels(channels.toList(growable: false)),
]);
// All has a foreign key relation with channels table.
await Future.wait([
bulkUpdateReads(channelWithReads),
bulkUpdateMembers(channelWithMembers),
bulkUpdateMessages(channelWithMessages),
bulkUpdatePinnedMessages(channelWithPinnedMessages),
]);
// Both has a foreign key relation with messages, pinnedMessages table.
await Future.wait([
updateReactions(reactions.toList(growable: false)),
updatePinnedMessageReactions(
pinnedReactions.toList(growable: false),
),
]);
}
}
@@ -2313,5 +2313,27 @@ void main() {
)).called(1);
verifyNoMoreInteractions(api.message);
});
test(
'''setting the `currentUser` should also compute and update the unreadCounts''',
() {
final state = client.state;
final initialUser = OwnUser.fromUser(user);
expect(state.currentUser, initialUser);
expect(state.totalUnreadCount, 0);
expect(state.unreadChannels, 0);
final updateUser = initialUser.copyWith(
totalUnreadCount: 33,
unreadChannels: 33,
);
state.currentUser = updateUser;
expect(state.currentUser, updateUser);
expect(state.totalUnreadCount, 33);
expect(state.unreadChannels, 33);
},
);
});
}
@@ -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 {
final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']);
const query = 'test-query';
@@ -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<AssertionError>());
}
},
);
});
});
}
@@ -138,6 +138,20 @@ void main() {
expect(filter.value, value);
});
test('empty', () {
final filter = Filter.empty();
expect(filter.value, {});
});
test('contains', () {
const key = 'testKey';
const values = 'testValue';
final filter = Filter.contains(key, values);
expect(filter.key, key);
expect(filter.value, values);
expect(filter.operator, FilterOperator.contains.rawValue);
});
group('groupedOperator', () {
final filter1 = Filter.equal('testKey', 'testValue');
final filter2 = Filter.in_('testKey', const ['testValue']);
@@ -38,6 +38,11 @@ class TestPersistenceClient extends ChatPersistenceClient {
Future<void> deleteReactionsByMessageId(List<String> messageIds) =>
Future.value();
@override
Future<void> deletePinnedMessageReactionsByMessageId(
List<String> messageIds) =>
Future.value();
@override
Future<void> disconnect({bool flush = false}) => throw UnimplementedError();
@@ -101,26 +106,30 @@ class TestPersistenceClient extends ChatPersistenceClient {
Future<void> updateLastSyncAt(DateTime lastSyncAt) =>
throw UnimplementedError();
@override
Future<void> updateMembers(String cid, List<Member> members) =>
Future.value();
@override
Future<void> updateMessages(String cid, List<Message> messages) =>
Future.value();
@override
Future<void> updatePinnedMessages(String cid, List<Message> messages) =>
Future.value();
@override
Future<void> updateReactions(List<Reaction> reactions) => Future.value();
@override
Future<void> updateReads(String cid, List<Read> reads) => Future.value();
Future<void> updatePinnedMessageReactions(List<Reaction> reactions) =>
Future.value();
@override
Future<void> updateUsers(List<User> users) => Future.value();
@override
Future<void> bulkUpdateMembers(Map<String, List<Member>> members) =>
Future.value();
@override
Future<void> bulkUpdateMessages(Map<String, List<Message>> messages) =>
Future.value();
@override
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>> messages) =>
Future.value();
@override
Future<void> bulkUpdateReads(Map<String, List<Read>> reads) => Future.value();
}
void main() {
+52 -48
View File
@@ -1,5 +1,22 @@
## 2.2.1
🛑️ Breaking Changes from `2.2.1`
- `MessageSearchListView` paginationParams property is now non-nullable with a default value.
```dart
paginationParams = const PaginationParams(limit: 30)
```
- `UserListView` pagination property is now non-nullable with a default value.
```dart
pagination = const PaginationParams(limit: 30)
```
🐞 Fixed
- Fixed `MessageSearchListView` pagination.
## 2.2.1
- Updated `stream_chat_flutter_core` dependency to 2.2.1
## 2.2.0
@@ -7,14 +24,13 @@
✅ Added
- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516):
Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image
is loading
Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image is loading
- Added a `backgroundColor` property to the following widgets:
- `ChannelHeader`
- `ChannelListHeader`
- `GalleryHeader`
- `GalleryFooter`
- `ThreadHeader`
- `ChannelHeader`
- `ChannelListHeader`
- `GalleryHeader`
- `GalleryFooter`
- `ThreadHeader`
- Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message.
- Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded.
This will override the default error alert behaviour.
@@ -34,9 +50,8 @@ You can call `.copyWith` to customize just a subset of properties.
🔄 Changed
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with
them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming
breakdown:
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with them, and have been
upgraded with some goodies like `lerp` functions. Here's the full naming breakdown:
* `AvatarTheme` is now `AvatarThemeData`
* `ChannelHeaderTheme` is now `ChannelHeaderThemeData`
@@ -53,19 +68,19 @@ breakdown:
🐞 Fixed
- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the
camera is null.
- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null.
- Fixed date dividers position/alignment in non reversed `MessageListView`.
- Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set.
- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text.
- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when
sending a message with no text.
- MessageInput can now be placed in any position on the screen.
## 2.1.2
🐞 Fixed
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no
members when sending message
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending
message
## 2.1.1
@@ -83,8 +98,7 @@ breakdown:
🔄 Changed
- `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`.
- `StreamChat.of(context).userStream` is now deprecated in favor
of `StreamChat.of(context).currentUserStream`.
- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`.
🐞 Fixed
@@ -137,8 +151,7 @@ You can call `.copyWith` to customize just a subset of properties
- Added video compress options (frame and quality) to `MessageInput`
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView
header/footer
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
- `MessageWidget` accepts a `userAvatarBuilder`
- Added pinMessage ui support
- Added `MessageListView.threadSeparatorBuilder` property
@@ -147,12 +160,10 @@ You can call `.copyWith` to customize just a subset of properties
🐞 Fixed
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text
box when editing message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator
use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
a reload
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
- `MessageListView` not rendering if the user is not a member of the channel
- Fix `MessageInput` overflow when there are no actions
- Minor fixes and improvements
@@ -205,18 +216,15 @@ You can call `.copyWith` to customize just a subset of properties.
✅ Added
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView
header/footer
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
- `MessageWidget` accepts a `userAvatarBuilder`
🐞 Fixed
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text
box when editing message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator
use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
a reload
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
- `MessageListView` not rendering if the user is not a member of the channel
## 2.0.0-nullsafety.7
@@ -286,8 +294,7 @@ You can call `.copyWith` to customize just a subset of properties.
- Show error messages as system and keep them in the message input
- Remove notification badge logic
- Use shimmer while loading images
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated
to `MessageInput`
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput`
- Add possibility to specify custom message actions using `MessageWidget.customActions`
- Added `MessageListView.onAttachmentTap` callback
- Fixed message newline issue
@@ -344,8 +351,7 @@ You can call `.copyWith` to customize just a subset of properties.
- Improved api documentation
- Updated `stream_chat` dependency to `^1.0.0-beta`
- Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples)
- Reimplemented existing widgets
using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
## 0.2.21
@@ -362,8 +368,8 @@ You can call `.copyWith` to customize just a subset of properties.
## 0.2.20+2
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the
list when a new message arrives
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message
arrives
## 0.2.20+1
@@ -397,8 +403,7 @@ You can call `.copyWith` to customize just a subset of properties.
## 0.2.16
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress
implementation
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation
- Make public autofocus field of the TextField of message_input
## 0.2.15
@@ -583,11 +588,10 @@ You can call `.copyWith` to customize just a subset of properties.
## 0.2.1-alpha+1
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have
the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to
your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in
the usual way, but you need to add a `StreamChat` widget to every route of your app.
Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget
as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of
your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to
every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
information.
```dart
@@ -689,8 +693,8 @@ Widget build(BuildContext context) {
- Add gesture (vertical drag down) to close the keyboard
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will
even close the keyboard)
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the
keyboard)
The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261
@@ -58,7 +58,7 @@ class MessageSearchListView extends StatefulWidget {
required this.filters,
this.messageQuery,
this.sortOptions,
this.paginationParams,
this.paginationParams = const PaginationParams(limit: 30),
this.messageFilters,
this.separatorBuilder,
this.itemBuilder,
@@ -93,7 +93,7 @@ class MessageSearchListView extends StatefulWidget {
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams? paginationParams;
final PaginationParams paginationParams;
/// The message query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
@@ -51,7 +51,7 @@ class UserListView extends StatefulWidget {
this.filter,
this.sort,
this.presence,
this.pagination,
this.pagination = const PaginationParams(limit: 30),
this.onUserTap,
this.onUserLongPress,
this.userWidget,
@@ -93,7 +93,7 @@ class UserListView extends StatefulWidget {
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams? pagination;
final PaginationParams pagination;
/// Function called when tapping on a channel
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
@@ -69,6 +69,7 @@ void main() {
body: MessageSearchBloc(
child: MessageSearchListView(
filters: Filter.in_('members', const ['test_id']),
messageQuery: 'test query',
),
),
);
@@ -100,6 +101,7 @@ void main() {
body: MessageSearchBloc(
child: MessageSearchListView(
filters: Filter.in_('members', const ['test_id']),
messageQuery: 'test query',
),
),
);
+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
- Updated `stream_chat` dependency to 2.2.1
@@ -5,13 +22,17 @@
## 2.2.0
🛑️ Breaking Changes from `2.1.1`
- Renamed `BetterStreamBuilder.loadingBuilder` to `.noDataBuilder`
🔄 Changed
- `BetterStreamBuilder.initialData` is now nullable/not-required.
🐞 Fixed
- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh
- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after
refresh
## 2.1.1
@@ -20,12 +41,15 @@
## 2.1.0
🛑️ Breaking Changes from `2.0.0`
- Changed default message filter of `MessageListCore`
✅ Added
- Added `MessageListCore.paginationLimit`
🔄 Changed
- `StreamChatCore.of(context).user` is now deprecated in favor of `StreamChatCore.of(context).currentUser`.
- `StreamChatCore.of(context).userStream` is now deprecated in favor of `StreamChatCore.of(context).currentUserStream`.
@@ -34,7 +58,8 @@
🛑️ Breaking Changes from `1.5.3`
- migrate this package to null safety
- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties
- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual
params/properties
- `options.state` -> bool state
- `options.watch` -> bool watch
- `options.presence` -> bool presence
@@ -51,6 +76,7 @@
- Performance improvements
## 2.0.0-nullsafety.9
- Update llc dependency
## 2.0.0-nullsafety.8
@@ -105,7 +105,8 @@ class ChannelsBlocState extends State<ChannelsBloc>
}) async {
final client = _streamChatCoreState!.client;
final clear = paginationParams.offset == 0;
final offset = paginationParams.offset;
final clear = offset == null || offset == 0;
if (clear && _paginationEnded) {
_paginationEnded = false;
}
@@ -43,6 +43,12 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
with AutomaticKeepAliveClientMixin {
late StreamChatCoreState _streamChatCoreState;
/// The key used to paginate next items.
String? nextId;
/// The key used to paginate previous items.
String? previousId;
/// The current messages list
List<GetMessageResponse>? get messageResponses =>
_messageResponses.valueOrNull;
@@ -59,6 +65,8 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
Stream<bool> get queryMessagesLoading =>
_queryMessagesLoadingController.stream;
bool _paginationEnded = false;
/// Calls [StreamChatClient.search] updating
/// [messagesStream] and [queryMessagesLoading] stream
Future<void> search({
@@ -66,21 +74,34 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
Filter? messageFilter,
List<SortOption>? sort,
String? query,
PaginationParams? pagination,
PaginationParams pagination = const PaginationParams(limit: 30),
}) async {
final client = _streamChatCoreState.client;
if (_queryMessagesLoadingController.value == true) return;
var clear = false;
if (sort != null) {
clear |= pagination.next == null;
} else {
final offset = pagination.offset;
clear |= offset == null || offset == 0;
}
if (clear && _paginationEnded) {
_paginationEnded = false;
}
if ((!clear && _paginationEnded) ||
_queryMessagesLoadingController.value == true) {
return;
}
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.add(true);
}
try {
final clear = pagination == null || pagination.offset == 0;
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
final messages = await client.search(
final response = await client.search(
filter,
sort: sort,
query: query,
@@ -88,15 +109,29 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
messageFilters: messageFilter,
);
final next = response.next;
final previous = response.previous;
nextId = next != null && next.isNotEmpty
? next
: /*reset nextId if we get nothing*/ null;
previousId = previous != null && previous.isNotEmpty
? previous
: /*reset previousId if we get nothing*/ null;
final newMessages = response.results;
if (clear) {
_messageResponses.add(messages.results);
_messageResponses.add(newMessages);
} else {
final temp = oldMessages + messages.results;
final temp = oldMessages + newMessages;
_messageResponses.add(temp);
}
if (_messageResponses.hasValue && _queryMessagesLoadingController.value) {
_queryMessagesLoadingController.add(false);
}
if (newMessages.isEmpty || newMessages.length < pagination.limit) {
_paginationEnded = true;
}
} catch (e, stk) {
// reset loading controller
_queryMessagesLoadingController.add(false);
@@ -47,10 +47,18 @@ class MessageSearchListCore extends StatefulWidget {
required this.filters,
this.messageQuery,
this.sortOptions,
this.paginationParams,
this.paginationParams = const PaginationParams(limit: 30),
this.messageFilters,
this.messageSearchListController,
}) : super(key: key);
}) : assert(
messageQuery != null || messageFilters != null,
'Provide at least `query` or `messageFilters`',
),
assert(
messageQuery == null || messageFilters == null,
"Can't provide both `query` and `messageFilters` at the same time",
),
super(key: key);
/// A [MessageSearchListController] allows reloading and pagination.
/// Use [MessageSearchListController.loadData] and
@@ -74,10 +82,9 @@ class MessageSearchListCore extends StatefulWidget {
final List<SortOption>? sortOptions;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// limit: the number of messages to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams? paginationParams;
final PaginationParams paginationParams;
/// The message query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
@@ -155,15 +162,25 @@ class MessageSearchListCoreState extends State<MessageSearchListCore> {
);
/// Fetches more messages with updated pagination and updates the widget
Future<void> 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<void> paginateData() {
PaginationParams pagination;
if (widget.sortOptions != null) {
pagination = widget.paginationParams.copyWith(
next: _messageSearchBloc?.nextId,
);
} else {
pagination = widget.paginationParams.copyWith(
offset: _messageSearchBloc?.messageResponses?.length,
);
}
return _messageSearchBloc!.search(
filter: widget.filters,
sort: widget.sortOptions,
pagination: pagination,
query: widget.messageQuery,
messageFilter: widget.messageFilters,
);
}
@override
void didUpdateWidget(MessageSearchListCore oldWidget) {
@@ -173,8 +190,8 @@ class MessageSearchListCoreState extends State<MessageSearchListCore> {
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() ||
widget.messageFilters?.toString() !=
oldWidget.messageFilters?.toString() ||
widget.paginationParams?.toJson().toString() !=
oldWidget.paginationParams?.toJson().toString()) {
widget.paginationParams.toJson().toString() !=
oldWidget.paginationParams.toJson().toString()) {
loadData();
}
@@ -66,7 +66,7 @@ class UserListCore extends StatefulWidget {
this.filter,
this.sort,
this.presence,
this.pagination,
this.pagination = const PaginationParams(limit: 30),
this.groupAlphabetically = false,
this.userListController,
}) : super(key: key);
@@ -106,7 +106,7 @@ class UserListCore extends StatefulWidget {
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams? pagination;
final PaginationParams pagination;
/// Set it to true to group users by their first character
///
@@ -201,7 +201,7 @@ class UserListCoreState extends State<UserListCore>
filter: widget.filter,
sort: widget.sort,
presence: widget.presence,
pagination: widget.pagination!.copyWith(
pagination: widget.pagination.copyWith(
offset: _usersBloc!.users?.length ?? 0,
),
);
@@ -212,8 +212,8 @@ class UserListCoreState extends State<UserListCore>
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.presence != oldWidget.presence ||
widget.pagination?.toJson().toString() !=
oldWidget.pagination?.toJson().toString()) {
widget.pagination.toJson().toString() !=
oldWidget.pagination.toJson().toString()) {
loadData();
}
@@ -57,6 +57,8 @@ class UsersBlocState extends State<UsersBloc>
late StreamChatCoreState _streamChatCore;
bool _paginationEnded = false;
/// The Query Users method allows you to search for users and see if they are
/// online/offline.
/// [API Reference](https://getstream.io/chat/docs/flutter-dart/query_users/?language=dart)
@@ -64,19 +66,27 @@ class UsersBlocState extends State<UsersBloc>
Filter? filter,
List<SortOption>? sort,
bool? presence,
PaginationParams? pagination,
PaginationParams pagination = const PaginationParams(limit: 30),
}) async {
final client = _streamChatCore.client;
if (_queryUsersLoadingController.value == true) return;
final offset = pagination.offset;
final clear = offset == null || offset == 0;
if (clear && _paginationEnded) {
_paginationEnded = false;
}
if ((!clear && _paginationEnded) ||
_queryUsersLoadingController.value == true) {
return;
}
if (_usersController.hasValue) {
_queryUsersLoadingController.add(true);
}
try {
final clear = pagination == null || pagination.offset == 0;
final oldUsers = List<User>.from(users ?? []);
final usersResponse = await client.queryUsers(
@@ -86,6 +96,7 @@ class UsersBlocState extends State<UsersBloc>
pagination: pagination,
);
final newUsers = usersResponse.users;
if (clear) {
_usersController.add(usersResponse.users);
} else {
@@ -95,6 +106,9 @@ class UsersBlocState extends State<UsersBloc>
if (_usersController.hasValue && _queryUsersLoadingController.value) {
_queryUsersLoadingController.add(false);
}
if (newUsers.isEmpty || newUsers.length < pagination.limit) {
_paginationEnded = true;
}
} catch (e, stk) {
// reset loading controller
_queryUsersLoadingController.add(false);
@@ -31,8 +31,7 @@ void main() {
);
testWidgets(
'messageSearchBlocState.search() should throw if used where '
'StreamChat is not present in the widget tree',
'''messageSearchBlocState.search() should throw if used where StreamChat is not present in the widget tree''',
(tester) async {
const messageSearchBloc = MessageSearchBloc(
child: Offstage(),
@@ -74,7 +73,10 @@ void main() {
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(filter: testFilter);
@@ -95,8 +97,7 @@ void main() {
);
testWidgets(
'messageSearchBlocState.messagesStream should emit error '
'if client.search() throws',
'''messageSearchBlocState.messagesStream should emit error if client.search() throws''',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
@@ -144,9 +145,7 @@ void main() {
);
testWidgets(
'calling messageSearchBlocState.search() again with an offset '
'should emit new data through messagesStream and also emit loading state '
'through queryMessagesLoading',
'''calling messageSearchBlocState.search() again with an offset should emit new data through messagesStream and also emit loading state through queryMessagesLoading''',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
@@ -168,19 +167,23 @@ void main() {
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
const pagination = PaginationParams(limit: 25);
final messageResponseList = _generateMessages(count: 25);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(filter: testFilter);
messageSearchBlocState.search(pagination: pagination, filter: testFilter);
await expectLater(
messageSearchBlocState.messagesStream,
@@ -192,22 +195,24 @@ void main() {
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
paginationParams: pagination,
)).called(1);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final pagination = PaginationParams(offset: offset);
final newPagination = pagination.copyWith(offset: offset);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
paginationParams: newPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = paginatedMessageResponseList,
(_) async => SearchMessagesResponse()
..results = paginatedMessageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(pagination: pagination, filter: testFilter);
@@ -236,9 +241,7 @@ void main() {
);
testWidgets(
'calling messageSearchBlocState.search() again with an offset '
'should emit error through queryUsersLoading if '
'client.search() throws',
'''calling messageSearchBlocState.search() again with an offset should emit error through queryUsersLoading if client.search() throws''',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
@@ -260,19 +263,23 @@ void main() {
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
const pagination = PaginationParams(limit: 25);
final messageResponseList = _generateMessages(count: 25);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(filter: testFilter);
messageSearchBlocState.search(pagination: pagination, filter: testFilter);
await expectLater(
messageSearchBlocState.messagesStream,
@@ -284,11 +291,11 @@ void main() {
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
paginationParams: pagination,
)).called(1);
final offset = messageResponseList.length;
final pagination = PaginationParams(offset: offset);
final newPagination = pagination.copyWith(offset: offset);
const error = 'Error! Error! Error!';
when(() => mockClient.search(
@@ -296,10 +303,13 @@ void main() {
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
paginationParams: newPagination,
)).thenThrow(error);
messageSearchBlocState.search(pagination: pagination, filter: testFilter);
messageSearchBlocState.search(
pagination: newPagination,
filter: testFilter,
);
await expectLater(
messageSearchBlocState.queryMessagesLoading,
@@ -311,8 +321,80 @@ void main() {
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
paginationParams: newPagination,
)).called(1);
},
);
testWidgets(
'''calling messageSearchBlocState.search() again with an offset should do nothing and return if pagination is completed''',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
const messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<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';
const testFilter = Filter.custom(operator: '\$test', value: 'testValue');
const testMessageFilter = Filter.custom(operator: '\$test', value: 'testValue');
void main() {
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(
'should throw if MessageSearchListCore is used where MessageSearchBloc '
'is not present in the widget tree',
@@ -40,6 +75,7 @@ void main() {
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object? error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
);
await tester.pumpWidget(messageSearchListCore);
@@ -61,6 +97,7 @@ void main() {
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object? error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
@@ -92,6 +129,7 @@ void main() {
errorBuilder: (BuildContext context, Object error) => const Offstage(),
messageSearchListController: controller,
filters: testFilter,
messageFilters: testMessageFilter,
);
expect(controller.loadData, isNull);
@@ -129,6 +167,7 @@ void main() {
key: errorWidgetKey,
),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
@@ -138,7 +177,7 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).thenThrow(error);
@@ -159,7 +198,7 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).called(1);
},
@@ -179,6 +218,7 @@ void main() {
const Offstage(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
@@ -188,10 +228,13 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
await tester.pumpWidget(
@@ -211,7 +254,7 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).called(1);
},
@@ -231,6 +274,7 @@ void main() {
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
@@ -240,10 +284,13 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
await tester.pumpWidget(
@@ -263,7 +310,7 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).called(1);
},
@@ -275,7 +322,7 @@ void main() {
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
const pagination = PaginationParams();
const pagination = PaginationParams(limit: 25);
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
@@ -289,19 +336,23 @@ void main() {
errorBuilder: (BuildContext context, Object error) => const Offstage(),
paginationParams: pagination,
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
final messageResponseList = _generateMessages(count: 25);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
await tester.pumpWidget(
@@ -332,7 +383,7 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: pagination,
)).called(1);
@@ -348,11 +399,13 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: updatedPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = paginatedMessageResponseList,
(_) async => SearchMessagesResponse()
..results = paginatedMessageResponseList
..next = null
..previous = null,
);
await messageSearchListCoreState.paginateData();
@@ -372,7 +425,7 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: updatedPagination,
)).called(1);
},
@@ -406,6 +459,7 @@ void main() {
const Offstage(),
paginationParams: pagination.copyWith(limit: limit),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
@@ -415,10 +469,13 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()..results = messageResponseList,
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
await tester.pumpWidget(
@@ -453,7 +510,7 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: pagination,
)).called(1);
@@ -466,11 +523,13 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: updatedPagination,
)).thenAnswer(
(_) async =>
SearchMessagesResponse()..results = updatedMessageResponseList,
(_) async => SearchMessagesResponse()
..results = updatedMessageResponseList
..next = null
..previous = null,
);
await tester.pumpAndSettle();
@@ -487,7 +546,7 @@ void main() {
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
messageFilters: testMessageFilter,
paginationParams: updatedPagination,
)).called(1);
},
@@ -321,7 +321,7 @@ void main() {
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
const pagination = PaginationParams();
const pagination = PaginationParams(limit: 15);
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
@@ -347,7 +347,7 @@ void main() {
final mockClient = MockClient();
final users = _generateUsers();
final users = _generateUsers(count: 15);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
@@ -164,16 +164,17 @@ void main() {
find.byKey(usersBlocKey),
);
final users = _generateUsers();
const pagination = PaginationParams(limit: 25);
final users = _generateUsers(count: 25);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
pagination: pagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
usersBlocState.queryUsers(pagination: pagination);
await expectLater(
usersBlocState.usersStream,
@@ -184,23 +185,23 @@ void main() {
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
pagination: pagination,
)).called(1);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final pagination = PaginationParams(offset: offset);
final newPagination = pagination.copyWith(offset: offset);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
))
.thenAnswer(
(_) async => QueryUsersResponse()..users = paginatedUsers);
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: newPagination,
)).thenAnswer(
(_) async => QueryUsersResponse()..users = paginatedUsers,
);
usersBlocState.queryUsers(pagination: pagination);
usersBlocState.queryUsers(pagination: newPagination);
await Future.wait([
expectLater(
@@ -217,7 +218,7 @@ void main() {
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
pagination: newPagination,
)).called(1);
},
);
@@ -247,13 +248,89 @@ void main() {
find.byKey(usersBlocKey),
);
final users = _generateUsers();
const pagination = PaginationParams(limit: 25);
final users = _generateUsers(count: 25);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
pagination: pagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers(pagination: pagination);
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
)).called(1);
final offset = users.length;
final newPagination = pagination.copyWith(offset: offset);
const error = 'Error! Error! Error!';
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: newPagination,
)).thenThrow(error);
usersBlocState.queryUsers(pagination: newPagination);
await expectLater(
usersBlocState.queryUsersLoading,
emitsError(error),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: newPagination,
)).called(1);
},
);
testWidgets(
'''calling usersBlocState.queryUsers() again with an offset should do nothing and return if pagination is completed''',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
const usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<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);
usersBlocState.queryUsers();
@@ -271,30 +348,16 @@ void main() {
)).called(1);
final offset = users.length;
final pagination = PaginationParams(offset: offset);
final newPagination = pagination.copyWith(offset: offset);
const error = 'Error! Error! Error!';
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
)).thenThrow(error);
usersBlocState.queryUsers(pagination: pagination);
usersBlocState.queryUsers(pagination: newPagination);
// should emit nothing.
await expectLater(
usersBlocState.queryUsersLoading,
emitsError(error),
// skipping the initial data (behaviorSubject).
usersBlocState.usersStream,
emitsInOrder([]),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
)).called(1);
},
);
}
@@ -1,3 +1,10 @@
## Upcoming
- [[#604]](https://github.com/GetStream/stream-chat-flutter/issues/604) Fix cascade deletion by
enabling `pragma foreign_keys`.
- Added a new table `PinnedMessageReactions` and dao `PinnedMessageReactionDao` specifically for pinned messages.
- Updated `stream_chat`: `^2.2.0` -> `TODO`
## 2.2.0
- Updated llc dependency
@@ -11,14 +18,17 @@
## 2.1.0
✅ Added
- Added support for `Message.i18n`
- Added support for `User.language`
## 2.0.0
* Migrate this package to null safety
* Minor fixes and improvements
## 2.0.0-nullsafety.8
* Updated llc dependency
* Upgraded moor dependencies and generated files with the latest dependency
@@ -42,10 +42,9 @@ class ChannelDao extends DatabaseAccessor<MoorChatDatabase>
/// Updates all the channels using the new [channelList] data
Future<void> updateChannels(List<ChannelModel> channelList) => batch(
(it) => it.insertAll(
(it) => it.insertAllOnConflictUpdate(
channels,
channelList.map((c) => c.toEntity()).toList(),
mode: InsertMode.insertOrReplace,
),
);
}
@@ -45,13 +45,12 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
}
await batch((it) {
it.insertAll(
it.insertAllOnConflictUpdate(
channelQueries,
cids
.map((cid) =>
ChannelQueryEntity(queryHash: hash, channelCid: cid))
.toList(),
mode: InsertMode.insertOrReplace,
);
});
});
@@ -113,8 +112,9 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
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) {
@@ -26,7 +26,7 @@ class ConnectionEventDao extends DatabaseAccessor<MoorChatDatabase>
/// Update stored connection event with latest data
Future<int> updateConnectionEvent(Event event) => transaction(() async {
final connectionInfo = await select(connectionEvents).getSingleOrNull();
return into(connectionEvents).insert(
return into(connectionEvents).insertOnConflictUpdate(
ConnectionEventEntity(
id: 1,
type: event.type,
@@ -38,7 +38,6 @@ class ConnectionEventDao extends DatabaseAccessor<MoorChatDatabase>
unreadChannels:
event.unreadChannels ?? connectionInfo?.unreadChannels,
),
mode: InsertMode.insertOrReplace,
);
});
@@ -4,6 +4,7 @@ export 'connection_event_dao.dart';
export 'member_dao.dart';
export 'message_dao.dart';
export 'pinned_message_dao.dart';
export 'pinned_message_reaction_dao.dart';
export 'reaction_dao.dart';
export 'read_dao.dart';
export 'user_dao.dart';
@@ -30,14 +30,19 @@ class MemberDao extends DatabaseAccessor<MoorChatDatabase>
}).get();
/// Updates all the members using the new [memberList] data
Future<void> updateMembers(String cid, List<Member> memberList) async =>
batch(
(it) => it.insertAll(
members,
memberList.map((m) => m.toEntity(cid: cid)).toList(),
mode: InsertMode.insertOrReplace,
),
);
Future<void> updateMembers(String cid, List<Member> memberList) =>
bulkUpdateMembers({cid: memberList});
/// Bulk updates the members data of multiple channels
Future<void> bulkUpdateMembers(Map<String, List<Member>> channelWithMembers) {
final entities = channelWithMembers.entries
.map((entry) => entry.value.map(
(member) => member.toEntity(cid: entry.key),
))
.expand((it) => it)
.toList(growable: false);
return batch((batch) => batch.insertAllOnConflictUpdate(members, entities));
}
/// Deletes all the members whose [Members.channelCid] is present in [cids]
Future<void> deleteMemberByCids(List<String> cids) async => batch((it) {
@@ -117,8 +117,9 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
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;
@@ -169,13 +170,21 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
/// Updates the message data of a particular channel with
/// the new [messageList] data
Future<void> updateMessages(String cid, List<Message> messageList) => batch(
(batch) {
batch.insertAll(
messages,
messageList.map((it) => it.toEntity(cid: cid)).toList(),
mode: InsertMode.insertOrReplace,
);
},
);
Future<void> updateMessages(String cid, List<Message> messageList) =>
bulkUpdateMessages({cid: messageList});
/// Bulk updates the message data of multiple channels
Future<void> bulkUpdateMessages(
Map<String, List<Message>> channelWithMessages,
) {
final entities = channelWithMessages.entries
.map((entry) => entry.value.map(
(message) => message.toEntity(cid: entry.key),
))
.expand((it) => it)
.toList(growable: false);
return batch(
(batch) => batch.insertAllOnConflictUpdate(messages, entities),
);
}
}
@@ -39,8 +39,10 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
final userEntity = rows.readTableOrNull(users);
final pinnedByEntity = rows.readTableOrNull(_pinnedByUsers);
final msgEntity = rows.readTable(pinnedMessages);
final latestReactions = await _db.reactionDao.getReactions(msgEntity.id);
final ownReactions = await _db.reactionDao.getReactionsByUserId(
final latestReactions =
await _db.pinnedMessageReactionDao.getReactions(msgEntity.id);
final ownReactions =
await _db.pinnedMessageReactionDao.getReactionsByUserId(
msgEntity.id,
_db.userId,
);
@@ -168,13 +170,21 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
/// Updates the message data of a particular channel with
/// the new [messageList] data
Future<void> updateMessages(String cid, List<Message> messageList) => batch(
(batch) {
batch.insertAll(
pinnedMessages,
messageList.map((it) => it.toPinnedEntity(cid: cid)).toList(),
mode: InsertMode.insertOrReplace,
);
},
);
Future<void> updateMessages(String cid, List<Message> messageList) =>
bulkUpdateMessages({cid: messageList});
/// Bulk updates the message data of multiple channels
Future<void> bulkUpdateMessages(
Map<String, List<Message>> channelWithMessages,
) {
final entities = channelWithMessages.entries
.map((entry) => entry.value.map(
(message) => message.toPinnedEntity(cid: entry.key),
))
.expand((it) => it)
.toList(growable: false);
return batch(
(batch) => batch.insertAllOnConflictUpdate(pinnedMessages, entities),
);
}
}
@@ -0,0 +1,60 @@
import 'package:moor/moor.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/pinned_message_reactions.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
part 'pinned_message_reaction_dao.g.dart';
/// The Data Access Object for operations in [PinnedMessageReactions] table.
@UseDao(tables: [PinnedMessageReactions, Users])
class PinnedMessageReactionDao extends DatabaseAccessor<MoorChatDatabase>
with _$PinnedMessageReactionDaoMixin {
/// Creates a new reaction dao instance
PinnedMessageReactionDao(MoorChatDatabase db) : super(db);
/// Returns all the reactions of a particular message by matching
/// [Reactions.messageId] with [messageId]
Future<List<Reaction>> getReactions(String messageId) =>
(select(pinnedMessageReactions).join([
leftOuterJoin(users, pinnedMessageReactions.userId.equalsExp(users.id)),
])
..where(pinnedMessageReactions.messageId.equals(messageId))
..orderBy([OrderingTerm.asc(pinnedMessageReactions.createdAt)]))
.map((rows) {
final userEntity = rows.readTableOrNull(users);
final reactionEntity = rows.readTable(pinnedMessageReactions);
return reactionEntity.toReaction(user: userEntity?.toUser());
}).get();
/// Returns all the reactions of a particular message
/// added by a particular user by matching
/// [Reactions.messageId] with [messageId] and
/// [Reactions.userId] with [userId]
Future<List<Reaction>> getReactionsByUserId(
String messageId,
String userId,
) async {
final reactions = await getReactions(messageId);
return reactions.where((it) => it.userId == userId).toList();
}
/// Updates the reactions data with the new [reactionList] data
Future<void> updateReactions(List<Reaction> reactionList) => batch((it) {
it.insertAllOnConflictUpdate(
pinnedMessageReactions,
reactionList.map((r) => r.toPinnedEntity()).toList(),
);
});
/// Deletes all the reactions whose [Reactions.messageId] is
/// present in [messageIds]
Future<void> deleteReactionsByMessageIds(List<String> messageIds) =>
batch((it) {
it.deleteWhere<PinnedMessageReactions, PinnedMessageReactionEntity>(
pinnedMessageReactions,
(r) => r.messageId.isIn(messageIds),
);
});
}
@@ -0,0 +1,13 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'pinned_message_reaction_dao.dart';
// **************************************************************************
// DaoGenerator
// **************************************************************************
mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor<MoorChatDatabase> {
$PinnedMessageReactionsTable get pinnedMessageReactions =>
attachedDatabase.pinnedMessageReactions;
$UsersTable get users => attachedDatabase.users;
}
@@ -42,10 +42,9 @@ class ReactionDao extends DatabaseAccessor<MoorChatDatabase>
/// Updates the reactions data with the new [reactionList] data
Future<void> updateReactions(List<Reaction> reactionList) => batch((it) {
it.insertAll(
it.insertAllOnConflictUpdate(
reactions,
reactionList.map((r) => r.toEntity()).toList(),
mode: InsertMode.insertOrReplace,
);
});
@@ -29,11 +29,17 @@ class ReadDao extends DatabaseAccessor<MoorChatDatabase> with _$ReadDaoMixin {
/// Updates the read data of a particular channel with
/// the new [readList] data
Future<void> updateReads(String cid, List<Read> readList) => batch(
(it) => it.insertAll(
reads,
readList.map((r) => r.toEntity(cid: cid)).toList(),
mode: InsertMode.insertOrReplace,
),
);
Future<void> updateReads(String cid, List<Read> readList) =>
bulkUpdateReads({cid: readList});
/// Bulk updates the reads data of multiple channels
Future<void> bulkUpdateReads(Map<String, List<Read>> channelWithReads) {
final entities = channelWithReads.entries
.map((entry) => entry.value.map(
(read) => read.toEntity(cid: entry.key),
))
.expand((it) => it)
.toList(growable: false);
return batch((batch) => batch.insertAllOnConflictUpdate(reads, entities));
}
}
@@ -14,10 +14,9 @@ class UserDao extends DatabaseAccessor<MoorChatDatabase> with _$UserDaoMixin {
/// Updates the users data with the new [userList] data
Future<void> updateUsers(List<User> userList) => batch(
(it) => it.insertAll(
(it) => it.insertAllOnConflictUpdate(
users,
userList.map((u) => u.toEntity()).toList(),
mode: InsertMode.insertOrReplace,
),
);
@@ -14,6 +14,7 @@ part 'moor_chat_database.g.dart';
Channels,
Messages,
PinnedMessages,
PinnedMessageReactions,
Reactions,
Users,
Members,
@@ -25,6 +26,7 @@ part 'moor_chat_database.g.dart';
ChannelDao,
MessageDao,
PinnedMessageDao,
PinnedMessageReactionDao,
MemberDao,
ReactionDao,
ReadDao,
@@ -51,10 +53,13 @@ class MoorChatDatabase extends _$MoorChatDatabase {
// you should bump this number whenever you change or add a table definition.
@override
int get schemaVersion => 5;
int get schemaVersion => 6;
@override
MigrationStrategy get migration => MigrationStrategy(
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
onUpgrade: (openingDetails, before, after) async {
if (before != after) {
final m = createMigrator();
@@ -645,7 +645,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
final String? pinnedByUserId;
/// The channel cid of which this message is part of
final String? channelCid;
final String channelCid;
/// A Map of [messageText] translations.
final Map<String, String>? i18n;
@@ -675,7 +675,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
this.pinnedAt,
this.pinExpires,
this.pinnedByUserId,
this.channelCid,
required this.channelCid,
this.i18n,
this.extraData});
factory MessageEntity.fromData(
@@ -728,7 +728,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
pinnedByUserId: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']),
channelCid: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']),
.mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!,
i18n: $MessagesTable.$converter5.mapToDart(const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}i18n'])),
extraData: $MessagesTable.$converter6.mapToDart(const StringType()
@@ -800,9 +800,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
if (!nullToAbsent || pinnedByUserId != null) {
map['pinned_by_user_id'] = Variable<String?>(pinnedByUserId);
}
if (!nullToAbsent || channelCid != null) {
map['channel_cid'] = Variable<String?>(channelCid);
}
map['channel_cid'] = Variable<String>(channelCid);
if (!nullToAbsent || i18n != null) {
final converter = $MessagesTable.$converter5;
map['i18n'] = Variable<String?>(converter.mapToSql(i18n));
@@ -842,7 +840,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
pinnedAt: serializer.fromJson<DateTime?>(json['pinnedAt']),
pinExpires: serializer.fromJson<DateTime?>(json['pinExpires']),
pinnedByUserId: serializer.fromJson<String?>(json['pinnedByUserId']),
channelCid: serializer.fromJson<String?>(json['channelCid']),
channelCid: serializer.fromJson<String>(json['channelCid']),
i18n: serializer.fromJson<Map<String, String>?>(json['i18n']),
extraData: serializer.fromJson<Map<String, Object?>?>(json['extraData']),
);
@@ -873,7 +871,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
'pinnedAt': serializer.toJson<DateTime?>(pinnedAt),
'pinExpires': serializer.toJson<DateTime?>(pinExpires),
'pinnedByUserId': serializer.toJson<String?>(pinnedByUserId),
'channelCid': serializer.toJson<String?>(channelCid),
'channelCid': serializer.toJson<String>(channelCid),
'i18n': serializer.toJson<Map<String, String>?>(i18n),
'extraData': serializer.toJson<Map<String, Object?>?>(extraData),
};
@@ -902,7 +900,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
Value<DateTime?> pinnedAt = const Value.absent(),
Value<DateTime?> pinExpires = const Value.absent(),
Value<String?> pinnedByUserId = const Value.absent(),
Value<String?> channelCid = const Value.absent(),
String? channelCid,
Value<Map<String, String>?> i18n = const Value.absent(),
Value<Map<String, Object?>?> extraData = const Value.absent()}) =>
MessageEntity(
@@ -934,7 +932,7 @@ class MessageEntity extends DataClass implements Insertable<MessageEntity> {
pinExpires: pinExpires.present ? pinExpires.value : this.pinExpires,
pinnedByUserId:
pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId,
channelCid: channelCid.present ? channelCid.value : this.channelCid,
channelCid: channelCid ?? this.channelCid,
i18n: i18n.present ? i18n.value : this.i18n,
extraData: extraData.present ? extraData.value : this.extraData,
);
@@ -1068,7 +1066,7 @@ class MessagesCompanion extends UpdateCompanion<MessageEntity> {
final Value<DateTime?> pinnedAt;
final Value<DateTime?> pinExpires;
final Value<String?> pinnedByUserId;
final Value<String?> channelCid;
final Value<String> channelCid;
final Value<Map<String, String>?> i18n;
final Value<Map<String, Object?>?> extraData;
const MessagesCompanion({
@@ -1121,12 +1119,13 @@ class MessagesCompanion extends UpdateCompanion<MessageEntity> {
this.pinnedAt = const Value.absent(),
this.pinExpires = const Value.absent(),
this.pinnedByUserId = const Value.absent(),
this.channelCid = const Value.absent(),
required String channelCid,
this.i18n = const Value.absent(),
this.extraData = const Value.absent(),
}) : id = Value(id),
attachments = Value(attachments),
mentionedUsers = Value(mentionedUsers);
mentionedUsers = Value(mentionedUsers),
channelCid = Value(channelCid);
static Insertable<MessageEntity> custom({
Expression<String>? id,
Expression<String?>? messageText,
@@ -1150,7 +1149,7 @@ class MessagesCompanion extends UpdateCompanion<MessageEntity> {
Expression<DateTime?>? pinnedAt,
Expression<DateTime?>? pinExpires,
Expression<String?>? pinnedByUserId,
Expression<String?>? channelCid,
Expression<String>? channelCid,
Expression<Map<String, String>?>? i18n,
Expression<Map<String, Object?>?>? extraData,
}) {
@@ -1206,7 +1205,7 @@ class MessagesCompanion extends UpdateCompanion<MessageEntity> {
Value<DateTime?>? pinnedAt,
Value<DateTime?>? pinExpires,
Value<String?>? pinnedByUserId,
Value<String?>? channelCid,
Value<String>? channelCid,
Value<Map<String, String>?>? i18n,
Value<Map<String, Object?>?>? extraData}) {
return MessagesCompanion(
@@ -1317,7 +1316,7 @@ class MessagesCompanion extends UpdateCompanion<MessageEntity> {
map['pinned_by_user_id'] = Variable<String?>(pinnedByUserId.value);
}
if (channelCid.present) {
map['channel_cid'] = Variable<String?>(channelCid.value);
map['channel_cid'] = Variable<String>(channelCid.value);
}
if (i18n.present) {
final converter = $MessagesTable.$converter5;
@@ -1491,11 +1490,10 @@ class $MessagesTable extends Messages
typeName: 'TEXT', requiredDuringInsert: false);
final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid');
late final GeneratedColumn<String?> channelCid = GeneratedColumn<String?>(
'channel_cid', aliasedName, true,
'channel_cid', aliasedName, false,
typeName: 'TEXT',
requiredDuringInsert: false,
$customConstraints:
'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE');
requiredDuringInsert: true,
$customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE');
final VerificationMeta _i18nMeta = const VerificationMeta('i18n');
late final GeneratedColumnWithTypeConverter<Map<String, String>, String?>
i18n = GeneratedColumn<String?>('i18n', aliasedName, true,
@@ -1634,6 +1632,8 @@ class $MessagesTable extends Messages
_channelCidMeta,
channelCid.isAcceptableOrUnknown(
data['channel_cid']!, _channelCidMeta));
} else if (isInserting) {
context.missing(_channelCidMeta);
}
context.handle(_i18nMeta, const VerificationResult.success());
context.handle(_extraDataMeta, const VerificationResult.success());
@@ -1739,7 +1739,7 @@ class PinnedMessageEntity extends DataClass
final String? pinnedByUserId;
/// The channel cid of which this message is part of
final String? channelCid;
final String channelCid;
/// A Map of [messageText] translations.
final Map<String, String>? i18n;
@@ -1769,7 +1769,7 @@ class PinnedMessageEntity extends DataClass
this.pinnedAt,
this.pinExpires,
this.pinnedByUserId,
this.channelCid,
required this.channelCid,
this.i18n,
this.extraData});
factory PinnedMessageEntity.fromData(
@@ -1825,7 +1825,7 @@ class PinnedMessageEntity extends DataClass
pinnedByUserId: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']),
channelCid: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']),
.mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!,
i18n: $PinnedMessagesTable.$converter5.mapToDart(const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}i18n'])),
extraData: $PinnedMessagesTable.$converter6.mapToDart(const StringType()
@@ -1897,9 +1897,7 @@ class PinnedMessageEntity extends DataClass
if (!nullToAbsent || pinnedByUserId != null) {
map['pinned_by_user_id'] = Variable<String?>(pinnedByUserId);
}
if (!nullToAbsent || channelCid != null) {
map['channel_cid'] = Variable<String?>(channelCid);
}
map['channel_cid'] = Variable<String>(channelCid);
if (!nullToAbsent || i18n != null) {
final converter = $PinnedMessagesTable.$converter5;
map['i18n'] = Variable<String?>(converter.mapToSql(i18n));
@@ -1939,7 +1937,7 @@ class PinnedMessageEntity extends DataClass
pinnedAt: serializer.fromJson<DateTime?>(json['pinnedAt']),
pinExpires: serializer.fromJson<DateTime?>(json['pinExpires']),
pinnedByUserId: serializer.fromJson<String?>(json['pinnedByUserId']),
channelCid: serializer.fromJson<String?>(json['channelCid']),
channelCid: serializer.fromJson<String>(json['channelCid']),
i18n: serializer.fromJson<Map<String, String>?>(json['i18n']),
extraData: serializer.fromJson<Map<String, Object?>?>(json['extraData']),
);
@@ -1970,7 +1968,7 @@ class PinnedMessageEntity extends DataClass
'pinnedAt': serializer.toJson<DateTime?>(pinnedAt),
'pinExpires': serializer.toJson<DateTime?>(pinExpires),
'pinnedByUserId': serializer.toJson<String?>(pinnedByUserId),
'channelCid': serializer.toJson<String?>(channelCid),
'channelCid': serializer.toJson<String>(channelCid),
'i18n': serializer.toJson<Map<String, String>?>(i18n),
'extraData': serializer.toJson<Map<String, Object?>?>(extraData),
};
@@ -1999,7 +1997,7 @@ class PinnedMessageEntity extends DataClass
Value<DateTime?> pinnedAt = const Value.absent(),
Value<DateTime?> pinExpires = const Value.absent(),
Value<String?> pinnedByUserId = const Value.absent(),
Value<String?> channelCid = const Value.absent(),
String? channelCid,
Value<Map<String, String>?> i18n = const Value.absent(),
Value<Map<String, Object?>?> extraData = const Value.absent()}) =>
PinnedMessageEntity(
@@ -2031,7 +2029,7 @@ class PinnedMessageEntity extends DataClass
pinExpires: pinExpires.present ? pinExpires.value : this.pinExpires,
pinnedByUserId:
pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId,
channelCid: channelCid.present ? channelCid.value : this.channelCid,
channelCid: channelCid ?? this.channelCid,
i18n: i18n.present ? i18n.value : this.i18n,
extraData: extraData.present ? extraData.value : this.extraData,
);
@@ -2165,7 +2163,7 @@ class PinnedMessagesCompanion extends UpdateCompanion<PinnedMessageEntity> {
final Value<DateTime?> pinnedAt;
final Value<DateTime?> pinExpires;
final Value<String?> pinnedByUserId;
final Value<String?> channelCid;
final Value<String> channelCid;
final Value<Map<String, String>?> i18n;
final Value<Map<String, Object?>?> extraData;
const PinnedMessagesCompanion({
@@ -2218,12 +2216,13 @@ class PinnedMessagesCompanion extends UpdateCompanion<PinnedMessageEntity> {
this.pinnedAt = const Value.absent(),
this.pinExpires = const Value.absent(),
this.pinnedByUserId = const Value.absent(),
this.channelCid = const Value.absent(),
required String channelCid,
this.i18n = const Value.absent(),
this.extraData = const Value.absent(),
}) : id = Value(id),
attachments = Value(attachments),
mentionedUsers = Value(mentionedUsers);
mentionedUsers = Value(mentionedUsers),
channelCid = Value(channelCid);
static Insertable<PinnedMessageEntity> custom({
Expression<String>? id,
Expression<String?>? messageText,
@@ -2247,7 +2246,7 @@ class PinnedMessagesCompanion extends UpdateCompanion<PinnedMessageEntity> {
Expression<DateTime?>? pinnedAt,
Expression<DateTime?>? pinExpires,
Expression<String?>? pinnedByUserId,
Expression<String?>? channelCid,
Expression<String>? channelCid,
Expression<Map<String, String>?>? i18n,
Expression<Map<String, Object?>?>? extraData,
}) {
@@ -2303,7 +2302,7 @@ class PinnedMessagesCompanion extends UpdateCompanion<PinnedMessageEntity> {
Value<DateTime?>? pinnedAt,
Value<DateTime?>? pinExpires,
Value<String?>? pinnedByUserId,
Value<String?>? channelCid,
Value<String>? channelCid,
Value<Map<String, String>?>? i18n,
Value<Map<String, Object?>?>? extraData}) {
return PinnedMessagesCompanion(
@@ -2414,7 +2413,7 @@ class PinnedMessagesCompanion extends UpdateCompanion<PinnedMessageEntity> {
map['pinned_by_user_id'] = Variable<String?>(pinnedByUserId.value);
}
if (channelCid.present) {
map['channel_cid'] = Variable<String?>(channelCid.value);
map['channel_cid'] = Variable<String>(channelCid.value);
}
if (i18n.present) {
final converter = $PinnedMessagesTable.$converter5;
@@ -2589,11 +2588,10 @@ class $PinnedMessagesTable extends PinnedMessages
typeName: 'TEXT', requiredDuringInsert: false);
final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid');
late final GeneratedColumn<String?> channelCid = GeneratedColumn<String?>(
'channel_cid', aliasedName, true,
'channel_cid', aliasedName, false,
typeName: 'TEXT',
requiredDuringInsert: false,
$customConstraints:
'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE');
requiredDuringInsert: true,
$customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE');
final VerificationMeta _i18nMeta = const VerificationMeta('i18n');
late final GeneratedColumnWithTypeConverter<Map<String, String>, String?>
i18n = GeneratedColumn<String?>('i18n', aliasedName, true,
@@ -2734,6 +2732,8 @@ class $PinnedMessagesTable extends PinnedMessages
_channelCidMeta,
channelCid.isAcceptableOrUnknown(
data['channel_cid']!, _channelCidMeta));
} else if (isInserting) {
context.missing(_channelCidMeta);
}
context.handle(_i18nMeta, const VerificationResult.success());
context.handle(_extraDataMeta, const VerificationResult.success());
@@ -2769,6 +2769,340 @@ class $PinnedMessagesTable extends PinnedMessages
MapConverter<Object?>();
}
class PinnedMessageReactionEntity extends DataClass
implements Insertable<PinnedMessageReactionEntity> {
/// The id of the user that sent the reaction
final String userId;
/// The messageId to which the reaction belongs
final String messageId;
/// The type of the reaction
final String type;
/// The DateTime on which the reaction is created
final DateTime createdAt;
/// The score of the reaction (ie. number of reactions sent)
final int score;
/// Reaction custom extraData
final Map<String, Object?>? extraData;
PinnedMessageReactionEntity(
{required this.userId,
required this.messageId,
required this.type,
required this.createdAt,
required this.score,
this.extraData});
factory PinnedMessageReactionEntity.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return PinnedMessageReactionEntity(
userId: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!,
messageId: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}message_id'])!,
type: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}type'])!,
createdAt: const DateTimeType()
.mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!,
score: const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}score'])!,
extraData: $PinnedMessageReactionsTable.$converter0.mapToDart(
const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])),
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['user_id'] = Variable<String>(userId);
map['message_id'] = Variable<String>(messageId);
map['type'] = Variable<String>(type);
map['created_at'] = Variable<DateTime>(createdAt);
map['score'] = Variable<int>(score);
if (!nullToAbsent || extraData != null) {
final converter = $PinnedMessageReactionsTable.$converter0;
map['extra_data'] = Variable<String?>(converter.mapToSql(extraData));
}
return map;
}
factory PinnedMessageReactionEntity.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return PinnedMessageReactionEntity(
userId: serializer.fromJson<String>(json['userId']),
messageId: serializer.fromJson<String>(json['messageId']),
type: serializer.fromJson<String>(json['type']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
score: serializer.fromJson<int>(json['score']),
extraData: serializer.fromJson<Map<String, Object?>?>(json['extraData']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<String>(userId),
'messageId': serializer.toJson<String>(messageId),
'type': serializer.toJson<String>(type),
'createdAt': serializer.toJson<DateTime>(createdAt),
'score': serializer.toJson<int>(score),
'extraData': serializer.toJson<Map<String, Object?>?>(extraData),
};
}
PinnedMessageReactionEntity copyWith(
{String? userId,
String? messageId,
String? type,
DateTime? createdAt,
int? score,
Value<Map<String, Object?>?> extraData = const Value.absent()}) =>
PinnedMessageReactionEntity(
userId: userId ?? this.userId,
messageId: messageId ?? this.messageId,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
score: score ?? this.score,
extraData: extraData.present ? extraData.value : this.extraData,
);
@override
String toString() {
return (StringBuffer('PinnedMessageReactionEntity(')
..write('userId: $userId, ')
..write('messageId: $messageId, ')
..write('type: $type, ')
..write('createdAt: $createdAt, ')
..write('score: $score, ')
..write('extraData: $extraData')
..write(')'))
.toString();
}
@override
int get hashCode => $mrjf($mrjc(
userId.hashCode,
$mrjc(
messageId.hashCode,
$mrjc(
type.hashCode,
$mrjc(createdAt.hashCode,
$mrjc(score.hashCode, extraData.hashCode))))));
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is PinnedMessageReactionEntity &&
other.userId == this.userId &&
other.messageId == this.messageId &&
other.type == this.type &&
other.createdAt == this.createdAt &&
other.score == this.score &&
other.extraData == this.extraData);
}
class PinnedMessageReactionsCompanion
extends UpdateCompanion<PinnedMessageReactionEntity> {
final Value<String> userId;
final Value<String> messageId;
final Value<String> type;
final Value<DateTime> createdAt;
final Value<int> score;
final Value<Map<String, Object?>?> extraData;
const PinnedMessageReactionsCompanion({
this.userId = const Value.absent(),
this.messageId = const Value.absent(),
this.type = const Value.absent(),
this.createdAt = const Value.absent(),
this.score = const Value.absent(),
this.extraData = const Value.absent(),
});
PinnedMessageReactionsCompanion.insert({
required String userId,
required String messageId,
required String type,
this.createdAt = const Value.absent(),
this.score = const Value.absent(),
this.extraData = const Value.absent(),
}) : userId = Value(userId),
messageId = Value(messageId),
type = Value(type);
static Insertable<PinnedMessageReactionEntity> custom({
Expression<String>? userId,
Expression<String>? messageId,
Expression<String>? type,
Expression<DateTime>? createdAt,
Expression<int>? score,
Expression<Map<String, Object?>?>? extraData,
}) {
return RawValuesInsertable({
if (userId != null) 'user_id': userId,
if (messageId != null) 'message_id': messageId,
if (type != null) 'type': type,
if (createdAt != null) 'created_at': createdAt,
if (score != null) 'score': score,
if (extraData != null) 'extra_data': extraData,
});
}
PinnedMessageReactionsCompanion copyWith(
{Value<String>? userId,
Value<String>? messageId,
Value<String>? type,
Value<DateTime>? createdAt,
Value<int>? score,
Value<Map<String, Object?>?>? extraData}) {
return PinnedMessageReactionsCompanion(
userId: userId ?? this.userId,
messageId: messageId ?? this.messageId,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
score: score ?? this.score,
extraData: extraData ?? this.extraData,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (userId.present) {
map['user_id'] = Variable<String>(userId.value);
}
if (messageId.present) {
map['message_id'] = Variable<String>(messageId.value);
}
if (type.present) {
map['type'] = Variable<String>(type.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
if (score.present) {
map['score'] = Variable<int>(score.value);
}
if (extraData.present) {
final converter = $PinnedMessageReactionsTable.$converter0;
map['extra_data'] =
Variable<String?>(converter.mapToSql(extraData.value));
}
return map;
}
@override
String toString() {
return (StringBuffer('PinnedMessageReactionsCompanion(')
..write('userId: $userId, ')
..write('messageId: $messageId, ')
..write('type: $type, ')
..write('createdAt: $createdAt, ')
..write('score: $score, ')
..write('extraData: $extraData')
..write(')'))
.toString();
}
}
class $PinnedMessageReactionsTable extends PinnedMessageReactions
with TableInfo<$PinnedMessageReactionsTable, PinnedMessageReactionEntity> {
final GeneratedDatabase _db;
final String? _alias;
$PinnedMessageReactionsTable(this._db, [this._alias]);
final VerificationMeta _userIdMeta = const VerificationMeta('userId');
late final GeneratedColumn<String?> userId = GeneratedColumn<String?>(
'user_id', aliasedName, false,
typeName: 'TEXT', requiredDuringInsert: true);
final VerificationMeta _messageIdMeta = const VerificationMeta('messageId');
late final GeneratedColumn<String?> messageId = GeneratedColumn<String?>(
'message_id', aliasedName, false,
typeName: 'TEXT',
requiredDuringInsert: true,
$customConstraints: 'REFERENCES pinned_messages(id) ON DELETE CASCADE');
final VerificationMeta _typeMeta = const VerificationMeta('type');
late final GeneratedColumn<String?> type = GeneratedColumn<String?>(
'type', aliasedName, false,
typeName: 'TEXT', requiredDuringInsert: true);
final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt');
late final GeneratedColumn<DateTime?> createdAt = GeneratedColumn<DateTime?>(
'created_at', aliasedName, false,
typeName: 'INTEGER',
requiredDuringInsert: false,
defaultValue: currentDateAndTime);
final VerificationMeta _scoreMeta = const VerificationMeta('score');
late final GeneratedColumn<int?> score = GeneratedColumn<int?>(
'score', aliasedName, false,
typeName: 'INTEGER',
requiredDuringInsert: false,
defaultValue: const Constant(0));
final VerificationMeta _extraDataMeta = const VerificationMeta('extraData');
late final GeneratedColumnWithTypeConverter<Map<String, Object?>, String?>
extraData = GeneratedColumn<String?>('extra_data', aliasedName, true,
typeName: 'TEXT', requiredDuringInsert: false)
.withConverter<Map<String, Object?>>(
$PinnedMessageReactionsTable.$converter0);
@override
List<GeneratedColumn> get $columns =>
[userId, messageId, type, createdAt, score, extraData];
@override
String get aliasedName => _alias ?? 'pinned_message_reactions';
@override
String get actualTableName => 'pinned_message_reactions';
@override
VerificationContext validateIntegrity(
Insertable<PinnedMessageReactionEntity> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('user_id')) {
context.handle(_userIdMeta,
userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta));
} else if (isInserting) {
context.missing(_userIdMeta);
}
if (data.containsKey('message_id')) {
context.handle(_messageIdMeta,
messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta));
} else if (isInserting) {
context.missing(_messageIdMeta);
}
if (data.containsKey('type')) {
context.handle(
_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta));
} else if (isInserting) {
context.missing(_typeMeta);
}
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
if (data.containsKey('score')) {
context.handle(
_scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta));
}
context.handle(_extraDataMeta, const VerificationResult.success());
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {messageId, type, userId};
@override
PinnedMessageReactionEntity map(Map<String, dynamic> data,
{String? tablePrefix}) {
return PinnedMessageReactionEntity.fromData(data, _db,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@override
$PinnedMessageReactionsTable createAlias(String alias) {
return $PinnedMessageReactionsTable(_db, alias);
}
static TypeConverter<Map<String, Object?>, String> $converter0 =
MapConverter<Object?>();
}
class ReactionEntity extends DataClass implements Insertable<ReactionEntity> {
/// The id of the user that sent the reaction
final String userId;
@@ -4897,6 +5231,8 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase {
late final $ChannelsTable channels = $ChannelsTable(this);
late final $MessagesTable messages = $MessagesTable(this);
late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this);
late final $PinnedMessageReactionsTable pinnedMessageReactions =
$PinnedMessageReactionsTable(this);
late final $ReactionsTable reactions = $ReactionsTable(this);
late final $UsersTable users = $UsersTable(this);
late final $MembersTable members = $MembersTable(this);
@@ -4909,6 +5245,8 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase {
late final MessageDao messageDao = MessageDao(this as MoorChatDatabase);
late final PinnedMessageDao pinnedMessageDao =
PinnedMessageDao(this as MoorChatDatabase);
late final PinnedMessageReactionDao pinnedMessageReactionDao =
PinnedMessageReactionDao(this as MoorChatDatabase);
late final MemberDao memberDao = MemberDao(this as MoorChatDatabase);
late final ReactionDao reactionDao = ReactionDao(this as MoorChatDatabase);
late final ReadDao readDao = ReadDao(this as MoorChatDatabase);
@@ -4923,6 +5261,7 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase {
channels,
messages,
pinnedMessages,
pinnedMessageReactions,
reactions,
users,
members,
@@ -3,6 +3,7 @@ export 'channels.dart';
export 'connection_events.dart';
export 'members.dart';
export 'messages.dart';
export 'pinned_message_reactions.dart';
export 'pinned_messages.dart';
export 'reactions.dart';
export 'reads.dart';
@@ -39,8 +39,5 @@ class Members extends Table {
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {
userId,
channelCid,
};
Set<Column> get primaryKey => {userId, channelCid};
}
@@ -77,8 +77,8 @@ class Messages extends Table {
TextColumn get pinnedByUserId => text().nullable()();
/// The channel cid of which this message is part of
TextColumn get channelCid => text().nullable().customConstraint(
'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE')();
TextColumn get channelCid =>
text().customConstraint('REFERENCES channels(cid) ON DELETE CASCADE')();
/// A Map of [messageText] translations.
TextColumn get i18n => text().nullable().map(MapConverter<String>())();
@@ -0,0 +1,13 @@
// coverage:ignore-file
import 'package:moor/moor.dart';
import 'package:stream_chat_persistence/src/entity/reactions.dart';
/// Represents a [PinnedMessageReactions] table in [MoorChatDatabase].
@DataClassName('PinnedMessageReactionEntity')
class PinnedMessageReactions extends Reactions {
/// The messageId to which the reaction belongs
@override
TextColumn get messageId => text()
.customConstraint('REFERENCES pinned_messages(id) ON DELETE CASCADE')();
}
@@ -3,6 +3,7 @@ export 'event_mapper.dart';
export 'member_mapper.dart';
export 'message_mapper.dart';
export 'pinned_message_mapper.dart';
export 'pinned_message_reaction_mapper.dart';
export 'reaction_mapper.dart';
export 'read_mapper.dart';
export 'user_mapper.dart';
@@ -51,7 +51,7 @@ extension MessageEntityX on MessageEntity {
/// Useful mapping functions for [Message]
extension MessageX on Message {
/// Maps a [Message] into [MessageEntity]
MessageEntity toEntity({String? cid}) => MessageEntity(
MessageEntity toEntity({required String cid}) => MessageEntity(
id: id,
attachments: attachments.map((it) => jsonEncode(it.toData())).toList(),
channelCid: cid,
@@ -51,7 +51,8 @@ extension PinnedMessageEntityX on PinnedMessageEntity {
/// Useful mapping functions for [Message]
extension PMessageX on Message {
/// Maps a [Message] into [PinnedMessageEntity]
PinnedMessageEntity toPinnedEntity({String? cid}) => PinnedMessageEntity(
PinnedMessageEntity toPinnedEntity({required String cid}) =>
PinnedMessageEntity(
id: id,
attachments: attachments.map((it) => jsonEncode(it.toData())).toList(),
channelCid: cid,
@@ -0,0 +1,29 @@
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// Useful mapping functions for [PinnedMessageReactionEntity]
extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity {
/// Maps a [PinnedMessageReactionEntity] into [Reaction]
Reaction toReaction({User? user}) => Reaction(
extraData: extraData ?? {},
type: type,
createdAt: createdAt,
userId: userId,
user: user,
messageId: messageId,
score: score,
);
}
/// Useful mapping functions for [Reaction]
extension PReactionX on Reaction {
/// Maps a [Reaction] into [ReactionEntity]
PinnedMessageReactionEntity toPinnedEntity() => PinnedMessageReactionEntity(
extraData: extraData,
type: type,
createdAt: createdAt,
userId: userId!,
messageId: messageId!,
score: score,
);
}
@@ -296,25 +296,34 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
}
@override
Future<void> updateMembers(String cid, List<Member> members) {
Future<void> bulkUpdateMembers(Map<String, List<Member>> members) {
assert(_debugIsConnected, '');
_logger.info('updateMembers');
return _readProtected(() => db!.memberDao.updateMembers(cid, members));
_logger.info('bulkUpdateMembers');
return _readProtected(() => db!.memberDao.bulkUpdateMembers(members));
}
@override
Future<void> updateMessages(String cid, List<Message> messages) {
Future<void> bulkUpdateMessages(Map<String, List<Message>> messages) {
assert(_debugIsConnected, '');
_logger.info('updateMessages');
return _readProtected(() => db!.messageDao.updateMessages(cid, messages));
_logger.info('bulkUpdateMessages');
return _readProtected(() => db!.messageDao.bulkUpdateMessages(messages));
}
@override
Future<void> updatePinnedMessages(String cid, List<Message> messages) {
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>> messages) {
assert(_debugIsConnected, '');
_logger.info('updatePinnedMessages');
_logger.info('bulkUpdatePinnedMessages');
return _readProtected(
() => db!.pinnedMessageDao.updateMessages(cid, messages),
() => db!.pinnedMessageDao.bulkUpdateMessages(messages),
);
}
@override
Future<void> updatePinnedMessageReactions(List<Reaction> reactions) {
assert(_debugIsConnected, '');
_logger.info('updatePinnedMessageReactions');
return _readProtected(
() => db!.pinnedMessageReactionDao.updateReactions(reactions),
);
}
@@ -326,10 +335,10 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
}
@override
Future<void> updateReads(String cid, List<Read> reads) {
Future<void> bulkUpdateReads(Map<String, List<Read>> reads) {
assert(_debugIsConnected, '');
_logger.info('updateReads');
return _readProtected(() => db!.readDao.updateReads(cid, reads));
_logger.info('bulkUpdateReads');
return _readProtected(() => db!.readDao.bulkUpdateReads(reads));
}
@override
@@ -339,6 +348,18 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
return _readProtected(() => db!.userDao.updateUsers(users));
}
@override
Future<void> deletePinnedMessageReactionsByMessageId(
List<String> messageIds,
) {
assert(_debugIsConnected, '');
_logger.info('deletePinnedMessageReactionsByMessageId');
return _readProtected(
() =>
db!.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds),
);
}
@override
Future<void> deleteReactionsByMessageId(List<String> messageIds) {
assert(_debugIsConnected, '');
@@ -34,6 +34,12 @@ class MockChatDatabase extends Mock implements MoorChatDatabase {
@override
ReactionDao get reactionDao => _reactionDao ??= MockReactionDao();
PinnedMessageReactionDao? _pinnedMessageReactionDao;
@override
PinnedMessageReactionDao get pinnedMessageReactionDao =>
_pinnedMessageReactionDao ??= MockPinnedMessageReactionDao();
ReadDao? _readDao;
@override
@@ -70,6 +76,9 @@ class MockMemberDao extends Mock implements MemberDao {}
class MockReactionDao extends Mock implements ReactionDao {}
class MockPinnedMessageReactionDao extends Mock
implements PinnedMessageReactionDao {}
class MockReadDao extends Mock implements ReadDao {}
class MockChannelQueryDao extends Mock implements ChannelQueryDao {}
@@ -61,12 +61,73 @@ void main() {
expect(updatedChannel.cid, cid);
expect(updatedChannel.type, type);
//Saving a dummy user
const userId = 'userId';
final dummyUser = User(id: userId);
await database.userDao.updateUsers([dummyUser]);
// Saving a dummy member
final dummyMember = Member(userId: userId, user: dummyUser);
await database.memberDao.updateMembers(cid, [dummyMember]);
// Should match the dummy member
final updatedMembers = await database.memberDao.getMembersByCid(cid);
expect(updatedMembers.length, 1);
expect(updatedMembers.first.userId, userId);
// Saving a dummy message
const messageId = 'messageId';
final dummyMessage = Message(id: messageId, user: dummyUser);
await database.messageDao.updateMessages(cid, [dummyMessage]);
// Should match the dummy message
final updatedMessages = await database.messageDao.getMessagesByCid(cid);
expect(updatedMessages.length, 1);
expect(updatedMessages.first.id, messageId);
// Saving a dummy read
final dummyRead = Read(lastRead: DateTime.now(), user: dummyUser);
await database.readDao.updateReads(cid, [dummyRead]);
// Should match the dummy read
final updatedReads = await database.readDao.getReadsByCid(cid);
expect(updatedReads.length, 1);
expect(updatedReads.first.user, dummyUser);
// Saving a dummy reaction
final dummyReaction =
Reaction(type: 'type', messageId: messageId, userId: userId);
await database.reactionDao.updateReactions([dummyReaction]);
// Should match the dummy reaction
final updatedReactions =
await database.reactionDao.getReactionsByUserId(messageId, userId);
expect(updatedReactions.length, 1);
expect(updatedReactions.first.messageId, messageId);
// Deleting the dummyChannel using cid
await channelDao.deleteChannelByCids([cid]);
// Fetched channel Should be null
final channel = await channelDao.getChannelByCid(cid);
expect(channel, isNull);
// Fetched members for passed cid should be empty
final members = await database.memberDao.getMembersByCid(cid);
expect(members, isEmpty);
// Fetched messages for passed cid should be empty
final messages = await database.messageDao.getMessagesByCid(cid);
expect(messages, isEmpty);
// Fetched reads for passed cid should be empty
final reads = await database.readDao.getReadsByCid(cid);
expect(reads, isEmpty);
// Fetched readtions for passed message id and user id should be empty
final reactions =
await database.reactionDao.getReactionsByUserId(messageId, userId);
expect(reactions, isEmpty);
});
test('cids', () async {
@@ -18,6 +18,7 @@ void main() {
});
Future<List<Member>> _prepareTestData(String cid) async {
final channels = [ChannelModel(cid: cid)];
final users = List.generate(3, (index) => User(id: 'testUserId$index'));
final memberList = List.generate(
3,
@@ -34,12 +35,13 @@ void main() {
),
);
await database.userDao.updateUsers(users);
await database.channelDao.updateChannels(channels);
await memberDao.updateMembers(cid, memberList);
return memberList;
}
test('getMembersByCid', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Should be empty initially
final members = await memberDao.getMembersByCid(cid);
@@ -70,7 +72,7 @@ void main() {
});
test('updateMembers', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Preparing test data
final memberList = await _prepareTestData(cid);
@@ -132,7 +134,7 @@ void main() {
});
test('deleteMemberByCids', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Preparing test data
final members = await _prepareTestData(cid);
@@ -23,6 +23,7 @@ void main() {
bool mapAllThreadToFirstMessage = false,
int count = 3,
}) async {
final channels = [ChannelModel(cid: cid)];
final users = List.generate(count, (index) => User(id: 'testUserId$index'));
final messages = List.generate(
count,
@@ -98,13 +99,20 @@ void main() {
if (quoted) ...quotedMessages,
if (threads) ...threadMessages
];
final reaction = Reaction(
type: 'type',
messageId: allMessages.first.id,
user: users.first,
);
await database.userDao.updateUsers(users);
await database.channelDao.updateChannels(channels);
await messageDao.updateMessages(cid, allMessages);
await database.reactionDao.updateReactions([reaction]);
return allMessages;
}
test('deleteMessageByIds', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Preparing test data
final insertedMessages = await _prepareTestData(cid);
@@ -113,66 +121,112 @@ void main() {
final messages = await messageDao.getMessagesByCid(cid);
expect(messages.length, insertedMessages.length);
final firstMessageId = messages.first.id;
// Fetched reactions list should have one reaction for given message id
final reactions = await database.reactionDao.getReactions(firstMessageId);
expect(reactions.length, 1);
// Deleting 2 messages from DB
await messageDao.deleteMessageByIds(
['testMessageId${cid}0', 'testMessageId${cid}1'],
[firstMessageId, 'testMessageId${cid}1'],
);
// New fetched messages length should 2 less than the
// previous fetched messages
final newMessages = await messageDao.getMessagesByCid(cid);
expect(newMessages.length, messages.length - 2);
// Reaction for the first message should be deleted too
final newReactions =
await database.reactionDao.getReactions(firstMessageId);
expect(newReactions, isEmpty);
});
group('deleteMessageByCids', () {
const cid1 = 'testCid1';
const cid2 = 'testCid2';
const cid1 = 'test:Cid1';
const cid2 = 'test:Cid2';
test('should delete all the messages of first channel', () async {
// Preparing test data
final cid1InsertedMessages = await _prepareTestData(cid1);
final cid2InsertedMessages = await _prepareTestData(cid2);
test(
'should delete all the messages and reactions of first channel',
() async {
// Preparing test data
final cid1InsertedMessages = await _prepareTestData(cid1);
final cid2InsertedMessages = await _prepareTestData(cid2);
// Fetched message list should match the test message list length
final cid1Messages = await messageDao.getMessagesByCid(cid1);
final cid2Messages = await messageDao.getMessagesByCid(cid2);
expect(cid1Messages.length, cid1InsertedMessages.length);
expect(cid2Messages.length, cid2InsertedMessages.length);
// Fetched message list should match the test message list length
final cid1Messages = await messageDao.getMessagesByCid(cid1);
final cid2Messages = await messageDao.getMessagesByCid(cid2);
expect(cid1Messages.length, cid1InsertedMessages.length);
expect(cid2Messages.length, cid2InsertedMessages.length);
// Deleting all the messages of cid1
await messageDao.deleteMessageByCids([cid1]);
// Fetched reactions list should have one reaction for given message id
final cid1firstMessageId = cid1Messages.first.id;
final cid1Reactions =
await database.reactionDao.getReactions(cid1firstMessageId);
expect(cid1Reactions.length, 1);
// Fetched messages length of only cid1 should be empty
final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1);
final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2);
expect(cid1FetchedMessages, isEmpty);
expect(cid2FetchedMessages, isNotEmpty);
});
// Deleting all the messages of cid1
await messageDao.deleteMessageByCids([cid1]);
test('should delete all the messages of both channel', () async {
// Preparing test data
final cid1InsertedMessages = await _prepareTestData(cid1);
final cid2InsertedMessages = await _prepareTestData(cid2);
// Fetched messages length of only cid1 should be empty
final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1);
final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2);
expect(cid1FetchedMessages, isEmpty);
expect(cid2FetchedMessages, isNotEmpty);
// Fetched message list should match the test message list length
final cid1Messages = await messageDao.getMessagesByCid(cid1);
final cid2Messages = await messageDao.getMessagesByCid(cid2);
expect(cid1Messages.length, cid1InsertedMessages.length);
expect(cid2Messages.length, cid2InsertedMessages.length);
// Reaction for the first message should be deleted too
final cid1FetchedReactions =
await database.reactionDao.getReactions(cid1firstMessageId);
expect(cid1FetchedReactions, isEmpty);
},
);
// Deleting all the messages of cid1
await messageDao.deleteMessageByCids([cid1, cid2]);
test(
'should delete all the messages and reactions of both channel',
() async {
// Preparing test data
final cid1InsertedMessages = await _prepareTestData(cid1);
final cid2InsertedMessages = await _prepareTestData(cid2);
// Fetched messages length of both cid1 and cid2 should be empty
final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1);
final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2);
expect(cid1FetchedMessages, isEmpty);
expect(cid2FetchedMessages, isEmpty);
});
// Fetched message list should match the test message list length
final cid1Messages = await messageDao.getMessagesByCid(cid1);
final cid2Messages = await messageDao.getMessagesByCid(cid2);
expect(cid1Messages.length, cid1InsertedMessages.length);
expect(cid2Messages.length, cid2InsertedMessages.length);
// Fetched reactions list should have one reaction for given message id
final cid1FirstMessageId = cid1Messages.first.id;
final cid1Reactions =
await database.reactionDao.getReactions(cid1FirstMessageId);
expect(cid1Reactions.length, 1);
final cid2FirstMessageId = cid2Messages.first.id;
final cid2Reactions =
await database.reactionDao.getReactions(cid2FirstMessageId);
expect(cid2Reactions.length, 1);
// Deleting all the messages of cid1
await messageDao.deleteMessageByCids([cid1, cid2]);
// Fetched messages length of both cid1 and cid2 should be empty
final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1);
final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2);
expect(cid1FetchedMessages, isEmpty);
expect(cid2FetchedMessages, isEmpty);
// Reaction for the first message should be deleted too
final cid1FetchedReactions =
await database.reactionDao.getReactions(cid1FirstMessageId);
expect(cid1FetchedReactions, isEmpty);
final cid2FetchedReactions =
await database.reactionDao.getReactions(cid2FirstMessageId);
expect(cid2FetchedReactions, isEmpty);
},
);
});
test('getMessageById', () async {
const cid = 'testCid';
const cid = 'test:Cid';
const id = 'testMessageId${cid}0';
// Should be null initially
@@ -190,7 +244,7 @@ void main() {
});
test('getThreadMessages', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Messages should be empty initially
final messages = await messageDao.getThreadMessages(cid);
@@ -209,7 +263,7 @@ void main() {
});
test('getThreadMessagesByParentId', () async {
const cid = 'testCid';
const cid = 'test:Cid';
const parentId = 'testMessageId${cid}0';
// Messages should be empty initially
@@ -228,7 +282,7 @@ void main() {
});
test('getThreadMessagesByParentId along with pagination', () async {
const cid = 'testCid';
const cid = 'test:Cid';
const parentId = 'testMessageId${cid}0';
const options = PaginationParams(
limit: 15,
@@ -262,7 +316,7 @@ void main() {
});
test('getMessagesByCid', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Should be empty initially
final messages = await messageDao.getMessagesByCid(cid);
@@ -283,7 +337,7 @@ void main() {
});
test('getMessagesByCid along with quotedMessage', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Should be empty initially
final messages = await messageDao.getMessagesByCid(cid);
@@ -301,7 +355,7 @@ void main() {
});
test('getMessagesByCid along with pagination', () async {
const cid = 'testCid';
const cid = 'test:Cid';
const limit = 15;
const lessThan = 'testMessageId${cid}25';
const greaterThanOrEqual = 'testMessageId${cid}5';
@@ -333,7 +387,7 @@ void main() {
});
test('updateMessages', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Preparing test data
final insertedMessages = await _prepareTestData(cid);
@@ -23,6 +23,7 @@ void main() {
bool mapAllThreadToFirstMessage = false,
int count = 3,
}) async {
final channels = [ChannelModel(cid: cid)];
final users = List.generate(count, (index) => User(id: 'testUserId$index'));
final messages = List.generate(
count,
@@ -83,13 +84,20 @@ void main() {
if (quoted) ...quotedMessages,
if (threads) ...threadMessages
];
final reaction = Reaction(
type: 'type',
messageId: allMessages.first.id,
user: users.first,
);
await database.userDao.updateUsers(users);
await database.channelDao.updateChannels(channels);
await pinnedMessageDao.updateMessages(cid, allMessages);
await database.pinnedMessageReactionDao.updateReactions([reaction]);
return allMessages;
}
test('deleteMessageByIds', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Preparing test data
final insertedMessages = await _prepareTestData(cid);
@@ -98,66 +106,117 @@ void main() {
final messages = await pinnedMessageDao.getMessagesByCid(cid);
expect(messages.length, insertedMessages.length);
final firstMessageId = messages.first.id;
// Fetched reactions list should have one reaction for given message id
final reactions =
await database.pinnedMessageReactionDao.getReactions(firstMessageId);
expect(reactions.length, 1);
// Deleting 2 messages from DB
await pinnedMessageDao.deleteMessageByIds(
['testMessageId${cid}0', 'testMessageId${cid}1'],
[firstMessageId, 'testMessageId${cid}1'],
);
// New fetched messages length should 2 less than the
// previous fetched messages
final newMessages = await pinnedMessageDao.getMessagesByCid(cid);
expect(newMessages.length, messages.length - 2);
// Reaction for the first message should be deleted too
final newReactions =
await database.pinnedMessageReactionDao.getReactions(firstMessageId);
expect(newReactions, isEmpty);
});
group('deleteMessageByCids', () {
const cid1 = 'testCid1';
const cid2 = 'testCid2';
const cid1 = 'test:Cid1';
const cid2 = 'test:Cid2';
test('should delete all the messages of first channel', () async {
// Preparing test data
final cid1InsertedMessages = await _prepareTestData(cid1);
final cid2InsertedMessages = await _prepareTestData(cid2);
test(
'should delete all the messages and reactions of first channel',
() async {
// Preparing test data
final cid1InsertedMessages = await _prepareTestData(cid1);
final cid2InsertedMessages = await _prepareTestData(cid2);
// Fetched message list should match the test message list length
final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1);
final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2);
expect(cid1Messages.length, cid1InsertedMessages.length);
expect(cid2Messages.length, cid2InsertedMessages.length);
// Fetched message list should match the test message list length
final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1);
final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2);
expect(cid1Messages.length, cid1InsertedMessages.length);
expect(cid2Messages.length, cid2InsertedMessages.length);
// Deleting all the messages of cid1
await pinnedMessageDao.deleteMessageByCids([cid1]);
// Fetched reactions list should have one reaction for given message id
final cid1firstMessageId = cid1Messages.first.id;
final cid1Reactions = await database.pinnedMessageReactionDao
.getReactions(cid1firstMessageId);
expect(cid1Reactions.length, 1);
// Fetched messages length of only cid1 should be empty
final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1);
final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2);
expect(cid1FetchedMessages, isEmpty);
expect(cid2FetchedMessages, isNotEmpty);
});
// Deleting all the messages of cid1
await pinnedMessageDao.deleteMessageByCids([cid1]);
test('should delete all the messages of both channel', () async {
// Preparing test data
final cid1InsertedMessages = await _prepareTestData(cid1);
final cid2InsertedMessages = await _prepareTestData(cid2);
// Fetched messages length of only cid1 should be empty
final cid1FetchedMessages =
await pinnedMessageDao.getMessagesByCid(cid1);
final cid2FetchedMessages =
await pinnedMessageDao.getMessagesByCid(cid2);
expect(cid1FetchedMessages, isEmpty);
expect(cid2FetchedMessages, isNotEmpty);
// Fetched message list should match the test message list length
final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1);
final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2);
expect(cid1Messages.length, cid1InsertedMessages.length);
expect(cid2Messages.length, cid2InsertedMessages.length);
// Reaction for the first message should be deleted too
final cid1FetchedReactions = await database.pinnedMessageReactionDao
.getReactions(cid1firstMessageId);
expect(cid1FetchedReactions, isEmpty);
},
);
// Deleting all the messages of cid1
await pinnedMessageDao.deleteMessageByCids([cid1, cid2]);
test(
'should delete all the messages and reactions of both channel',
() async {
// Preparing test data
final cid1InsertedMessages = await _prepareTestData(cid1);
final cid2InsertedMessages = await _prepareTestData(cid2);
// Fetched messages length of both cid1 and cid2 should be empty
final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1);
final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2);
expect(cid1FetchedMessages, isEmpty);
expect(cid2FetchedMessages, isEmpty);
});
// Fetched message list should match the test message list length
final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1);
final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2);
expect(cid1Messages.length, cid1InsertedMessages.length);
expect(cid2Messages.length, cid2InsertedMessages.length);
// Fetched reactions list should have one reaction for given message id
final cid1FirstMessageId = cid1Messages.first.id;
final cid1Reactions = await database.pinnedMessageReactionDao
.getReactions(cid1FirstMessageId);
expect(cid1Reactions.length, 1);
final cid2FirstMessageId = cid2Messages.first.id;
final cid2Reactions = await database.pinnedMessageReactionDao
.getReactions(cid2FirstMessageId);
expect(cid2Reactions.length, 1);
// Deleting all the messages of cid1
await pinnedMessageDao.deleteMessageByCids([cid1, cid2]);
// Fetched messages length of both cid1 and cid2 should be empty
final cid1FetchedMessages =
await pinnedMessageDao.getMessagesByCid(cid1);
final cid2FetchedMessages =
await pinnedMessageDao.getMessagesByCid(cid2);
expect(cid1FetchedMessages, isEmpty);
expect(cid2FetchedMessages, isEmpty);
// Reaction for the first message should be deleted too
final cid1FetchedReactions = await database.pinnedMessageReactionDao
.getReactions(cid1FirstMessageId);
expect(cid1FetchedReactions, isEmpty);
final cid2FetchedReactions = await database.pinnedMessageReactionDao
.getReactions(cid2FirstMessageId);
expect(cid2FetchedReactions, isEmpty);
},
);
});
test('getMessageById', () async {
const cid = 'testCid';
const cid = 'test:Cid';
const id = 'testMessageId${cid}0';
// Should be null initially
@@ -175,7 +234,7 @@ void main() {
});
test('getThreadMessages', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Messages should be empty initially
final messages = await pinnedMessageDao.getThreadMessages(cid);
@@ -194,7 +253,7 @@ void main() {
});
test('getThreadMessagesByParentId', () async {
const cid = 'testCid';
const cid = 'test:Cid';
const parentId = 'testMessageId${cid}0';
// Messages should be empty initially
@@ -214,7 +273,7 @@ void main() {
});
test('getThreadMessagesByParentId along with pagination', () async {
const cid = 'testCid';
const cid = 'test:Cid';
const parentId = 'testMessageId${cid}0';
const options = PaginationParams(
limit: 15,
@@ -248,7 +307,7 @@ void main() {
});
test('getMessagesByCid', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Should be empty initially
final messages = await pinnedMessageDao.getMessagesByCid(cid);
@@ -269,7 +328,7 @@ void main() {
});
test('getMessagesByCid along with quotedMessage', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Should be empty initially
final messages = await pinnedMessageDao.getMessagesByCid(cid);
@@ -287,7 +346,7 @@ void main() {
});
test('getMessagesByCid along with pagination', () async {
const cid = 'testCid';
const cid = 'test:Cid';
const limit = 15;
const lessThan = 'testMessageId${cid}25';
const greaterThanOrEqual = 'testMessageId${cid}5';
@@ -319,7 +378,7 @@ void main() {
});
test('updateMessages', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Preparing test data
final insertedMessages = await _prepareTestData(cid);
@@ -0,0 +1,205 @@
import 'dart:math' as math;
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/dao/pinned_message_reaction_dao.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:test/test.dart';
import '../../stream_chat_persistence_client_test.dart';
void main() {
late PinnedMessageReactionDao pinnedMessageReactionDao;
late MoorChatDatabase database;
setUp(() {
database = testDatabaseProvider('testUserId');
pinnedMessageReactionDao = database.pinnedMessageReactionDao;
});
Future<List<Reaction>> _prepareReactionData(
String messageId, {
String? userId,
int count = 3,
}) async {
const cid = 'test:Cid';
final channels = [ChannelModel(cid: cid)];
final users = List.generate(count, (index) => User(id: 'testUserId$index'));
final message = Message(
id: messageId,
type: 'testType',
user: users.first,
createdAt: DateTime.now(),
shadowed: math.Random().nextBool(),
showInChannel: math.Random().nextBool(),
replyCount: 3,
updatedAt: DateTime.now(),
extraData: const {'extra_test_field': 'extraTestData'},
text: 'Dummy text',
pinned: math.Random().nextBool(),
pinnedAt: DateTime.now(),
pinnedBy: users.first,
);
final reactions = List.generate(
count,
(index) => Reaction(
type: 'testType$index',
createdAt: DateTime.now(),
userId: userId ?? users[index].id,
messageId: message.id,
score: count + 3,
extraData: {'extra_test_field': 'extraTestData'},
),
);
await database.userDao.updateUsers(users);
await database.channelDao.updateChannels(channels);
await database.pinnedMessageDao.updateMessages(cid, [message]);
await pinnedMessageReactionDao.updateReactions(reactions);
return reactions;
}
test('getReactions', () async {
const messageId = 'testMessageId';
// Should be empty initially
final reactions = await pinnedMessageReactionDao.getReactions(messageId);
expect(reactions, isEmpty);
// Adding sample reactions
final insertedReactions = await _prepareReactionData(messageId);
expect(insertedReactions, isNotEmpty);
// Fetched reaction length should match inserted reactions length.
// Every reaction messageId should match the provided messageId.
final fetchedReactions =
await pinnedMessageReactionDao.getReactions(messageId);
expect(fetchedReactions.length, insertedReactions.length);
expect(fetchedReactions.every((it) => it.messageId == messageId), true);
});
test('getReactionsByUserId', () async {
const messageId = 'testMessageId';
const userId = 'testUserId';
// Should be empty initially
final reactions =
await pinnedMessageReactionDao.getReactionsByUserId(messageId, userId);
expect(reactions, isEmpty);
// Adding sample reactions
final insertedReactions =
await _prepareReactionData(messageId, userId: userId);
expect(insertedReactions, isNotEmpty);
// Fetched reaction length should match inserted reactions length.
// Every reaction messageId should match the provided messageId.
// Every reaction userId should match the provided userId.
final fetchedReactions =
await pinnedMessageReactionDao.getReactionsByUserId(messageId, userId);
expect(fetchedReactions.length, insertedReactions.length);
expect(fetchedReactions.every((it) => it.messageId == messageId), true);
expect(fetchedReactions.every((it) => it.userId == userId), true);
});
test('updateReactions', () async {
const messageId = 'testMessageId';
// Preparing test data
final reactions = await _prepareReactionData(messageId);
// Modifying one of the reaction and also adding one new
final copyReaction = reactions.first.copyWith(score: 33);
final newReaction = Reaction(
type: 'testType3',
createdAt: DateTime.now(),
userId: 'testUserId3',
messageId: messageId,
score: 30,
extraData: {'extra_test_field': 'extraTestData'},
);
await pinnedMessageReactionDao.updateReactions([copyReaction, newReaction]);
// Fetched reaction length should be one more than inserted reactions.
// copyReaction `score` modified field should be 33.
// Fetched reactions should contain the newReaction.
final fetchedReactions =
await pinnedMessageReactionDao.getReactions(messageId);
expect(fetchedReactions.length, reactions.length + 1);
expect(
fetchedReactions
.firstWhere((it) =>
it.userId == copyReaction.userId && it.type == copyReaction.type)
.score,
33,
);
expect(
fetchedReactions
.where((it) =>
it.userId == newReaction.userId && it.type == newReaction.type)
.isNotEmpty,
true,
);
});
group('deleteReactionsByMessageIds', () {
const messageId1 = 'testMessageId1';
const messageId2 = 'testMessageId2';
test('should delete all the reactions of first message', () async {
// Preparing test data
final insertedReactions1 = await _prepareReactionData(messageId1);
final insertedReactions2 = await _prepareReactionData(messageId2);
// Fetched reaction list length should match
// the inserted reactions list length
final reactions1 =
await pinnedMessageReactionDao.getReactions(messageId1);
final reactions2 =
await pinnedMessageReactionDao.getReactions(messageId2);
expect(reactions1.length, insertedReactions1.length);
expect(reactions2.length, insertedReactions2.length);
// Deleting all the reactions of messageId1
await pinnedMessageReactionDao.deleteReactionsByMessageIds([messageId1]);
// Fetched reactions length of only messageId1 should be empty
final fetchedReactions1 =
await pinnedMessageReactionDao.getReactions(messageId1);
final fetchedReactions2 =
await pinnedMessageReactionDao.getReactions(messageId2);
expect(fetchedReactions1, isEmpty);
expect(fetchedReactions2, isNotEmpty);
});
test('should delete all the messages of both message', () async {
// Preparing test data
final insertedReactions1 = await _prepareReactionData(messageId1);
final insertedReactions2 = await _prepareReactionData(messageId2);
// Fetched reaction list length should match
// the inserted reactions list length
final reactions1 =
await pinnedMessageReactionDao.getReactions(messageId1);
final reactions2 =
await pinnedMessageReactionDao.getReactions(messageId2);
expect(reactions1.length, insertedReactions1.length);
expect(reactions2.length, insertedReactions2.length);
// Deleting all the reactions of messageId1 and messageId2
await pinnedMessageReactionDao
.deleteReactionsByMessageIds([messageId1, messageId2]);
// Fetched reactions length of both messages should be empty
final fetchedReactions1 =
await pinnedMessageReactionDao.getReactions(messageId1);
final fetchedReactions2 =
await pinnedMessageReactionDao.getReactions(messageId2);
expect(fetchedReactions1, isEmpty);
expect(fetchedReactions2, isEmpty);
});
});
tearDown(() async {
await database.disconnect();
});
}
@@ -21,6 +21,8 @@ void main() {
String? userId,
int count = 3,
}) async {
const cid = 'test:Cid';
final channels = [ChannelModel(cid: cid)];
final users = List.generate(count, (index) => User(id: 'testUserId$index'));
final message = Message(
id: messageId,
@@ -50,7 +52,8 @@ void main() {
);
await database.userDao.updateUsers(users);
await database.messageDao.updateMessages('testCid', [message]);
await database.channelDao.updateChannels(channels);
await database.messageDao.updateMessages(cid, [message]);
await reactionDao.updateReactions(reactions);
return reactions;
@@ -16,6 +16,7 @@ void main() {
});
Future<List<Read>> _prepareReadData(String cid, {int count = 3}) async {
final channels = [ChannelModel(cid: cid)];
final users = List.generate(count, (index) => User(id: 'testUserId$index'));
final reads = List.generate(
count,
@@ -27,12 +28,13 @@ void main() {
);
await database.userDao.updateUsers(users);
await database.channelDao.updateChannels(channels);
await readDao.updateReads(cid, reads);
return reads;
}
test('getReadsByCid', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Should be empty initially
final reads = await readDao.getReadsByCid(cid);
@@ -55,7 +57,7 @@ void main() {
});
test('updateReads', () async {
const cid = 'testCid';
const cid = 'test:Cid';
// Preparing test data
final insertedReads = await _prepareReadData(cid);
@@ -0,0 +1,52 @@
import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/mapper/pinned_message_reaction_mapper.dart';
import '../utils/date_matcher.dart';
void main() {
test('toReaction should map the entity into Reaction', () {
final user = User(id: 'testUserId');
final message = Message(id: 'testMessageId');
final entity = PinnedMessageReactionEntity(
userId: user.id,
messageId: message.id,
type: 'haha',
score: 33,
createdAt: DateTime.now(),
extraData: {'extra_test_data': 'extraData'},
);
final reaction = entity.toReaction(user: user);
expect(reaction, isA<Reaction>());
expect(reaction.userId, entity.userId);
expect(reaction.messageId, entity.messageId);
expect(reaction.type, entity.type);
expect(reaction.score, entity.score);
expect(reaction.createdAt, isSameDateAs(entity.createdAt));
expect(reaction.extraData, entity.extraData);
});
test('toEntity should map reaction into PinnedMessageReactionEntity', () {
final user = User(id: 'testUserId');
final message = Message(id: 'testMessageId');
final reaction = Reaction(
userId: user.id,
messageId: message.id,
type: 'haha',
score: 33,
createdAt: DateTime.now(),
extraData: {'extra_test_data': 'extraData'},
);
final entity = reaction.toPinnedEntity();
expect(entity, isA<PinnedMessageReactionEntity>());
expect(entity.userId, reaction.userId);
expect(entity.messageId, reaction.messageId);
expect(entity.type, reaction.type);
expect(entity.score, reaction.score);
expect(entity.createdAt, isSameDateAs(reaction.createdAt));
expect(entity.extraData, reaction.extraData);
});
}
@@ -397,23 +397,26 @@ void main() {
test('updateMessages', () async {
const cid = 'testCid';
final messages = List.generate(3, (index) => Message());
when(() => mockDatabase.messageDao.updateMessages(cid, messages))
when(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages}))
.thenAnswer((_) => Future.value());
await client.updateMessages(cid, messages);
verify(() => mockDatabase.messageDao.updateMessages(cid, messages))
verify(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages}))
.called(1);
});
test('updatePinnedMessages', () async {
const cid = 'testCid';
final messages = List.generate(3, (index) => Message());
when(() => mockDatabase.pinnedMessageDao.updateMessages(cid, messages))
.thenAnswer((_) => Future.value());
when(
() => mockDatabase.pinnedMessageDao.bulkUpdateMessages({cid: messages}),
).thenAnswer((_) => Future.value());
await client.updatePinnedMessages(cid, messages);
verify(() => mockDatabase.pinnedMessageDao.updateMessages(cid, messages))
.called(1);
verify(
() => mockDatabase.pinnedMessageDao.bulkUpdateMessages({cid: messages}),
).called(1);
});
test('getChannelThreads', () async {
@@ -456,11 +459,11 @@ void main() {
test('updateMembers', () async {
const cid = 'testCid';
final members = List.generate(3, (index) => Member());
when(() => mockDatabase.memberDao.updateMembers(cid, members))
when(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members}))
.thenAnswer((_) => Future.value());
await client.updateMembers(cid, members);
verify(() => mockDatabase.memberDao.updateMembers(cid, members))
verify(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members}))
.called(1);
});
@@ -473,11 +476,12 @@ void main() {
lastRead: DateTime.now(),
),
);
when(() => mockDatabase.readDao.updateReads(cid, reads))
when(() => mockDatabase.readDao.bulkUpdateReads({cid: reads}))
.thenAnswer((_) => Future.value());
await client.updateReads(cid, reads);
verify(() => mockDatabase.readDao.updateReads(cid, reads)).called(1);
verify(() => mockDatabase.readDao.bulkUpdateReads({cid: reads}))
.called(1);
});
test('updateUsers', () async {
@@ -502,6 +506,21 @@ void main() {
.called(1);
});
test('updatePinnedMessageReactions', () async {
final reactions = List.generate(
3,
(index) => Reaction(type: 'testType$index'),
);
when(() =>
mockDatabase.pinnedMessageReactionDao.updateReactions(reactions))
.thenAnswer((_) => Future.value());
await client.updatePinnedMessageReactions(reactions);
verify(() =>
mockDatabase.pinnedMessageReactionDao.updateReactions(reactions))
.called(1);
});
test('deleteReactionsByMessageId', () async {
final messageIds = <String>[];
when(() =>
@@ -514,6 +533,17 @@ void main() {
.called(1);
});
test('deletePinnedMessageReactionsByMessageId', () async {
final messageIds = <String>[];
when(() => mockDatabase.pinnedMessageReactionDao
.deleteReactionsByMessageIds(messageIds))
.thenAnswer((_) => Future.value());
await client.deletePinnedMessageReactionsByMessageId(messageIds);
verify(() => mockDatabase.pinnedMessageReactionDao
.deleteReactionsByMessageIds(messageIds)).called(1);
});
test('deleteMembersByCids', () async {
final cids = <String>[];
when(() => mockDatabase.memberDao.deleteMemberByCids(cids))