Skip to content

Commit a5525e7

Browse files
committed
action_sheet: Add "Mark as unread from here" button
Fixes: #131
1 parent 4a86408 commit a5525e7

File tree

4 files changed

+131
-1
lines changed

4 files changed

+131
-1
lines changed

assets/l10n/app_en.arb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
"@actionSheetOptionCopyMessageLink": {
5252
"description": "Label for copy message link button on action sheet."
5353
},
54+
"actionSheetMarkAsUnread": "Mark as unread from here",
55+
"@actionSheetMarkAsUnread": {
56+
"description": "Label for mark as unread button on action sheet."
57+
},
5458
"actionSheetOptionShare": "Share",
5559
"@actionSheetOptionShare": {
5660
"description": "Label for share button on action sheet."
@@ -436,6 +440,21 @@
436440
"@errorMarkAsReadFailedTitle": {
437441
"description": "Error title when mark as read action failed."
438442
},
443+
"markAsUnreadComplete": "Marked {num, plural, =1{1 message} other{{num} messages}} as unread.",
444+
"@markAsUnreadComplete": {
445+
"description": "Message when marking messages as unread has completed.",
446+
"placeholders": {
447+
"num": {"type": "int", "example": "4"}
448+
}
449+
},
450+
"markAsUnreadInProgress": "Marking messages as unread...",
451+
"@markAsUnreadInProgress": {
452+
"description": "Progress message when marking messages as unread."
453+
},
454+
"errorMarkAsUnreadFailedTitle": "Mark as unread failed",
455+
"@errorMarkAsUnreadFailedTitle": {
456+
"description": "Error title when mark as unread action failed."
457+
},
439458
"today": "Today",
440459
"@today": {
441460
"description": "Term to use to reference the current day."

lib/widgets/action_sheet.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../api/model/model.dart';
88
import '../api/route/messages.dart';
99
import '../model/internal_link.dart';
1010
import '../model/narrow.dart';
11+
import 'actions.dart';
1112
import 'clipboard.dart';
1213
import 'compose_box.dart';
1314
import 'dialog.dart';
@@ -28,6 +29,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes
2829
// any message list, so that's fine.
2930
final messageListPage = MessageListPage.ancestorOf(context);
3031
final isComposeBoxOffered = messageListPage.composeBoxController != null;
32+
final narrow = messageListPage.narrow;
33+
final isMessageRead = message.flags.contains(MessageFlag.read);
3134

3235
final hasThumbsUpReactionVote = message.reactions
3336
?.aggregated.any((reactionWithVotes) =>
@@ -46,6 +49,11 @@ void showMessageActionSheet({required BuildContext context, required Message mes
4649
message: message,
4750
messageListContext: context,
4851
),
52+
if (isMessageRead) MarkAsUnreadButton(
53+
message: message,
54+
messageListContext: context,
55+
narrow: narrow,
56+
),
4957
CopyMessageTextButton(message: message, messageListContext: context),
5058
CopyMessageLinkButton(message: message, messageListContext: context),
5159
ShareButton(message: message, messageListContext: context),
@@ -402,3 +410,27 @@ class ShareButton extends MessageActionSheetMenuItemButton {
402410
}
403411
}
404412
}
413+
414+
class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
415+
MarkAsUnreadButton({
416+
super.key,
417+
required super.message,
418+
required super.messageListContext,
419+
required this.narrow,
420+
});
421+
422+
final Narrow narrow;
423+
424+
@override IconData get icon => Icons.mark_chat_unread_outlined;
425+
426+
@override
427+
String label(ZulipLocalizations zulipLocalizations) {
428+
return zulipLocalizations.actionSheetMarkAsUnread;
429+
}
430+
431+
@override void onPressed(BuildContext context) async {
432+
if (!context.mounted) return;
433+
Navigator.of(context).pop();
434+
markNarrowAsUnreadFromMessage(messageListContext, message, narrow);
435+
}
436+
}

lib/widgets/actions.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,24 @@ Future<void> markNarrowAsRead(BuildContext context, Narrow narrow) async {
6767
}
6868
}
6969

