@@ -438,30 +438,40 @@ class ScrollToBottomButton extends StatelessWidget {
438438 }
439439}
440440
441- class MarkAsReadWidget extends StatelessWidget {
441+ class MarkAsReadWidget extends StatefulWidget {
442442 const MarkAsReadWidget ({super .key, required this .narrow});
443443
444444 final Narrow narrow;
445445
446+ @override
447+ State <MarkAsReadWidget > createState () => MarkAsReadWidgetState ();
448+ }
449+
450+ class MarkAsReadWidgetState extends State <MarkAsReadWidget > {
451+ bool isLoading = false ;
452+
446453 void _handlePress (BuildContext context) async {
447454 if (! context.mounted) return ;
448455
449456 final store = PerAccountStoreWidget .of (context);
450457 final connection = store.connection;
451458 final useLegacy = connection.zulipFeatureLevel! < 155 ;
459+ setState (() => isLoading = true );
452460
453461 try {
454- await markNarrowAsRead (context, narrow, useLegacy);
462+ await markNarrowAsRead (context, widget. narrow, useLegacy);
455463 } catch (e) {
456464 if (! context.mounted) return ;
457465 final zulipLocalizations = ZulipLocalizations .of (context);
458466 await showErrorDialog (context: context,
459467 title: zulipLocalizations.errorMarkAsReadFailedTitle,
460468 message: e.toString ()); // TODO(#741): extract user-facing message better
461469 return ;
470+ } finally {
471+ setState (() => isLoading = false );
462472 }
463473 if (! context.mounted) return ;
464- if (narrow is CombinedFeedNarrow && ! useLegacy) {
474+ if (widget. narrow is CombinedFeedNarrow && ! useLegacy) {
465475 PerAccountStoreWidget .of (context).unreads.handleAllMessagesReadSuccess ();
466476 }
467477 }
@@ -470,15 +480,14 @@ class MarkAsReadWidget extends StatelessWidget {
470480 Widget build (BuildContext context) {
471481 final zulipLocalizations = ZulipLocalizations .of (context);
472482 final store = PerAccountStoreWidget .of (context);
473- final unreadCount = store.unreads.countInNarrow (narrow);
483+ final unreadCount = store.unreads.countInNarrow (widget. narrow);
474484 final areMessagesRead = unreadCount == 0 ;
475485
476486 return IgnorePointer (
477487 ignoring: areMessagesRead,
478- child: AnimatedOpacity (
479- opacity: areMessagesRead ? 0 : 1 ,
480- duration: Duration (milliseconds: areMessagesRead ? 2000 : 300 ),
481- curve: Curves .easeOut,
488+ child: MarkAsReadAnimation (
489+ visible: ! areMessagesRead,
490+ loading: isLoading,
482491 child: SizedBox (width: double .infinity,
483492 // Design referenced from:
484493 // https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?type=design&node-id=132-9684&mode=design&t=jJwHzloKJ0TMOG4M-0
@@ -505,8 +514,7 @@ class MarkAsReadWidget extends StatelessWidget {
505514 ),
506515 onPressed: () => _handlePress (context),
507516 icon: const Icon (Icons .playlist_add_check),
508- label: Text (zulipLocalizations.markAllAsReadLabel))))),
509- );
517+ label: Text (zulipLocalizations.markAllAsReadLabel))))));
510518 }
511519}
512520
@@ -643,6 +651,35 @@ class _UnreadMarker extends StatelessWidget {
643651 }
644652}
645653
654+ class MarkAsReadAnimation extends StatelessWidget {
655+ const MarkAsReadAnimation ({
656+ super .key,
657+ required this .visible,
658+ required this .loading,
659+ required this .child});
660+
661+ final bool visible;
662+ final bool loading;
663+ final Widget child;
664+
665+ @override
666+ Widget build (BuildContext context) {
667+ final opacity = loading ? 0.5 : visible ? 1.0 : 0.0 ;
668+ final scale = loading ? 0.95 : 1.0 ;
669+ const duration = Duration (milliseconds: 300 );
670+ const curve = Curves .easeOut;
671+ return AnimatedScale (
672+ scale: scale,
673+ duration: duration,
674+ curve: curve,
675+ child: AnimatedOpacity (
676+ opacity: opacity,
677+ duration: duration,
678+ curve: curve,
679+ child: child));
680+ }
681+ }
682+
646683class StreamMessageRecipientHeader extends StatelessWidget {
647684 const StreamMessageRecipientHeader ({
648685 super .key,
0 commit comments