Skip to content

Commit

Permalink
fix(ui): microseconds diffs result in wrong unread indicator (#1932)
Browse files Browse the repository at this point in the history
Take into account the lastReadMessageId property
  • Loading branch information
esarbanis authored Jun 3, 2024
1 parent 27cbb03 commit 752691b
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 11 deletions.
3 changes: 3 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,17 +520,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
Widget _buildListView(List<Message> 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;
Expand Down
27 changes: 27 additions & 0 deletions packages/stream_chat_flutter/lib/src/utils/extensions.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -563,3 +564,29 @@ extension OriginalSizeX on Attachment {
return Size(width.toDouble(), height.toDouble());
}
}

/// Useful extensions on [List<Message>].
extension MessageListX on Iterable<Message> {
/// 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;
}
}
120 changes: 120 additions & 0 deletions packages/stream_chat_flutter/test/src/utils/extension_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <Message>[];
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>[
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>[
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>[
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>[
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>[
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');
});
});
});
}

0 comments on commit 752691b

Please sign in to comment.