From 70fdd42e58f3c03f42f1a4118d7d776141a72049 Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sun, 8 Aug 2021 21:42:08 +0300 Subject: [PATCH 01/27] fix(persistence): enable pragma foreign_keys --- .../stream_chat_persistence/lib/src/db/moor_chat_database.dart | 3 +++ 1 file changed, 3 insertions(+) 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..4dc2a1e4 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 @@ -64,6 +64,9 @@ class MoorChatDatabase extends _$MoorChatDatabase { } } }, + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON;'); + }, ); /// Deletes all the tables From 8adc38f8e47a3f7b7162d6477680a35ff3fe1c30 Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sun, 8 Aug 2021 21:42:27 +0300 Subject: [PATCH 02/27] fix(persistence): change mode insert or replace to upsert --- packages/stream_chat_persistence/lib/src/dao/channel_dao.dart | 3 +-- .../stream_chat_persistence/lib/src/dao/channel_query_dao.dart | 3 +-- .../lib/src/dao/connection_event_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/member_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/message_dao.dart | 3 +-- .../lib/src/dao/pinned_message_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/read_dao.dart | 3 +-- packages/stream_chat_persistence/lib/src/dao/user_dao.dart | 3 +-- 9 files changed, 9 insertions(+), 18 deletions(-) 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 9100e76d..c87b94d4 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/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index 157b0a9a..1f39904c 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart @@ -32,10 +32,9 @@ class MemberDao extends DatabaseAccessor /// Updates all the members using the new [memberList] data Future updateMembers(String cid, List memberList) async => batch( - (it) => it.insertAll( + (it) => it.insertAllOnConflictUpdate( members, memberList.map((m) => m.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, ), ); 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 c2e1cde2..eb02e8aa 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -171,10 +171,9 @@ class MessageDao extends DatabaseAccessor /// the new [messageList] data Future updateMessages(String cid, List messageList) => batch( (batch) { - batch.insertAll( + batch.insertAllOnConflictUpdate( messages, messageList.map((it) => it.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, ); }, ); 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..e0c4b876 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 @@ -170,10 +170,9 @@ class PinnedMessageDao extends DatabaseAccessor /// the new [messageList] data Future updateMessages(String cid, List messageList) => batch( (batch) { - batch.insertAll( + batch.insertAllOnConflictUpdate( pinnedMessages, messageList.map((it) => it.toPinnedEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, ); }, ); 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..007cb3a7 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart @@ -30,10 +30,9 @@ 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( + (it) => it.insertAllOnConflictUpdate( reads, readList.map((r) => r.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, ), ); } 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, ), ); From 68311a6c6cb4ac8b50c996376e43a151cc3eae3f Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sun, 8 Aug 2021 21:42:44 +0300 Subject: [PATCH 03/27] test(persistence): update tests after enabling foreign keys --- .../test/src/dao/channel_dao_test.dart | 61 +++++++ .../test/src/dao/member_dao_test.dart | 8 +- .../test/src/dao/message_dao_test.dart | 146 +++++++++++------ .../test/src/dao/pinned_message_dao_test.dart | 150 ++++++++++++------ .../test/src/dao/reaction_dao_test.dart | 5 +- .../test/src/dao/read_dao_test.dart | 6 +- 6 files changed, 278 insertions(+), 98 deletions(-) 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..1d6a8c66 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.reactionDao.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,116 @@ 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.reactionDao.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.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 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.reactionDao.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.reactionDao.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.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 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.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 @@ -175,7 +233,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 +252,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 +272,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 +306,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 +327,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 +345,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 +377,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/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); From 5db0efadbae24b8ce7ab395de230c572d96a110b Mon Sep 17 00:00:00 2001 From: Dmitry Zhifarsky Date: Sat, 28 Aug 2021 10:04:05 +0300 Subject: [PATCH 04/27] 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)) From b369f189224f922d17994fbf7d9aa3a072d787ba Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 30 Aug 2021 16:22:36 +0530 Subject: [PATCH 05/27] feat(llc): add crud for pinned message reactions Signed-off-by: xsahil03x --- .../lib/src/db/chat_persistence_client.dart | 26 +++++++++++++++++++ .../src/db/chat_persistence_client_test.dart | 9 +++++++ 2 files changed, 35 insertions(+) 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..bf2ef3a8 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -170,9 +170,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); @@ -187,6 +193,12 @@ abstract class ChatPersistenceClient { .map((m) => m.id) .toList(growable: false)); + final deletePinnedMessageReactions = + deletePinnedMessageReactionsByMessageId(channelStates + .expand((it) => it.pinnedMessages) + .map((m) => m.id) + .toList(growable: false)); + final cleanedChannelStates = channelStates.where((it) => it.channel != null); @@ -196,6 +208,7 @@ abstract class ChatPersistenceClient { await Future.wait([ deleteReactions, + deletePinnedMessageReactions, deleteMembers, ]); @@ -211,6 +224,16 @@ abstract class ChatPersistenceClient { ]) .withNullifyer; + final pinnedMessageReactions = cleanedChannelStates + .expand((it) => it.pinnedMessages) + .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, @@ -261,6 +284,9 @@ abstract class ChatPersistenceClient { updateUsers(users.toList(growable: false)), updateChannels(channels.toList(growable: false)), updateReactions(reactions.toList(growable: false)), + updatePinnedMessageReactions( + pinnedMessageReactions.toList(growable: false), + ), ]); } } 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..d7de4745 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(); @@ -116,6 +121,10 @@ class TestPersistenceClient extends ChatPersistenceClient { @override Future updateReactions(List reactions) => Future.value(); + @override + Future updatePinnedMessageReactions(List reactions) => + Future.value(); + @override Future updateReads(String cid, List reads) => Future.value(); From f184da13dbc518d0a4f16eb28a0563b373e6446e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 31 Aug 2021 15:52:15 +0530 Subject: [PATCH 06/27] chore(persistence): update schema version Signed-off-by: xsahil03x --- .../lib/src/db/moor_chat_database.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 741ac957..07f86821 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 @@ -53,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(); @@ -66,9 +69,6 @@ class MoorChatDatabase extends _$MoorChatDatabase { } } }, - beforeOpen: (details) async { - await customStatement('PRAGMA foreign_keys = ON;'); - }, ); /// Deletes all the tables From 7e0997838e1bc06ee45ae83145fdea26779bd73a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 6 Sep 2021 19:21:28 +0530 Subject: [PATCH 07/27] fix: `updateChannelStates` invocation sequence as per foreign keys relations. Signed-off-by: xsahil03x --- .../lib/src/db/chat_persistence_client.dart | 183 +++++++++--------- .../src/db/chat_persistence_client_test.dart | 28 +-- .../lib/src/dao/member_dao.dart | 20 +- .../lib/src/dao/message_dao.dart | 25 ++- .../lib/src/dao/pinned_message_dao.dart | 25 ++- .../lib/src/dao/read_dao.dart | 19 +- .../lib/src/db/moor_chat_database.dart | 2 +- .../lib/src/db/moor_chat_database.g.dart | 80 ++++---- .../lib/src/entity/members.dart | 5 +- .../lib/src/entity/messages.dart | 4 +- .../lib/src/mapper/message_mapper.dart | 2 +- .../lib/src/mapper/pinned_message_mapper.dart | 3 +- .../src/stream_chat_persistence_client.dart | 24 +-- .../stream_chat_persistence_client_test.dart | 24 ++- 14 files changed, 240 insertions(+), 204 deletions(-) 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 bf2ef3a8..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); @@ -188,104 +204,91 @@ 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 deletePinnedMessageReactions = - deletePinnedMessageReactionsByMessageId(channelStates - .expand((it) => it.pinnedMessages) - .map((m) => m.id) - .toList(growable: false)); + final channels = []; + final channelWithMessages = >{}; + final channelWithPinnedMessages = >{}; + final channelWithReads = >{}; + final channelWithMembers = >{}; - final cleanedChannelStates = - channelStates.where((it) => it.channel != null); + final users = []; + final reactions = []; + final pinnedReactions = []; - final deleteMembers = deleteMembersByCids( - cleanedChannelStates.map((it) => it.channel!.cid).toList(growable: false), - ); + 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, - deletePinnedMessageReactions, - 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 pinnedMessageReactions = cleanedChannelStates - .expand((it) => it.pinnedMessages) - .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( - pinnedMessageReactions.toList(growable: false), + pinnedReactions.toList(growable: false), ), ]); } 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 d7de4745..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 @@ -106,18 +106,6 @@ 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(); @@ -126,10 +114,22 @@ class TestPersistenceClient extends ChatPersistenceClient { Future.value(); @override - Future updateReads(String cid, List reads) => Future.value(); + Future updateUsers(List users) => Future.value(); @override - Future updateUsers(List users) => Future.value(); + 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_persistence/lib/src/dao/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index 1f39904c..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,13 +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.insertAllOnConflictUpdate( - members, - memberList.map((m) => m.toEntity(cid: cid)).toList(), - ), - ); + 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 eb02e8aa..b7d9a5db 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -169,12 +169,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.insertAllOnConflictUpdate( - messages, - messageList.map((it) => it.toEntity(cid: cid)).toList(), - ); - }, - ); + 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 065dc40d..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 @@ -170,12 +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.insertAllOnConflictUpdate( - pinnedMessages, - messageList.map((it) => it.toPinnedEntity(cid: cid)).toList(), - ); - }, - ); + 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/read_dao.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart index 007cb3a7..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,10 +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.insertAllOnConflictUpdate( - reads, - readList.map((r) => r.toEntity(cid: cid)).toList(), - ), - ); + 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/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 07f86821..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 @@ -58,7 +58,7 @@ class MoorChatDatabase extends _$MoorChatDatabase { @override MigrationStrategy get migration => MigrationStrategy( beforeOpen: (details) async { - await customStatement('PRAGMA foreign_keys = ON;'); + await customStatement('PRAGMA foreign_keys = ON'); }, onUpgrade: (openingDetails, before, after) async { if (before != after) { 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 c0991f3e..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()); 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/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/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 5df7a124..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,25 @@ 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), ); } @@ -335,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 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 37e82e93..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 { From 4ddb0703b30cfc3b255d815304b95fb090281ed1 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 6 Sep 2021 19:35:04 +0530 Subject: [PATCH 08/27] fix(llc, ui): message search pagination Signed-off-by: xsahil03x --- .../lib/src/core/api/general_api.dart | 4 ++ .../lib/src/core/api/requests.dart | 16 +++++-- .../lib/src/core/api/requests.g.dart | 2 + .../lib/src/core/api/responses.dart | 6 +++ .../lib/src/core/api/responses.g.dart | 4 +- .../test/src/core/api/general_api_test.dart | 18 +++++++ .../test/src/core/api/requests_test.dart | 22 +++++++-- .../lib/src/message_search_bloc.dart | 27 +++++++++-- .../lib/src/message_search_list_core.dart | 47 ++++++++++++++----- .../lib/src/dao/channel_query_dao.dart | 5 +- .../lib/src/dao/message_dao.dart | 5 +- 11 files changed, 127 insertions(+), 29 deletions(-) diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index c6243148..fa08a475 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -36,6 +36,10 @@ class GeneralApi { PaginationParams? pagination, Filter? messageFilters, }) async { + assert( + pagination?.offset == null || pagination?.offset == 0 || sort == null, + 'Cannot specify `offset` with `sort` parameter', + ); assert(() { if (query == null && messageFilters == null) { throw ArgumentError('Provide at least `query` or `messageFilters`'); diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 29112d72..0bc49fc7 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -60,12 +60,16 @@ class PaginationParams extends Equatable { /// ``` const PaginationParams({ this.limit = 10, - this.offset = 0, + this.offset, + this.next, this.greaterThan, this.greaterThanOrEqual, this.lessThan, this.lessThanOrEqual, - }); + }) : assert( + offset == null || offset == 0 || next == null, + 'Cannot specify non-zero `offset` with `next` parameter', + ); /// Create a new instance from a json factory PaginationParams.fromJson(Map json) => @@ -75,7 +79,10 @@ class PaginationParams extends Equatable { final int limit; /// The offset of requesting items. - final int offset; + final int? offset; + + /// A key used to paginate. + final String? next; /// Filter on ids greater than the given value. @JsonKey(name: 'id_gt') @@ -100,6 +107,7 @@ class PaginationParams extends Equatable { PaginationParams copyWith({ int? limit, int? offset, + String? next, String? greaterThan, String? greaterThanOrEqual, String? lessThan, @@ -108,6 +116,7 @@ class PaginationParams extends Equatable { PaginationParams( limit: limit ?? this.limit, offset: offset ?? this.offset, + next: next ?? this.next, greaterThan: greaterThan ?? this.greaterThan, greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, lessThan: lessThan ?? this.lessThan, @@ -118,6 +127,7 @@ class PaginationParams extends Equatable { List get props => [ limit, offset, + next, greaterThan, greaterThanOrEqual, lessThan, diff --git a/packages/stream_chat/lib/src/core/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart index 6cd935b8..fd0ac624 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -23,6 +23,7 @@ PaginationParams _$PaginationParamsFromJson(Map json) { return PaginationParams( limit: json['limit'] as int, offset: json['offset'] as int, + next: json['next'] as String?, greaterThan: json['id_gt'] as String?, greaterThanOrEqual: json['id_gte'] as String?, lessThan: json['id_lt'] as String?, @@ -42,6 +43,7 @@ Map _$PaginationParamsToJson(PaginationParams instance) { } } + writeNotNull('next', instance.next); writeNotNull('id_gt', instance.greaterThan); writeNotNull('id_gte', instance.greaterThanOrEqual); writeNotNull('id_lt', instance.lessThan); diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 033e91b2..5b52b65d 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -253,6 +253,12 @@ class SearchMessagesResponse extends _BaseResponse { @JsonKey(defaultValue: []) late List results; + /// Message id of where to start searching from for next [results] + late String? next; + + /// Message id of where to start searching from for previous [results] + late String? previous; + /// Create a new instance from a json static SearchMessagesResponse fromJson(Map json) => _$SearchMessagesResponseFromJson(json); diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 15b64a05..10d96682 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -161,7 +161,9 @@ SearchMessagesResponse _$SearchMessagesResponseFromJson( ..results = (json['results'] as List?) ?.map((e) => GetMessageResponse.fromJson(e as Map)) .toList() ?? - []; + [] + ..next = json['next'] as String? + ..previous = json['previous'] as String?; } GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson( diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index afb3f6aa..570d7c77 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -86,6 +86,24 @@ void main() { }, ); + test( + 'should throw if `pagination.offset` and `sort` both are provided', + () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const sort = [SortOption('test-field')]; + const pagination = PaginationParams(offset: 10); + try { + await generalApi.searchMessages( + filter, + sort: sort, + pagination: pagination, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + test('should run successfully with `query`', () async { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); const query = 'test-query'; diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 76253403..876a3f61 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -9,11 +9,23 @@ void main() { expect(j, {'field': 'name', 'direction': -1}); }); - test('PaginationParams', () { - const option = PaginationParams(); - final j = option.toJson(); - expect(j, containsPair('limit', 10)); - expect(j, containsPair('offset', 0)); + group('PaginationParams', () { + test('default', () { + const option = PaginationParams(); + final j = option.toJson(); + expect(j, containsPair('limit', 10)); + }); + + test( + 'should throw if non-zero `offset` and `next` both are provided', + () { + try { + PaginationParams(offset: 10, next: 'next-message-id'); + } catch (e) { + expect(e, isA()); + } + }, + ); }); }); } diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index 44ac9b3f..d9d5933d 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart @@ -43,6 +43,9 @@ class MessageSearchBlocState extends State with AutomaticKeepAliveClientMixin { late StreamChatCoreState _streamChatCoreState; + String? nextId; + String? previousId; + /// The current messages list List? get messageResponses => _messageResponses.valueOrNull; @@ -76,11 +79,17 @@ class MessageSearchBlocState extends State _queryMessagesLoadingController.add(true); } try { - final clear = pagination == null || pagination.offset == 0; + var clear = pagination == null; + if (sort != null) { + clear |= pagination?.next == null; + } else { + final offset = pagination?.offset; + clear |= offset == null || offset == 0; + } final oldMessages = List.from(messageResponses ?? []); - final messages = await client.search( + final response = await client.search( filter, sort: sort, query: query, @@ -88,10 +97,20 @@ class MessageSearchBlocState extends State messageFilters: messageFilter, ); + final next = response.next; + final previous = response.previous; + + nextId = next != null && next.isNotEmpty + ? next + : /*reset nextId if we get nothing*/ null; + previousId = previous != null && previous.isNotEmpty + ? previous + : /*reset previousId if we get nothing*/ null; + if (clear) { - _messageResponses.add(messages.results); + _messageResponses.add(response.results); } else { - final temp = oldMessages + messages.results; + final temp = oldMessages + response.results; _messageResponses.add(temp); } if (_messageResponses.hasValue && _queryMessagesLoadingController.value) { diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index 4901c332..2e9f6c61 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -37,7 +37,7 @@ class MessageSearchListCore extends StatefulWidget { /// * [errorBuilder] /// * [loadingBuilder] /// * [childBuilder] - const MessageSearchListCore({ + MessageSearchListCore({ Key? key, required this.emptyBuilder, required this.errorBuilder, @@ -49,7 +49,21 @@ class MessageSearchListCore extends StatefulWidget { this.paginationParams, this.messageFilters, this.messageSearchListController, - }) : super(key: key); + }) : assert( + messageQuery != null || messageFilters != null, + 'Provide at least `query` or `messageFilters`', + ), + assert( + messageQuery == null || messageFilters == null, + "Can't provide both `query` and `messageFilters` at the same time", + ), + assert( + paginationParams?.offset == null || + paginationParams?.offset == 0 || + sortOptions == null, + 'Cannot specify `offset` with `sortOptions` parameter', + ), + super(key: key); /// A [MessageSearchListController] allows reloading and pagination. /// Use [MessageSearchListController.loadData] and @@ -73,9 +87,8 @@ class MessageSearchListCore extends StatefulWidget { final List? sortOptions; /// Pagination parameters - /// limit: the number of users to return (max is 30) + /// limit: the number of messages to return (max is 30) /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel final PaginationParams? paginationParams; /// The message query filters to use. @@ -159,15 +172,25 @@ class MessageSearchListCoreState extends State { ); /// Fetches more messages with updated pagination and updates the widget - Future paginateData() => _messageSearchBloc!.search( - filter: widget.filters, - sort: widget.sortOptions, - pagination: widget.paginationParams!.copyWith( - offset: _messageSearchBloc!.messageResponses?.length ?? 0, - ), - query: widget.messageQuery, - messageFilter: widget.messageFilters, + Future paginateData() { + PaginationParams? pagination; + if (widget.sortOptions != null) { + pagination = widget.paginationParams?.copyWith( + next: _messageSearchBloc?.nextId, ); + } else { + pagination = widget.paginationParams?.copyWith( + offset: _messageSearchBloc?.messageResponses?.length, + ); + } + return _messageSearchBloc!.search( + filter: widget.filters, + sort: widget.sortOptions, + pagination: pagination, + query: widget.messageQuery, + messageFilter: widget.messageFilters, + ); + } @override void didUpdateWidget(MessageSearchListCore oldWidget) { 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 9100e76d..c1781732 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 @@ -113,8 +113,9 @@ class ChannelQueryDao extends DatabaseAccessor cachedChannels.sort(chainedComparator); - if (paginationParams?.offset != null && cachedChannels.isNotEmpty) { - cachedChannels.removeRange(0, paginationParams!.offset); + final offset = paginationParams?.offset; + if (offset != null && offset > 0 && cachedChannels.isNotEmpty) { + cachedChannels.removeRange(0, offset); } if (paginationParams?.limit != null) { 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 c2e1cde2..fcedc157 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -117,8 +117,9 @@ class MessageDao extends DatabaseAccessor msgList.removeRange(0, greaterThanIndex); } } - if (options?.limit != null) { - return msgList.take(options!.limit).toList(); + final limit = options?.limit; + if (limit != null && limit > 0) { + return msgList.take(limit).toList(); } } return msgList; From 885c1f2cae5140fa6b8913c60296a85287ca688e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 7 Sep 2021 13:10:38 +0530 Subject: [PATCH 09/27] chore(llc, persistence): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 33 +++++++++++++++---- packages/stream_chat_persistence/CHANGELOG.md | 10 ++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 67a80e17..85bf5956 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,15 @@ +## Upcoming + +🛑️ Breaking Changes from `2.2.1` + +- Added 6 new methods in `ChatPersistenceClient`. + - `bulkUpdateMessages` + - `bulkUpdatePinnedMessages` + - `bulkUpdateMembers` + - `bulkUpdateReads` + - `updatePinnedMessageReactions` + - `deletePinnedMessageReactionsByMessageId` + ## 2.2.1 🐞 Fixed @@ -13,11 +25,16 @@ ✅ Added -- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. -- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. -- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized. -- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a channel has been initialized. +- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key + on `extraData`, so `user.image` and `user.extraData['image']` is the same. +- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, + so `user.name` and `user.extraData['name']` is the same. +- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a + channel has been initialized. +- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a + channel has been initialized. - Added slow mode which allows a cooldown period after a user sends a message. + ## 2.1.1 🐞 Fixed @@ -42,7 +59,7 @@ 🐞 Fixed -- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working +- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working - [#575](https://github.com/GetStream/stream-chat-flutter/issues/575): Wrong `OwnUser.*` ## 2.0.0 @@ -70,11 +87,11 @@ 🐞 Fixed -- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection +- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet + connection - several minor fixes - performance improvements - ✅ Added - New `Location` enum is introduced for easily changing the client location/baseUrl. @@ -89,6 +106,7 @@ ## 2.0.0-nullsafety.8 🐞 Fixed + - Export `PushProvider` enum ## 2.0.0-nullsafety.7 @@ -124,6 +142,7 @@ - Fix thread reply not working with attachments - Minor fixes + ## 2.0.0-nullsafety.5 - Minor fixes 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 From 395f5e6c643b4ce358eabc8365a470542f7def31 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 7 Sep 2021 10:02:05 +0200 Subject: [PATCH 10/27] feat(llc): add contains and empty filters --- packages/stream_chat/lib/src/core/models/filter.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/stream_chat/lib/src/core/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart index eb3932ff..6551d403 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 values + 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, List values) => + Filter._(operator: FilterOperator.contains, key: key, value: values); + /// 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 From d18286dc8df052379f863bac85f47ea32f46c9a7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 7 Sep 2021 10:10:10 +0200 Subject: [PATCH 11/27] feat(llc): add tests --- .../test/src/core/models/filter_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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..6a70f313 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']); From fe73cf0b71e0f5474746f8aadf568f0684067932 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 7 Sep 2021 10:36:53 +0200 Subject: [PATCH 12/27] fix(llc): fix contains filter params --- packages/stream_chat/lib/src/core/models/filter.dart | 6 +++--- packages/stream_chat/test/src/core/models/filter_test.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart index 6551d403..d4536eab 100644 --- a/packages/stream_chat/lib/src/core/models/filter.dart +++ b/packages/stream_chat/lib/src/core/models/filter.dart @@ -52,7 +52,7 @@ enum FilterOperator { /// Matches none of the values specified in an array. nor, - /// Matches any list that contains the specified values + /// Matches any list that contains the specified value contains, } @@ -162,8 +162,8 @@ class Filter extends Equatable { Filter._(operator: FilterOperator.exists, key: key, value: exists); /// Matches any list that contains the specified values - factory Filter.contains(String key, List values) => - Filter._(operator: FilterOperator.contains, key: key, value: 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({ 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 6a70f313..6e3dbf64 100644 --- a/packages/stream_chat/test/src/core/models/filter_test.dart +++ b/packages/stream_chat/test/src/core/models/filter_test.dart @@ -145,7 +145,7 @@ void main() { test('contains', () { const key = 'testKey'; - const values = ['testValue']; + const values = 'testValue'; final filter = Filter.contains(key, values); expect(filter.key, key); expect(filter.value, values); From 3e1d5f7f929fb33701919f6ef8de62182371b355 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 7 Sep 2021 10:37:13 +0200 Subject: [PATCH 13/27] docs(llc): add documentation for new filters --- .../Flutter/guides/understanding_filters.mdx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index 9d169a80..72b5eca3 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -107,6 +107,36 @@ 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, +}); +``` + ### Group Queries #### Filter.and From 6f75af2658fbfacc21dbf1c4795e89bce66b7988 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 7 Sep 2021 10:38:14 +0200 Subject: [PATCH 14/27] chore(llc): update changelog --- packages/stream_chat/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 67a80e17..ca4eec3f 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +✅ Added + +- Added `Filter.contains` and `Filter.empty` + ## 2.2.1 🐞 Fixed From 21647040e13423bd5b4b8b9089039632d5280ae2 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 7 Sep 2021 17:59:56 +0530 Subject: [PATCH 15/27] feat(core, ui): Improve pagination invocation by using paginationEnded flag. Signed-off-by: xsahil03x --- .../lib/src/message_search_list_view.dart | 4 +- .../lib/src/user_list_view.dart | 4 +- .../lib/src/channels_bloc.dart | 3 +- .../lib/src/message_search_bloc.dart | 37 +++-- .../lib/src/message_search_list_core.dart | 18 +-- .../lib/src/user_list_core.dart | 10 +- .../lib/src/users_bloc.dart | 21 ++- .../test/message_search_bloc_test.dart | 140 ++++++++++++++---- .../test/users_bloc_test.dart | 135 ++++++++++++----- 9 files changed, 272 insertions(+), 100 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 0fd45ea1..f80c688f 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -58,7 +58,7 @@ class MessageSearchListView extends StatefulWidget { required this.filters, this.messageQuery, this.sortOptions, - this.paginationParams, + this.paginationParams = const PaginationParams(limit: 30), this.messageFilters, this.separatorBuilder, this.itemBuilder, @@ -93,7 +93,7 @@ class MessageSearchListView extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams? paginationParams; + final PaginationParams paginationParams; /// The message query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index eb45f918..4d650192 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -51,7 +51,7 @@ class UserListView extends StatefulWidget { this.filter, this.sort, this.presence, - this.pagination, + this.pagination = const PaginationParams(limit: 30), this.onUserTap, this.onUserLongPress, this.userWidget, @@ -93,7 +93,7 @@ class UserListView extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams? pagination; + final PaginationParams pagination; /// Function called when tapping on a channel /// By default it calls [Navigator.push] building a [MaterialPageRoute] diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 6687eef5..25e31ed7 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -105,7 +105,8 @@ class ChannelsBlocState extends State }) async { final client = _streamChatCoreState!.client; - final clear = paginationParams.offset == 0; + final clear = + paginationParams.offset == null || paginationParams.offset == 0; if (clear && _paginationEnded) { _paginationEnded = false; } diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index d9d5933d..c99036a7 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart @@ -62,6 +62,8 @@ class MessageSearchBlocState extends State Stream get queryMessagesLoading => _queryMessagesLoadingController.stream; + bool _paginationEnded = false; + /// Calls [StreamChatClient.search] updating /// [messagesStream] and [queryMessagesLoading] stream Future search({ @@ -69,24 +71,31 @@ class MessageSearchBlocState extends State Filter? messageFilter, List? sort, String? query, - PaginationParams? pagination, + PaginationParams pagination = const PaginationParams(limit: 30), }) async { final client = _streamChatCoreState.client; - if (_queryMessagesLoadingController.value == true) return; + var clear = false; + if (sort != null) { + clear |= pagination.next == null; + } else { + final offset = pagination.offset; + clear |= offset == null || offset == 0; + } + + if (clear && _paginationEnded) { + _paginationEnded = false; + } + + if ((!clear && _paginationEnded) || + _queryMessagesLoadingController.value == true) { + return; + } if (_messageResponses.hasValue) { _queryMessagesLoadingController.add(true); } try { - var clear = pagination == null; - if (sort != null) { - clear |= pagination?.next == null; - } else { - final offset = pagination?.offset; - clear |= offset == null || offset == 0; - } - final oldMessages = List.from(messageResponses ?? []); final response = await client.search( @@ -107,15 +116,19 @@ class MessageSearchBlocState extends State ? previous : /*reset previousId if we get nothing*/ null; + final newMessages = response.results; if (clear) { - _messageResponses.add(response.results); + _messageResponses.add(newMessages); } else { - final temp = oldMessages + response.results; + final temp = oldMessages + newMessages; _messageResponses.add(temp); } if (_messageResponses.hasValue && _queryMessagesLoadingController.value) { _queryMessagesLoadingController.add(false); } + if (newMessages.isEmpty || newMessages.length < pagination.limit) { + _paginationEnded = true; + } } catch (e, stk) { // reset loading controller _queryMessagesLoadingController.add(false); diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index 47ffe28b..6e3d04fb 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -47,7 +47,7 @@ class MessageSearchListCore extends StatefulWidget { required this.filters, this.messageQuery, this.sortOptions, - this.paginationParams, + this.paginationParams = const PaginationParams(limit: 30), this.messageFilters, this.messageSearchListController, }) : assert( @@ -59,8 +59,8 @@ class MessageSearchListCore extends StatefulWidget { "Can't provide both `query` and `messageFilters` at the same time", ), assert( - paginationParams?.offset == null || - paginationParams?.offset == 0 || + paginationParams.offset == null || + paginationParams.offset == 0 || sortOptions == null, 'Cannot specify `offset` with `sortOptions` parameter', ), @@ -90,7 +90,7 @@ class MessageSearchListCore extends StatefulWidget { /// Pagination parameters /// limit: the number of messages to return (max is 30) /// offset: the offset (max is 1000) - final PaginationParams? paginationParams; + final PaginationParams paginationParams; /// The message query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. @@ -169,13 +169,13 @@ class MessageSearchListCoreState extends State { /// Fetches more messages with updated pagination and updates the widget Future paginateData() { - PaginationParams? pagination; + PaginationParams pagination; if (widget.sortOptions != null) { - pagination = widget.paginationParams?.copyWith( + pagination = widget.paginationParams.copyWith( next: _messageSearchBloc?.nextId, ); } else { - pagination = widget.paginationParams?.copyWith( + pagination = widget.paginationParams.copyWith( offset: _messageSearchBloc?.messageResponses?.length, ); } @@ -196,8 +196,8 @@ class MessageSearchListCoreState extends State { widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() || widget.messageFilters?.toString() != oldWidget.messageFilters?.toString() || - widget.paginationParams?.toJson().toString() != - oldWidget.paginationParams?.toJson().toString()) { + widget.paginationParams.toJson().toString() != + oldWidget.paginationParams.toJson().toString()) { loadData(); } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index 57d61fe6..e403be4e 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -66,7 +66,7 @@ class UserListCore extends StatefulWidget { this.filter, this.sort, this.presence, - this.pagination, + this.pagination = const PaginationParams(limit: 30), this.groupAlphabetically = false, this.userListController, }) : super(key: key); @@ -106,7 +106,7 @@ class UserListCore extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams? pagination; + final PaginationParams pagination; /// Set it to true to group users by their first character /// @@ -201,7 +201,7 @@ class UserListCoreState extends State filter: widget.filter, sort: widget.sort, presence: widget.presence, - pagination: widget.pagination!.copyWith( + pagination: widget.pagination.copyWith( offset: _usersBloc!.users?.length ?? 0, ), ); @@ -212,8 +212,8 @@ class UserListCoreState extends State if (widget.filter?.toString() != oldWidget.filter?.toString() || jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || widget.presence != oldWidget.presence || - widget.pagination?.toJson().toString() != - oldWidget.pagination?.toJson().toString()) { + widget.pagination.toJson().toString() != + oldWidget.pagination.toJson().toString()) { loadData(); } diff --git a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart index e7916478..c119badc 100644 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -57,6 +57,8 @@ class UsersBlocState extends State late StreamChatCoreState _streamChatCore; + bool _paginationEnded = false; + /// The Query Users method allows you to search for users and see if they are /// online/offline. /// [API Reference](https://getstream.io/chat/docs/flutter-dart/query_users/?language=dart) @@ -64,19 +66,26 @@ class UsersBlocState extends State Filter? filter, List? sort, bool? presence, - PaginationParams? pagination, + PaginationParams pagination = const PaginationParams(limit: 30), }) async { final client = _streamChatCore.client; - if (_queryUsersLoadingController.value == true) return; + final clear = pagination.offset == null || pagination.offset == 0; + + if (clear && _paginationEnded) { + _paginationEnded = false; + } + + if ((!clear && _paginationEnded) || + _queryUsersLoadingController.value == true) { + return; + } if (_usersController.hasValue) { _queryUsersLoadingController.add(true); } try { - final clear = pagination == null || pagination.offset == 0; - final oldUsers = List.from(users ?? []); final usersResponse = await client.queryUsers( @@ -86,6 +95,7 @@ class UsersBlocState extends State pagination: pagination, ); + final newUsers = usersResponse.users; if (clear) { _usersController.add(usersResponse.users); } else { @@ -95,6 +105,9 @@ class UsersBlocState extends State if (_usersController.hasValue && _queryUsersLoadingController.value) { _queryUsersLoadingController.add(false); } + if (newUsers.isEmpty || newUsers.length < pagination.limit) { + _paginationEnded = true; + } } catch (e, stk) { // reset loading controller _queryUsersLoadingController.add(false); diff --git a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart index 979ca5b2..c689d90c 100644 --- a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart @@ -31,8 +31,7 @@ void main() { ); testWidgets( - 'messageSearchBlocState.search() should throw if used where ' - 'StreamChat is not present in the widget tree', + '''messageSearchBlocState.search() should throw if used where StreamChat is not present in the widget tree''', (tester) async { const messageSearchBloc = MessageSearchBloc( child: Offstage(), @@ -74,7 +73,10 @@ void main() { messageFilters: any(named: 'messageFilters'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(filter: testFilter); @@ -95,8 +97,7 @@ void main() { ); testWidgets( - 'messageSearchBlocState.messagesStream should emit error ' - 'if client.search() throws', + '''messageSearchBlocState.messagesStream should emit error if client.search() throws''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -144,9 +145,7 @@ void main() { ); testWidgets( - 'calling messageSearchBlocState.search() again with an offset ' - 'should emit new data through messagesStream and also emit loading state ' - 'through queryMessagesLoading', + '''calling messageSearchBlocState.search() again with an offset should emit new data through messagesStream and also emit loading state through queryMessagesLoading''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -168,19 +167,23 @@ void main() { find.byKey(messageSearchBlocKey), ); - final messageResponseList = _generateMessages(); + const pagination = PaginationParams(limit: 25); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); - messageSearchBlocState.search(filter: testFilter); + messageSearchBlocState.search(pagination: pagination, filter: testFilter); await expectLater( messageSearchBlocState.messagesStream, @@ -192,22 +195,24 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).called(1); final offset = messageResponseList.length; final paginatedMessageResponseList = _generateMessages(offset: offset); - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = paginatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = paginatedMessageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(pagination: pagination, filter: testFilter); @@ -236,9 +241,7 @@ void main() { ); testWidgets( - 'calling messageSearchBlocState.search() again with an offset ' - 'should emit error through queryUsersLoading if ' - 'client.search() throws', + '''calling messageSearchBlocState.search() again with an offset should emit error through queryUsersLoading if client.search() throws''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -260,19 +263,23 @@ void main() { find.byKey(messageSearchBlocKey), ); - final messageResponseList = _generateMessages(); + const pagination = PaginationParams(limit: 25); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); - messageSearchBlocState.search(filter: testFilter); + messageSearchBlocState.search(pagination: pagination, filter: testFilter); await expectLater( messageSearchBlocState.messagesStream, @@ -284,11 +291,11 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).called(1); final offset = messageResponseList.length; - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); const error = 'Error! Error! Error!'; when(() => mockClient.search( @@ -296,10 +303,13 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).thenThrow(error); - messageSearchBlocState.search(pagination: pagination, filter: testFilter); + messageSearchBlocState.search( + pagination: newPagination, + filter: testFilter, + ); await expectLater( messageSearchBlocState.queryMessagesLoading, @@ -311,8 +321,80 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).called(1); }, ); + + testWidgets( + '''calling messageSearchBlocState.search() again with an offset should do nothing and return if pagination is completed''', + (tester) async { + const messageSearchBlocKey = Key('messageSearchBloc'); + const childKey = Key('child'); + const messageSearchBloc = MessageSearchBloc( + key: messageSearchBlocKey, + child: Offstage(key: childKey), + ); + + final mockClient = MockClient(); + + await tester.pumpWidget( + StreamChatCore( + client: mockClient, + child: messageSearchBloc, + ), + ); + + final messageSearchBlocState = tester.state( + find.byKey(messageSearchBlocKey), + ); + + const pagination = PaginationParams(limit: 25); + + final messageResponseList = _generateMessages(count: 20); + + when(() => mockClient.search( + testFilter, + query: any(named: 'query'), + sort: any(named: 'sort'), + messageFilters: any(named: 'messageFilters'), + paginationParams: pagination, + )).thenAnswer( + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, + ); + + messageSearchBlocState.search(pagination: pagination, filter: testFilter); + + await expectLater( + messageSearchBlocState.messagesStream, + emits(isSameMessageResponseListAs(messageResponseList)), + ); + + verify(() => mockClient.search( + testFilter, + query: any(named: 'query'), + sort: any(named: 'sort'), + messageFilters: any(named: 'messageFilters'), + paginationParams: pagination, + )).called(1); + + final offset = messageResponseList.length; + final newPagination = pagination.copyWith(offset: offset); + + messageSearchBlocState.search( + filter: testFilter, + pagination: newPagination, + ); + + // should emit nothing. + await expectLater( + // skipping the initial data (behaviorSubject). + messageSearchBlocState.messagesStream.skip(1), + emitsInOrder([]), + ); + }, + ); } diff --git a/packages/stream_chat_flutter_core/test/users_bloc_test.dart b/packages/stream_chat_flutter_core/test/users_bloc_test.dart index af088a76..549f9291 100644 --- a/packages/stream_chat_flutter_core/test/users_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/users_bloc_test.dart @@ -164,16 +164,17 @@ void main() { find.byKey(usersBlocKey), ); - final users = _generateUsers(); + const pagination = PaginationParams(limit: 25); + final users = _generateUsers(count: 25); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, )).thenAnswer((_) async => QueryUsersResponse()..users = users); - usersBlocState.queryUsers(); + usersBlocState.queryUsers(pagination: pagination); await expectLater( usersBlocState.usersStream, @@ -184,23 +185,23 @@ void main() { filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, )).called(1); final offset = users.length; final paginatedUsers = _generateUsers(offset: offset); - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )) - .thenAnswer( - (_) async => QueryUsersResponse()..users = paginatedUsers); + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).thenAnswer( + (_) async => QueryUsersResponse()..users = paginatedUsers, + ); - usersBlocState.queryUsers(pagination: pagination); + usersBlocState.queryUsers(pagination: newPagination); await Future.wait([ expectLater( @@ -217,7 +218,7 @@ void main() { filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: pagination, + pagination: newPagination, )).called(1); }, ); @@ -247,13 +248,89 @@ void main() { find.byKey(usersBlocKey), ); - final users = _generateUsers(); + const pagination = PaginationParams(limit: 25); + final users = _generateUsers(count: 25); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, + )).thenAnswer((_) async => QueryUsersResponse()..users = users); + + usersBlocState.queryUsers(pagination: pagination); + + await expectLater( + usersBlocState.usersStream, + emits(isSameUserListAs(users)), + ); + + verify(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: pagination, + )).called(1); + + final offset = users.length; + final newPagination = pagination.copyWith(offset: offset); + + const error = 'Error! Error! Error!'; + + when(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).thenThrow(error); + + usersBlocState.queryUsers(pagination: newPagination); + + await expectLater( + usersBlocState.queryUsersLoading, + emitsError(error), + ); + + verify(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).called(1); + }, + ); + + testWidgets( + '''calling usersBlocState.queryUsers() again with an offset should do nothing and return if pagination is completed''', + (tester) async { + const usersBlocKey = Key('usersBloc'); + const childKey = Key('child'); + const usersBloc = UsersBloc( + key: usersBlocKey, + child: Offstage(key: childKey), + ); + + final mockClient = MockClient(); + + await tester.pumpWidget( + StreamChatCore( + client: mockClient, + child: usersBloc, + ), + ); + + final usersBlocState = tester.state( + find.byKey(usersBlocKey), + ); + + const pagination = PaginationParams(limit: 30); + final users = _generateUsers(count: 25); + + when(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: pagination, )).thenAnswer((_) async => QueryUsersResponse()..users = users); usersBlocState.queryUsers(); @@ -271,30 +348,16 @@ void main() { )).called(1); final offset = users.length; - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); - const error = 'Error! Error! Error!'; - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).thenThrow(error); - - usersBlocState.queryUsers(pagination: pagination); + usersBlocState.queryUsers(pagination: newPagination); + // should emit nothing. await expectLater( - usersBlocState.queryUsersLoading, - emitsError(error), + // skipping the initial data (behaviorSubject). + usersBlocState.usersStream, + emitsInOrder([]), ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).called(1); }, ); } From 9dc04acf8295e20dd92d33c1a9ca769a4ef6fc08 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 7 Sep 2021 15:43:04 +0200 Subject: [PATCH 16/27] docs(llc): add documentation for filter.custom --- .../docs/Flutter/guides/understanding_filters.mdx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index 72b5eca3..e9d72ecf 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -137,6 +137,18 @@ Filter.raw(value: { }); ``` +#### 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 From d9de5e15ada2bd299bb2b2edd0c1035b63ab6e39 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 7 Sep 2021 19:45:31 +0530 Subject: [PATCH 17/27] test(core): fix message search list core tests Signed-off-by: xsahil03x --- .../lib/src/message_search_list_core.dart | 8 +- .../test/message_search_list_core_test.dart | 103 ++++++++++++++---- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index 47ffe28b..4cc6c46b 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -38,7 +38,7 @@ class MessageSearchListCore extends StatefulWidget { /// * [errorBuilder] /// * [loadingBuilder] /// * [childBuilder] - MessageSearchListCore({ + const MessageSearchListCore({ Key? key, required this.emptyBuilder, required this.errorBuilder, @@ -58,12 +58,6 @@ class MessageSearchListCore extends StatefulWidget { messageQuery == null || messageFilters == null, "Can't provide both `query` and `messageFilters` at the same time", ), - assert( - paginationParams?.offset == null || - paginationParams?.offset == 0 || - sortOptions == null, - 'Cannot specify `offset` with `sortOptions` parameter', - ), super(key: key); /// A [MessageSearchListController] allows reloading and pagination. diff --git a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart index 6b700c76..2c398013 100644 --- a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart @@ -7,6 +7,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'mocks.dart'; const testFilter = Filter.custom(operator: '\$test', value: 'testValue'); +const testMessageFilter = Filter.custom(operator: '\$test', value: 'testValue'); void main() { List _generateMessages({ @@ -28,6 +29,40 @@ void main() { }, ); + testWidgets( + 'should throw if both `messageQuery` and `messageFilters` are provided', + (tester) async { + expect( + () => MessageSearchListCore( + childBuilder: (_) => const Offstage(), + loadingBuilder: (_) => const Offstage(), + emptyBuilder: (_) => const Offstage(), + errorBuilder: (_, __) => const Offstage(), + filters: testFilter, + messageFilters: testMessageFilter, + messageQuery: 'test', + ), + throwsAssertionError, + ); + }, + ); + + testWidgets( + 'should throw if both `messageQuery` and `messageFilters` are not provided', + (tester) async { + expect( + () => MessageSearchListCore( + childBuilder: (_) => const Offstage(), + loadingBuilder: (_) => const Offstage(), + emptyBuilder: (_) => const Offstage(), + errorBuilder: (_, __) => const Offstage(), + filters: testFilter, + ), + throwsAssertionError, + ); + }, + ); + testWidgets( 'should throw if MessageSearchListCore is used where MessageSearchBloc ' 'is not present in the widget tree', @@ -40,6 +75,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object? error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); await tester.pumpWidget(messageSearchListCore); @@ -61,6 +97,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object? error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -92,6 +129,7 @@ void main() { errorBuilder: (BuildContext context, Object error) => const Offstage(), messageSearchListController: controller, filters: testFilter, + messageFilters: testMessageFilter, ); expect(controller.loadData, isNull); @@ -129,6 +167,7 @@ void main() { key: errorWidgetKey, ), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -138,7 +177,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenThrow(error); @@ -159,7 +198,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -179,6 +218,7 @@ void main() { const Offstage(key: emptyWidgetKey), errorBuilder: (BuildContext context, Object error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -188,10 +228,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -211,7 +254,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -231,6 +274,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -240,10 +284,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -263,7 +310,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -289,6 +336,7 @@ void main() { errorBuilder: (BuildContext context, Object error) => const Offstage(), paginationParams: pagination, filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -298,10 +346,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -332,7 +383,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).called(1); @@ -348,11 +399,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = paginatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = paginatedMessageResponseList + ..next = null + ..previous = null, ); await messageSearchListCoreState.paginateData(); @@ -372,7 +425,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).called(1); }, @@ -406,6 +459,7 @@ void main() { const Offstage(), paginationParams: pagination.copyWith(limit: limit), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -415,10 +469,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -453,7 +510,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).called(1); @@ -466,11 +523,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = updatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = updatedMessageResponseList + ..next = null + ..previous = null, ); await tester.pumpAndSettle(); @@ -487,7 +546,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).called(1); }, From 3069f8762935a1f972c3298deed678ed054bc735 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 7 Sep 2021 20:20:49 +0530 Subject: [PATCH 18/27] test(core): fix tests Signed-off-by: xsahil03x --- .../lib/src/channels_bloc.dart | 3 ++- .../lib/src/users_bloc.dart | 4 +++- .../test/message_search_bloc_test.dart | 21 ++++++++++++++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 6687eef5..1aed7198 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -105,7 +105,8 @@ class ChannelsBlocState extends State }) async { final client = _streamChatCoreState!.client; - final clear = paginationParams.offset == 0; + final offset = paginationParams.offset; + final clear = offset == null || offset == 0; if (clear && _paginationEnded) { _paginationEnded = false; } diff --git a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart index e7916478..f2ec10d9 100644 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -75,7 +75,9 @@ class UsersBlocState extends State } try { - final clear = pagination == null || pagination.offset == 0; + final clear = pagination == null || + pagination.offset == null || + pagination.offset == 0; final oldUsers = List.from(users ?? []); diff --git a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart index 979ca5b2..3e56ca76 100644 --- a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart @@ -74,7 +74,10 @@ void main() { messageFilters: any(named: 'messageFilters'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(filter: testFilter); @@ -177,7 +180,10 @@ void main() { messageFilters: any(named: 'messageFilters'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(filter: testFilter); @@ -206,8 +212,10 @@ void main() { messageFilters: any(named: 'messageFilters'), paginationParams: pagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = paginatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = paginatedMessageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(pagination: pagination, filter: testFilter); @@ -269,7 +277,10 @@ void main() { messageFilters: any(named: 'messageFilters'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(filter: testFilter); From da475285fcd4d7459c98e8a96b4c327a3858bb91 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 7 Sep 2021 21:08:34 +0530 Subject: [PATCH 19/27] test(core): fix tests Signed-off-by: xsahil03x --- .../test/message_search_list_core_test.dart | 4 ++-- .../stream_chat_flutter_core/test/user_list_core_test.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart index 2c398013..03396513 100644 --- a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart @@ -322,7 +322,7 @@ void main() { (tester) async { const messageSearchListCoreKey = Key('messageSearchListCore'); const childWidgetKey = Key('childWidget'); - const pagination = PaginationParams(); + const pagination = PaginationParams(limit: 25); final messageSearchListCore = MessageSearchListCore( key: messageSearchListCoreKey, childBuilder: (List messages) => Container( @@ -341,7 +341,7 @@ void main() { final mockClient = MockClient(); - final messageResponseList = _generateMessages(); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart index eac8dc71..77977bb8 100644 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/user_list_core_test.dart @@ -321,7 +321,7 @@ void main() { (tester) async { const userListCoreKey = Key('userListCore'); const listWidgetKey = Key('listWidget'); - const pagination = PaginationParams(); + const pagination = PaginationParams(limit: 15); final userListCore = UserListCore( key: userListCoreKey, listBuilder: (_, items) => Container( @@ -347,7 +347,7 @@ void main() { final mockClient = MockClient(); - final users = _generateUsers(); + final users = _generateUsers(count: 15); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), From 01e259a6ae141139e354d1b27ac04edec756074c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 7 Sep 2021 21:12:33 +0530 Subject: [PATCH 20/27] test(llc): fix tests Signed-off-by: xsahil03x --- .../test/src/theme/message_search_list_view_theme_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart index 4da1c7dc..69c63abe 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart @@ -69,6 +69,7 @@ void main() { body: MessageSearchBloc( child: MessageSearchListView( filters: Filter.in_('members', const ['test_id']), + messageQuery: 'test query', ), ), ); @@ -100,6 +101,7 @@ void main() { body: MessageSearchBloc( child: MessageSearchListView( filters: Filter.in_('members', const ['test_id']), + messageQuery: 'test query', ), ), ); From b5f40dd417cc7eac42b0bb73142df3b20e73f134 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Sep 2021 15:23:55 +0530 Subject: [PATCH 21/27] chore(ui, core): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 100 +++++++++--------- .../stream_chat_flutter_core/CHANGELOG.md | 30 +++++- 2 files changed, 80 insertions(+), 50 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 02e2b214..ed8f5e06 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,5 +1,22 @@ ## 2.2.1 +🛑️ Breaking Changes from `2.2.1` + +- `MessageSearchListView` paginationParams property is now non-nullable with a default value. + ```dart + paginationParams = const PaginationParams(limit: 30) + ``` +- `UserListView` pagination property is now non-nullable with a default value. + ```dart + pagination = const PaginationParams(limit: 30) + ``` + +🐞 Fixed + +- Fixed `MessageSearchListView` pagination. + +## 2.2.1 + - Updated `stream_chat_flutter_core` dependency to 2.2.1 ## 2.2.0 @@ -7,14 +24,13 @@ ✅ Added - [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): - Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image - is loading + Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image is loading - Added a `backgroundColor` property to the following widgets: - - `ChannelHeader` - - `ChannelListHeader` - - `GalleryHeader` - - `GalleryFooter` - - `ThreadHeader` + - `ChannelHeader` + - `ChannelListHeader` + - `GalleryHeader` + - `GalleryFooter` + - `ThreadHeader` - Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message. - Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded. This will override the default error alert behaviour. @@ -34,9 +50,8 @@ You can call `.copyWith` to customize just a subset of properties. 🔄 Changed -Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with -them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming -breakdown: +Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with them, and have been +upgraded with some goodies like `lerp` functions. Here's the full naming breakdown: * `AvatarTheme` is now `AvatarThemeData` * `ChannelHeaderTheme` is now `ChannelHeaderThemeData` @@ -53,18 +68,18 @@ breakdown: 🐞 Fixed -- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the - camera is null. +- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. - Fixed date dividers position/alignment in non reversed `MessageListView`. - Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set. -- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text. +- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when + sending a message with no text. ## 2.1.2 🐞 Fixed -- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no - members when sending message +- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending + message ## 2.1.1 @@ -82,8 +97,7 @@ breakdown: 🔄 Changed - `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`. -- `StreamChat.of(context).userStream` is now deprecated in favor - of `StreamChat.of(context).currentUserStream`. +- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`. 🐞 Fixed @@ -136,8 +150,7 @@ You can call `.copyWith` to customize just a subset of properties - Added video compress options (frame and quality) to `MessageInput` - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView - header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer - `MessageWidget` accepts a `userAvatarBuilder` - Added pinMessage ui support - Added `MessageListView.threadSeparatorBuilder` property @@ -146,12 +159,10 @@ You can call `.copyWith` to customize just a subset of properties 🐞 Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text - box when editing message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator - use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without - a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing + message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload - `MessageListView` not rendering if the user is not a member of the channel - Fix `MessageInput` overflow when there are no actions - Minor fixes and improvements @@ -204,18 +215,15 @@ You can call `.copyWith` to customize just a subset of properties. ✅ Added - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView - header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer - `MessageWidget` accepts a `userAvatarBuilder` 🐞 Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text - box when editing message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator - use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without - a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing + message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload - `MessageListView` not rendering if the user is not a member of the channel ## 2.0.0-nullsafety.7 @@ -285,8 +293,7 @@ You can call `.copyWith` to customize just a subset of properties. - Show error messages as system and keep them in the message input - Remove notification badge logic - Use shimmer while loading images -- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated - to `MessageInput` +- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput` - Add possibility to specify custom message actions using `MessageWidget.customActions` - Added `MessageListView.onAttachmentTap` callback - Fixed message newline issue @@ -343,8 +350,7 @@ You can call `.copyWith` to customize just a subset of properties. - Improved api documentation - Updated `stream_chat` dependency to `^1.0.0-beta` - Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples) -- Reimplemented existing widgets - using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) +- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) ## 0.2.21 @@ -361,8 +367,8 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.20+2 -- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the - list when a new message arrives +- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message + arrives ## 0.2.20+1 @@ -396,8 +402,7 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.16 -- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress - implementation +- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation - Make public autofocus field of the TextField of message_input ## 0.2.15 @@ -582,11 +587,10 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.1-alpha+1 -- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have - the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to - your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in - the usual way, but you need to add a `StreamChat` widget to every route of your app. - Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more +- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget + as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of + your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to + every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more information. ```dart @@ -688,8 +692,8 @@ Widget build(BuildContext context) { - Add gesture (vertical drag down) to close the keyboard -- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will - even close the keyboard) +- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the + keyboard) The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 1378ced0..d0d89a6b 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,20 @@ +## Upcoming + +🛑️ Breaking Changes from `2.2.1` + +- `MessageSearchListViewCore` paginationParams property is now non-nullable with a default value. + ```dart + paginationParams = const PaginationParams(limit: 30) + ``` +- `UserListViewCore` pagination property is now non-nullable with a default value. + ```dart + pagination = const PaginationParams(limit: 30) + ``` + +🐞 Fixed + +- Fixed `MessageSearchBloc` pagination. + ## 2.2.1 - Updated `stream_chat` dependency to 2.2.1 @@ -5,13 +22,17 @@ ## 2.2.0 🛑️ Breaking Changes from `2.1.1` + - Renamed `BetterStreamBuilder.loadingBuilder` to `.noDataBuilder` 🔄 Changed + - `BetterStreamBuilder.initialData` is now nullable/not-required. 🐞 Fixed -- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh + +- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after + refresh ## 2.1.1 @@ -20,12 +41,15 @@ ## 2.1.0 🛑️ Breaking Changes from `2.0.0` + - Changed default message filter of `MessageListCore` ✅ Added + - Added `MessageListCore.paginationLimit` 🔄 Changed + - `StreamChatCore.of(context).user` is now deprecated in favor of `StreamChatCore.of(context).currentUser`. - `StreamChatCore.of(context).userStream` is now deprecated in favor of `StreamChatCore.of(context).currentUserStream`. @@ -34,7 +58,8 @@ 🛑️ Breaking Changes from `1.5.3` - migrate this package to null safety -- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual + params/properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence @@ -51,6 +76,7 @@ - Performance improvements ## 2.0.0-nullsafety.9 + - Update llc dependency ## 2.0.0-nullsafety.8 From e27ec9edb71913211678e46e180dae6b917ac00e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Sep 2021 15:25:21 +0530 Subject: [PATCH 22/27] chore(core): add doc comments Signed-off-by: xsahil03x --- .../stream_chat_flutter_core/lib/src/message_search_bloc.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index d9d5933d..cd4fa9f7 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart @@ -43,7 +43,10 @@ class MessageSearchBlocState extends State with AutomaticKeepAliveClientMixin { late StreamChatCoreState _streamChatCoreState; + /// The key used to paginate next items. String? nextId; + + /// The key used to paginate previous items. String? previousId; /// The current messages list From f37412059957a5b96169ee67a97fa27d60304804 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Sep 2021 15:47:52 +0530 Subject: [PATCH 23/27] chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 67a80e17..f54ab8d3 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +✅ Added + +- Add support for `next`, `previous` value pagination in `client.search`. + ## 2.2.1 🐞 Fixed @@ -13,11 +19,16 @@ ✅ Added -- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. -- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. -- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized. -- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a channel has been initialized. +- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key + on `extraData`, so `user.image` and `user.extraData['image']` is the same. +- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, + so `user.name` and `user.extraData['name']` is the same. +- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a + channel has been initialized. +- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a + channel has been initialized. - Added slow mode which allows a cooldown period after a user sends a message. + ## 2.1.1 🐞 Fixed @@ -42,7 +53,7 @@ 🐞 Fixed -- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working +- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working - [#575](https://github.com/GetStream/stream-chat-flutter/issues/575): Wrong `OwnUser.*` ## 2.0.0 @@ -70,11 +81,11 @@ 🐞 Fixed -- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet connection +- [#369](https://github.com/GetStream/stream-chat-flutter/issues/369): Client does not return without internet + connection - several minor fixes - performance improvements - ✅ Added - New `Location` enum is introduced for easily changing the client location/baseUrl. @@ -89,6 +100,7 @@ ## 2.0.0-nullsafety.8 🐞 Fixed + - Export `PushProvider` enum ## 2.0.0-nullsafety.7 @@ -124,6 +136,7 @@ - Fix thread reply not working with attachments - Minor fixes + ## 2.0.0-nullsafety.5 - Minor fixes From 977add6ab9fbb57c4cafaf75c347f55613ecaf45 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Sep 2021 15:49:47 +0530 Subject: [PATCH 24/27] chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index f54ab8d3..1f7d7bce 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -2,7 +2,7 @@ ✅ Added -- Add support for `next`, `previous` value pagination in `client.search`. +- Add support for `next`, `previous` value pagination in `client.search`, [read more.](https://getstream.io/chat/docs/other-rest/search/#pagination) ## 2.2.1 From f4e37e23d6dbe0b8dfea0727f80eb3a6375a6949 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Sep 2021 16:37:34 +0530 Subject: [PATCH 25/27] fix(llc): Fix unread count not updating when the current user is set. Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 4 ++++ .../stream_chat/lib/src/client/client.dart | 24 +++++++++++-------- .../lib/src/core/models/own_user.dart | 6 ++--- .../lib/src/core/models/own_user.g.dart | 2 +- .../test/src/client/client_test.dart | 22 +++++++++++++++++ 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 61f00005..76b65833 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -14,6 +14,10 @@ - Added `Filter.contains` and `Filter.empty` +🐞 Fixed + +- [[#659]](https://github.com/GetStream/stream-chat-flutter/issues/659) Fixed unread count not updating correctly. + ## 2.2.1 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index f9d10a92..9253b623 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1366,16 +1366,7 @@ class ClientState { .where((event) => event.me != null && event.type != EventType.healthCheck) .map((e) => e.me!) - .listen((user) { - currentUser = currentUser?.merge(user) ?? user; - final totalUnreadCount = user.totalUnreadCount; - _totalUnreadCountController.add(totalUnreadCount); - - final unreadChannels = user.unreadChannels; - if (unreadChannels != null) { - _unreadChannelsController.add(unreadChannels); - } - }), + .listen((user) => currentUser = currentUser?.merge(user) ?? user), _client .on() .map((event) => event.unreadChannels) @@ -1441,6 +1432,7 @@ class ClientState { /// Sets the user currently interacting with the client /// note: this fully overrides the [currentUser] set currentUser(OwnUser? user) { + _computeUnreadCounts(user); _currentUserController.add(user); } @@ -1506,6 +1498,18 @@ class ClientState { _channelsController.add(newChannels); } + void _computeUnreadCounts(OwnUser? user) { + final totalUnreadCount = user?.totalUnreadCount; + if (totalUnreadCount != null) { + _totalUnreadCountController.add(totalUnreadCount); + } + + final unreadChannels = user?.unreadChannels; + if (unreadChannels != null) { + _unreadChannelsController.add(unreadChannels); + } + } + final _channelsController = BehaviorSubject>.seeded({}); final _currentUserController = BehaviorSubject(); final _usersController = BehaviorSubject>.seeded({}); diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index a65052f4..07c6cf30 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -17,7 +17,7 @@ class OwnUser extends User { this.devices = const [], this.mutes = const [], this.totalUnreadCount = 0, - this.unreadChannels, + this.unreadChannels = 0, this.channelMutes = const [], required String id, String? role, @@ -151,8 +151,8 @@ class OwnUser extends User { final int totalUnreadCount; /// Total unread channels by the user. - @JsonKey(includeIfNull: false) - final int? unreadChannels; + @JsonKey(includeIfNull: false, defaultValue: 0) + final int unreadChannels; /// Known top level fields. /// diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index ca4acdea..185b6dd0 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -17,7 +17,7 @@ OwnUser _$OwnUserFromJson(Map json) { .toList() ?? [], totalUnreadCount: json['total_unread_count'] as int? ?? 0, - unreadChannels: json['unread_channels'] as int?, + unreadChannels: json['unread_channels'] as int? ?? 0, channelMutes: (json['channel_mutes'] as List?) ?.map((e) => Mute.fromJson(e as Map)) .toList() ?? diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index a1a3fc96..b59faa38 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -2313,5 +2313,27 @@ void main() { )).called(1); verifyNoMoreInteractions(api.message); }); + + test( + '''setting the `currentUser` should also compute and update the unreadCounts''', + () { + final state = client.state; + final initialUser = OwnUser.fromUser(user); + + expect(state.currentUser, initialUser); + expect(state.totalUnreadCount, 0); + expect(state.unreadChannels, 0); + + final updateUser = initialUser.copyWith( + totalUnreadCount: 33, + unreadChannels: 33, + ); + state.currentUser = updateUser; + + expect(state.currentUser, updateUser); + expect(state.totalUnreadCount, 33); + expect(state.unreadChannels, 33); + }, + ); }); } From 94636f2115f50ce2dc93cd6f7ca696daa8984cde Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Sep 2021 18:43:32 +0530 Subject: [PATCH 26/27] chore(llc): Update current user in `event.unreadChannels`,`.totalUnreadCount` Signed-off-by: xsahil03x --- packages/stream_chat/lib/src/client/client.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 9253b623..9487e6e7 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1371,12 +1371,16 @@ class ClientState { .on() .map((event) => event.unreadChannels) .whereType() - .listen(_unreadChannelsController.add), + .listen((count) { + currentUser = currentUser?.copyWith(unreadChannels: count); + }), _client .on() .map((event) => event.totalUnreadCount) .whereType() - .listen(_totalUnreadCountController.add), + .listen((count) { + currentUser = currentUser?.copyWith(totalUnreadCount: count); + }), ]); _listenChannelDeleted(); From 720c0a80edc51f8998f23b39f710b2312d6c9ee1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 9 Sep 2021 10:25:08 +0200 Subject: [PATCH 27/27] chore(repo): update flutter version in workflow --- .github/workflows/stream_flutter_workflow.yml | 2 +- .../lib/src/core/http/interceptor/auth_interceptor.dart | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 0baaaff7..2e81d732 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -2,7 +2,7 @@ name: stream_flutter_workflow env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - flutter_version: "2.2.2" + flutter_version: "2.5.0" on: pull_request: diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index 9947eefa..37f618f5 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -38,7 +38,9 @@ class AuthInterceptor extends Interceptor { 'Authorization': token.rawValue, 'stream-auth-type': token.authType.raw, }; - options..queryParameters.addAll(params)..headers.addAll(headers); + options + ..queryParameters.addAll(params) + ..headers.addAll(headers); return handler.next(options); }