Skip to content

Commit e258773

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: #1045
1 parent 21caff2 commit e258773

File tree

2 files changed

+50
-7
lines changed

2 files changed

+50
-7
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

+44
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';
@@ -438,6 +439,49 @@ void main() {
438439
});
439440
});
440441

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

0 commit comments

Comments
 (0)