Skip to content

Commit c38d6a9

Browse files
committed
msglist: Handle loading state in MarkAsReadWidget
MarkAsReadWidget needs to be disabled during loading state to prevent multiple requests to the server.
1 parent a24104b commit c38d6a9

File tree

2 files changed

+83
-9
lines changed

2 files changed

+83
-9
lines changed

lib/widgets/message_list.dart

+24-9
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,8 +530,15 @@ 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),
528543
label: Text(zulipLocalizations.markAllAsReadLabel))))));
529544
}

test/widgets/message_list_test.dart

+59
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,65 @@ void main() {
891891
unreadMessageIds: [message.id]),
892892
]);
893893

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

0 commit comments

Comments
 (0)