Skip to content

Commit d8801d9

Browse files
author
chimnayajith
committed
action_sheet: Add channel action sheet with mark as read option
Fixes: zulip#1226
1 parent 1bb22c4 commit d8801d9

13 files changed

+218
-0
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272
"@permissionsDeniedReadExternalStorage": {
7373
"description": "Message for dialog asking the user to grant permissions for external storage read access."
7474
},
75+
"actionSheetOptionMarkChannelAsRead": "Mark channel as read",
76+
"@actionSheetOptionMarkChannelAsRead": {
77+
"description": "Label for marking a channel as read."
78+
},
7579
"actionSheetOptionMuteTopic": "Mute topic",
7680
"@actionSheetOptionMuteTopic": {
7781
"description": "Label for muting a topic on action sheet."

lib/generated/l10n/zulip_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ abstract class ZulipLocalizations {
213213
/// **'To upload files, please grant Zulip additional permissions in Settings.'**
214214
String get permissionsDeniedReadExternalStorage;
215215

216+
/// Label for marking a channel as read.
217+
///
218+
/// In en, this message translates to:
219+
/// **'Mark channel as read'**
220+
String get actionSheetOptionMarkChannelAsRead;
221+
216222
/// Label for muting a topic on action sheet.
217223
///
218224
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
6464
@override
6565
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';
6666

67+
@override
68+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
69+
6770
@override
6871
String get actionSheetOptionMuteTopic => 'Mute topic';
6972

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
6464
@override
6565
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';
6666

67+
@override
68+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
69+
6770
@override
6871
String get actionSheetOptionMuteTopic => 'Mute topic';
6972

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
6464
@override
6565
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';
6666

67+
@override
68+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
69+
6770
@override
6871
String get actionSheetOptionMuteTopic => 'Mute topic';
6972

lib/generated/l10n/zulip_localizations_nb.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
6464
@override
6565
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';
6666

67+
@override
68+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
69+
6770
@override
6871
String get actionSheetOptionMuteTopic => 'Mute topic';
6972

lib/generated/l10n/zulip_localizations_pl.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
6464
@override
6565
String get permissionsDeniedReadExternalStorage => 'Aby odebrać pliki Zulip musi uzyskać dodatkowe uprawnienia w Ustawieniach.';
6666

67+
@override
68+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
69+
6770
@override
6871
String get actionSheetOptionMuteTopic => 'Wycisz wątek';
6972

lib/generated/l10n/zulip_localizations_ru.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
6464
@override
6565
String get permissionsDeniedReadExternalStorage => 'Для загрузки файлов, пожалуйста, предоставьте Zulip дополнительные разрешения в настройках.';
6666

67+
@override
68+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
69+
6770
@override
6871
String get actionSheetOptionMuteTopic => 'Отключить тему';
6972

lib/generated/l10n/zulip_localizations_sk.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
6464
@override
6565
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';
6666

67+
@override
68+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
69+
6770
@override
6871
String get actionSheetOptionMuteTopic => 'Stlmiť tému';
6972

lib/widgets/action_sheet.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,67 @@ class ActionSheetCancelButton extends StatelessWidget {
162162
}
163163
}
164164

165+
/// Show a sheet of actions you can take on a channel.
166+
void showChannelActionSheet(BuildContext context, {
167+
required int streamId,
168+
}) {
169+
final store = PerAccountStoreWidget.of(context);
170+
171+
final optionButtons = <ActionSheetMenuItemButton>[];
172+
final unreadCount = store.unreads.countInChannelNarrow(streamId);
173+
if(unreadCount > 0){
174+
optionButtons.add(
175+
MarkChannelAsReadButton(
176+
streamId: streamId,
177+
pageContext: context,
178+
)
179+
);
180+
}
181+
if (optionButtons.isEmpty) {
182+
// TODO(a11y): This case makes a no-op gesture handler; as a consequence,
183+
// we're presenting some UI (to people who use screen-reader software) as
184+
// though it offers a gesture interaction that it doesn't meaningfully
185+
// offer, which is confusing. The solution here is probably to remove this
186+
// is-empty case by having at least one button that's always present,
187+
// such as "copy link to topic".
188+
return;
189+
}
190+
_showActionSheet(context, optionButtons: optionButtons);
191+
}
192+
193+
class MarkChannelAsReadButton extends ActionSheetMenuItemButton {
194+
const MarkChannelAsReadButton({
195+
super.key,
196+
required this.streamId,
197+
required super.pageContext
198+
});
199+
final int streamId;
200+
201+
@override
202+
IconData get icon => ZulipIcons.message_checked;
203+
204+
@override
205+
String label(ZulipLocalizations zulipLocalizations) {
206+
return zulipLocalizations.actionSheetOptionMarkChannelAsRead;
207+
}
208+
209+
@override
210+
void onPressed() async {
211+
try {
212+
final narrow = ChannelNarrow(streamId);
213+
await markNarrowAsRead(pageContext, narrow);
214+
} catch (e) {
215+
if (!pageContext.mounted) return;
216+
217+
showErrorDialog(
218+
context: pageContext,
219+
title: "Failed to mark channel as read",
220+
message: e.toString(),
221+
);
222+
}
223+
}
224+
}
225+
165226
/// Show a sheet of actions you can take on a topic.
166227
void showTopicActionSheet(BuildContext context, {
167228
required int channelId,

lib/widgets/inbox.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ abstract class _HeaderItem extends StatelessWidget {
254254
}
255255

256256
Future<void> onRowTap();
257+
Future<void> onLongPress();
257258

258259
@override
259260
Widget build(BuildContext context) {
@@ -270,6 +271,7 @@ abstract class _HeaderItem extends StatelessWidget {
270271
// But that's in tension with the Figma, which gives these header rows
271272
// 40px min height.
272273
onTap: onCollapseButtonTap,
274+
onLongPress: onLongPress,
273275
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
274276
Padding(padding: const EdgeInsets.all(10),
275277
child: Icon(size: 20, color: designVariables.sectionCollapseIcon,
@@ -327,6 +329,8 @@ class _AllDmsHeaderItem extends _HeaderItem {
327329
pageState.allDmsCollapsed = !collapsed;
328330
}
329331
@override Future<void> onRowTap() => onCollapseButtonTap(); // TODO open all-DMs narrow?
332+
333+
@override Future<void> onLongPress() async {}
330334
}
331335

332336
class _AllDmsSection extends StatelessWidget {
@@ -456,6 +460,11 @@ class _StreamHeaderItem extends _HeaderItem {
456460
}
457461
}
458462
@override Future<void> onRowTap() => onCollapseButtonTap(); // TODO open channel narrow
463+
464+
@override
465+
Future<void> onLongPress() async {
466+
showChannelActionSheet(sectionContext, streamId: subscription.streamId);
467+
}
459468
}
460469

461470
class _StreamSection extends StatelessWidget {

lib/widgets/subscription_list.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import '../api/model/model.dart';
44
import '../model/narrow.dart';
55
import '../model/unreads.dart';
6+
import 'action_sheet.dart';
67
import 'icons.dart';
78
import 'message_list.dart';
89
import 'store.dart';
@@ -230,6 +231,7 @@ class SubscriptionItem extends StatelessWidget {
230231
MessageListPage.buildRoute(context: context,
231232
narrow: ChannelNarrow(subscription.streamId)));
232233
},
234+
onLongPress: () => showChannelActionSheet(context, streamId: subscription.streamId),
233235
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
234236
const SizedBox(width: 16),
235237
Padding(

test/widgets/action_sheet_test.dart

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import 'package:zulip/widgets/icons.dart';
3030
import 'package:zulip/widgets/inbox.dart';
3131
import 'package:zulip/widgets/message_list.dart';
3232
import 'package:share_plus_platform_interface/method_channel/method_channel_share.dart';
33+
import 'package:zulip/widgets/subscription_list.dart';
3334
import '../api/fake_api.dart';
3435

3536
import '../example_data.dart' as eg;
@@ -1084,6 +1085,120 @@ void main() {
10841085
});
10851086
});
10861087
});
1088+
group('channel action sheet', () {
1089+
late ZulipStream someChannel;
1090+
late PerAccountStore store;
1091+
1092+
Future<void> setupToChannelActionSheet(WidgetTester tester, {
1093+
required ZulipStream channel,
1094+
required UnreadMessagesSnapshot unreadMsgs,
1095+
}) async {
1096+
someChannel = channel;
1097+
addTearDown(testBinding.reset);
1098+
1099+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
1100+
realmUsers: [eg.selfUser, eg.otherUser],
1101+
streams: [someChannel],
1102+
subscriptions: [eg.subscription(someChannel)],
1103+
unreadMsgs: unreadMsgs));
1104+
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
1105+
connection = store.connection as FakeApiConnection;
1106+
}
1107+
1108+
Future<void> showFromInbox(WidgetTester tester) async {
1109+
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
1110+
child: const HomePage()));
1111+
await tester.pumpAndSettle();
1112+
check(find.byType(InboxPageBody)).findsOne();
1113+
1114+
await tester.pump();
1115+
await tester.longPress(find.text(someChannel.name).hitTestable());
1116+
await tester.pump(const Duration(milliseconds: 250));
1117+
}
1118+
1119+
Future<void> showFromSubscriptionList(WidgetTester tester) async {
1120+
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
1121+
child: const SubscriptionListPageBody()));
1122+
await tester.pumpAndSettle();
1123+
check(find.byType(SubscriptionListPageBody)).findsOne();
1124+
1125+
await tester.pump();
1126+
await tester.longPress(find.text(someChannel.name).hitTestable());
1127+
await tester.pump(const Duration(milliseconds: 250));
1128+
}
1129+
1130+
final actionSheetFinder = find.byType(BottomSheet);
1131+
Finder findButtonForLabel(String label) =>
1132+
find.descendant(of: actionSheetFinder, matching: find.text(label));
1133+
1134+
void checkActionSheet() {
1135+
check(actionSheetFinder).findsOne();
1136+
}
1137+
1138+
void checkButton(String label) {
1139+
check(findButtonForLabel(label)).findsOne();
1140+
}
1141+
1142+
group('showChannelActionSheet', () {
1143+
testWidgets('button shown when channel has unread messages', (tester) async {
1144+
final stream = eg.stream();
1145+
final unreadMsgs = eg.unreadMsgs(channels: [
1146+
eg.unreadChannelMsgs(streamId: stream.streamId, topic: 'topic', unreadMessageIds: [1, 2])
1147+
]);
1148+
1149+
await setupToChannelActionSheet(tester,
1150+
channel: stream,
1151+
unreadMsgs: unreadMsgs);
1152+
await showFromInbox(tester);
1153+
1154+
checkActionSheet();
1155+
checkButton('Mark channel as read');
1156+
});
1157+
1158+
testWidgets('show from inbox with unread messages', (tester) async {
1159+
final stream = eg.stream();
1160+
final unreadMsgs = eg.unreadMsgs(channels: [
1161+
eg.unreadChannelMsgs(streamId: stream.streamId, topic: 'topic', unreadMessageIds: [1, 2])
1162+
]);
1163+
1164+
await setupToChannelActionSheet(tester,
1165+
channel: stream,
1166+
unreadMsgs: unreadMsgs);
1167+
await showFromInbox(tester);
1168+
1169+
checkActionSheet();
1170+
checkButton('Mark channel as read');
1171+
});
1172+
1173+
testWidgets('show from subscription list', (tester) async {
1174+
final stream = eg.stream();
1175+
final unreadMsgs = eg.unreadMsgs(channels: [
1176+
eg.unreadChannelMsgs(streamId: stream.streamId, topic: 'topic', unreadMessageIds: [1])
1177+
]);
1178+
1179+
await setupToChannelActionSheet(tester,
1180+
channel: stream,
1181+
unreadMsgs: unreadMsgs);
1182+
await showFromSubscriptionList(tester);
1183+
1184+
checkActionSheet();
1185+
checkButton('Mark channel as read');
1186+
});
1187+
1188+
// This should be changed when default item is added to the sheet
1189+
testWidgets('show from subscription list with no unread messages does not show action sheet', (tester) async {
1190+
final stream = eg.stream();
1191+
final unreadMsgs = eg.unreadMsgs();
1192+
1193+
await setupToChannelActionSheet(tester,
1194+
channel: stream,
1195+
unreadMsgs: unreadMsgs);
1196+
await showFromSubscriptionList(tester);
1197+
1198+
check(actionSheetFinder).findsNothing();
1199+
});
1200+
});
1201+
});
10871202
}
10881203

10891204
extension UnicodeEmojiWidgetChecks on Subject<UnicodeEmojiWidget> {

0 commit comments

Comments
 (0)