Skip to content

Commit e620858

Browse files
committed
msglist: Handle loading state in MarkAsReadWidget
MarkAsReadWidget needs to be disabled during loading state to prevent multiple requests to the server. Additionally it is needed for styling/animation convenience.
1 parent 515fefa commit e620858

File tree

2 files changed

+85
-9
lines changed

2 files changed

+85
-9
lines changed

lib/widgets/message_list.dart

+23-9
Original file line numberDiff line numberDiff line change
@@ -459,30 +459,41 @@ class ScrollToBottomButton extends StatelessWidget {
459459
}
460460
}
461461

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

465465
final Narrow narrow;
466466

467+
@override
468+
State<MarkAsReadWidget> createState() => MarkAsReadWidgetState();
469+
}
470+
471+
class MarkAsReadWidgetState extends State<MarkAsReadWidget> {
472+
@visibleForTesting
473+
bool loading = false;
474+
467475
void _handlePress(BuildContext context) async {
468476
if (!context.mounted) return;
469477

470478
final store = PerAccountStoreWidget.of(context);
471479
final connection = store.connection;
472480
final useLegacy = connection.zulipFeatureLevel! < 155;
481+
setState(() => loading = true);
473482

474483
try {
475-
await markNarrowAsRead(context, narrow, useLegacy);
484+
await markNarrowAsRead(context, widget.narrow, useLegacy);
476485
} catch (e) {
477486
if (!context.mounted) return;
478487
final zulipLocalizations = ZulipLocalizations.of(context);
479-
await showErrorDialog(context: context,
488+
showErrorDialog(context: context,
480489
title: zulipLocalizations.errorMarkAsReadFailedTitle,
481490
message: e.toString()); // TODO(#741): extract user-facing message better
482491
return;
492+
} finally {
493+
setState(() => loading = false);
483494
}
484495
if (!context.mounted) return;
485-
if (narrow is CombinedFeedNarrow && !useLegacy) {
496+
if (widget.narrow is CombinedFeedNarrow && !useLegacy) {
486497
PerAccountStoreWidget.of(context).unreads.handleAllMessagesReadSuccess();
487498
}
488499
}
@@ -491,13 +502,13 @@ class MarkAsReadWidget extends StatelessWidget {
491502
Widget build(BuildContext context) {
492503
final zulipLocalizations = ZulipLocalizations.of(context);
493504
final store = PerAccountStoreWidget.of(context);
494-
final unreadCount = store.unreads.countInNarrow(narrow);
505+
final unreadCount = store.unreads.countInNarrow(widget.narrow);
495506
final areMessagesRead = unreadCount == 0;
496507

497508
return IgnorePointer(
498509
ignoring: areMessagesRead,
499510
child: AnimatedOpacity(
500-
opacity: areMessagesRead ? 0 : 1,
511+
opacity: areMessagesRead ? 0 : loading ? 0.5 : 1,
501512
duration: Duration(milliseconds: areMessagesRead ? 2000 : 300),
502513
curve: Curves.easeOut,
503514
child: SizedBox(width: double.infinity,
@@ -508,8 +519,6 @@ class MarkAsReadWidget extends StatelessWidget {
508519
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10 - ((48 - 38) / 2)),
509520
child: FilledButton.icon(
510521
style: FilledButton.styleFrom(
511-
// TODO(#95) need dark-theme colors (foreground and background)
512-
backgroundColor: _UnreadMarker.color,
513522
minimumSize: const Size.fromHeight(38),
514523
textStyle:
515524
// Restate [FilledButton]'s default, which inherits from
@@ -523,8 +532,13 @@ class MarkAsReadWidget extends StatelessWidget {
523532
height: (23 / 18))
524533
.merge(weightVariableTextStyle(context, wght: 400))),
525534
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
535+
).copyWith(
536+
// clobber `FilledButton`'s default disabled state
537+
// TODO(#95) need dark-theme colors (foreground and background)
538+
foregroundColor: WidgetStateColor.resolveWith((_) => Colors.white),
539+
backgroundColor: WidgetStateColor.resolveWith((_) => _UnreadMarker.color),
526540
),
527-
onPressed: () => _handlePress(context),
541+
onPressed: loading ? null : () => _handlePress(context),
528542
icon: const Icon(Icons.playlist_add_check),
529543
label: Text(zulipLocalizations.markAllAsReadLabel))))));
530544
}

test/widgets/message_list_test.dart

+62
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,68 @@ void main() {
888888
unreadMessageIds: [message.id]),
889889
]);
890890

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

0 commit comments

Comments
 (0)