Skip to content

Commit

Permalink
action_sheet: Add channel action sheet with mark as read option
Browse files Browse the repository at this point in the history
Fixes: zulip#1226
  • Loading branch information
chimnayajith committed Feb 19, 2025
1 parent dfe5949 commit fe82a57
Show file tree
Hide file tree
Showing 14 changed files with 331 additions and 27 deletions.
4 changes: 4 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
"@permissionsDeniedReadExternalStorage": {
"description": "Message for dialog asking the user to grant permissions for external storage read access."
},
"actionSheetOptionMarkChannelAsRead": "Mark channel as read",
"@actionSheetOptionMarkChannelAsRead": {
"description": "Label for marking a channel as read."
},
"actionSheetOptionMuteTopic": "Mute topic",
"@actionSheetOptionMuteTopic": {
"description": "Label for muting a topic on action sheet."
Expand Down
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ abstract class ZulipLocalizations {
/// **'To upload files, please grant Zulip additional permissions in Settings.'**
String get permissionsDeniedReadExternalStorage;

/// Label for marking a channel as read.
///
/// In en, this message translates to:
/// **'Mark channel as read'**
String get actionSheetOptionMarkChannelAsRead;

/// Label for muting a topic on action sheet.
///
/// In en, this message translates to:
Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';

@override
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';

@override
String get actionSheetOptionMuteTopic => 'Mute topic';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';

@override
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';

@override
String get actionSheetOptionMuteTopic => 'Mute topic';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';

@override
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';

@override
String get actionSheetOptionMuteTopic => 'Mute topic';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_nb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';

@override
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';

@override
String get actionSheetOptionMuteTopic => 'Mute topic';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get permissionsDeniedReadExternalStorage => 'Aby odebrać pliki Zulip musi uzyskać dodatkowe uprawnienia w Ustawieniach.';

@override
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';

@override
String get actionSheetOptionMuteTopic => 'Wycisz wątek';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get permissionsDeniedReadExternalStorage => 'Для загрузки файлов, пожалуйста, предоставьте Zulip дополнительные разрешения в настройках.';

@override
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';

@override
String get actionSheetOptionMuteTopic => 'Отключить тему';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_sk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get permissionsDeniedReadExternalStorage => 'To upload files, please grant Zulip additional permissions in Settings.';

@override
String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read';

@override
String get actionSheetOptionMuteTopic => 'Stlmiť tému';

Expand Down
49 changes: 49 additions & 0 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,55 @@ class ActionSheetCancelButton extends StatelessWidget {
}
}

/// Show a sheet of actions you can take on a channel.
void showChannelActionSheet(BuildContext context, {
required int channelId,
}) {
final pageContext = PageRoot.contextOf(context);
final store = PerAccountStoreWidget.of(pageContext);

final optionButtons = <ActionSheetMenuItemButton>[];
final unreadCount = store.unreads.countInChannelNarrow(channelId);
if (unreadCount > 0) {
optionButtons.add(
MarkChannelAsReadButton(pageContext: pageContext,streamId: channelId));
}
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
// though it offers a gesture interaction that it doesn't meaningfully
// offer, which is confusing. The solution here is probably to remove this
// is-empty case by having at least one button that's always present,
// such as "copy link to channel".
return;
}
_showActionSheet(pageContext, optionButtons: optionButtons);
}

class MarkChannelAsReadButton extends ActionSheetMenuItemButton {
const MarkChannelAsReadButton({
super.key,
required this.streamId,
required super.pageContext
});

final int streamId;

@override
IconData get icon => ZulipIcons.message_checked;

@override
String label(ZulipLocalizations zulipLocalizations) {
return zulipLocalizations.actionSheetOptionMarkChannelAsRead;
}

@override
void onPressed() async {
final narrow = ChannelNarrow(streamId);
await ZulipAction.markNarrowAsRead(pageContext, narrow);
}
}

