Skip to content

Commit d5d56e5

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 55d3f86 commit d5d56e5

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
@@ -438,30 +438,41 @@ 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+
@visibleForTesting
452+
bool loading = false;
453+
446454
void _handlePress(BuildContext context) async {
447455
if (!context.mounted) return;
448456

449457
final store = PerAccountStoreWidget.of(context);
450458
final connection = store.connection;
451459
final useLegacy = connection.zulipFeatureLevel! < 155;
460+
setState(() => loading = true);
452461

453462
try {
454-
await markNarrowAsRead(context, narrow, useLegacy);
463+
await markNarrowAsRead(context, widget.narrow, useLegacy);
455464
} catch (e) {
456465
if (!context.mounted) return;
457466
final zulipLocalizations = ZulipLocalizations.of(context);
458-
await showErrorDialog(context: context,
467+
showErrorDialog(context: context,
459468
title: zulipLocalizations.errorMarkAsReadFailedTitle,
460469
message: e.toString()); // TODO(#741): extract user-facing message better
461470
return;
471+
} finally {
472+
setState(() => loading = false);
462473
}
463474
if (!context.mounted) return;
464-
if (narrow is CombinedFeedNarrow && !useLegacy) {
475+
if (widget.narrow is CombinedFeedNarrow && !useLegacy) {
465476
PerAccountStoreWidget.of(context).unreads.handleAllMessagesReadSuccess();
466477
}
467478
}
@@ -470,13 +481,13 @@ class MarkAsReadWidget extends StatelessWidget {
470481
Widget build(BuildContext context) {
471482
final zulipLocalizations = ZulipLocalizations.of(context);
472483
final store = PerAccountStoreWidget.of(context);
473-
final unreadCount = store.unreads.countInNarrow(narrow);
484+
final unreadCount = store.unreads.countInNarrow(widget.narrow);
474485
final areMessagesRead = unreadCount == 0;
475486

476487
return IgnorePointer(
477488
ignoring: areMessagesRead,
478489
child: AnimatedOpacity(
479-
opacity: areMessagesRead ? 0 : 1,
490+
opacity: areMessagesRead ? 0 : loading ? 0.5 : 1,
480491
duration: Duration(milliseconds: areMessagesRead ? 2000 : 300),
481492
curve: Curves.easeOut,
482493
child: SizedBox(width: double.infinity,
@@ -487,8 +498,6 @@ class MarkAsReadWidget extends StatelessWidget {
487498
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10 - ((48 - 38) / 2)),
488499
child: FilledButton.icon(
489500
style: FilledButton.styleFrom(
490-
// TODO(#95) need dark-theme colors (foreground and background)
491-
backgroundColor: _UnreadMarker.color,
492501
minimumSize: const Size.fromHeight(38),
493502
textStyle:
494503
// Restate [FilledButton]'s default, which inherits from
@@ -502,8 +511,13 @@ class MarkAsReadWidget extends StatelessWidget {
502511
height: (23 / 18))
503512
.merge(weightVariableTextStyle(context, wght: 400))),
504513
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
514+
).copyWith(
515+
// clobber `FilledButton`'s default disabled state
516+
// TODO(#95) need dark-theme colors (foreground and background)
517+
foregroundColor: WidgetStateColor.resolveWith((_) => Colors.white),
518+
backgroundColor: WidgetStateColor.resolveWith((_) => _UnreadMarker.color),
505519
),
506-
onPressed: () => _handlePress(context),
520+
onPressed: loading ? null : () => _handlePress(context),
507521
icon: const Icon(Icons.playlist_add_check),
508522
label: Text(zulipLocalizations.markAllAsReadLabel))))));
509523
}

test/widgets/message_list_test.dart

+62
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,68 @@ void main() {
753753
unreadMessageIds: [message.id]),
754754
]);
755755

756+
group('MarkAsReadAnimation', () {
757+
void checkAppearsLoading(WidgetTester tester, bool expected) {
758+
final semantics = tester.firstWidget<Semantics>(find.descendant(
759+
of: find.byType(MarkAsReadWidget),
760+
matching: find.byType(Semantics)));
761+
check(semantics.properties.enabled).equals(!expected);
762+
763+
final opacity = tester.widget<AnimatedOpacity>(find.descendant(
764+
of: find.byType(MarkAsReadWidget),
765+
matching: find.byType(AnimatedOpacity)));
766+
check(opacity.opacity).equals(expected ? 0.5 : 1.0);
767+
}
768+
769+
testWidgets('loading is changed correctly', (WidgetTester tester) async {
770+
final narrow = TopicNarrow.ofMessage(message);
771+
await setupMessageListPage(tester,
772+
narrow: narrow, messages: [message], unreadMsgs: unreadMsgs);
773+
check(isMarkAsReadButtonVisible(tester)).isTrue();
774+
775+
connection.prepare(
776+
delay: const Duration(milliseconds: 2000),
777+
json: UpdateMessageFlagsForNarrowResult(
778+
processedCount: 11, updatedCount: 3,
779+
firstProcessedId: null, lastProcessedId: null,
780+
foundOldest: true, foundNewest: true).toJson());
781+
782+
checkAppearsLoading(tester, false);
783+
784+
await tester.tap(find.byType(MarkAsReadWidget));
785+
await tester.pump();
786+
checkAppearsLoading(tester, true);
787+
788+
await tester.pump(const Duration(milliseconds: 2000));
789+
checkAppearsLoading(tester, false);
790+
});
791+
792+
testWidgets('loading is changed correctly if request fails', (WidgetTester tester) async {
793+
final narrow = TopicNarrow.ofMessage(message);
794+
await setupMessageListPage(tester,
795+
narrow: narrow, messages: [message], unreadMsgs: unreadMsgs);
796+
check(isMarkAsReadButtonVisible(tester)).isTrue();
797+
798+
connection.prepare(httpStatus: 400, json: {
799+
'code': 'BAD_REQUEST',
800+
'msg': 'Invalid message(s)',
801+
'result': 'error',
802+
});
803+
804+
final state = tester.state<MarkAsReadWidgetState>(find.byType(MarkAsReadWidget));
805+
check(state.loading).isFalse();
806+
807+
await tester.tap(find.byType(MarkAsReadWidget));
808+
await tester.pump();
809+
check(state.loading).isTrue();
810+
811+
await tester.idle();
812+
await tester.pump();
813+
814+
check(state.loading).isFalse();
815+
});
816+
});
817+
756818
testWidgets('smoke test on modern server', (WidgetTester tester) async {
757819
final narrow = TopicNarrow.ofMessage(message);
758820
await setupMessageListPage(tester,

0 commit comments

Comments
 (0)