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