From 752691bc5cb8c8c64948f486b07135592767a15f Mon Sep 17 00:00:00 2001 From: Efthymis Sarmpanis Date: Mon, 3 Jun 2024 12:42:01 +0300 Subject: [PATCH] fix(ui): microseconds diffs result in wrong unread indicator (#1932) Take into account the lastReadMessageId property --- packages/stream_chat_flutter/CHANGELOG.md | 3 + .../message_list_view/message_list_view.dart | 12 +- .../lib/src/utils/extensions.dart | 27 ++++ .../test/src/utils/extension_test.dart | 120 ++++++++++++++++++ 4 files changed, 151 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index ef501a167..baf388660 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -3,6 +3,9 @@ ✅ Added - Added `VoiceRecordingAttachmentBuilder`, for displaying voice recording attachments in the chat. +🐞 Fixed +- Fixed wrong calculation of the last unread message indicator. + ## 7.2.0-hotfix.1 🔄 Changed diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 9ebae75dd..452d8e965 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -520,17 +520,7 @@ class _StreamMessageListViewState extends State { Widget _buildListView(List data) { messages = data; - if (_userRead != null && - messages.isNotEmpty && - messages.first.createdAt.isAfter(_userRead!.lastRead) && - messages.last.createdAt.isBefore(_userRead!.lastRead)) { - _oldestUnreadMessage = messages.lastWhereOrNull( - (it) => - it.user?.id != - streamChannel?.channel.client.state.currentUser?.id && - it.createdAt.compareTo(_userRead!.lastRead) > 0, - ); - } + _oldestUnreadMessage = messages.lastUnreadMessage(_userRead); for (var index = 0; index < messages.length; index++) { messagesIndex[messages[index].id] = index; diff --git a/packages/stream_chat_flutter/lib/src/utils/extensions.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart index 3e0a59d64..3987817c0 100644 --- a/packages/stream_chat_flutter/lib/src/utils/extensions.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:diacritic/diacritic.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; @@ -563,3 +564,29 @@ extension OriginalSizeX on Attachment { return Size(width.toDouble(), height.toDouble()); } } + +/// Useful extensions on [List]. +extension MessageListX on Iterable { + /// Returns the last unread message in the list. + /// Returns null if the list is empty or the userRead is null. + /// + /// The [userRead] is the last read message by the user. + /// + /// The last unread message is the last message in the list that is not + /// sent by the current user and is sent after the last read message. + Message? lastUnreadMessage(Read? userRead) { + if (isEmpty || userRead == null) return null; + + if (first.createdAt.isAfter(userRead.lastRead) && + last.createdAt.isBefore(userRead.lastRead)) { + return lastWhereOrNull( + (it) => + it.user?.id != userRead.user.id && + it.id != userRead.lastReadMessageId && + it.createdAt.compareTo(userRead.lastRead) > 0, + ); + } + + return null; + } +} diff --git a/packages/stream_chat_flutter/test/src/utils/extension_test.dart b/packages/stream_chat_flutter/test/src/utils/extension_test.dart index 52a6cc5f5..0ea96fd9d 100644 --- a/packages/stream_chat_flutter/test/src/utils/extension_test.dart +++ b/packages/stream_chat_flutter/test/src/utils/extension_test.dart @@ -270,4 +270,124 @@ void main() { expect(modifiedMessage.text, isNot(contains('@Alice'))); }); }); + + group('Message List Extension Tests', () { + group('lastUnreadMessage', () { + test('should return null when list is empty', () { + final messages = []; + final userRead = Read( + lastRead: DateTime.now(), + user: User(id: 'user1'), + ); + expect(messages.lastUnreadMessage(userRead), isNull); + }); + + test('should return null when userRead is null', () { + final messages = [ + Message(id: '1'), + Message(id: '2'), + ]; + expect(messages.lastUnreadMessage(null), isNull); + }); + + test('should return null when all messages are read', () { + final lastRead = DateTime.now(); + final messages = [ + Message( + id: '1', + createdAt: lastRead.subtract(const Duration(seconds: 1))), + Message(id: '2', createdAt: lastRead), + ]; + final userRead = Read( + lastRead: lastRead, + user: User(id: 'user1'), + ); + expect(messages.lastUnreadMessage(userRead), isNull); + }); + + test('should return null when all messages are mine', () { + final lastRead = DateTime.now(); + final userRead = Read( + lastRead: lastRead, + user: User(id: 'user1'), + ); + final messages = [ + Message( + id: '1', + user: userRead.user, + createdAt: lastRead.add(const Duration(seconds: 1))), + Message(id: '2', user: userRead.user, createdAt: lastRead), + ]; + expect(messages.lastUnreadMessage(userRead), isNull); + }); + + test('should return the message', () { + final lastRead = DateTime.now(); + final otherUser = User(id: 'user2'); + final userRead = Read( + lastRead: lastRead, + user: User(id: 'user1'), + ); + + final messages = [ + Message( + id: '1', + user: otherUser, + createdAt: lastRead.add(const Duration(seconds: 2)), + ), + Message( + id: '2', + user: otherUser, + createdAt: lastRead.add(const Duration(seconds: 1)), + ), + Message( + id: '3', + user: otherUser, + createdAt: lastRead.subtract(const Duration(seconds: 1)), + ), + ]; + + final lastUnreadMessage = messages.lastUnreadMessage(userRead); + expect(lastUnreadMessage, isNotNull); + expect(lastUnreadMessage!.id, '2'); + }); + + test('should not return the last message read', () { + final lastRead = DateTime.timestamp(); + final otherUser = User(id: 'user2'); + final userRead = Read( + lastRead: lastRead, + user: User(id: 'user1'), + lastReadMessageId: '3', + ); + + final messages = [ + Message( + id: '1', + user: otherUser, + createdAt: lastRead.add(const Duration(seconds: 2)), + ), + Message( + id: '2', + user: otherUser, + createdAt: lastRead.add(const Duration(milliseconds: 1)), + ), + Message( + id: '3', + user: otherUser, + createdAt: lastRead.add(const Duration(microseconds: 1)), + ), + Message( + id: '4', + user: otherUser, + createdAt: lastRead.subtract(const Duration(seconds: 1)), + ), + ]; + + final lastUnreadMessage = messages.lastUnreadMessage(userRead); + expect(lastUnreadMessage, isNotNull); + expect(lastUnreadMessage!.id, '2'); + }); + }); + }); }