From 5db0efadbae24b8ce7ab395de230c572d96a110b Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sat, 28 Aug 2021 10:04:05 +0300 Subject: [PATCH] feat(persistence): add pinned message reactions table --- .../lib/src/dao/dao.dart | 1 + .../lib/src/dao/pinned_message_dao.dart | 6 +- .../src/dao/pinned_message_reaction_dao.dart | 60 ++++ .../dao/pinned_message_reaction_dao.g.dart | 13 + .../lib/src/db/moor_chat_database.dart | 2 + .../lib/src/db/moor_chat_database.g.dart | 339 ++++++++++++++++++ .../lib/src/entity/entity.dart | 1 + .../src/entity/pinned_message_reactions.dart | 13 + .../lib/src/mapper/mapper.dart | 1 + .../pinned_message_reaction_mapper.dart | 29 ++ .../src/stream_chat_persistence_client.dart | 21 ++ .../test/mock_chat_database.dart | 9 + .../test/src/dao/pinned_message_dao_test.dart | 31 +- .../dao/pinned_message_reaction_dao_test.dart | 205 +++++++++++ .../pinned_message_reaction_mapper_test.dart | 52 +++ .../stream_chat_persistence_client_test.dart | 26 ++ 16 files changed, 792 insertions(+), 17 deletions(-) create mode 100644 packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart create mode 100644 packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart create mode 100644 packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart create mode 100644 packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart create mode 100644 packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart create mode 100644 packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart 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/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index e0c4b876..065dc40d 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, ); 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/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 4dc2a1e4..741ac957 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, 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..c0991f3e 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 @@ -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/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/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..5df7a124 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 @@ -318,6 +318,15 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); } + @override + Future updatePinnedMessageReactions(List reactions) { + assert(_debugIsConnected, ''); + _logger.info('updatePinnedMessageReactions'); + return _readProtected( + () => db!.pinnedMessageReactionDao.updateReactions(reactions), + ); + } + @override Future updateReactions(List reactions) { assert(_debugIsConnected, ''); @@ -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/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart index 1d6a8c66..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 @@ -92,7 +92,7 @@ void main() { await database.userDao.updateUsers(users); await database.channelDao.updateChannels(channels); await pinnedMessageDao.updateMessages(cid, allMessages); - await database.reactionDao.updateReactions([reaction]); + await database.pinnedMessageReactionDao.updateReactions([reaction]); return allMessages; } @@ -109,7 +109,8 @@ void main() { final firstMessageId = messages.first.id; // Fetched reactions list should have one reaction for given message id - final reactions = await database.reactionDao.getReactions(firstMessageId); + final reactions = + await database.pinnedMessageReactionDao.getReactions(firstMessageId); expect(reactions.length, 1); // Deleting 2 messages from DB @@ -124,7 +125,7 @@ void main() { // Reaction for the first message should be deleted too final newReactions = - await database.reactionDao.getReactions(firstMessageId); + await database.pinnedMessageReactionDao.getReactions(firstMessageId); expect(newReactions, isEmpty); }); @@ -147,8 +148,8 @@ void main() { // Fetched reactions list should have one reaction for given message id final cid1firstMessageId = cid1Messages.first.id; - final cid1Reactions = - await database.reactionDao.getReactions(cid1firstMessageId); + final cid1Reactions = await database.pinnedMessageReactionDao + .getReactions(cid1firstMessageId); expect(cid1Reactions.length, 1); // Deleting all the messages of cid1 @@ -163,8 +164,8 @@ void main() { expect(cid2FetchedMessages, isNotEmpty); // Reaction for the first message should be deleted too - final cid1FetchedReactions = - await database.reactionDao.getReactions(cid1firstMessageId); + final cid1FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid1firstMessageId); expect(cid1FetchedReactions, isEmpty); }, ); @@ -184,12 +185,12 @@ void main() { // Fetched reactions list should have one reaction for given message id final cid1FirstMessageId = cid1Messages.first.id; - final cid1Reactions = - await database.reactionDao.getReactions(cid1FirstMessageId); + final cid1Reactions = await database.pinnedMessageReactionDao + .getReactions(cid1FirstMessageId); expect(cid1Reactions.length, 1); final cid2FirstMessageId = cid2Messages.first.id; - final cid2Reactions = - await database.reactionDao.getReactions(cid2FirstMessageId); + final cid2Reactions = await database.pinnedMessageReactionDao + .getReactions(cid2FirstMessageId); expect(cid2Reactions.length, 1); // Deleting all the messages of cid1 @@ -204,11 +205,11 @@ void main() { expect(cid2FetchedMessages, isEmpty); // Reaction for the first message should be deleted too - final cid1FetchedReactions = - await database.reactionDao.getReactions(cid1FirstMessageId); + final cid1FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid1FirstMessageId); expect(cid1FetchedReactions, isEmpty); - final cid2FetchedReactions = - await database.reactionDao.getReactions(cid2FirstMessageId); + final cid2FetchedReactions = await database.pinnedMessageReactionDao + .getReactions(cid2FirstMessageId); expect(cid2FetchedReactions, isEmpty); }, ); 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/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..37e82e93 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 @@ -502,6 +502,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 +529,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))