Skip to content

Commit

Permalink
action_sheet: Add "Mark Topic As Read" button
Browse files Browse the repository at this point in the history
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: #1225
  • Loading branch information
lakshya1goel committed Feb 1, 2025
1 parent 51d71a9 commit 59fb9c2
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 0 deletions.
8 changes: 8 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -716,5 +716,13 @@
"emojiPickerSearchEmoji": "Search emoji",
"@emojiPickerSearchEmoji": {
"description": "Hint text for the emoji picker search text field."
},
"actionSheetOptionMarkTopicAsRead": "Mark Topic As Read",
"@actionSheetOptionMarkTopicAsRead": {
"description": "Option to mark a specific topic as read in the action sheet."
},
"errorMarkTopicAsReadFailed": "Failed to mark the topic as read. Please try again.",
"@errorMarkTopicAsReadFailed": {
"description": "Error message displayed when marking a topic as read fails."
}
}
12 changes: 12 additions & 0 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,18 @@ abstract class ZulipLocalizations {
/// In en, this message translates to:
/// **'Search emoji'**
String get emojiPickerSearchEmoji;

/// 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 message displayed when marking a topic as read fails.
///
/// In en, this message translates to:
/// **'Failed to mark the topic as read. Please try again.'**
String get errorMarkTopicAsReadFailed;
}

class _ZulipLocalizationsDelegate extends LocalizationsDelegate<ZulipLocalizations> {
Expand Down
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,10 @@ class ZulipLocalizationsAr extends ZulipLocalizations {

@override
String get emojiPickerSearchEmoji => 'Search emoji';

@override
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';

@override
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
}
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,10 @@ class ZulipLocalizationsEn extends ZulipLocalizations {

@override
String get emojiPickerSearchEmoji => 'Search emoji';

@override
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';

@override
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
}
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,10 @@ class ZulipLocalizationsJa extends ZulipLocalizations {

@override
String get emojiPickerSearchEmoji => 'Search emoji';

@override
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';

@override
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
}
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations_nb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,10 @@ class ZulipLocalizationsNb extends ZulipLocalizations {

@override
String get emojiPickerSearchEmoji => 'Search emoji';

@override
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';

@override
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
}
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,10 @@ class ZulipLocalizationsPl extends ZulipLocalizations {

@override
String get emojiPickerSearchEmoji => 'Szukaj emoji';

@override
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';

@override
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
}
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,10 @@ class ZulipLocalizationsRu extends ZulipLocalizations {

@override
String get emojiPickerSearchEmoji => 'Поиск эмодзи';

@override
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';

@override
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
}
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations_sk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,10 @@ class ZulipLocalizationsSk extends ZulipLocalizations {

@override
String get emojiPickerSearchEmoji => 'Hľadať emotikon';

@override
String get actionSheetOptionMarkTopicAsRead => 'Mark Topic As Read';

@override
String get errorMarkTopicAsReadFailed => 'Failed to mark the topic as read. Please try again.';
}
33 changes: 33 additions & 0 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ void showTopicActionSheet(BuildContext context, {
pageContext: context);
}));

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
Expand Down Expand Up @@ -372,6 +380,31 @@ class UserTopicUpdateButton 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 {
if (!pageContext.mounted) return;
final narrow = TopicNarrow(channelId, topic);
await markNarrowAsRead(pageContext, narrow);
}
}

/// Show a sheet of actions you can take on a message in the message list.
///
/// Must have a [MessageListPage] ancestor.
Expand Down
76 changes: 76 additions & 0 deletions test/widgets/action_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,82 @@ void main() {
});
});

group('MarkTopicAsReadButton', () {
Future<void> setupToTopicActionSheetWithUnreadMessages(WidgetTester tester, {
ZulipStream? channel,
}) async {
addTearDown(testBinding.reset);

final effectiveChannel = channel ?? eg.stream();
const topicName = TopicName('test topic');
final message = eg.streamMessage(stream: effectiveChannel, topic: 'test topic');
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
realmUsers: [eg.selfUser],
streams: [effectiveChannel],
subscriptions: [eg.subscription(effectiveChannel)]));
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
connection = store.connection as FakeApiConnection;

connection.prepare(json: eg.newestGetMessagesResult(
foundOldest: true, messages: [message]).toJson());

await store.addMessage(message);
store.unreads.streams[effectiveChannel.streamId] ??= {};
store.unreads.streams[effectiveChannel.streamId]![topicName] ??= QueueList<int>();
store.unreads.streams[effectiveChannel.streamId]![topicName]!.add(message.id);

await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: MessageListPage(initNarrow: TopicNarrow(effectiveChannel.streamId, topicName))));
await tester.pumpAndSettle();

await tester.longPress(find.byType(ZulipAppBar));
await tester.pump(const Duration(milliseconds: 250));
}

Future<void> setupToTopicActionSheetWithNoUnreadMessages(WidgetTester tester) async {
addTearDown(testBinding.reset);

final channel = eg.stream();
const topicName = TopicName('test topic');
final message = eg.streamMessage(stream: channel, topic: 'test topic', flags: [MessageFlag.read]);

await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
realmUsers: [eg.selfUser],
streams: [channel],
subscriptions: [eg.subscription(channel)]));
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
connection = store.connection as FakeApiConnection;

connection.prepare(json: eg.newestGetMessagesResult(
foundOldest: true, messages: [message]).toJson());

await store.addMessage(message);

await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: MessageListPage(initNarrow: TopicNarrow(channel.streamId, topicName))));
await tester.pumpAndSettle();

await tester.longPress(find.byType(ZulipAppBar));
await tester.pump(const Duration(milliseconds: 250));
}

group('visibility', () {
testWidgets('shows button when topic has unread messages', (tester) async {
await setupToTopicActionSheetWithUnreadMessages(tester);

final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
check(find.text(zulipLocalizations.actionSheetOptionMarkTopicAsRead)).findsOne();
});

testWidgets('hides button when topic has no unread messages', (tester) async {
await setupToTopicActionSheetWithNoUnreadMessages(tester);

final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
check(find.text(zulipLocalizations.actionSheetOptionMarkTopicAsRead)).findsNothing();
});
});
});

group('MessageActionSheetCancelButton', () {
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;

Expand Down

0 comments on commit 59fb9c2

Please sign in to comment.