Skip to content

Commit 59fb9c2

Browse files
committed
action_sheet: Add "Mark Topic As Read" button
Adds button to mark all messages in a topic as read. The button: - Appears only when the topic has unread messages - Uses mark_topic_as_read API for server feature level < 155 - Uses messages/flags/narrow API for server feature level >= 155 - Shows error dialog if the request fails fixes: zulip#1225
1 parent 51d71a9 commit 59fb9c2

11 files changed

+171
-0
lines changed

assets/l10n/app_en.arb

+8
Original file line numberDiff line numberDiff line change
@@ -716,5 +716,13 @@
716716
"emojiPickerSearchEmoji": "Search emoji",
717717
"@emojiPickerSearchEmoji": {
718718
"description": "Hint text for the emoji picker search text field."
719+
},
720+
"actionSheetOptionMarkTopicAsRead": "Mark Topic As Read",
721+
"@actionSheetOptionMarkTopicAsRead": {
722+
"description": "Option to mark a specific topic as read in the action sheet."
723+
},
724+
"errorMarkTopicAsReadFailed": "Failed to mark the topic as read. Please try again.",
725+
"@errorMarkTopicAsReadFailed": {
726+
"description": "Error message displayed when marking a topic as read fails."
719727
}
720728
}

lib/generated/l10n/zulip_localizations.dart

+12
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,18 @@ abstract class ZulipLocalizations {
10701070
/// In en, this message translates to:
10711071
/// **'Search emoji'**
10721072
String get emojiPickerSearchEmoji;
1073+
1074+
/// Option to mark a specific topic as read in the action sheet.
1075+
///
1076+
/// In en, this message translates to:
1077+
/// **'Mark Topic As Read'**
1078+
String get actionSheetOptionMarkTopicAsRead;
1079+
1080+
/// Error message displayed when marking a topic as read fails.
1081+
///
1082+
/// In en, this message translates to:
1083+
/// **'Failed to mark the topic as read. Please try again.'**
1084+
String get errorMarkTopicAsReadFailed;
10731085
}
10741086

10751087
class _ZulipLocalizationsDelegate extends LocalizationsDelegate<ZulipLocalizations> {

lib/generated/l10n/zulip_localizations_ar.dart

+6
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,10 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
564564

565565
@override
566566
String get emojiPickerSearchEmoji => 'Search emoji';
567+
568+
@override
569+
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';
570+
571+
@override
572+
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
567573
}

lib/generated/l10n/zulip_localizations_en.dart

+6
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,10 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
564564

565565
@override
566566
String get emojiPickerSearchEmoji => 'Search emoji';
567+
568+
@override
569+
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';
570+
571+
@override
572+
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
567573
}

lib/generated/l10n/zulip_localizations_ja.dart

+6
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,10 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
564564

565565
@override
566566
String get emojiPickerSearchEmoji => 'Search emoji';
567+
568+
@override
569+
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';
570+
571+
@override
572+
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
567573
}

lib/generated/l10n/zulip_localizations_nb.dart

+6
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,10 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
564564

565565
@override
566566
String get emojiPickerSearchEmoji => 'Search emoji';
567+
568+
@override
569+
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';
570+
571+
@override
572+
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
567573
}

lib/generated/l10n/zulip_localizations_pl.dart

+6
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,10 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
564564

565565
@override
566566
String get emojiPickerSearchEmoji => 'Szukaj emoji';
567+
568+
@override
569+
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';
570+
571+
@override
572+
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
567573
}

lib/generated/l10n/zulip_localizations_ru.dart

+6
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,10 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
564564

565565
@override
566566
String get emojiPickerSearchEmoji => 'Поиск эмодзи';
567+
568+
@override
569+
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';
570+
571+
@override
572+
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
567573
}

lib/generated/l10n/zulip_localizations_sk.dart

+6
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,10 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
564564

565565
@override
566566
String get emojiPickerSearchEmoji => 'Hľadať emotikon';
567+
568+
@override
569+
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';
570+
571+
@override
572+
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
567573
}

lib/widgets/action_sheet.dart

+33
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ void showTopicActionSheet(BuildContext context, {
240240
pageContext: context);
241241
}));
242242

243+
final unreadCount = store.unreads.countInTopicNarrow(channelId, topic);
244+
if (unreadCount > 0) {
245+
optionButtons.add(MarkTopicAsReadButton(
246+
channelId: channelId,
247+
topic: topic,
248+
pageContext: context));
249+
}
250+
243251
if (optionButtons.isEmpty) {
244252
// TODO(a11y): This case makes a no-op gesture handler; as a consequence,
245253
// we're presenting some UI (to people who use screen-reader software) as
@@ -372,6 +380,31 @@ class UserTopicUpdateButton extends ActionSheetMenuItemButton {
372380
}
373381
}
374382

