diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 7ff3d395b8..45e4fa9d13 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -15,6 +15,7 @@ import '../model/message_list.dart'; import '../model/narrow.dart'; import '../model/store.dart'; import '../model/typing_status.dart'; +import '../model/unreads.dart'; import 'action_sheet.dart'; import 'actions.dart'; import 'app_bar.dart'; @@ -1416,9 +1417,30 @@ class MarkAsReadWidget extends StatefulWidget { State createState() => _MarkAsReadWidgetState(); } -class _MarkAsReadWidgetState extends State { +class _MarkAsReadWidgetState extends State with PerAccountStoreAwareStateMixin { + Unreads? unreadsModel; + bool _loading = false; + void _unreadsModelChanged() { + setState(() { + // The actual state lives in [unreadsModel]. + }); + } + + @override + void onNewStore() { + final newStore = PerAccountStoreWidget.of(context); + unreadsModel?.removeListener(_unreadsModelChanged); + unreadsModel = newStore.unreads..addListener(_unreadsModelChanged); + } + + @override + void dispose() { + unreadsModel?.removeListener(_unreadsModelChanged); + super.dispose(); + } + void _handlePress(BuildContext context) async { if (!context.mounted) return; setState(() => _loading = true); @@ -1429,8 +1451,7 @@ class _MarkAsReadWidgetState extends State { @override Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); - final store = PerAccountStoreWidget.of(context); - final unreadCount = store.unreads.countInNarrow(widget.narrow); + final unreadCount = unreadsModel!.countInNarrow(widget.narrow); final shouldHide = unreadCount == 0; final messageListTheme = MessageListTheme.of(context); diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 89b2a31bd8..2e78ac44db 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -1001,6 +1001,43 @@ void main() { check(isMarkAsReadButtonVisible(tester)).isFalse(); }); + testWidgets('listens to Unreads model, not just PerAccountStore and MessageListView', (tester) async { + // Regression test for an edge case where the button wouldn't disappear + // when the narrow's unreads were cleared, because the button would only + // respond to notifications from PerAccountStore and MessageListView, + // not Unreads. To test this, we simulate an event that causes Unreads + // but not PerAccountStore and MessageListView to notify listeners, + // and check that the button responds. + + final message = eg.streamMessage(flags: [MessageFlag.mentioned]); + final unreadMsgs = eg.unreadMsgs( + channels: [ + UnreadChannelSnapshot( + topic: message.topic, + streamId: message.streamId, + unreadMessageIds: [message.id], + ), + ], + mentions: [message.id], + ); + await setupMessageListPage(tester, + narrow: MentionsNarrow(), + unreadMsgs: unreadMsgs, + // omit `message`; if present, MessageListView would notify listeners + messages: List.generate(300, (i) => + eg.streamMessage(id: 950 + i, sender: eg.selfUser, + flags: [MessageFlag.read, MessageFlag.mentioned])), + foundOldest: false); + check(isMarkAsReadButtonVisible(tester)).isTrue(); + + // The message no longer has an @-mention. + // It was the only unread message with an @-mention, + // so the button should disappear. + await store.handleEvent(eg.updateMessageEditEvent(message, flags: [])); + await tester.pumpAndSettle(); + check(isMarkAsReadButtonVisible(tester)).isFalse(); + }); + testWidgets("messages don't shift position", (tester) async { final message = eg.streamMessage(flags: []); final unreadMsgs = eg.unreadMsgs(channels:[