70+
Future<void> markNarrowAsUnreadFromMessage(
71+
BuildContext context,
72+
Message message,
73+
Narrow narrow,
74+
) async {
75+
final zulipLocalizations = ZulipLocalizations.of(context);
76+
await updateMessageFlagsStartingFromAnchor(
77+
context: context,
78+
apiNarrow: narrow.apiEncode(),
79+
startingAnchor: NumericAnchor(message.id),
80+
includeAnchor: true,
81+
op: UpdateMessageFlagsOp.remove,
82+
flag: MessageFlag.read,
83+
onCompletedMessage: zulipLocalizations.markAsUnreadComplete,
84+
progressMessage: zulipLocalizations.markAsUnreadInProgress,
85+
onFailedTitle: zulipLocalizations.errorMarkAsUnreadFailedTitle);
86+
}
87+
7088
/// Updates message flags by applying given operation `op` using given `flag`
7189
/// the update happens on given `apiNarrow` starting from given `startingAnchor`
7290
///

test/widgets/action_sheet_test.dart

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import 'compose_box_checks.dart';
3131
import 'dialog_checks.dart';
3232
import 'test_app.dart';
3333

34+
late FakeApiConnection connection;
35+
3436
/// Simulates loading a [MessageListPage] and long-pressing on [message].
3537
Future<void> setupToMessageActionSheet(WidgetTester tester, {
3638
required Message message,
@@ -46,7 +48,7 @@ Future<void> setupToMessageActionSheet(WidgetTester tester, {
4648
await store.addStream(stream);
4749
await store.addSubscription(eg.subscription(stream));
4850
}
49-
final connection = store.connection as FakeApiConnection;
51+
connection = store.connection as FakeApiConnection;
5052

5153
// prepare message list data
5254
connection.prepare(json: GetMessagesResult(
@@ -542,4 +544,63 @@ void main() {
542544
check(mockSharePlus.sharedString).isNull();
543545
});
544546
});
547+
548+
group('MarkAsUnread', () {
549+
testWidgets('not visible if message is not read', (WidgetTester tester) async {
550+
final unreadMessage = eg.streamMessage(flags: []);
551+
await setupToMessageActionSheet(tester, message: unreadMessage, narrow: TopicNarrow.ofMessage(unreadMessage));
552+
553+
check(find.byIcon(Icons.mark_chat_unread_outlined).evaluate()).isEmpty();
554+
});
555+
556+
testWidgets('visible if message is read', (WidgetTester tester) async {
557+
final readMessage = eg.streamMessage(flags: [MessageFlag.read]);
558+
await setupToMessageActionSheet(tester, message: readMessage, narrow: TopicNarrow.ofMessage(readMessage));
559+
560+
check(find.byIcon(Icons.mark_chat_unread_outlined).evaluate()).single;
561+
});
562+
563+
group('onPressed', () {
564+
testWidgets('smoke test', (WidgetTester tester) async {
565+
final message = eg.streamMessage(flags: [MessageFlag.read]);
566+
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
567+
568+
connection.prepare(json: UpdateMessageFlagsForNarrowResult(
569+
processedCount: 11, updatedCount: 3,
570+
firstProcessedId: 1, lastProcessedId: 1980,
571+
foundOldest: true, foundNewest: true).toJson());
572+
573+
await tester.ensureVisible(find.byIcon(Icons.mark_chat_unread_outlined, skipOffstage: false));
574+
await tester.tap(find.byIcon(Icons.mark_chat_unread_outlined, skipOffstage: false));
575+
await tester.pumpAndSettle();
576+
check(connection.lastRequest).isA<http.Request>()
577+
..method.equals('POST')
578+
..url.path.equals('/api/v1/messages/flags/narrow')
579+
..bodyFields.deepEquals({
580+
'anchor': '${message.id}',
581+
'include_anchor': 'true',
582+
'num_before': '0',
583+
'num_after': '1000',
584+
'narrow': jsonEncode(TopicNarrow.ofMessage(message).apiEncode()),
585+
'op': 'remove',
586+
'flag': 'read',
587+
});
588+
});
589+
590+
testWidgets('shows error when fails', (WidgetTester tester) async {
591+
final message = eg.streamMessage(flags: [MessageFlag.read]);
592+
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
593+
594+
connection.prepare(exception: http.ClientException('Oops'));
595+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
596+
597+
await tester.ensureVisible(find.byIcon(Icons.mark_chat_unread_outlined, skipOffstage: false));
598+
await tester.tap(find.byIcon(Icons.mark_chat_unread_outlined, skipOffstage: false));
599+
await tester.pumpAndSettle();
600+
checkErrorDialog(tester,
601+
expectedTitle: zulipLocalizations.errorMarkAsUnreadFailedTitle,
602+
expectedMessage: 'NetworkException: Oops (ClientException: Oops)');
603+
});
604+
});
605+
});
545606
}

0 commit comments

Comments
 (0)