Skip to content

Commit 6bbddfa

Browse files
committed
action_sheet: Add channel action sheet with mark as read option
Fixes: zulip#1226
1 parent 1021c36 commit 6bbddfa

14 files changed

+345
-27
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@
7676
"@permissionsDeniedReadExternalStorage": {
7777
"description": "Message for dialog asking the user to grant permissions for external storage read access."
7878
},
79+
"actionSheetOptionMarkChannelAsRead": "Mark channel as read",
80+
"@actionSheetOptionMarkChannelAsRead": {
81+
"description": "Label for marking a channel as read."
82+
},
7983
"actionSheetOptionMuteTopic": "Mute topic",
8084
"@actionSheetOptionMuteTopic": {
8185
"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
@@ -219,6 +219,12 @@ abstract class ZulipLocalizations {
219219
/// **'To upload files, please grant Zulip additional permissions in Settings.'**
220220
String get permissionsDeniedReadExternalStorage;
221221

222+
/// Label for marking a channel as read.
223+
///
224+
/// In en, this message translates to:
225+
/// **'Mark channel as read'**
226+
String get actionSheetOptionMarkChannelAsRead;
227+
222228
/// Label for muting a topic on action sheet.
223229
///
224230
/// 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
@@ -67,6 +67,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
6767
@override
6868
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';
6969

70+
@override
71+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
72+
7073
@override
7174
String get actionSheetOptionMuteTopic => 'Mute topic';
7275

lib/generated/l10n/zulip_localizations_en.dart

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

70+
@override
71+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
72+
7073
@override
7174
String get actionSheetOptionMuteTopic => 'Mute topic';
7275

lib/generated/l10n/zulip_localizations_ja.dart

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

70+
@override
71+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
72+
7073
@override
7174
String get actionSheetOptionMuteTopic => 'Mute topic';
7275

lib/generated/l10n/zulip_localizations_nb.dart

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

70+
@override
71+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
72+
7073
@override
7174
String get actionSheetOptionMuteTopic => 'Mute topic';
7275

lib/generated/l10n/zulip_localizations_pl.dart

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

70+
@override
71+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
72+
7073
@override
7174
String get actionSheetOptionMuteTopic => 'Wycisz wątek';
7275

lib/generated/l10n/zulip_localizations_ru.dart

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

70+
@override
71+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
72+
7073
@override
7174
String get actionSheetOptionMuteTopic => 'Отключить тему';
7275

lib/generated/l10n/zulip_localizations_sk.dart

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

70+
@override
71+
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';
72+
7073
@override
7174
String get actionSheetOptionMuteTopic => 'Stlmiť tému';
7275

lib/widgets/action_sheet.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,57 @@ class ActionSheetCancelButton extends StatelessWidget {
163163
}
164164
}
165165

166+
/// Show a sheet of actions you can take on a channel.
167+
///
168+
/// Needs a [PageRoot] ancestor.
169+
void showChannelActionSheet(BuildContext context, {
170+
required int channelId,
171+
}) {
172+
final pageContext = PageRoot.contextOf(context);
173+
final store = PerAccountStoreWidget.of(pageContext);
174+
175+
final optionButtons = <ActionSheetMenuItemButton>[];
176+
final unreadCount = store.unreads.countInChannelNarrow(channelId);
177+
if (unreadCount > 0) {
178+
optionButtons.add(
179+
MarkChannelAsReadButton(pageContext: pageContext, channelId: channelId));
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 channel".
188+
return;
189+
}
190+
_showActionSheet(pageContext, optionButtons: optionButtons);
191+
}
192+
193+
class MarkChannelAsReadButton extends ActionSheetMenuItemButton {
194+
const MarkChannelAsReadButton({
195+
super.key,
196+
required this.channelId,
197+
required super.pageContext,
198+
});
199+
200+
final int channelId;
201+
202+
@override
203+
IconData get icon => ZulipIcons.message_checked;
204+
205+
@override
206+
String label(ZulipLocalizations zulipLocalizations) {
207+
return zulipLocalizations.actionSheetOptionMarkChannelAsRead;
208+
}
209+
210+
@override
211+
void onPressed() async {
212+
final narrow = ChannelNarrow(channelId);
213+
await ZulipAction.markNarrowAsRead(pageContext, narrow);
214+
}
215+
}
216+
166217
/// Show a sheet of actions you can take on a topic.
167218
///
168219
/// Needs a [PageRoot] ancestor.

lib/widgets/inbox.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ abstract class _HeaderItem extends StatelessWidget {
272272
// But that's in tension with the Figma, which gives these header rows
273273
// 40px min height.
274274
onTap: onCollapseButtonTap,
275+
onLongPress: this is _LongPressable
276+
? (this as _LongPressable).onLongPress
277+
: null,
275278
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
276279
Padding(padding: const EdgeInsets.all(10),
277280
child: Icon(size: 20, color: designVariables.sectionCollapseIcon,
@@ -425,7 +428,13 @@ class _DmItem extends StatelessWidget {
425428
}
426429
}
427430

428-
class _StreamHeaderItem extends _HeaderItem {
431+
mixin _LongPressable on _HeaderItem {
432+
// TODO(#1272) move to _HeaderItem base class
433+
// when DM headers become long-pressable; remove mixin
434+
Future<void> onLongPress();
435+
}
436+
437+
class _StreamHeaderItem extends _HeaderItem with _LongPressable {
429438
final Subscription subscription;
430439

431440
const _StreamHeaderItem({
@@ -458,6 +467,11 @@ class _StreamHeaderItem extends _HeaderItem {
458467
}
459468
}
460469
@override Future<void> onRowTap() => onCollapseButtonTap(); // TODO open channel narrow
470+
471+
@override
472+
Future<void> onLongPress() async {
473+
showChannelActionSheet(sectionContext, channelId: subscription.streamId);
474+
}
461475
}
462476

463477
class _StreamSection extends StatelessWidget {

lib/widgets/message_list.dart

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -367,36 +367,56 @@ class MessageListAppBarTitle extends StatelessWidget {
367367
case ChannelNarrow(:var streamId):
368368
final store = PerAccountStoreWidget.of(context);
369369
final stream = store.streams[streamId];
370-
return _buildStreamRow(context, stream: stream);
370+
final alignment = willCenterTitle
371+
? Alignment.center
372+
: AlignmentDirectional.centerStart;
373+
return Column(
374+
crossAxisAlignment: CrossAxisAlignment.stretch,
375+
children: [
376+
GestureDetector(
377+
behavior: HitTestBehavior.translucent,
378+
onLongPress: () {
379+
showChannelActionSheet(context, channelId: streamId);
380+
},
381+
child: Align(alignment: alignment,
382+
child: _buildStreamRow(context, stream: stream))),
383+
]);
371384

372385
case TopicNarrow(:var streamId, :var topic):
373386
final store = PerAccountStoreWidget.of(context);
374387
final stream = store.streams[streamId];
375-
return SizedBox(
376-
width: double.infinity,
377-
child: GestureDetector(
378-
behavior: HitTestBehavior.translucent,
379-
onLongPress: () {
380-
final someMessage = MessageListPage.ancestorOf(context)
381-
.model?.messages.firstOrNull;
382-
// If someMessage is null, the topic action sheet won't have a
383-
// resolve/unresolve button. That seems OK; in that case we're
384-
// either still fetching messages (and the user can reopen the
385-
// sheet after that finishes) or there aren't any messages to
386-
// act on anyway.
387-
assert(someMessage == null || narrow.containsMessage(someMessage));
388-
showTopicActionSheet(context,
389-
channelId: streamId,
390-
topic: topic,
391-
someMessageIdInTopic: someMessage?.id);
392-
},
393-
child: Column(
394-
crossAxisAlignment: willCenterTitle ? CrossAxisAlignment.center
395-
: CrossAxisAlignment.start,
396-
children: [
397-
_buildStreamRow(context, stream: stream),
398-
_buildTopicRow(context, stream: stream, topic: topic),
399-
])));
388+
final alignment = willCenterTitle
389+
? Alignment.center
390+
: AlignmentDirectional.centerStart;
391+
return Column(
392+
crossAxisAlignment: CrossAxisAlignment.stretch,
393+
children: [
394+
GestureDetector(
395+
behavior: HitTestBehavior.translucent,
396+
onLongPress: () {
397+
showChannelActionSheet(context, channelId: streamId);
398+
},
399+
child: Align(alignment: alignment,
400+
child: _buildStreamRow(context, stream: stream))),
401+
GestureDetector(
402+
behavior: HitTestBehavior.translucent,
403+
onLongPress: () {
404+
final someMessage = MessageListPage.ancestorOf(context)
405+
.model?.messages.firstOrNull;
406+
// If someMessage is null, the topic action sheet won't have a
407+
// resolve/unresolve button. That seems OK; in that case we're
408+
// either still fetching messages (and the user can reopen the
409+
// sheet after that finishes) or there aren't any messages to
410+
// act on anyway.
411+
assert(someMessage == null || narrow.containsMessage(someMessage));
412+
showTopicActionSheet(context,
413+
channelId: streamId,
414+
topic: topic,
415+
someMessageIdInTopic: someMessage?.id);
416+
},
417+
child: Align(alignment: alignment,
418+
child: _buildTopicRow(context, stream: stream, topic: topic))),
419+
]);
400420

401421
case DmNarrow(:var otherRecipientIds):
402422
final store = PerAccountStoreWidget.of(context);
@@ -1058,6 +1078,7 @@ class StreamMessageRecipientHeader extends StatelessWidget {
10581078
onTap: () => Navigator.push(context,
10591079
MessageListPage.buildRoute(context: context,
10601080
narrow: ChannelNarrow(message.streamId))),
1081+
onLongPress: () => showChannelActionSheet(context, channelId: message.streamId),
10611082
child: Row(
10621083
crossAxisAlignment: CrossAxisAlignment.center,
10631084
children: [

lib/widgets/subscription_list.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import '../api/model/model.dart';
44
import '../generated/l10n/zulip_localizations.dart';
55
import '../model/narrow.dart';
66
import '../model/unreads.dart';
7+
import 'action_sheet.dart';
78
import 'icons.dart';
89
import 'message_list.dart';
910
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, channelId: subscription.streamId),
233235
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
234236
const SizedBox(width: 16),
235237
Padding(

0 commit comments

Comments
 (0)