diff --git a/lib/model/message.dart b/lib/model/message.dart index 4ccf45d2ca0..628d0b57037 100644 --- a/lib/model/message.dart +++ b/lib/model/message.dart @@ -175,7 +175,13 @@ class MessageStoreImpl with MessageStore { } void handleDeleteMessageEvent(DeleteMessageEvent event) { - // TODO handle DeleteMessageEvent, particularly in MessageListView + for (final messageId in event.messageIds) { + if (messages.containsKey(messageId)) messages.remove(messageId); + } + for (final view in _messageListViews) { + if (view.messages.isEmpty) continue; + view.messagesRemoved(event.messageIds); + } } void handleUpdateMessageFlagsEvent(UpdateMessageFlagsEvent event) { diff --git a/lib/model/message_list.dart b/lib/model/message_list.dart index 117b30e614e..f525bf0f95b 100644 --- a/lib/model/message_list.dart +++ b/lib/model/message_list.dart @@ -155,6 +155,27 @@ mixin _MessageSequence { _processMessage(messages.length - 1); } + /// Given message ids, remove the corresponding messages from the view, and + /// return true if at least one message has been removed, or false otherwise. + /// + /// If none of the message ids provided can be found, this is essentially + /// a no-op. + bool _removeMessagesById(Iterable messageIds) { + bool hasRemovedMessage = false; + for (final messageId in messageIds) { + final messageIndex = _findMessageWithId(messageId); + if (messageIndex == -1) continue; + + assert(contents.length == messages.length); + messages.removeAt(messageIndex); + contents.removeAt(messageIndex); + assert(contents.length == messages.length); + hasRemovedMessage = true; + } + if (hasRemovedMessage) _reprocessAll(); + return hasRemovedMessage; + } + void _insertAllMessages(int index, Iterable toInsert) { // TODO parse/process messages in smaller batches, to not drop frames. // On a Pixel 5, a batch of 100 messages takes ~15-20ms in _insertAllMessages. @@ -478,6 +499,12 @@ class MessageListView with ChangeNotifier, _MessageSequence { } } + void messagesRemoved(List messageIds) { + if (_removeMessagesById(messageIds)) { + notifyListeners(); + } + } + /// Called when the app is reassembled during debugging, e.g. for hot reload. /// /// This will redo from scratch any computations we can, such as parsing diff --git a/test/example_data.dart b/test/example_data.dart index 36fc60fec05..a30916eb3b5 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -388,6 +388,16 @@ const _unreadMsgs = unreadMsgs; // Events. // +DeleteMessageEvent deleteMessageEvent(List messages) { + return DeleteMessageEvent( + id: 0, + messageIds: messages.map((message) => message.id).toList(), + messageType: MessageType.stream, + streamId: messages[0].streamId, + topic: messages[0].topic, + ); +} + UpdateMessageEvent updateMessageEditEvent( Message origMessage, { int? userId = -1, // null means null; default is [selfUser.userId] diff --git a/test/model/message_test.dart b/test/model/message_test.dart index 10e4334fcf3..d88539779f1 100644 --- a/test/model/message_test.dart +++ b/test/model/message_test.dart @@ -375,6 +375,40 @@ void main() { }); }); + group('handleDeleteMessageEvent', () { + + test('delete unknown message', () async { + final message1 = eg.streamMessage(); + final message2 = eg.streamMessage(); + await prepare(); + await prepareMessages([message1]); + await store.handleEvent(eg.deleteMessageEvent([message2])); + checkNotNotified(); + check(store).messages.values.single.id.equals(message1.id); + }); + + test('delete messages', () async { + final message1 = eg.streamMessage(); + final message2 = eg.streamMessage(); + await prepare(); + await prepareMessages([message1, message2]); + await store.handleEvent(eg.deleteMessageEvent([message1, message2])); + checkNotifiedOnce(); + check(store).messages.isEmpty(); + }); + + test('delete an unknown messages with a known messages', () async { + final message1 = eg.streamMessage(); + final message2 = eg.streamMessage(); + final message3 = eg.streamMessage(); + await prepare(); + await prepareMessages([message1, message2]); + await store.handleEvent(eg.deleteMessageEvent([message2, message3])); + checkNotifiedOnce(); + check(store).messages.values.single.id.equals(message1.id); + }); + }); + group('handleReactionEvent', () { test('add reaction', () async { final originalMessage = eg.streamMessage(reactions: []);