Skip to content

Commit 0d577a0

Browse files
Khader-1gnprice
authored andcommitted
msglist: Handle loading state in MarkAsReadWidget
MarkAsReadWidget needs to be disabled during loading state to prevent multiple requests to the server.
1 parent 016f22a commit 0d577a0

File tree

2 files changed

+84
-11
lines changed

2 files changed

+84
-11
lines changed

lib/widgets/message_list.dart

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -458,30 +458,40 @@ class ScrollToBottomButton extends StatelessWidget {
458458
}
459459
}
460460

461-
class MarkAsReadWidget extends StatelessWidget {
461+
class MarkAsReadWidget extends StatefulWidget {
462462
const MarkAsReadWidget({super.key, required this.narrow});
463463

464464
final Narrow narrow;
465465

466+
@override
467+
State<MarkAsReadWidget> createState() => _MarkAsReadWidgetState();
468+
}
469+
470+
class _MarkAsReadWidgetState extends State<MarkAsReadWidget> {
471+
bool _loading = false;
472+
466473
void _handlePress(BuildContext context) async {
467474
if (!context.mounted) return;
468475

469476
final store = PerAccountStoreWidget.of(context);
470477
final connection = store.connection;
471478
final useLegacy = connection.zulipFeatureLevel! < 155;
479+
setState(() => _loading = true);
472480

473481
try {
474-
await markNarrowAsRead(context, narrow, useLegacy);
482+
await markNarrowAsRead(context, widget.narrow, useLegacy);
475483
} catch (e) {
476484
if (!context.mounted) return;
477485
final zulipLocalizations = ZulipLocalizations.of(context);
478-
await showErrorDialog(context: context,
486+
showErrorDialog(context: context,
479487
title: zulipLocalizations.errorMarkAsReadFailedTitle,
480488
message: e.toString()); // TODO(#741): extract user-facing message better
481489
return;
490+
} finally {
491+
setState(() => _loading = false);
482492
}
483493
if (!context.mounted) return;
484-
if (narrow is CombinedFeedNarrow && !useLegacy) {
494+
if (widget.narrow is CombinedFeedNarrow && !useLegacy) {
485495
PerAccountStoreWidget.of(context).unreads.handleAllMessagesReadSuccess();
486496
}
487497
}
@@ -490,13 +500,13 @@ class MarkAsReadWidget extends StatelessWidget {
490500
Widget build(BuildContext context) {
491501
final zulipLocalizations = ZulipLocalizations.of(context);
492502
final store = PerAccountStoreWidget.of(context);
493-
final unreadCount = store.unreads.countInNarrow(narrow);
503+
final unreadCount = store.unreads.countInNarrow(widget.narrow);
494504
final areMessagesRead = unreadCount == 0;
495505

496506
return IgnorePointer(
497507
ignoring: areMessagesRead,
498508
child: AnimatedOpacity(
499-
opacity: areMessagesRead ? 0 : 1,
509+
opacity: areMessagesRead ? 0 : _loading ? 0.5 : 1,
500510
duration: Duration(milliseconds: areMessagesRead ? 2000 : 300),
501511
curve: Curves.easeOut,
502512
child: SizedBox(width: double.infinity,
@@ -507,8 +517,6 @@ class MarkAsReadWidget extends StatelessWidget {
507517
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10 - ((48 - 38) / 2)),
508518
child: FilledButton.icon(
509519
style: FilledButton.styleFrom(
510-
// TODO(#95) need dark-theme colors (foreground and background)
511-
backgroundColor: _UnreadMarker.color,
512520
minimumSize: const Size.fromHeight(38),
513521
textStyle:
514522
// Restate [FilledButton]'s default, which inherits from
@@ -522,11 +530,17 @@ class MarkAsReadWidget extends StatelessWidget {
522530
height: (23 / 18))
523531
.merge(weightVariableTextStyle(context, wght: 400))),
524532
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
533+
).copyWith(
534+
// Give the buttons a constant color regardless of whether their
535+
// state is disabled, pressed, etc. We handle those states
536+
// separately, via MarkAsReadAnimation.
537+
// TODO(#95) need dark-theme colors (foreground and background)
538+
foregroundColor: WidgetStateColor.resolveWith((_) => Colors.white),
539+
backgroundColor: WidgetStateColor.resolveWith((_) => _UnreadMarker.color),
525540
),
526-
onPressed: () => _handlePress(context),
541+
onPressed: _loading ? null : () => _handlePress(context),
527542
icon: const Icon(Icons.playlist_add_check),
528-
label: Text(zulipLocalizations.markAllAsReadLabel))))),
529-
);
543+
label: Text(zulipLocalizations.markAllAsReadLabel))))));
530544
}
531545
}
532546

test/widgets/message_list_test.dart

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,65 @@ void main() {
883883
unreadMessageIds: [message.id]),
884884
]);
885885

886+
group('MarkAsReadAnimation', () {
887+
void checkAppearsLoading(WidgetTester tester, bool expected) {
888+
final semantics = tester.firstWidget<Semantics>(find.descendant(
889+
of: find.byType(MarkAsReadWidget),
890+
matching: find.byType(Semantics)));
891+
check(semantics.properties.enabled).equals(!expected);
892+
893+
final opacity = tester.widget<AnimatedOpacity>(find.descendant(
894+
of: find.byType(MarkAsReadWidget),
895+
matching: find.byType(AnimatedOpacity)));
896+
check(opacity.opacity).equals(expected ? 0.5 : 1.0);
897+
}
898+
899+
testWidgets('loading is changed correctly', (WidgetTester tester) async {
900+
final narrow = TopicNarrow.ofMessage(message);
901+
await setupMessageListPage(tester,
902+
narrow: narrow, messages: [message], unreadMsgs: unreadMsgs);
903+
check(isMarkAsReadButtonVisible(tester)).isTrue();
904+
905+
connection.prepare(
906+
delay: const Duration(milliseconds: 2000),
907+
json: UpdateMessageFlagsForNarrowResult(
908+
processedCount: 11, updatedCount: 3,
909+
firstProcessedId: null, lastProcessedId: null,
910+
foundOldest: true, foundNewest: true).toJson());
911+
912+
checkAppearsLoading(tester, false);
913+
914+
await tester.tap(find.byType(MarkAsReadWidget));
915+
await tester.pump();
916+
checkAppearsLoading(tester, true);
917+
918+
await tester.pump(const Duration(milliseconds: 2000));
919+
checkAppearsLoading(tester, false);
920+
});
921+
922+
testWidgets('loading is changed correctly if request fails', (WidgetTester tester) async {
923+
final narrow = TopicNarrow.ofMessage(message);
924+
await setupMessageListPage(tester,
925+
narrow: narrow, messages: [message], unreadMsgs: unreadMsgs);
926+
check(isMarkAsReadButtonVisible(tester)).isTrue();
927+
928+
connection.prepare(httpStatus: 400, json: {
929+
'code': 'BAD_REQUEST',
930+
'msg': 'Invalid message(s)',
931+
'result': 'error',
932+
});
933+
934+
checkAppearsLoading(tester, false);
935+
936+
await tester.tap(find.byType(MarkAsReadWidget));
937+
await tester.pump();
938+
checkAppearsLoading(tester, true);
939+
940+
await tester.pump(const Duration(milliseconds: 2000));
941+
checkAppearsLoading(tester, false);
942+
});
943+
});
944+
886945
testWidgets('smoke test on modern server', (WidgetTester tester) async {
887946
final narrow = TopicNarrow.ofMessage(message);
888947
await setupMessageListPage(tester,

0 commit comments

Comments
 (0)