383+
class MarkTopicAsReadButton extends ActionSheetMenuItemButton {
384+
const MarkTopicAsReadButton({
385+
super.key,
386+
required this.channelId,
387+
required this.topic,
388+
required super.pageContext,
389+
});
390+
391+
final int channelId;
392+
final TopicName topic;
393+
394+
@override IconData get icon => ZulipIcons.message_checked;
395+
396+
@override
397+
String label(ZulipLocalizations zulipLocalizations) {
398+
return zulipLocalizations.actionSheetOptionMarkTopicAsRead;
399+
}
400+
401+
@override void onPressed() async {
402+
if (!pageContext.mounted) return;
403+
final narrow = TopicNarrow(channelId, topic);
404+
await markNarrowAsRead(pageContext, narrow);
405+
}
406+
}
407+
375408
/// Show a sheet of actions you can take on a message in the message list.
376409
///
377410
/// Must have a [MessageListPage] ancestor.

test/widgets/action_sheet_test.dart

+76
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,82 @@ void main() {
10611061
});
10621062
});
10631063

1064+
group('MarkTopicAsReadButton', () {
1065+
Future<void> setupToTopicActionSheetWithUnreadMessages(WidgetTester tester, {
1066+
ZulipStream? channel,
1067+
}) async {
1068+
addTearDown(testBinding.reset);
1069+
1070+
final effectiveChannel = channel ?? eg.stream();
1071+
const topicName = TopicName('test topic');
1072+
final message = eg.streamMessage(stream: effectiveChannel, topic: 'test topic');
1073+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
1074+
realmUsers: [eg.selfUser],
1075+
streams: [effectiveChannel],
1076+
subscriptions: [eg.subscription(effectiveChannel)]));
1077+
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
1078+
connection = store.connection as FakeApiConnection;
1079+
1080+
connection.prepare(json: eg.newestGetMessagesResult(
1081+
foundOldest: true, messages: [message]).toJson());
1082+
1083+
await store.addMessage(message);
1084+
store.unreads.streams[effectiveChannel.streamId] ??= {};
1085+
store.unreads.streams[effectiveChannel.streamId]![topicName] ??= QueueList<int>();
1086+
store.unreads.streams[effectiveChannel.streamId]![topicName]!.add(message.id);
1087+
1088+
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
1089+
child: MessageListPage(initNarrow: TopicNarrow(effectiveChannel.streamId, topicName))));
1090+
await tester.pumpAndSettle();
1091+
1092+
await tester.longPress(find.byType(ZulipAppBar));
1093+
await tester.pump(const Duration(milliseconds: 250));
1094+
}
1095+
1096+
Future<void> setupToTopicActionSheetWithNoUnreadMessages(WidgetTester tester) async {
1097+
addTearDown(testBinding.reset);
1098+
1099+
final channel = eg.stream();
1100+
const topicName = TopicName('test topic');
1101+
final message = eg.streamMessage(stream: channel, topic: 'test topic', flags: [MessageFlag.read]);
1102+
1103+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
1104+
realmUsers: [eg.selfUser],
1105+
streams: [channel],
1106+
subscriptions: [eg.subscription(channel)]));
1107+
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
1108+
connection = store.connection as FakeApiConnection;
1109+
1110+
connection.prepare(json: eg.newestGetMessagesResult(
1111+
foundOldest: true, messages: [message]).toJson());
1112+
1113+
await store.addMessage(message);
1114+
1115+
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
1116+
child: MessageListPage(initNarrow: TopicNarrow(channel.streamId, topicName))));
1117+
await tester.pumpAndSettle();
1118+
1119+
await tester.longPress(find.byType(ZulipAppBar));
1120+
await tester.pump(const Duration(milliseconds: 250));
1121+
}
1122+
1123+
group('visibility', () {
1124+
testWidgets('shows button when topic has unread messages', (tester) async {
1125+
await setupToTopicActionSheetWithUnreadMessages(tester);
1126+
1127+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
1128+
check(find.text(zulipLocalizations.actionSheetOptionMarkTopicAsRead)).findsOne();
1129+
});
1130+
1131+
testWidgets('hides button when topic has no unread messages', (tester) async {
1132+
await setupToTopicActionSheetWithNoUnreadMessages(tester);
1133+
1134+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
1135+
check(find.text(zulipLocalizations.actionSheetOptionMarkTopicAsRead)).findsNothing();
1136+
});
1137+
});
1138+
});
1139+
10641140
group('MessageActionSheetCancelButton', () {
10651141
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
10661142

0 commit comments

Comments
 (0)