Skip to content

Commit 3fc179a

Browse files
committed
action_sheet: Add "Mark as unread from here" button
Fixes: #131
1 parent 69bd093 commit 3fc179a

File tree

4 files changed

+134
-1
lines changed

4 files changed

+134
-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+
"actionSheetOptionMarkAsUnread": "Mark as unread from here",
55+
"@actionSheetOptionMarkAsUnread": {
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: 33 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,10 @@ 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);
34+
final markAsUnreadSupported = store.connection.zulipFeatureLevel! >= 155; // TODO(server-6)
35+
final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead;
3136

3237
final hasThumbsUpReactionVote = message.reactions
3338
?.aggregated.any((reactionWithVotes) =>
@@ -46,6 +51,11 @@ void showMessageActionSheet({required BuildContext context, required Message mes
4651
message: message,
4752
messageListContext: context,
4853
),
54+
if (showMarkAsUnreadButton) MarkAsUnreadButton(
55+
message: message,
56+
messageListContext: context,
57+
narrow: narrow,
58+
),
4959
CopyMessageTextButton(message: message, messageListContext: context),
5060
CopyMessageLinkButton(message: message, messageListContext: context),
5161
ShareButton(message: message, messageListContext: context),
@@ -278,6 +288,29 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
278288
}
279289
}
280290

291+
class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
292+
MarkAsUnreadButton({
293+
super.key,
294+
required super.message,
295+
required super.messageListContext,
296+
required this.narrow,
297+
});
298+
299+
final Narrow narrow;
300+
301+
@override IconData get icon => Icons.mark_chat_unread_outlined;
302+
303+
@override
304+
String label(ZulipLocalizations zulipLocalizations) {
305+
return zulipLocalizations.actionSheetOptionMarkAsUnread;
306+
}
307+
308+
@override void onPressed(BuildContext context) async {
309+
Navigator.of(context).pop();
310+
markNarrowAsUnreadFromMessage(messageListContext, message, narrow);
311+
}
312+
}
313+
281314
class CopyMessageTextButton extends MessageActionSheetMenuItemButton {
282315
CopyMessageTextButton({
283316
super.key,

lib/widgets/actions.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ Future<void> markNarrowAsRead(BuildContext context, Narrow narrow) async {
6262
}
6363
}
6464

65+
Future<void> markNarrowAsUnreadFromMessage(
66+
BuildContext context,
67+
Message message,
68+
Narrow narrow,
69+
) async {
70+
final connection = PerAccountStoreWidget.of(context).connection;
71+
assert(connection.zulipFeatureLevel! >= 155); // TODO(server-6)
72+
final zulipLocalizations = ZulipLocalizations.of(context);
73+
await updateMessageFlagsStartingFromAnchor(
74+
context: context,
75+
apiNarrow: narrow.apiEncode(),
76+
startingAnchor: NumericAnchor(message.id),
77+
includeAnchor: true,
78+
op: UpdateMessageFlagsOp.remove,
79+
flag: MessageFlag.read,
80+
onCompletedMessage: zulipLocalizations.markAsUnreadComplete,
81+
progressMessage: zulipLocalizations.markAsUnreadInProgress,
82+
onFailedTitle: zulipLocalizations.errorMarkAsUnreadFailedTitle);
83+
}
84+
6585
/// Updates message flags by applying given operation `op` using given `flag`
6686
/// the update happens on given `apiNarrow` starting from given `startingAnchor`
6787
///

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)