Skip to content

Commit 2cf1f6d

Browse files
committed
action_sheet: Mark as unread in current narrow, vs. from when action sheet opened
The narrow of a given MessageListPage can change over time, in particular if viewing a topic that gets moved. If it does, the mark-unread action should follow along. Otherwise we'd have a frustrating race where mark-unread just didn't work if, for example, someone happened to resolve the topic while you were in the middle of trying to mark as unread. Fixes: zulip#1045
1 parent 66f842c commit 2cf1f6d

File tree

2 files changed

+57
-17
lines changed

2 files changed

+57
-17
lines changed

lib/widgets/action_sheet.dart

+6-7
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ void showMessageActionSheet({required BuildContext context, required Message mes
2929

3030
// The UI that's conditioned on this won't live-update during this appearance
3131
// of the action sheet (we avoid calling composeBoxControllerOf in a build
32-
// method; see its doc). But currently it will be constant through the life of
33-
// any message list, so that's fine.
32+
// method; see its doc).
33+
// So we rely on the fact that isComposeBoxOffered for any given message list
34+
// will be constant through the page's life.
3435
final messageListPage = MessageListPage.ancestorOf(context);
3536
final isComposeBoxOffered = messageListPage.composeBoxController != null;
36-
final narrow = messageListPage.narrow;
37+
3738
final isMessageRead = message.flags.contains(MessageFlag.read);
3839
final markAsUnreadSupported = store.connection.zulipFeatureLevel! >= 155; // TODO(server-6)
3940
final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead;
@@ -52,7 +53,7 @@ void showMessageActionSheet({required BuildContext context, required Message mes
5253
if (isComposeBoxOffered)
5354
QuoteAndReplyButton(message: message, pageContext: context),
5455
if (showMarkAsUnreadButton)
55-
MarkAsUnreadButton(message: message, pageContext: context, narrow: narrow),
56+
MarkAsUnreadButton(message: message, pageContext: context),
5657
CopyMessageTextButton(message: message, pageContext: context),
5758
CopyMessageLinkButton(message: message, pageContext: context),
5859
ShareButton(message: message, pageContext: context),
@@ -383,11 +384,8 @@ class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
383384
super.key,
384385
required super.message,
385386
required super.pageContext,
386-
required this.narrow,
387387
});
388388

389-
final Narrow narrow;
390-
391389
@override IconData get icon => Icons.mark_chat_unread_outlined;
392390

393391
@override
@@ -396,6 +394,7 @@ class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
396394
}
397395

398396
@override void onPressed() async {
397+
final narrow = findMessageListPage().narrow;
399398
unawaited(markNarrowAsUnreadFromMessage(pageContext, message, narrow));
400399
}
401400
}

test/widgets/action_sheet_test.dart

+51-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
55
import 'package:flutter/services.dart';
66
import 'package:flutter_test/flutter_test.dart';
77
import 'package:http/http.dart' as http;
8+
import 'package:zulip/api/model/events.dart';
89
import 'package:zulip/api/model/model.dart';
910
import 'package:zulip/api/route/channels.dart';
1011
import 'package:zulip/api/route/messages.dart';
@@ -32,6 +33,17 @@ import 'compose_box_checks.dart';
3233
import 'dialog_checks.dart';
3334
import 'test_app.dart';
3435

36+
GetMessagesResult _getMessagesResult(Message message) {
37+
return GetMessagesResult(
38+
anchor: message.id,
39+
foundNewest: true,
40+
foundOldest: true,
41+
foundAnchor: true,
42+
historyLimited: false,
43+
messages: [message],
44+
);
45+
}
46+
3547
late FakeApiConnection connection;
3648

3749
/// Simulates loading a [MessageListPage] and long-pressing on [message].
@@ -51,16 +63,7 @@ Future<void> setupToMessageActionSheet(WidgetTester tester, {
5163
}
5264
connection = store.connection as FakeApiConnection;
5365

54-
// prepare message list data
55-
connection.prepare(json: GetMessagesResult(
56-
anchor: message.id,
57-
foundNewest: true,
58-
foundOldest: true,
59-
foundAnchor: true,
60-
historyLimited: false,
61-
messages: [message],
62-
).toJson());
63-
66+
connection.prepare(json: _getMessagesResult(message).toJson());
6467
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
6568
child: MessageListPage(initNarrow: narrow)));
6669

@@ -442,6 +445,44 @@ void main() {
442445
});
443446
});
444447

448+
testWidgets('on topic move, acts on new topic', (tester) async {
449+
final stream = eg.stream();
450+
const topic = 'old topic';
451+
final message = eg.streamMessage(flags: [MessageFlag.read],
452+
stream: stream, topic: topic);
453+
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
454+
// Get the action sheet fully deployed while the old narrow applies.
455+
await tester.pumpAndSettle();
456+
457+
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
458+
final newStream = eg.stream();
459+
const newTopic = 'other topic';
460+
// This result isn't quite realistic for this request: it should get
461+
// the updated channel/stream ID and topic, because we don't even
462+
// start the request until after we get the move event.
463+
// But constructing the right result is annoying at the moment, and
464+
// it doesn't matter anyway: [MessageStoreImpl.reconcileMessages] will
465+
// keep the version updated by the event. If that somehow changes in
466+
// some future refactor, it'll cause this test to fail.
467+
connection.prepare(json: _getMessagesResult(message).toJson());
468+
await store.handleEvent(eg.updateMessageEventMoveFrom(
469+
newStreamId: newStream.streamId, newTopic: newTopic,
470+
propagateMode: PropagateMode.changeAll,
471+
origMessages: [message]));
472+
473+
connection.prepare(json: UpdateMessageFlagsForNarrowResult(
474+
processedCount: 11, updatedCount: 3,
475+
firstProcessedId: 1, lastProcessedId: 1980,
476+
foundOldest: true, foundNewest: true).toJson());
477+
await tester.tap(find.byIcon(Icons.mark_chat_unread_outlined, skipOffstage: false));
478+
await tester.pumpAndSettle();
479+
check(connection.lastRequest).isA<http.Request>()
480+
..method.equals('POST')
481+
..url.path.equals('/api/v1/messages/flags/narrow')
482+
..bodyFields['narrow'].equals(
483+
jsonEncode(TopicNarrow(newStream.streamId, newTopic).apiEncode()));
484+
});
485+
445486
testWidgets('shows error when fails', (tester) async {
446487
final message = eg.streamMessage(flags: [MessageFlag.read]);
447488
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));

0 commit comments

Comments
 (0)