diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index d80f31bea4..be64a3ceae 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -461,34 +461,39 @@ class MarkAsReadWidget extends StatelessWidget { final zulipLocalizations = ZulipLocalizations.of(context); final store = PerAccountStoreWidget.of(context); final unreadCount = store.unreads.countInNarrow(narrow); - return AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: (unreadCount > 0) ? CrossFadeState.showSecond : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: SizedBox(width: double.infinity, - // Design referenced from: - // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?type=design&node-id=132-9684&mode=design&t=jJwHzloKJ0TMOG4M-0 - child: Padding( - // vertical padding adjusted for tap target height (48px) of button - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10 - ((48 - 38) / 2)), - child: FilledButton.icon( - style: FilledButton.styleFrom( - backgroundColor: _UnreadMarker.color, - minimumSize: const Size.fromHeight(38), - textStyle: - // Restate [FilledButton]'s default, which inherits from - // [zulipTypography]… - Theme.of(context).textTheme.labelLarge! - // …then clobber some attributes to follow Figma: - .merge(const TextStyle( - fontSize: 18, - height: (23 / 18)) - .merge(weightVariableTextStyle(context, wght: 400))), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)), - ), - onPressed: () => _handlePress(context), - icon: const Icon(Icons.playlist_add_check), - label: Text(zulipLocalizations.markAllAsReadLabel))))); + final areMessagesRead = unreadCount == 0; + + return IgnorePointer( + ignoring: areMessagesRead, + child: AnimatedOpacity( + opacity: areMessagesRead ? 0 : 1, + duration: Duration(milliseconds: areMessagesRead ? 2000 : 300), + curve: Curves.easeOut, + child: SizedBox(width: double.infinity, + // Design referenced from: + // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?type=design&node-id=132-9684&mode=design&t=jJwHzloKJ0TMOG4M-0 + child: Padding( + // vertical padding adjusted for tap target height (48px) of button + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10 - ((48 - 38) / 2)), + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: _UnreadMarker.color, + minimumSize: const Size.fromHeight(38), + textStyle: + // Restate [FilledButton]'s default, which inherits from + // [zulipTypography]… + Theme.of(context).textTheme.labelLarge! + // …then clobber some attributes to follow Figma: + .merge(const TextStyle( + fontSize: 18, + height: (23 / 18)) + .merge(weightVariableTextStyle(context, wght: 400))), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)), + ), + onPressed: () => _handlePress(context), + icon: const Icon(Icons.playlist_add_check), + label: Text(zulipLocalizations.markAllAsReadLabel))))), + ); } } diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index b8ee2e0e84..0052c0c4b4 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -610,13 +610,10 @@ void main() { group('MarkAsReadWidget', () { bool isMarkAsReadButtonVisible(WidgetTester tester) { - // Zero height elements on the edge of a scrolling viewport - // are treated as invisible for hit-testing, see - // [SliverMultiBoxAdaptorElement.debugVisitOnstageChildren]. - // Set `skipOffstage: false` here to safely target the - // [MarkAsReadWidget] even when it is inactive. - return tester.getSize( - find.byType(MarkAsReadWidget, skipOffstage: false)).height > 0; + final zulipLocalizations = GlobalLocalizations.zulipLocalizations; + final finder = find.text( + zulipLocalizations.markAllAsReadLabel).hitTestable(); + return finder.evaluate().isNotEmpty; } testWidgets('from read to unread', (WidgetTester tester) async { @@ -648,6 +645,31 @@ void main() { check(isMarkAsReadButtonVisible(tester)).isFalse(); }); + testWidgets("messages don't shift position", (WidgetTester tester) async { + final message = eg.streamMessage(flags: []); + final unreadMsgs = eg.unreadMsgs(streams:[ + UnreadStreamSnapshot(topic: message.subject, streamId: message.streamId, + unreadMessageIds: [message.id]) + ]); + await setupMessageListPage(tester, + messages: [message], unreadMsgs: unreadMsgs); + check(isMarkAsReadButtonVisible(tester)).isTrue(); + check(tester.widgetList(find.byType(MessageItem))).length.equals(1); + final before = tester.getTopLeft(find.byType(MessageItem)).dy; + + store.handleEvent(UpdateMessageFlagsAddEvent( + id: 1, + flag: MessageFlag.read, + messages: [message.id], + all: false, + )); + await tester.pumpAndSettle(); + check(isMarkAsReadButtonVisible(tester)).isFalse(); + check(tester.widgetList(find.byType(MessageItem))).length.equals(1); + final after = tester.getTopLeft(find.byType(MessageItem)).dy; + check(after).equals(before); + }); + group('onPressed behavior', () { final message = eg.streamMessage(flags: []); final unreadMsgs = eg.unreadMsgs(streams: [