diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index 9d169a80..e9d72ecf 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -107,6 +107,48 @@ The 'exists' filter matches values that exist, or don't exist, based on the spec 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 #### Filter.and diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 1f7d7bce..9ec385ab 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,8 +1,20 @@ ## Upcoming +🛑️ Breaking Changes from `2.2.1` + +- Added 6 new methods in `ChatPersistenceClient`. + - `bulkUpdateMessages` + - `bulkUpdatePinnedMessages` + - `bulkUpdateMembers` + - `bulkUpdateReads` + - `updatePinnedMessageReactions` + - `deletePinnedMessageReactionsByMessageId` + ✅ Added -- Add support for `next`, `previous` value pagination in `client.search`, [read more.](https://getstream.io/chat/docs/other-rest/search/#pagination) +- Added `Filter.contains` and `Filter.empty` +- Added support for `next`, `previous` value pagination in `client.search` + , [read more.](https://getstream.io/chat/docs/other-rest/search/#pagination) ## 2.2.1 diff --git a/packages/stream_chat/lib/src/core/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart index eb3932ff..d4536eab 100644 --- a/packages/stream_chat/lib/src/core/models/filter.dart +++ b/packages/stream_chat/lib/src/core/models/filter.dart @@ -51,6 +51,9 @@ enum FilterOperator { /// Matches none of the values specified in an array. nor, + + /// Matches any list that contains the specified value + contains, } /// Helper extension for [FilterOperator] @@ -71,6 +74,7 @@ extension FilterOperatorX on FilterOperator { FilterOperator.and: '\$and', FilterOperator.or: '\$or', FilterOperator.nor: '\$nor', + FilterOperator.contains: '\$contains', }[this]!; } @@ -157,6 +161,10 @@ class Filter extends Equatable { factory Filter.exists(String key, {bool exists = true}) => Filter._(operator: FilterOperator.exists, key: key, value: exists); + /// Matches any list that contains the specified values + factory Filter.contains(String key, Object value) => + Filter._(operator: FilterOperator.contains, key: key, value: value); + /// Creates a custom [Filter] if there isn't one already available. const factory Filter.custom({ required Object value, @@ -164,6 +172,9 @@ class Filter extends Equatable { String? key, }) = Filter.__; + /// An empty filter + factory Filter.empty() => const Filter.raw(value: {}); + /// Creates a custom [Filter] from a raw map value /// /// ```dart diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 2470e403..aa0ac245 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -143,11 +143,19 @@ abstract class ChatPersistenceClient { /// Updates the message data of a particular channel [cid] with /// the new [messages] data - Future updateMessages(String cid, List messages); + Future updateMessages(String cid, List messages) => + bulkUpdateMessages({cid: messages}); + + /// Bulk updates the message data of multiple channels. + Future bulkUpdateMessages(Map> messages); /// Updates the pinned message data of a particular channel [cid] with /// the new [messages] data - Future updatePinnedMessages(String cid, List messages); + Future updatePinnedMessages(String cid, List messages) => + bulkUpdatePinnedMessages({cid: messages}); + + /// Bulk updates the message data of multiple channels. + Future bulkUpdatePinnedMessages(Map> messages); /// Returns all the threads by parent message of a particular channel by /// providing channel [cid] @@ -158,11 +166,19 @@ abstract class ChatPersistenceClient { /// Updates all the members of a particular channle [cid] /// with the new [members] data - Future updateMembers(String cid, List members); + Future updateMembers(String cid, List members) => + bulkUpdateMembers({cid: members}); + + /// Bulk updates the members data of multiple channels. + Future bulkUpdateMembers(Map> members); /// Updates the read data of a particular channel [cid] with /// the new [reads] data - Future updateReads(String cid, List reads); + Future updateReads(String cid, List reads) => + bulkUpdateReads({cid: reads}); + + /// Bulk updates the read data of multiple channels. + Future bulkUpdateReads(Map> reads); /// Updates the users data with the new [users] data Future updateUsers(List users); @@ -170,9 +186,15 @@ abstract class ChatPersistenceClient { /// Updates the reactions data with the new [reactions] data Future updateReactions(List reactions); + /// Updates the pinned message reactions data with the new [reactions] data + Future updatePinnedMessageReactions(List reactions); + /// Deletes all the reactions by [messageIds] Future deleteReactionsByMessageId(List messageIds); + /// Deletes all the pinned messages reactions by [messageIds] + Future deletePinnedMessageReactionsByMessageId(List messageIds); + /// Deletes all the members by channel [cids] Future deleteMembersByCids(List cids); @@ -182,85 +204,92 @@ abstract class ChatPersistenceClient { /// Update list of channel states Future updateChannelStates(List channelStates) async { - final deleteReactions = deleteReactionsByMessageId(channelStates - .expand((it) => it.messages) - .map((m) => m.id) - .toList(growable: false)); + final reactionsToDelete = []; + final pinnedReactionsToDelete = []; + final membersToDelete = []; - final cleanedChannelStates = - channelStates.where((it) => it.channel != null); + final channels = []; + final channelWithMessages = >{}; + final channelWithPinnedMessages = >{}; + final channelWithReads = >{}; + final channelWithMembers = >{}; - final deleteMembers = deleteMembersByCids( - cleanedChannelStates.map((it) => it.channel!.cid).toList(growable: false), - ); + final users = []; + final reactions = []; + final pinnedReactions = []; + 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 expandReactions(Message message) { + final own = message.ownReactions; + final latest = message.latestReactions; + return [ + if (own != null) ...own.where((r) => r.userId != null), + if (latest != null) ...latest.where((r) => r.userId != null), + ]; + } + + reactions.addAll(messages.expand(expandReactions)); + pinnedReactions.addAll(pinnedMessages.expand(expandReactions)); + + users.addAll([ + channel.createdBy, + ...reads.map((it) => it.user), + ...members.map((it) => it.user), + ...reactions.map((it) => it.user), + ...pinnedReactions.map((it) => it.user), + ].withNullifyer); + } + } + + // Removing old members and reactions data as they may have + // changes over the time. await Future.wait([ - deleteReactions, - deleteMembers, + deleteMembersByCids(membersToDelete), + deleteReactionsByMessageId(reactionsToDelete), + deletePinnedMessageReactionsByMessageId(pinnedReactionsToDelete), ]); - final channels = cleanedChannelStates.map((it) => it.channel).withNullifyer; - - final reactions = cleanedChannelStates - .expand((it) => it.messages) - .expand((it) => [ - if (it.ownReactions != null) - ...it.ownReactions!.where((r) => r.userId != null), - if (it.latestReactions != null) - ...it.latestReactions!.where((r) => r.userId != null), - ]) - .withNullifyer; - - final users = cleanedChannelStates - .map((cs) => [ - cs.channel?.createdBy, - ...cs.messages - .map((m) => [ - m.user, - if (m.latestReactions != null) - ...m.latestReactions!.map((r) => r.user), - if (m.ownReactions != null) - ...m.ownReactions!.map((r) => r.user), - ]) - .expand((v) => v), - ...cs.read.map((r) => r.user), - ...cs.members.map((m) => m.user), - ]) - .expand((it) => it) - .withNullifyer; - - final updateMessagesFuture = cleanedChannelStates.map((it) { - final cid = it.channel!.cid; - final messages = it.messages; - return updateMessages(cid, messages.toList(growable: false)); - }).toList(growable: false); - - final updatePinnedMessagesFuture = cleanedChannelStates.map((it) { - final cid = it.channel!.cid; - final messages = it.pinnedMessages; - return updatePinnedMessages(cid, messages.toList(growable: false)); - }).toList(growable: false); - - final updateReadsFuture = cleanedChannelStates.map((it) { - final cid = it.channel!.cid; - final reads = it.read; - return updateReads(cid, reads.toList(growable: false)); - }).toList(growable: false); - - final updateMembersFuture = cleanedChannelStates.map((it) { - final cid = it.channel!.cid; - final members = it.members; - return updateMembers(cid, members.toList(growable: false)); - }).toList(growable: false); - + // Updating first as does not depend on any other table. await Future.wait([ - ...updateMessagesFuture, - ...updatePinnedMessagesFuture, - ...updateReadsFuture, - ...updateMembersFuture, updateUsers(users.toList(growable: false)), updateChannels(channels.toList(growable: false)), + ]); + + // All has a foreign key relation with channels table. + await Future.wait([ + bulkUpdateReads(channelWithReads), + bulkUpdateMembers(channelWithMembers), + bulkUpdateMessages(channelWithMessages), + bulkUpdatePinnedMessages(channelWithPinnedMessages), + ]); + + // Both has a foreign key relation with messages, pinnedMessages table. + await Future.wait([ updateReactions(reactions.toList(growable: false)), + updatePinnedMessageReactions( + pinnedReactions.toList(growable: false), + ), ]); } } diff --git a/packages/stream_chat/test/src/core/models/filter_test.dart b/packages/stream_chat/test/src/core/models/filter_test.dart index d60ebb50..6e3dbf64 100644 --- a/packages/stream_chat/test/src/core/models/filter_test.dart +++ b/packages/stream_chat/test/src/core/models/filter_test.dart @@ -138,6 +138,20 @@ void main() { expect(filter.value, value); }); + test('empty', () { + final filter = Filter.empty(); + expect(filter.value, {}); + }); + + test('contains', () { + const key = 'testKey'; + const values = 'testValue'; + final filter = Filter.contains(key, values); + expect(filter.key, key); + expect(filter.value, values); + expect(filter.operator, FilterOperator.contains.rawValue); + }); + group('groupedOperator', () { final filter1 = Filter.equal('testKey', 'testValue'); final filter2 = Filter.in_('testKey', const ['testValue']); diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index 22aee1a9..897bbcb0 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -38,6 +38,11 @@ class TestPersistenceClient extends ChatPersistenceClient { Future deleteReactionsByMessageId(List messageIds) => Future.value(); + @override + Future deletePinnedMessageReactionsByMessageId( + List messageIds) => + Future.value(); + @override Future disconnect({bool flush = false}) => throw UnimplementedError(); @@ -101,26 +106,30 @@ class TestPersistenceClient extends ChatPersistenceClient { Future updateLastSyncAt(DateTime lastSyncAt) => throw UnimplementedError(); - @override - Future updateMembers(String cid, List members) => - Future.value(); - - @override - Future updateMessages(String cid, List messages) => - Future.value(); - - @override - Future updatePinnedMessages(String cid, List messages) => - Future.value(); - @override Future updateReactions(List reactions) => Future.value(); @override - Future updateReads(String cid, List reads) => Future.value(); + Future updatePinnedMessageReactions(List reactions) => + Future.value(); @override Future updateUsers(List users) => Future.value(); + + @override + Future bulkUpdateMembers(Map> members) => + Future.value(); + + @override + Future bulkUpdateMessages(Map> messages) => + Future.value(); + + @override + Future bulkUpdatePinnedMessages(Map> messages) => + Future.value(); + + @override + Future bulkUpdateReads(Map> reads) => Future.value(); } void main() { diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index ace5f310..59b4a73a 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -4,8 +4,8 @@ import 'package:stream_chat_flutter/src/group_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image_paint.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png) /// /// It shows the current [Channel] image. /// diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index ada04271..1c12b132 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -8,8 +8,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png) /// /// It shows the current [Channel] information. /// diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index d6cd454b..35fc6d73 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -22,8 +22,8 @@ typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); /// Callback for when 'View Info' is tapped typedef ViewInfoCallback = void Function(Channel); -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_list_view.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_list_view_paint.png) /// /// It shows the list of current channels. /// diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index f3984ab9..09cbfcc4 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -8,8 +8,8 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png) /// /// It shows the current [Channel] preview. /// diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 651a204c..9646608c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -110,11 +110,14 @@ const _kMinMediaPickerSize = 360.0; const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes /// Inactive state -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input_paint.png) +/// +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input_paint.png) +/// /// Focused state -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2_paint.png) +/// +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2_paint.png) /// /// Widget used to enter the message and add attachments /// diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 731742b7..ab4ae04f 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -91,8 +91,8 @@ class MessageDetails { final int index; } -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png) /// /// It shows the list of messages of the current channel. /// diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index ca1aa0bf..7619f21b 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -40,8 +40,8 @@ enum DisplayWidget { show, } -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_widget.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_widget_paint.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.png) /// /// It shows a message with reactions, replies and user avatar. /// diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 3e1e5e50..b5a685bd 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker_paint.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png) /// /// It shows a reaction picker /// diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index abbd9bf0..55a270fc 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -4,8 +4,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.png) /// /// It shows the current thread information. /// diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 85b9e486..139056bd 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,10 @@ +## Upcoming + +- [[#604]](https://github.com/GetStream/stream-chat-flutter/issues/604) Fix cascade deletion by + enabling `pragma foreign_keys`. +- Added a new table `PinnedMessageReactions` and dao `PinnedMessageReactionDao` specifically for pinned messages. +- Updated `stream_chat`: `^2.2.0` -> `TODO` + ## 2.2.0 - Updated llc dependency @@ -11,14 +18,17 @@ ## 2.1.0 ✅ Added + - Added support for `Message.i18n` - Added support for `User.language` ## 2.0.0 + * Migrate this package to null safety * Minor fixes and improvements ## 2.0.0-nullsafety.8 + * Updated llc dependency * Upgraded moor dependencies and generated files with the latest dependency diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart index fc488511..36013f51 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart @@ -42,10 +42,9 @@ class ChannelDao extends DatabaseAccessor /// Updates all the channels using the new [channelList] data Future updateChannels(List channelList) => batch( - (it) => it.insertAll( + (it) => it.insertAllOnConflictUpdate( channels, channelList.map((c) => c.toEntity()).toList(), - mode: InsertMode.insertOrReplace, ), ); } diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index c1781732..d1ed0925 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -45,13 +45,12 @@ class ChannelQueryDao extends DatabaseAccessor } await batch((it) { - it.insertAll( + it.insertAllOnConflictUpdate( channelQueries, cids .map((cid) => ChannelQueryEntity(queryHash: hash, channelCid: cid)) .toList(), - mode: InsertMode.insertOrReplace, ); }); }); diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart index 0cef23b0..88a03b29 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart @@ -26,7 +26,7 @@ class ConnectionEventDao extends DatabaseAccessor /// Update stored connection event with latest data Future updateConnectionEvent(Event event) => transaction(() async { final connectionInfo = await select(connectionEvents).getSingleOrNull(); - return into(connectionEvents).insert( + return into(connectionEvents).insertOnConflictUpdate( ConnectionEventEntity( id: 1, type: event.type, @@ -38,7 +38,6 @@ class ConnectionEventDao extends DatabaseAccessor unreadChannels: event.unreadChannels ?? connectionInfo?.unreadChannels, ), - mode: InsertMode.insertOrReplace, ); }); diff --git a/packages/stream_chat_persistence/lib/src/dao/dao.dart b/packages/stream_chat_persistence/lib/src/dao/dao.dart index 53c2f372..56998c98 100644 --- a/packages/stream_chat_persistence/lib/src/dao/dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/dao.dart @@ -4,6 +4,7 @@ export 'connection_event_dao.dart'; export 'member_dao.dart'; export 'message_dao.dart'; export 'pinned_message_dao.dart'; +export 'pinned_message_reaction_dao.dart'; export 'reaction_dao.dart'; export 'read_dao.dart'; export 'user_dao.dart'; diff --git a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index 157b0a9a..e471b1b5 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart @@ -30,14 +30,19 @@ class MemberDao extends DatabaseAccessor }).get(); /// Updates all the members using the new [memberList] data - Future updateMembers(String cid, List memberList) async => - batch( - (it) => it.insertAll( - members, - memberList.map((m) => m.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, - ), - ); + Future updateMembers(String cid, List memberList) => + bulkUpdateMembers({cid: memberList}); + + /// Bulk updates the members data of multiple channels + Future bulkUpdateMembers(Map> channelWithMembers) { + final entities = channelWithMembers.entries + .map((entry) => entry.value.map( + (member) => member.toEntity(cid: entry.key), + )) + .expand((it) => it) + .toList(growable: false); + return batch((batch) => batch.insertAllOnConflictUpdate(members, entities)); + } /// Deletes all the members whose [Members.channelCid] is present in [cids] Future deleteMemberByCids(List cids) async => batch((it) { diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index fcedc157..ef140239 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -170,13 +170,21 @@ class MessageDao extends DatabaseAccessor /// Updates the message data of a particular channel with /// the new [messageList] data - Future updateMessages(String cid, List messageList) => batch( - (batch) { - batch.insertAll( - messages, - messageList.map((it) => it.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, - ); - }, - ); + Future updateMessages(String cid, List messageList) => + bulkUpdateMessages({cid: messageList}); + + /// Bulk updates the message data of multiple channels + Future bulkUpdateMessages( + Map> 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), + ); + } } diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index af6497f9..9bc0fa64 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -39,8 +39,10 @@ class PinnedMessageDao extends DatabaseAccessor final userEntity = rows.readTableOrNull(users); final pinnedByEntity = rows.readTableOrNull(_pinnedByUsers); final msgEntity = rows.readTable(pinnedMessages); - final latestReactions = await _db.reactionDao.getReactions(msgEntity.id); - final ownReactions = await _db.reactionDao.getReactionsByUserId( + final latestReactions = + await _db.pinnedMessageReactionDao.getReactions(msgEntity.id); + final ownReactions = + await _db.pinnedMessageReactionDao.getReactionsByUserId( msgEntity.id, _db.userId, ); @@ -168,13 +170,21 @@ class PinnedMessageDao extends DatabaseAccessor /// Updates the message data of a particular channel with /// the new [messageList] data - Future updateMessages(String cid, List messageList) => batch( - (batch) { - batch.insertAll( - pinnedMessages, - messageList.map((it) => it.toPinnedEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, - ); - }, - ); + Future updateMessages(String cid, List messageList) => + bulkUpdateMessages({cid: messageList}); + + /// Bulk updates the message data of multiple channels + Future bulkUpdateMessages( + Map> 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), + ); + } } diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart new file mode 100644 index 00000000..932bb838 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart @@ -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 + 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> 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> 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 updateReactions(List reactionList) => batch((it) { + it.insertAllOnConflictUpdate( + pinnedMessageReactions, + reactionList.map((r) => r.toPinnedEntity()).toList(), + ); + }); + + /// Deletes all the reactions whose [Reactions.messageId] is + /// present in [messageIds] + Future deleteReactionsByMessageIds(List messageIds) => + batch((it) { + it.deleteWhere( + pinnedMessageReactions, + (r) => r.messageId.isIn(messageIds), + ); + }); +} diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart new file mode 100644 index 00000000..707a7c12 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pinned_message_reaction_dao.dart'; + +// ************************************************************************** +// DaoGenerator +// ************************************************************************** + +mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor { + $PinnedMessageReactionsTable get pinnedMessageReactions => + attachedDatabase.pinnedMessageReactions; + $UsersTable get users => attachedDatabase.users; +} diff --git a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart index 89da6081..c178ae03 100644 --- a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart @@ -42,10 +42,9 @@ class ReactionDao extends DatabaseAccessor /// Updates the reactions data with the new [reactionList] data Future updateReactions(List reactionList) => batch((it) { - it.insertAll( + it.insertAllOnConflictUpdate( reactions, reactionList.map((r) => r.toEntity()).toList(), - mode: InsertMode.insertOrReplace, ); }); diff --git a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart index 05d807fd..87bfa19b 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart @@ -29,11 +29,17 @@ class ReadDao extends DatabaseAccessor with _$ReadDaoMixin { /// Updates the read data of a particular channel with /// the new [readList] data - Future updateReads(String cid, List readList) => batch( - (it) => it.insertAll( - reads, - readList.map((r) => r.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, - ), - ); + Future updateReads(String cid, List readList) => + bulkUpdateReads({cid: readList}); + + /// Bulk updates the reads data of multiple channels + Future bulkUpdateReads(Map> 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)); + } } diff --git a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart index 9cdff8e7..b1e29e7b 100644 --- a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart @@ -14,10 +14,9 @@ class UserDao extends DatabaseAccessor with _$UserDaoMixin { /// Updates the users data with the new [userList] data Future updateUsers(List userList) => batch( - (it) => it.insertAll( + (it) => it.insertAllOnConflictUpdate( users, userList.map((u) => u.toEntity()).toList(), - mode: InsertMode.insertOrReplace, ), ); diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 7f5bd4d0..85f7b2de 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -14,6 +14,7 @@ part 'moor_chat_database.g.dart'; Channels, Messages, PinnedMessages, + PinnedMessageReactions, Reactions, Users, Members, @@ -25,6 +26,7 @@ part 'moor_chat_database.g.dart'; ChannelDao, MessageDao, PinnedMessageDao, + PinnedMessageReactionDao, MemberDao, ReactionDao, ReadDao, @@ -51,10 +53,13 @@ class MoorChatDatabase extends _$MoorChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 5; + int get schemaVersion => 6; @override MigrationStrategy get migration => MigrationStrategy( + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, onUpgrade: (openingDetails, before, after) async { if (before != after) { final m = createMigrator(); diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart index a61d1e6e..7360f9ab 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart @@ -645,7 +645,7 @@ class MessageEntity extends DataClass implements Insertable { final String? pinnedByUserId; /// The channel cid of which this message is part of - final String? channelCid; + final String channelCid; /// A Map of [messageText] translations. final Map? i18n; @@ -675,7 +675,7 @@ class MessageEntity extends DataClass implements Insertable { this.pinnedAt, this.pinExpires, this.pinnedByUserId, - this.channelCid, + required this.channelCid, this.i18n, this.extraData}); factory MessageEntity.fromData( @@ -728,7 +728,7 @@ class MessageEntity extends DataClass implements Insertable { pinnedByUserId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), channelCid: const StringType() - .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), + .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, i18n: $MessagesTable.$converter5.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}i18n'])), extraData: $MessagesTable.$converter6.mapToDart(const StringType() @@ -800,9 +800,7 @@ class MessageEntity extends DataClass implements Insertable { if (!nullToAbsent || pinnedByUserId != null) { map['pinned_by_user_id'] = Variable(pinnedByUserId); } - if (!nullToAbsent || channelCid != null) { - map['channel_cid'] = Variable(channelCid); - } + map['channel_cid'] = Variable(channelCid); if (!nullToAbsent || i18n != null) { final converter = $MessagesTable.$converter5; map['i18n'] = Variable(converter.mapToSql(i18n)); @@ -842,7 +840,7 @@ class MessageEntity extends DataClass implements Insertable { pinnedAt: serializer.fromJson(json['pinnedAt']), pinExpires: serializer.fromJson(json['pinExpires']), pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), - channelCid: serializer.fromJson(json['channelCid']), + channelCid: serializer.fromJson(json['channelCid']), i18n: serializer.fromJson?>(json['i18n']), extraData: serializer.fromJson?>(json['extraData']), ); @@ -873,7 +871,7 @@ class MessageEntity extends DataClass implements Insertable { 'pinnedAt': serializer.toJson(pinnedAt), 'pinExpires': serializer.toJson(pinExpires), 'pinnedByUserId': serializer.toJson(pinnedByUserId), - 'channelCid': serializer.toJson(channelCid), + 'channelCid': serializer.toJson(channelCid), 'i18n': serializer.toJson?>(i18n), 'extraData': serializer.toJson?>(extraData), }; @@ -902,7 +900,7 @@ class MessageEntity extends DataClass implements Insertable { Value pinnedAt = const Value.absent(), Value pinExpires = const Value.absent(), Value pinnedByUserId = const Value.absent(), - Value channelCid = const Value.absent(), + String? channelCid, Value?> i18n = const Value.absent(), Value?> extraData = const Value.absent()}) => MessageEntity( @@ -934,7 +932,7 @@ class MessageEntity extends DataClass implements Insertable { pinExpires: pinExpires.present ? pinExpires.value : this.pinExpires, pinnedByUserId: pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, - channelCid: channelCid.present ? channelCid.value : this.channelCid, + channelCid: channelCid ?? this.channelCid, i18n: i18n.present ? i18n.value : this.i18n, extraData: extraData.present ? extraData.value : this.extraData, ); @@ -1068,7 +1066,7 @@ class MessagesCompanion extends UpdateCompanion { final Value pinnedAt; final Value pinExpires; final Value pinnedByUserId; - final Value channelCid; + final Value channelCid; final Value?> i18n; final Value?> extraData; const MessagesCompanion({ @@ -1121,12 +1119,13 @@ class MessagesCompanion extends UpdateCompanion { this.pinnedAt = const Value.absent(), this.pinExpires = const Value.absent(), this.pinnedByUserId = const Value.absent(), - this.channelCid = const Value.absent(), + required String channelCid, this.i18n = const Value.absent(), this.extraData = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), - mentionedUsers = Value(mentionedUsers); + mentionedUsers = Value(mentionedUsers), + channelCid = Value(channelCid); static Insertable custom({ Expression? id, Expression? messageText, @@ -1150,7 +1149,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? pinnedAt, Expression? pinExpires, Expression? pinnedByUserId, - Expression? channelCid, + Expression? channelCid, Expression?>? i18n, Expression?>? extraData, }) { @@ -1206,7 +1205,7 @@ class MessagesCompanion extends UpdateCompanion { Value? pinnedAt, Value? pinExpires, Value? pinnedByUserId, - Value? channelCid, + Value? channelCid, Value?>? i18n, Value?>? extraData}) { return MessagesCompanion( @@ -1317,7 +1316,7 @@ class MessagesCompanion extends UpdateCompanion { map['pinned_by_user_id'] = Variable(pinnedByUserId.value); } if (channelCid.present) { - map['channel_cid'] = Variable(channelCid.value); + map['channel_cid'] = Variable(channelCid.value); } if (i18n.present) { final converter = $MessagesTable.$converter5; @@ -1491,11 +1490,10 @@ class $MessagesTable extends Messages typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); late final GeneratedColumn channelCid = GeneratedColumn( - 'channel_cid', aliasedName, true, + 'channel_cid', aliasedName, false, typeName: 'TEXT', - requiredDuringInsert: false, - $customConstraints: - 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); + requiredDuringInsert: true, + $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); final VerificationMeta _i18nMeta = const VerificationMeta('i18n'); late final GeneratedColumnWithTypeConverter, String?> i18n = GeneratedColumn('i18n', aliasedName, true, @@ -1634,6 +1632,8 @@ class $MessagesTable extends Messages _channelCidMeta, channelCid.isAcceptableOrUnknown( data['channel_cid']!, _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); } context.handle(_i18nMeta, const VerificationResult.success()); context.handle(_extraDataMeta, const VerificationResult.success()); @@ -1739,7 +1739,7 @@ class PinnedMessageEntity extends DataClass final String? pinnedByUserId; /// The channel cid of which this message is part of - final String? channelCid; + final String channelCid; /// A Map of [messageText] translations. final Map? i18n; @@ -1769,7 +1769,7 @@ class PinnedMessageEntity extends DataClass this.pinnedAt, this.pinExpires, this.pinnedByUserId, - this.channelCid, + required this.channelCid, this.i18n, this.extraData}); factory PinnedMessageEntity.fromData( @@ -1825,7 +1825,7 @@ class PinnedMessageEntity extends DataClass pinnedByUserId: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), channelCid: const StringType() - .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), + .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, i18n: $PinnedMessagesTable.$converter5.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}i18n'])), extraData: $PinnedMessagesTable.$converter6.mapToDart(const StringType() @@ -1897,9 +1897,7 @@ class PinnedMessageEntity extends DataClass if (!nullToAbsent || pinnedByUserId != null) { map['pinned_by_user_id'] = Variable(pinnedByUserId); } - if (!nullToAbsent || channelCid != null) { - map['channel_cid'] = Variable(channelCid); - } + map['channel_cid'] = Variable(channelCid); if (!nullToAbsent || i18n != null) { final converter = $PinnedMessagesTable.$converter5; map['i18n'] = Variable(converter.mapToSql(i18n)); @@ -1939,7 +1937,7 @@ class PinnedMessageEntity extends DataClass pinnedAt: serializer.fromJson(json['pinnedAt']), pinExpires: serializer.fromJson(json['pinExpires']), pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), - channelCid: serializer.fromJson(json['channelCid']), + channelCid: serializer.fromJson(json['channelCid']), i18n: serializer.fromJson?>(json['i18n']), extraData: serializer.fromJson?>(json['extraData']), ); @@ -1970,7 +1968,7 @@ class PinnedMessageEntity extends DataClass 'pinnedAt': serializer.toJson(pinnedAt), 'pinExpires': serializer.toJson(pinExpires), 'pinnedByUserId': serializer.toJson(pinnedByUserId), - 'channelCid': serializer.toJson(channelCid), + 'channelCid': serializer.toJson(channelCid), 'i18n': serializer.toJson?>(i18n), 'extraData': serializer.toJson?>(extraData), }; @@ -1999,7 +1997,7 @@ class PinnedMessageEntity extends DataClass Value pinnedAt = const Value.absent(), Value pinExpires = const Value.absent(), Value pinnedByUserId = const Value.absent(), - Value channelCid = const Value.absent(), + String? channelCid, Value?> i18n = const Value.absent(), Value?> extraData = const Value.absent()}) => PinnedMessageEntity( @@ -2031,7 +2029,7 @@ class PinnedMessageEntity extends DataClass pinExpires: pinExpires.present ? pinExpires.value : this.pinExpires, pinnedByUserId: pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, - channelCid: channelCid.present ? channelCid.value : this.channelCid, + channelCid: channelCid ?? this.channelCid, i18n: i18n.present ? i18n.value : this.i18n, extraData: extraData.present ? extraData.value : this.extraData, ); @@ -2165,7 +2163,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value pinnedAt; final Value pinExpires; final Value pinnedByUserId; - final Value channelCid; + final Value channelCid; final Value?> i18n; final Value?> extraData; const PinnedMessagesCompanion({ @@ -2218,12 +2216,13 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.pinnedAt = const Value.absent(), this.pinExpires = const Value.absent(), this.pinnedByUserId = const Value.absent(), - this.channelCid = const Value.absent(), + required String channelCid, this.i18n = const Value.absent(), this.extraData = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), - mentionedUsers = Value(mentionedUsers); + mentionedUsers = Value(mentionedUsers), + channelCid = Value(channelCid); static Insertable custom({ Expression? id, Expression? messageText, @@ -2247,7 +2246,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Expression? pinnedAt, Expression? pinExpires, Expression? pinnedByUserId, - Expression? channelCid, + Expression? channelCid, Expression?>? i18n, Expression?>? extraData, }) { @@ -2303,7 +2302,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Value? pinnedAt, Value? pinExpires, Value? pinnedByUserId, - Value? channelCid, + Value? channelCid, Value?>? i18n, Value?>? extraData}) { return PinnedMessagesCompanion( @@ -2414,7 +2413,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { map['pinned_by_user_id'] = Variable(pinnedByUserId.value); } if (channelCid.present) { - map['channel_cid'] = Variable(channelCid.value); + map['channel_cid'] = Variable(channelCid.value); } if (i18n.present) { final converter = $PinnedMessagesTable.$converter5; @@ -2589,11 +2588,10 @@ class $PinnedMessagesTable extends PinnedMessages typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); late final GeneratedColumn channelCid = GeneratedColumn( - 'channel_cid', aliasedName, true, + 'channel_cid', aliasedName, false, typeName: 'TEXT', - requiredDuringInsert: false, - $customConstraints: - 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); + requiredDuringInsert: true, + $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); final VerificationMeta _i18nMeta = const VerificationMeta('i18n'); late final GeneratedColumnWithTypeConverter, String?> i18n = GeneratedColumn('i18n', aliasedName, true, @@ -2734,6 +2732,8 @@ class $PinnedMessagesTable extends PinnedMessages _channelCidMeta, channelCid.isAcceptableOrUnknown( data['channel_cid']!, _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); } context.handle(_i18nMeta, const VerificationResult.success()); context.handle(_extraDataMeta, const VerificationResult.success()); @@ -2769,6 +2769,340 @@ class $PinnedMessagesTable extends PinnedMessages MapConverter(); } +class PinnedMessageReactionEntity extends DataClass + implements Insertable { + /// 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? extraData; + PinnedMessageReactionEntity( + {required this.userId, + required this.messageId, + required this.type, + required this.createdAt, + required this.score, + this.extraData}); + factory PinnedMessageReactionEntity.fromData( + Map 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 toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['message_id'] = Variable(messageId); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['score'] = Variable(score); + if (!nullToAbsent || extraData != null) { + final converter = $PinnedMessageReactionsTable.$converter0; + map['extra_data'] = Variable(converter.mapToSql(extraData)); + } + return map; + } + + factory PinnedMessageReactionEntity.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return PinnedMessageReactionEntity( + userId: serializer.fromJson(json['userId']), + messageId: serializer.fromJson(json['messageId']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + score: serializer.fromJson(json['score']), + extraData: serializer.fromJson?>(json['extraData']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'messageId': serializer.toJson(messageId), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'score': serializer.toJson(score), + 'extraData': serializer.toJson?>(extraData), + }; + } + + PinnedMessageReactionEntity copyWith( + {String? userId, + String? messageId, + String? type, + DateTime? createdAt, + int? score, + Value?> 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 { + final Value userId; + final Value messageId; + final Value type; + final Value createdAt; + final Value score; + final Value?> 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 custom({ + Expression? userId, + Expression? messageId, + Expression? type, + Expression? createdAt, + Expression? score, + Expression?>? 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? userId, + Value? messageId, + Value? type, + Value? createdAt, + Value? score, + Value?>? 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 toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (score.present) { + map['score'] = Variable(score.value); + } + if (extraData.present) { + final converter = $PinnedMessageReactionsTable.$converter0; + map['extra_data'] = + Variable(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 userId = GeneratedColumn( + 'user_id', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); + final VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); + late final GeneratedColumn messageId = GeneratedColumn( + '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 type = GeneratedColumn( + 'type', aliasedName, false, + typeName: 'TEXT', requiredDuringInsert: true); + final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + final VerificationMeta _scoreMeta = const VerificationMeta('score'); + late final GeneratedColumn score = GeneratedColumn( + 'score', aliasedName, false, + typeName: 'INTEGER', + requiredDuringInsert: false, + defaultValue: const Constant(0)); + final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); + late final GeneratedColumnWithTypeConverter, String?> + extraData = GeneratedColumn('extra_data', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>( + $PinnedMessageReactionsTable.$converter0); + @override + List 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 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 get $primaryKey => {messageId, type, userId}; + @override + PinnedMessageReactionEntity map(Map data, + {String? tablePrefix}) { + return PinnedMessageReactionEntity.fromData(data, _db, + prefix: tablePrefix != null ? '$tablePrefix.' : null); + } + + @override + $PinnedMessageReactionsTable createAlias(String alias) { + return $PinnedMessageReactionsTable(_db, alias); + } + + static TypeConverter, String> $converter0 = + MapConverter(); +} + class ReactionEntity extends DataClass implements Insertable { /// The id of the user that sent the reaction final String userId; @@ -4897,6 +5231,8 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase { late final $ChannelsTable channels = $ChannelsTable(this); late final $MessagesTable messages = $MessagesTable(this); late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this); + late final $PinnedMessageReactionsTable pinnedMessageReactions = + $PinnedMessageReactionsTable(this); late final $ReactionsTable reactions = $ReactionsTable(this); late final $UsersTable users = $UsersTable(this); late final $MembersTable members = $MembersTable(this); @@ -4909,6 +5245,8 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase { late final MessageDao messageDao = MessageDao(this as MoorChatDatabase); late final PinnedMessageDao pinnedMessageDao = PinnedMessageDao(this as MoorChatDatabase); + late final PinnedMessageReactionDao pinnedMessageReactionDao = + PinnedMessageReactionDao(this as MoorChatDatabase); late final MemberDao memberDao = MemberDao(this as MoorChatDatabase); late final ReactionDao reactionDao = ReactionDao(this as MoorChatDatabase); late final ReadDao readDao = ReadDao(this as MoorChatDatabase); @@ -4923,6 +5261,7 @@ abstract class _$MoorChatDatabase extends GeneratedDatabase { channels, messages, pinnedMessages, + pinnedMessageReactions, reactions, users, members, diff --git a/packages/stream_chat_persistence/lib/src/entity/entity.dart b/packages/stream_chat_persistence/lib/src/entity/entity.dart index d5218768..736fe405 100644 --- a/packages/stream_chat_persistence/lib/src/entity/entity.dart +++ b/packages/stream_chat_persistence/lib/src/entity/entity.dart @@ -3,6 +3,7 @@ export 'channels.dart'; export 'connection_events.dart'; export 'members.dart'; export 'messages.dart'; +export 'pinned_message_reactions.dart'; export 'pinned_messages.dart'; export 'reactions.dart'; export 'reads.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/members.dart b/packages/stream_chat_persistence/lib/src/entity/members.dart index 8d3d4a57..2773ddaa 100644 --- a/packages/stream_chat_persistence/lib/src/entity/members.dart +++ b/packages/stream_chat_persistence/lib/src/entity/members.dart @@ -39,8 +39,5 @@ class Members extends Table { DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); @override - Set get primaryKey => { - userId, - channelCid, - }; + Set get primaryKey => {userId, channelCid}; } diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 0aef2b84..108b903f 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -77,8 +77,8 @@ class Messages extends Table { TextColumn get pinnedByUserId => text().nullable()(); /// The channel cid of which this message is part of - TextColumn get channelCid => text().nullable().customConstraint( - 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE')(); + TextColumn get channelCid => + text().customConstraint('REFERENCES channels(cid) ON DELETE CASCADE')(); /// A Map of [messageText] translations. TextColumn get i18n => text().nullable().map(MapConverter())(); diff --git a/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart b/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart new file mode 100644 index 00000000..fbc9f5ca --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart @@ -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')(); +} diff --git a/packages/stream_chat_persistence/lib/src/mapper/mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/mapper.dart index 66bf3cda..32de5859 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/mapper.dart @@ -3,6 +3,7 @@ export 'event_mapper.dart'; export 'member_mapper.dart'; export 'message_mapper.dart'; export 'pinned_message_mapper.dart'; +export 'pinned_message_reaction_mapper.dart'; export 'reaction_mapper.dart'; export 'read_mapper.dart'; export 'user_mapper.dart'; diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 85692296..f49631a9 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -51,7 +51,7 @@ extension MessageEntityX on MessageEntity { /// Useful mapping functions for [Message] extension MessageX on Message { /// Maps a [Message] into [MessageEntity] - MessageEntity toEntity({String? cid}) => MessageEntity( + MessageEntity toEntity({required String cid}) => MessageEntity( id: id, attachments: attachments.map((it) => jsonEncode(it.toData())).toList(), channelCid: cid, diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index b1a7849a..d47aa109 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -51,7 +51,8 @@ extension PinnedMessageEntityX on PinnedMessageEntity { /// Useful mapping functions for [Message] extension PMessageX on Message { /// Maps a [Message] into [PinnedMessageEntity] - PinnedMessageEntity toPinnedEntity({String? cid}) => PinnedMessageEntity( + PinnedMessageEntity toPinnedEntity({required String cid}) => + PinnedMessageEntity( id: id, attachments: attachments.map((it) => jsonEncode(it.toData())).toList(), channelCid: cid, diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart new file mode 100644 index 00000000..e4602bb0 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart @@ -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, + ); +} diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 20c4e327..d7db7924 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -296,25 +296,34 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { } @override - Future updateMembers(String cid, List members) { + Future bulkUpdateMembers(Map> members) { assert(_debugIsConnected, ''); - _logger.info('updateMembers'); - return _readProtected(() => db!.memberDao.updateMembers(cid, members)); + _logger.info('bulkUpdateMembers'); + return _readProtected(() => db!.memberDao.bulkUpdateMembers(members)); } @override - Future updateMessages(String cid, List messages) { + Future bulkUpdateMessages(Map> messages) { assert(_debugIsConnected, ''); - _logger.info('updateMessages'); - return _readProtected(() => db!.messageDao.updateMessages(cid, messages)); + _logger.info('bulkUpdateMessages'); + return _readProtected(() => db!.messageDao.bulkUpdateMessages(messages)); } @override - Future updatePinnedMessages(String cid, List messages) { + Future bulkUpdatePinnedMessages(Map> messages) { assert(_debugIsConnected, ''); - _logger.info('updatePinnedMessages'); + _logger.info('bulkUpdatePinnedMessages'); return _readProtected( - () => db!.pinnedMessageDao.updateMessages(cid, messages), + () => db!.pinnedMessageDao.bulkUpdateMessages(messages), + ); + } + + @override + Future updatePinnedMessageReactions(List reactions) { + assert(_debugIsConnected, ''); + _logger.info('updatePinnedMessageReactions'); + return _readProtected( + () => db!.pinnedMessageReactionDao.updateReactions(reactions), ); } @@ -326,10 +335,10 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { } @override - Future updateReads(String cid, List reads) { + Future bulkUpdateReads(Map> reads) { assert(_debugIsConnected, ''); - _logger.info('updateReads'); - return _readProtected(() => db!.readDao.updateReads(cid, reads)); + _logger.info('bulkUpdateReads'); + return _readProtected(() => db!.readDao.bulkUpdateReads(reads)); } @override @@ -339,6 +348,18 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { return _readProtected(() => db!.userDao.updateUsers(users)); } + @override + Future deletePinnedMessageReactionsByMessageId( + List messageIds, + ) { + assert(_debugIsConnected, ''); + _logger.info('deletePinnedMessageReactionsByMessageId'); + return _readProtected( + () => + db!.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds), + ); + } + @override Future deleteReactionsByMessageId(List messageIds) { assert(_debugIsConnected, ''); diff --git a/packages/stream_chat_persistence/test/mock_chat_database.dart b/packages/stream_chat_persistence/test/mock_chat_database.dart index b421fad1..19b115e5 100644 --- a/packages/stream_chat_persistence/test/mock_chat_database.dart +++ b/packages/stream_chat_persistence/test/mock_chat_database.dart @@ -34,6 +34,12 @@ class MockChatDatabase extends Mock implements MoorChatDatabase { @override ReactionDao get reactionDao => _reactionDao ??= MockReactionDao(); + PinnedMessageReactionDao? _pinnedMessageReactionDao; + + @override + PinnedMessageReactionDao get pinnedMessageReactionDao => + _pinnedMessageReactionDao ??= MockPinnedMessageReactionDao(); + ReadDao? _readDao; @override @@ -70,6 +76,9 @@ class MockMemberDao extends Mock implements MemberDao {} class MockReactionDao extends Mock implements ReactionDao {} +class MockPinnedMessageReactionDao extends Mock + implements PinnedMessageReactionDao {} + class MockReadDao extends Mock implements ReadDao {} class MockChannelQueryDao extends Mock implements ChannelQueryDao {} diff --git a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart index 9a458574..c0a00100 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart @@ -61,12 +61,73 @@ void main() { expect(updatedChannel.cid, cid); expect(updatedChannel.type, type); + //Saving a dummy user + const userId = 'userId'; + final dummyUser = User(id: userId); + await database.userDao.updateUsers([dummyUser]); + + // Saving a dummy member + final dummyMember = Member(userId: userId, user: dummyUser); + await database.memberDao.updateMembers(cid, [dummyMember]); + + // Should match the dummy member + final updatedMembers = await database.memberDao.getMembersByCid(cid); + expect(updatedMembers.length, 1); + expect(updatedMembers.first.userId, userId); + + // Saving a dummy message + const messageId = 'messageId'; + final dummyMessage = Message(id: messageId, user: dummyUser); + await database.messageDao.updateMessages(cid, [dummyMessage]); + + // Should match the dummy message + final updatedMessages = await database.messageDao.getMessagesByCid(cid); + expect(updatedMessages.length, 1); + expect(updatedMessages.first.id, messageId); + + // Saving a dummy read + final dummyRead = Read(lastRead: DateTime.now(), user: dummyUser); + await database.readDao.updateReads(cid, [dummyRead]); + + // Should match the dummy read + final updatedReads = await database.readDao.getReadsByCid(cid); + expect(updatedReads.length, 1); + expect(updatedReads.first.user, dummyUser); + + // Saving a dummy reaction + final dummyReaction = + Reaction(type: 'type', messageId: messageId, userId: userId); + await database.reactionDao.updateReactions([dummyReaction]); + + // Should match the dummy reaction + final updatedReactions = + await database.reactionDao.getReactionsByUserId(messageId, userId); + expect(updatedReactions.length, 1); + expect(updatedReactions.first.messageId, messageId); + // Deleting the dummyChannel using cid await channelDao.deleteChannelByCids([cid]); // Fetched channel Should be null final channel = await channelDao.getChannelByCid(cid); expect(channel, isNull); + + // Fetched members for passed cid should be empty + final members = await database.memberDao.getMembersByCid(cid); + expect(members, isEmpty); + + // Fetched messages for passed cid should be empty + final messages = await database.messageDao.getMessagesByCid(cid); + expect(messages, isEmpty); + + // Fetched reads for passed cid should be empty + final reads = await database.readDao.getReadsByCid(cid); + expect(reads, isEmpty); + + // Fetched readtions for passed message id and user id should be empty + final reactions = + await database.reactionDao.getReactionsByUserId(messageId, userId); + expect(reactions, isEmpty); }); test('cids', () async { diff --git a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart index 8ce704a4..4ca3880b 100644 --- a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart @@ -18,6 +18,7 @@ void main() { }); Future> _prepareTestData(String cid) async { + final channels = [ChannelModel(cid: cid)]; final users = List.generate(3, (index) => User(id: 'testUserId$index')); final memberList = List.generate( 3, @@ -34,12 +35,13 @@ void main() { ), ); await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); await memberDao.updateMembers(cid, memberList); return memberList; } test('getMembersByCid', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final members = await memberDao.getMembersByCid(cid); @@ -70,7 +72,7 @@ void main() { }); test('updateMembers', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final memberList = await _prepareTestData(cid); @@ -132,7 +134,7 @@ void main() { }); test('deleteMemberByCids', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final members = await _prepareTestData(cid); diff --git a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart index fd0a43ce..d069f9f9 100644 --- a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart @@ -23,6 +23,7 @@ void main() { bool mapAllThreadToFirstMessage = false, int count = 3, }) async { + final channels = [ChannelModel(cid: cid)]; final users = List.generate(count, (index) => User(id: 'testUserId$index')); final messages = List.generate( count, @@ -98,13 +99,20 @@ void main() { if (quoted) ...quotedMessages, if (threads) ...threadMessages ]; + final reaction = Reaction( + type: 'type', + messageId: allMessages.first.id, + user: users.first, + ); await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); await messageDao.updateMessages(cid, allMessages); + await database.reactionDao.updateReactions([reaction]); return allMessages; } test('deleteMessageByIds', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedMessages = await _prepareTestData(cid); @@ -113,66 +121,112 @@ void main() { final messages = await messageDao.getMessagesByCid(cid); expect(messages.length, insertedMessages.length); + final firstMessageId = messages.first.id; + + // Fetched reactions list should have one reaction for given message id + final reactions = await database.reactionDao.getReactions(firstMessageId); + expect(reactions.length, 1); + // Deleting 2 messages from DB await messageDao.deleteMessageByIds( - ['testMessageId${cid}0', 'testMessageId${cid}1'], + [firstMessageId, 'testMessageId${cid}1'], ); // New fetched messages length should 2 less than the // previous fetched messages final newMessages = await messageDao.getMessagesByCid(cid); expect(newMessages.length, messages.length - 2); + + // Reaction for the first message should be deleted too + final newReactions = + await database.reactionDao.getReactions(firstMessageId); + expect(newReactions, isEmpty); }); group('deleteMessageByCids', () { - const cid1 = 'testCid1'; - const cid2 = 'testCid2'; + const cid1 = 'test:Cid1'; + const cid2 = 'test:Cid2'; - test('should delete all the messages of first channel', () async { - // Preparing test data - final cid1InsertedMessages = await _prepareTestData(cid1); - final cid2InsertedMessages = await _prepareTestData(cid2); + test( + 'should delete all the messages and reactions of first channel', + () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); - // Fetched message list should match the test message list length - final cid1Messages = await messageDao.getMessagesByCid(cid1); - final cid2Messages = await messageDao.getMessagesByCid(cid2); - expect(cid1Messages.length, cid1InsertedMessages.length); - expect(cid2Messages.length, cid2InsertedMessages.length); + // Fetched message list should match the test message list length + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); - // Deleting all the messages of cid1 - await messageDao.deleteMessageByCids([cid1]); + // Fetched reactions list should have one reaction for given message id + final cid1firstMessageId = cid1Messages.first.id; + final cid1Reactions = + await database.reactionDao.getReactions(cid1firstMessageId); + expect(cid1Reactions.length, 1); - // Fetched messages length of only cid1 should be empty - final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); - expect(cid1FetchedMessages, isEmpty); - expect(cid2FetchedMessages, isNotEmpty); - }); + // Deleting all the messages of cid1 + await messageDao.deleteMessageByCids([cid1]); - test('should delete all the messages of both channel', () async { - // Preparing test data - final cid1InsertedMessages = await _prepareTestData(cid1); - final cid2InsertedMessages = await _prepareTestData(cid2); + // Fetched messages length of only cid1 should be empty + final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isNotEmpty); - // Fetched message list should match the test message list length - final cid1Messages = await messageDao.getMessagesByCid(cid1); - final cid2Messages = await messageDao.getMessagesByCid(cid2); - expect(cid1Messages.length, cid1InsertedMessages.length); - expect(cid2Messages.length, cid2InsertedMessages.length); + // Reaction for the first message should be deleted too + final cid1FetchedReactions = + await database.reactionDao.getReactions(cid1firstMessageId); + expect(cid1FetchedReactions, isEmpty); + }, + ); - // Deleting all the messages of cid1 - await messageDao.deleteMessageByCids([cid1, cid2]); + test( + 'should delete all the messages and reactions of both channel', + () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); - // Fetched messages length of both cid1 and cid2 should be empty - final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); - expect(cid1FetchedMessages, isEmpty); - expect(cid2FetchedMessages, isEmpty); - }); + // Fetched message list should match the test message list length + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); + + // Fetched reactions list should have one reaction for given message id + final cid1FirstMessageId = cid1Messages.first.id; + final cid1Reactions = + await database.reactionDao.getReactions(cid1FirstMessageId); + expect(cid1Reactions.length, 1); + final cid2FirstMessageId = cid2Messages.first.id; + final cid2Reactions = + await database.reactionDao.getReactions(cid2FirstMessageId); + expect(cid2Reactions.length, 1); + + // Deleting all the messages of cid1 + await messageDao.deleteMessageByCids([cid1, cid2]); + + // Fetched messages length of both cid1 and cid2 should be empty + final cid1FetchedMessages = await messageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await messageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isEmpty); + + // Reaction for the first message should be deleted too + final cid1FetchedReactions = + await database.reactionDao.getReactions(cid1FirstMessageId); + expect(cid1FetchedReactions, isEmpty); + final cid2FetchedReactions = + await database.reactionDao.getReactions(cid2FirstMessageId); + expect(cid2FetchedReactions, isEmpty); + }, + ); }); test('getMessageById', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const id = 'testMessageId${cid}0'; // Should be null initially @@ -190,7 +244,7 @@ void main() { }); test('getThreadMessages', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Messages should be empty initially final messages = await messageDao.getThreadMessages(cid); @@ -209,7 +263,7 @@ void main() { }); test('getThreadMessagesByParentId', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const parentId = 'testMessageId${cid}0'; // Messages should be empty initially @@ -228,7 +282,7 @@ void main() { }); test('getThreadMessagesByParentId along with pagination', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const parentId = 'testMessageId${cid}0'; const options = PaginationParams( limit: 15, @@ -262,7 +316,7 @@ void main() { }); test('getMessagesByCid', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final messages = await messageDao.getMessagesByCid(cid); @@ -283,7 +337,7 @@ void main() { }); test('getMessagesByCid along with quotedMessage', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final messages = await messageDao.getMessagesByCid(cid); @@ -301,7 +355,7 @@ void main() { }); test('getMessagesByCid along with pagination', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const limit = 15; const lessThan = 'testMessageId${cid}25'; const greaterThanOrEqual = 'testMessageId${cid}5'; @@ -333,7 +387,7 @@ void main() { }); test('updateMessages', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedMessages = await _prepareTestData(cid); diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart index 42a54107..f8fae456 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart @@ -23,6 +23,7 @@ void main() { bool mapAllThreadToFirstMessage = false, int count = 3, }) async { + final channels = [ChannelModel(cid: cid)]; final users = List.generate(count, (index) => User(id: 'testUserId$index')); final messages = List.generate( count, @@ -83,13 +84,20 @@ void main() { if (quoted) ...quotedMessages, if (threads) ...threadMessages ]; + final reaction = Reaction( + type: 'type', + messageId: allMessages.first.id, + user: users.first, + ); await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); await pinnedMessageDao.updateMessages(cid, allMessages); + await database.pinnedMessageReactionDao.updateReactions([reaction]); return allMessages; } test('deleteMessageByIds', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedMessages = await _prepareTestData(cid); @@ -98,66 +106,117 @@ void main() { final messages = await pinnedMessageDao.getMessagesByCid(cid); expect(messages.length, insertedMessages.length); + final firstMessageId = messages.first.id; + + // Fetched reactions list should have one reaction for given message id + final reactions = + await database.pinnedMessageReactionDao.getReactions(firstMessageId); + expect(reactions.length, 1); + // Deleting 2 messages from DB await pinnedMessageDao.deleteMessageByIds( - ['testMessageId${cid}0', 'testMessageId${cid}1'], + [firstMessageId, 'testMessageId${cid}1'], ); // New fetched messages length should 2 less than the // previous fetched messages final newMessages = await pinnedMessageDao.getMessagesByCid(cid); expect(newMessages.length, messages.length - 2); + + // Reaction for the first message should be deleted too + final newReactions = + await database.pinnedMessageReactionDao.getReactions(firstMessageId); + expect(newReactions, isEmpty); }); group('deleteMessageByCids', () { - const cid1 = 'testCid1'; - const cid2 = 'testCid2'; + const cid1 = 'test:Cid1'; + const cid2 = 'test:Cid2'; - test('should delete all the messages of first channel', () async { - // Preparing test data - final cid1InsertedMessages = await _prepareTestData(cid1); - final cid2InsertedMessages = await _prepareTestData(cid2); + test( + 'should delete all the messages and reactions of first channel', + () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); - // Fetched message list should match the test message list length - final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); - final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); - expect(cid1Messages.length, cid1InsertedMessages.length); - expect(cid2Messages.length, cid2InsertedMessages.length); + // Fetched message list should match the test message list length + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); - // Deleting all the messages of cid1 - await pinnedMessageDao.deleteMessageByCids([cid1]); + // Fetched reactions list should have one reaction for given message id + final cid1firstMessageId = cid1Messages.first.id; + final cid1Reactions = await database.pinnedMessageReactionDao + .getReactions(cid1firstMessageId); + expect(cid1Reactions.length, 1); - // Fetched messages length of only cid1 should be empty - final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2); - expect(cid1FetchedMessages, isEmpty); - expect(cid2FetchedMessages, isNotEmpty); - }); + // Deleting all the messages of cid1 + await pinnedMessageDao.deleteMessageByCids([cid1]); - test('should delete all the messages of both channel', () async { - // Preparing test data - final cid1InsertedMessages = await _prepareTestData(cid1); - final cid2InsertedMessages = await _prepareTestData(cid2); + // Fetched messages length of only cid1 should be empty + final cid1FetchedMessages = + await pinnedMessageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = + await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isNotEmpty); - // Fetched message list should match the test message list length - final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); - final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); - expect(cid1Messages.length, cid1InsertedMessages.length); - expect(cid2Messages.length, cid2InsertedMessages.length); + // Reaction for the first message should be deleted too + final cid1FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid1firstMessageId); + expect(cid1FetchedReactions, isEmpty); + }, + ); - // Deleting all the messages of cid1 - await pinnedMessageDao.deleteMessageByCids([cid1, cid2]); + test( + 'should delete all the messages and reactions of both channel', + () async { + // Preparing test data + final cid1InsertedMessages = await _prepareTestData(cid1); + final cid2InsertedMessages = await _prepareTestData(cid2); - // Fetched messages length of both cid1 and cid2 should be empty - final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2); - expect(cid1FetchedMessages, isEmpty); - expect(cid2FetchedMessages, isEmpty); - }); + // Fetched message list should match the test message list length + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1Messages.length, cid1InsertedMessages.length); + expect(cid2Messages.length, cid2InsertedMessages.length); + + // Fetched reactions list should have one reaction for given message id + final cid1FirstMessageId = cid1Messages.first.id; + final cid1Reactions = await database.pinnedMessageReactionDao + .getReactions(cid1FirstMessageId); + expect(cid1Reactions.length, 1); + final cid2FirstMessageId = cid2Messages.first.id; + final cid2Reactions = await database.pinnedMessageReactionDao + .getReactions(cid2FirstMessageId); + expect(cid2Reactions.length, 1); + + // Deleting all the messages of cid1 + await pinnedMessageDao.deleteMessageByCids([cid1, cid2]); + + // Fetched messages length of both cid1 and cid2 should be empty + final cid1FetchedMessages = + await pinnedMessageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = + await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1FetchedMessages, isEmpty); + expect(cid2FetchedMessages, isEmpty); + + // Reaction for the first message should be deleted too + final cid1FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid1FirstMessageId); + expect(cid1FetchedReactions, isEmpty); + final cid2FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid2FirstMessageId); + expect(cid2FetchedReactions, isEmpty); + }, + ); }); test('getMessageById', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const id = 'testMessageId${cid}0'; // Should be null initially @@ -175,7 +234,7 @@ void main() { }); test('getThreadMessages', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Messages should be empty initially final messages = await pinnedMessageDao.getThreadMessages(cid); @@ -194,7 +253,7 @@ void main() { }); test('getThreadMessagesByParentId', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const parentId = 'testMessageId${cid}0'; // Messages should be empty initially @@ -214,7 +273,7 @@ void main() { }); test('getThreadMessagesByParentId along with pagination', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const parentId = 'testMessageId${cid}0'; const options = PaginationParams( limit: 15, @@ -248,7 +307,7 @@ void main() { }); test('getMessagesByCid', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final messages = await pinnedMessageDao.getMessagesByCid(cid); @@ -269,7 +328,7 @@ void main() { }); test('getMessagesByCid along with quotedMessage', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final messages = await pinnedMessageDao.getMessagesByCid(cid); @@ -287,7 +346,7 @@ void main() { }); test('getMessagesByCid along with pagination', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; const limit = 15; const lessThan = 'testMessageId${cid}25'; const greaterThanOrEqual = 'testMessageId${cid}5'; @@ -319,7 +378,7 @@ void main() { }); test('updateMessages', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedMessages = await _prepareTestData(cid); diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart new file mode 100644 index 00000000..aa92c5cc --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart @@ -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> _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(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart index 4edda680..21570d4e 100644 --- a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart @@ -21,6 +21,8 @@ void main() { String? userId, int count = 3, }) async { + const cid = 'test:Cid'; + final channels = [ChannelModel(cid: cid)]; final users = List.generate(count, (index) => User(id: 'testUserId$index')); final message = Message( id: messageId, @@ -50,7 +52,8 @@ void main() { ); await database.userDao.updateUsers(users); - await database.messageDao.updateMessages('testCid', [message]); + await database.channelDao.updateChannels(channels); + await database.messageDao.updateMessages(cid, [message]); await reactionDao.updateReactions(reactions); return reactions; diff --git a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart index 287dcad4..ca556632 100644 --- a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart @@ -16,6 +16,7 @@ void main() { }); Future> _prepareReadData(String cid, {int count = 3}) async { + final channels = [ChannelModel(cid: cid)]; final users = List.generate(count, (index) => User(id: 'testUserId$index')); final reads = List.generate( count, @@ -27,12 +28,13 @@ void main() { ); await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); await readDao.updateReads(cid, reads); return reads; } test('getReadsByCid', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Should be empty initially final reads = await readDao.getReadsByCid(cid); @@ -55,7 +57,7 @@ void main() { }); test('updateReads', () async { - const cid = 'testCid'; + const cid = 'test:Cid'; // Preparing test data final insertedReads = await _prepareReadData(cid); diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart new file mode 100644 index 00000000..bd8d0ba8 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart @@ -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()); + 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()); + 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); + }); +} diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index daaebe61..25e926b1 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -397,23 +397,26 @@ void main() { test('updateMessages', () async { const cid = 'testCid'; final messages = List.generate(3, (index) => Message()); - when(() => mockDatabase.messageDao.updateMessages(cid, messages)) + + when(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages})) .thenAnswer((_) => Future.value()); await client.updateMessages(cid, messages); - verify(() => mockDatabase.messageDao.updateMessages(cid, messages)) + verify(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages})) .called(1); }); test('updatePinnedMessages', () async { const cid = 'testCid'; final messages = List.generate(3, (index) => Message()); - when(() => mockDatabase.pinnedMessageDao.updateMessages(cid, messages)) - .thenAnswer((_) => Future.value()); + when( + () => mockDatabase.pinnedMessageDao.bulkUpdateMessages({cid: messages}), + ).thenAnswer((_) => Future.value()); await client.updatePinnedMessages(cid, messages); - verify(() => mockDatabase.pinnedMessageDao.updateMessages(cid, messages)) - .called(1); + verify( + () => mockDatabase.pinnedMessageDao.bulkUpdateMessages({cid: messages}), + ).called(1); }); test('getChannelThreads', () async { @@ -456,11 +459,11 @@ void main() { test('updateMembers', () async { const cid = 'testCid'; final members = List.generate(3, (index) => Member()); - when(() => mockDatabase.memberDao.updateMembers(cid, members)) + when(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members})) .thenAnswer((_) => Future.value()); await client.updateMembers(cid, members); - verify(() => mockDatabase.memberDao.updateMembers(cid, members)) + verify(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members})) .called(1); }); @@ -473,11 +476,12 @@ void main() { lastRead: DateTime.now(), ), ); - when(() => mockDatabase.readDao.updateReads(cid, reads)) + when(() => mockDatabase.readDao.bulkUpdateReads({cid: reads})) .thenAnswer((_) => Future.value()); await client.updateReads(cid, reads); - verify(() => mockDatabase.readDao.updateReads(cid, reads)).called(1); + verify(() => mockDatabase.readDao.bulkUpdateReads({cid: reads})) + .called(1); }); test('updateUsers', () async { @@ -502,6 +506,21 @@ void main() { .called(1); }); + test('updatePinnedMessageReactions', () async { + final reactions = List.generate( + 3, + (index) => Reaction(type: 'testType$index'), + ); + when(() => + mockDatabase.pinnedMessageReactionDao.updateReactions(reactions)) + .thenAnswer((_) => Future.value()); + + await client.updatePinnedMessageReactions(reactions); + verify(() => + mockDatabase.pinnedMessageReactionDao.updateReactions(reactions)) + .called(1); + }); + test('deleteReactionsByMessageId', () async { final messageIds = []; when(() => @@ -514,6 +533,17 @@ void main() { .called(1); }); + test('deletePinnedMessageReactionsByMessageId', () async { + final messageIds = []; + when(() => mockDatabase.pinnedMessageReactionDao + .deleteReactionsByMessageIds(messageIds)) + .thenAnswer((_) => Future.value()); + + await client.deletePinnedMessageReactionsByMessageId(messageIds); + verify(() => mockDatabase.pinnedMessageReactionDao + .deleteReactionsByMessageIds(messageIds)).called(1); + }); + test('deleteMembersByCids', () async { final cids = []; when(() => mockDatabase.memberDao.deleteMemberByCids(cids))