diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf
index 0be670d5cb..a2d20d40cd 100644
Binary files a/assets/icons/ZulipIcons.ttf and b/assets/icons/ZulipIcons.ttf differ
diff --git a/assets/icons/message_checked.svg b/assets/icons/message_checked.svg
new file mode 100644
index 0000000000..5c598ae87e
--- /dev/null
+++ b/assets/icons/message_checked.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index 118ab83c70..9bc14daf7c 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -136,6 +136,10 @@
"@actionSheetOptionUnstarMessage": {
"description": "Label for unstar button on action sheet."
},
+ "actionSheetOptionMarkTopicAsRead": "Mark topic as read",
+ "@actionSheetOptionMarkTopicAsRead": {
+ "description": "Option to mark a specific topic as read in the action sheet."
+ },
"errorWebAuthOperationalErrorTitle": "Something went wrong",
"@errorWebAuthOperationalErrorTitle": {
"description": "Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials)."
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index 9579683908..2756fe632e 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -309,6 +309,12 @@ abstract class ZulipLocalizations {
/// **'Unstar message'**
String get actionSheetOptionUnstarMessage;
+ /// Option to mark a specific topic as read in the action sheet.
+ ///
+ /// In en, this message translates to:
+ /// **'Mark topic as read'**
+ String get actionSheetOptionMarkTopicAsRead;
+
/// Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials).
///
/// In en, this message translates to:
diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart
index 71bf06d8ce..70d76b017f 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -112,6 +112,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Unstar message';
+ @override
+ String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Something went wrong';
diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index 7a33e33567..d926cdb0c1 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -112,6 +112,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Unstar message';
+ @override
+ String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Something went wrong';
diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index 137883e5e9..c2f5692864 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -112,6 +112,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Unstar message';
+ @override
+ String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Something went wrong';
diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index 3dec7d9b5a..13fa7ea71a 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -112,6 +112,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Unstar message';
+ @override
+ String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Something went wrong';
diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index 83a777bfc4..f074d87acc 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -112,6 +112,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Odbierz gwiazdkę';
+ @override
+ String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Coś poszło nie tak';
diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index 827aaf0155..848075052a 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -112,6 +112,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Снять отметку с сообщения';
+ @override
+ String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Что-то пошло не так';
diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index 38a3f8a240..c013ab3886 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -112,6 +112,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Odhviezdičkovať správu';
+ @override
+ String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Niečo sa pokazilo';
diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart
index e943d7eb36..fb10e8d185 100644
--- a/lib/widgets/action_sheet.dart
+++ b/lib/widgets/action_sheet.dart
@@ -255,6 +255,14 @@ void showTopicActionSheet(BuildContext context, {
someMessageIdInTopic: someMessageIdInTopic));
}
+ final unreadCount = store.unreads.countInTopicNarrow(channelId, topic);
+ if (unreadCount > 0) {
+ optionButtons.add(MarkTopicAsReadButton(
+ channelId: channelId,
+ topic: topic,
+ pageContext: context));
+ }
+
if (optionButtons.isEmpty) {
// TODO(a11y): This case makes a no-op gesture handler; as a consequence,
// we're presenting some UI (to people who use screen-reader software) as
@@ -461,6 +469,29 @@ class ResolveUnresolveButton extends ActionSheetMenuItemButton {
}
}
+class MarkTopicAsReadButton extends ActionSheetMenuItemButton {
+ const MarkTopicAsReadButton({
+ super.key,
+ required this.channelId,
+ required this.topic,
+ required super.pageContext,
+ });
+
+ final int channelId;
+ final TopicName topic;
+
+ @override IconData get icon => ZulipIcons.message_checked;
+
+ @override
+ String label(ZulipLocalizations zulipLocalizations) {
+ return zulipLocalizations.actionSheetOptionMarkTopicAsRead;
+ }
+
+ @override void onPressed() async {
+ await ZulipAction.markNarrowAsRead(pageContext, TopicNarrow(channelId, topic));
+ }
+}
+
/// Show a sheet of actions you can take on a message in the message list.
///
/// Must have a [MessageListPage] ancestor.
diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart
index 82cb83704b..c3a68085c7 100644
--- a/lib/widgets/icons.dart
+++ b/lib/widgets/icons.dart
@@ -99,44 +99,47 @@ abstract final class ZulipIcons {
/// The Zulip custom icon "menu".
static const IconData menu = IconData(0xf119, fontFamily: "Zulip Icons");
+ /// The Zulip custom icon "message_checked".
+ static const IconData message_checked = IconData(0xf11a, fontFamily: "Zulip Icons");
+
/// The Zulip custom icon "message_feed".
- static const IconData message_feed = IconData(0xf11a, fontFamily: "Zulip Icons");
+ static const IconData message_feed = IconData(0xf11b, fontFamily: "Zulip Icons");
/// The Zulip custom icon "mute".
- static const IconData mute = IconData(0xf11b, fontFamily: "Zulip Icons");
+ static const IconData mute = IconData(0xf11c, fontFamily: "Zulip Icons");
/// The Zulip custom icon "read_receipts".
- static const IconData read_receipts = IconData(0xf11c, fontFamily: "Zulip Icons");
+ static const IconData read_receipts = IconData(0xf11d, fontFamily: "Zulip Icons");
/// The Zulip custom icon "send".
- static const IconData send = IconData(0xf11d, fontFamily: "Zulip Icons");
+ static const IconData send = IconData(0xf11e, fontFamily: "Zulip Icons");
/// The Zulip custom icon "share".
- static const IconData share = IconData(0xf11e, fontFamily: "Zulip Icons");
+ static const IconData share = IconData(0xf11f, fontFamily: "Zulip Icons");
/// The Zulip custom icon "share_ios".
- static const IconData share_ios = IconData(0xf11f, fontFamily: "Zulip Icons");
+ static const IconData share_ios = IconData(0xf120, fontFamily: "Zulip Icons");
/// The Zulip custom icon "smile".
- static const IconData smile = IconData(0xf120, fontFamily: "Zulip Icons");
+ static const IconData smile = IconData(0xf121, fontFamily: "Zulip Icons");
/// The Zulip custom icon "star".
- static const IconData star = IconData(0xf121, fontFamily: "Zulip Icons");
+ static const IconData star = IconData(0xf122, fontFamily: "Zulip Icons");
/// The Zulip custom icon "star_filled".
- static const IconData star_filled = IconData(0xf122, fontFamily: "Zulip Icons");
+ static const IconData star_filled = IconData(0xf123, fontFamily: "Zulip Icons");
/// The Zulip custom icon "three_person".
- static const IconData three_person = IconData(0xf123, fontFamily: "Zulip Icons");
+ static const IconData three_person = IconData(0xf124, fontFamily: "Zulip Icons");
/// The Zulip custom icon "topic".
- static const IconData topic = IconData(0xf124, fontFamily: "Zulip Icons");
+ static const IconData topic = IconData(0xf125, fontFamily: "Zulip Icons");
/// The Zulip custom icon "unmute".
- static const IconData unmute = IconData(0xf125, fontFamily: "Zulip Icons");
+ static const IconData unmute = IconData(0xf126, fontFamily: "Zulip Icons");
/// The Zulip custom icon "user".
- static const IconData user = IconData(0xf126, fontFamily: "Zulip Icons");
+ static const IconData user = IconData(0xf127, fontFamily: "Zulip Icons");
// END GENERATED ICON DATA
}
diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart
index 48c0d70b59..26460781f2 100644
--- a/lib/widgets/message_list.dart
+++ b/lib/widgets/message_list.dart
@@ -852,7 +852,7 @@ class _MarkAsReadWidgetState extends State {
backgroundColor: WidgetStatePropertyAll(messageListTheme.unreadMarker),
),
onPressed: _loading ? null : () => _handlePress(context),
- icon: const Icon(Icons.playlist_add_check),
+ icon: const Icon(ZulipIcons.message_checked),
label: Text(zulipLocalizations.markAllAsReadLabel))))));
}
}
diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart
index 5a4c22c604..286b26d3ee 100644
--- a/test/widgets/action_sheet_test.dart
+++ b/test/widgets/action_sheet_test.dart
@@ -10,6 +10,7 @@ import 'package:http/http.dart' as http;
import 'package:zulip/api/model/events.dart';
import 'package:zulip/api/model/initial_snapshot.dart';
import 'package:zulip/api/model/model.dart';
+import 'package:zulip/api/model/narrow.dart';
import 'package:zulip/api/route/channels.dart';
import 'package:zulip/api/route/messages.dart';
import 'package:zulip/model/binding.dart';
@@ -562,6 +563,47 @@ void main() {
expectedTitle: 'Failed to mark topic as unresolved');
});
});
+
+ group('MarkTopicAsReadButton', () {
+ testWidgets('visible if topic has unread messages', (tester) async {
+ await prepare();
+ final message = eg.streamMessage(stream: someChannel, topic: someTopic, flags: []);
+ await store.addMessage(message);
+ await showFromAppBar(tester, messages: [message]);
+ check(find.text('Mark topic as read')).findsOne();
+ });
+
+ testWidgets('not visible if topic has no unread messages', (tester) async {
+ await prepare();
+ final message = eg.streamMessage(stream: someChannel, topic: someTopic, flags: [MessageFlag.read]);
+ await store.addMessage(message);
+ await showFromAppBar(tester, messages: [message]);
+ check(find.text('Mark topic as read')).findsNothing();
+ });
+
+ testWidgets('marks topic as read when pressed', (tester) async {
+ await prepare();
+ final message = eg.streamMessage(stream: someChannel, topic: someTopic);
+ await store.handleEvent(MessageEvent(id: 0, message: message));
+ await showFromAppBar(tester, messages: [message]);
+
+ connection.prepare(json: UpdateMessageFlagsForNarrowResult(
+ processedCount: 1, updatedCount: 1,
+ firstProcessedId: message.id, lastProcessedId: message.id,
+ foundOldest: true, foundNewest: true).toJson());
+ await tester.tap(find.text('Mark topic as read'));
+ await tester.pumpAndSettle();
+
+ check(connection.lastRequest).isA()
+ ..url.path.equals('/api/v1/messages/flags/narrow')
+ ..bodyFields['narrow'].equals(jsonEncode([
+ ...eg.topicNarrow(someChannel.streamId, someTopic).apiEncode(),
+ ApiNarrowIs(IsOperand.unread),
+ ]))
+ ..bodyFields['op'].equals('add')
+ ..bodyFields['flag'].equals('read');
+ });
+ });
});
group('message action sheet', () {