/// Show a sheet of actions you can take on a topic.
///
/// Needs a [PageRoot] ancestor.
Expand Down
16 changes: 15 additions & 1 deletion lib/widgets/inbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ abstract class _HeaderItem extends StatelessWidget {
// But that's in tension with the Figma, which gives these header rows
// 40px min height.
onTap: onCollapseButtonTap,
onLongPress: this is _LongPressable
? (this as _LongPressable).onLongPress
: null,
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Padding(padding: const EdgeInsets.all(10),
child: Icon(size: 20, color: designVariables.sectionCollapseIcon,
Expand Down Expand Up @@ -431,7 +434,13 @@ class _DmItem extends StatelessWidget {
}
}

class _StreamHeaderItem extends _HeaderItem {
mixin _LongPressable on _HeaderItem {
// TODO(#1272) move to _HeaderItem base class
// when DM headers become long-pressable; remove mixin
Future<void> onLongPress();
}

class _StreamHeaderItem extends _HeaderItem with _LongPressable {
final Subscription subscription;

const _StreamHeaderItem({
Expand Down Expand Up @@ -464,6 +473,11 @@ class _StreamHeaderItem extends _HeaderItem {
}
}
@override Future<void> onRowTap() => onCollapseButtonTap(); // TODO open channel narrow

@override
Future<void> onLongPress() async {
showChannelActionSheet(sectionContext, channelId: subscription.streamId);
}
}

class _StreamSection extends StatelessWidget {
Expand Down
64 changes: 38 additions & 26 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -395,36 +395,47 @@ class MessageListAppBarTitle extends StatelessWidget {
case ChannelNarrow(:var streamId):
final store = PerAccountStoreWidget.of(context);
final stream = store.streams[streamId];
return _buildStreamRow(context, stream: stream);
return GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () {
showChannelActionSheet(context, channelId: streamId);
},
child: _buildStreamRow(context, stream: stream));

case TopicNarrow(:var streamId, :var topic):
final store = PerAccountStoreWidget.of(context);
final stream = store.streams[streamId];
return SizedBox(
width: double.infinity,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () {
final someMessage = MessageListPage.ancestorOf(context)
.model?.messages.firstOrNull;
// If someMessage is null, the topic action sheet won't have a
// resolve/unresolve button. That seems OK; in that case we're
// either still fetching messages (and the user can reopen the
// sheet after that finishes) or there aren't any messages to
// act on anyway.
assert(someMessage == null || narrow.containsMessage(someMessage));
showTopicActionSheet(context,
channelId: streamId,
topic: topic,
someMessageIdInTopic: someMessage?.id);
},
child: Column(
crossAxisAlignment: willCenterTitle ? CrossAxisAlignment.center
: CrossAxisAlignment.start,
children: [
_buildStreamRow(context, stream: stream),
_buildTopicRow(context, stream: stream, topic: topic),
])));
final alignment = willCenterTitle
? Alignment.center
: AlignmentDirectional.centerStart;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () {
showChannelActionSheet(context, channelId: streamId);
},
child: Align(alignment: alignment,
child: _buildStreamRow(context, stream: stream))),
GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () {
final someMessage = MessageListPage.ancestorOf(context)
.model?.messages.firstOrNull;
// If someMessage is null, the topic action sheet won't have a
// resolve/unresolve button. That seems OK; in that case we're
// either still fetching messages (and the user can reopen the
// sheet after that finishes) or there aren't any messages to
// act on anyway.
assert(someMessage == null || narrow.containsMessage(someMessage));
showTopicActionSheet(context,
channelId: streamId,
topic: topic,
someMessageIdInTopic: someMessage?.id);
},
child: Align(alignment: alignment,
child: _buildTopicRow(context, stream: stream, topic: topic)))]);

case DmNarrow(:var otherRecipientIds):
final store = PerAccountStoreWidget.of(context);
Expand Down Expand Up @@ -1083,6 +1094,7 @@ class StreamMessageRecipientHeader extends StatelessWidget {
onTap: () => Navigator.push(context,
MessageListPage.buildRoute(context: context,
narrow: ChannelNarrow(message.streamId))),
onLongPress: () => showChannelActionSheet(context, channelId: message.streamId),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expand Down
2 changes: 2 additions & 0 deletions lib/widgets/subscription_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '../api/model/model.dart';
import '../generated/l10n/zulip_localizations.dart';
import '../model/narrow.dart';
import '../model/unreads.dart';
import 'action_sheet.dart';
import 'icons.dart';
import 'message_list.dart';
import 'store.dart';
Expand Down Expand Up @@ -230,6 +231,7 @@ class SubscriptionItem extends StatelessWidget {
MessageListPage.buildRoute(context: context,
narrow: ChannelNarrow(subscription.streamId)));
},
onLongPress: () => showChannelActionSheet(context, channelId: subscription.streamId),
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
const SizedBox(width: 16),
Padding(
Expand Down
Loading

0 comments on commit fe82a57

Please sign in to comment.