Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

action_sheet: Offer "Mark channel as read" in channel action sheet #1317

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/icons/ZulipIcons.ttf
Binary file not shown.
4 changes: 4 additions & 0 deletions assets/icons/message_checked.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
52 changes: 52 additions & 0 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,58 @@ class ActionSheetCancelButton extends StatelessWidget {
}
}

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

final optionButtons = <ActionSheetMenuItemButton>[];
final unreadCount = store.unreads.countInChannelNarrow(streamId);
if (unreadCount > 0) {
optionButtons.add(
MarkChannelAsReadButton(
streamId: streamId,
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
// 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(context, 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.
void showTopicActionSheet(BuildContext context, {
required int channelId,
Expand Down
99 changes: 52 additions & 47 deletions lib/widgets/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,57 @@ import '../notifications/receive.dart';
import 'dialog.dart';
import 'store.dart';

/// High-level operations that combine API calls with UI feedback.
///
/// Methods in this class provide UI feedback while performing API operations.
abstract final class ZulipAction {
static Future<void> markNarrowAsRead(BuildContext context, Narrow narrow) async {
final store = PerAccountStoreWidget.of(context);
final connection = store.connection;
final zulipLocalizations = ZulipLocalizations.of(context);
final useLegacy = connection.zulipFeatureLevel! < 155; // TODO(server-6)
if (useLegacy) {
try {
await _legacyMarkNarrowAsRead(context, narrow);
return;
} catch (e) {
if (!context.mounted) return;
showErrorDialog(context: context,
title: zulipLocalizations.errorMarkAsReadFailedTitle,
message: e.toString()); // TODO(#741): extract user-facing message better
return;
}
}

final didPass = await updateMessageFlagsStartingFromAnchor(
context: context,
// Include `is:unread` in the narrow. That has a database index, so
// this can be an important optimization in narrows with a lot of history.
// The server applies the same optimization within the (deprecated)
// specialized endpoints for marking messages as read; see
// `do_mark_stream_messages_as_read` in `zulip:zerver/actions/message_flags.py`.
apiNarrow: narrow.apiEncode()..add(ApiNarrowIs(IsOperand.unread)),
// Use [AnchorCode.oldest], because [AnchorCode.firstUnread]
// will be the oldest non-muted unread message, which would
// result in muted unreads older than the first unread not
// being processed.
anchor: AnchorCode.oldest,
// [AnchorCode.oldest] is an anchor ID lower than any valid
// message ID.
includeAnchor: false,
op: UpdateMessageFlagsOp.add,
flag: MessageFlag.read,
onCompletedMessage: zulipLocalizations.markAsReadComplete,
progressMessage: zulipLocalizations.markAsReadInProgress,
onFailedTitle: zulipLocalizations.errorMarkAsReadFailedTitle);

if (!didPass || !context.mounted) return;
if (narrow is CombinedFeedNarrow) {
PerAccountStoreWidget.of(context).unreads.handleAllMessagesReadSuccess();
}
}
}

Future<void> logOutAccount(BuildContext context, int accountId) async {
final globalStore = GlobalStoreWidget.of(context);

Expand All @@ -32,7 +83,7 @@ Future<void> logOutAccount(BuildContext context, int accountId) async {
await globalStore.removeAccount(accountId);
}

Future<void> unregisterToken(GlobalStore globalStore, int accountId) async {
Future<void> unregisterToken(GlobalStore globalStore, int accountId) async {
final account = globalStore.getAccount(accountId);
if (account == null) return; // TODO(log)

Expand All @@ -53,52 +104,6 @@ Future<void> unregisterToken(GlobalStore globalStore, int accountId) async {
}
}

Future<void> markNarrowAsRead(BuildContext context, Narrow narrow) async {
final store = PerAccountStoreWidget.of(context);
final connection = store.connection;
final zulipLocalizations = ZulipLocalizations.of(context);
final useLegacy = connection.zulipFeatureLevel! < 155; // TODO(server-6)
if (useLegacy) {
try {
await _legacyMarkNarrowAsRead(context, narrow);
return;
} catch (e) {
if (!context.mounted) return;
showErrorDialog(context: context,
title: zulipLocalizations.errorMarkAsReadFailedTitle,
message: e.toString()); // TODO(#741): extract user-facing message better
return;
}
}

final didPass = await updateMessageFlagsStartingFromAnchor(
context: context,
// Include `is:unread` in the narrow. That has a database index, so
// this can be an important optimization in narrows with a lot of history.
// The server applies the same optimization within the (deprecated)
// specialized endpoints for marking messages as read; see
// `do_mark_stream_messages_as_read` in `zulip:zerver/actions/message_flags.py`.
apiNarrow: narrow.apiEncode()..add(ApiNarrowIs(IsOperand.unread)),
// Use [AnchorCode.oldest], because [AnchorCode.firstUnread]
// will be the oldest non-muted unread message, which would
// result in muted unreads older than the first unread not
// being processed.
anchor: AnchorCode.oldest,
// [AnchorCode.oldest] is an anchor ID lower than any valid
// message ID.
includeAnchor: false,
op: UpdateMessageFlagsOp.add,
flag: MessageFlag.read,
onCompletedMessage: zulipLocalizations.markAsReadComplete,
progressMessage: zulipLocalizations.markAsReadInProgress,
onFailedTitle: zulipLocalizations.errorMarkAsReadFailedTitle);

if (!didPass || !context.mounted) return;
if (narrow is CombinedFeedNarrow) {
PerAccountStoreWidget.of(context).unreads.handleAllMessagesReadSuccess();
}
}

Future<void> markNarrowAsUnreadFromMessage(
BuildContext context,
Message message,
Expand Down
29 changes: 16 additions & 13 deletions lib/widgets/icons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,44 +93,47 @@ abstract final class ZulipIcons {
/// The Zulip custom icon "menu".
static const IconData menu = IconData(0xf117, fontFamily: "Zulip Icons");

/// The Zulip custom icon "message_checked".
static const IconData message_checked = IconData(0xf118, fontFamily: "Zulip Icons");
chimnayajith marked this conversation as resolved.
Show resolved Hide resolved

/// The Zulip custom icon "message_feed".
static const IconData message_feed = IconData(0xf118, fontFamily: "Zulip Icons");
static const IconData message_feed = IconData(0xf119, fontFamily: "Zulip Icons");

/// The Zulip custom icon "mute".
static const IconData mute = IconData(0xf119, fontFamily: "Zulip Icons");
static const IconData mute = IconData(0xf11a, fontFamily: "Zulip Icons");

/// The Zulip custom icon "read_receipts".
static const IconData read_receipts = IconData(0xf11a, fontFamily: "Zulip Icons");
static const IconData read_receipts = IconData(0xf11b, fontFamily: "Zulip Icons");

/// The Zulip custom icon "send".
static const IconData send = IconData(0xf11b, fontFamily: "Zulip Icons");
static const IconData send = IconData(0xf11c, fontFamily: "Zulip Icons");

/// The Zulip custom icon "share".
static const IconData share = IconData(0xf11c, fontFamily: "Zulip Icons");
static const IconData share = IconData(0xf11d, fontFamily: "Zulip Icons");

/// The Zulip custom icon "share_ios".
static const IconData share_ios = IconData(0xf11d, fontFamily: "Zulip Icons");
static const IconData share_ios = IconData(0xf11e, fontFamily: "Zulip Icons");

/// The Zulip custom icon "smile".
static const IconData smile = IconData(0xf11e, fontFamily: "Zulip Icons");
static const IconData smile = IconData(0xf11f, fontFamily: "Zulip Icons");

/// The Zulip custom icon "star".
static const IconData star = IconData(0xf11f, fontFamily: "Zulip Icons");
static const IconData star = IconData(0xf120, fontFamily: "Zulip Icons");

/// The Zulip custom icon "star_filled".
static const IconData star_filled = IconData(0xf120, fontFamily: "Zulip Icons");
static const IconData star_filled = IconData(0xf121, fontFamily: "Zulip Icons");

/// The Zulip custom icon "three_person".
static const IconData three_person = IconData(0xf121, fontFamily: "Zulip Icons");
static const IconData three_person = IconData(0xf122, fontFamily: "Zulip Icons");

/// The Zulip custom icon "topic".
static const IconData topic = IconData(0xf122, fontFamily: "Zulip Icons");
static const IconData topic = IconData(0xf123, fontFamily: "Zulip Icons");

/// The Zulip custom icon "unmute".
static const IconData unmute = IconData(0xf123, fontFamily: "Zulip Icons");
static const IconData unmute = IconData(0xf124, fontFamily: "Zulip Icons");

/// The Zulip custom icon "user".
static const IconData user = IconData(0xf124, fontFamily: "Zulip Icons");
static const IconData user = IconData(0xf125, fontFamily: "Zulip Icons");

// END GENERATED ICON DATA
}
Expand Down
13 changes: 13 additions & 0 deletions lib/widgets/inbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ abstract class _HeaderItem extends StatelessWidget {
}

Future<void> onRowTap();
Future<void> onLongPress();

@override
Widget build(BuildContext context) {
Expand All @@ -272,6 +273,7 @@ abstract class _HeaderItem extends StatelessWidget {
// But that's in tension with the Figma, which gives these header rows
// 40px min height.
onTap: onCollapseButtonTap,
onLongPress: onLongPress,
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Padding(padding: const EdgeInsets.all(10),
child: Icon(size: 20, color: designVariables.sectionCollapseIcon,
Expand Down Expand Up @@ -330,6 +332,12 @@ class _AllDmsHeaderItem extends _HeaderItem {
pageState.allDmsCollapsed = !collapsed;
}
@override Future<void> onRowTap() => onCollapseButtonTap(); // TODO open all-DMs narrow?

@override
Future<void> onLongPress() async {
// TODO(#1272) action sheet for DM conversation
return;
}
Comment on lines +336 to +340
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump on #1317 (comment) ; let me know if it doesn't make sense.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't figure this out. All the other changes have been pushed.

Continuing from the discussion in CZO added a commit 6791e61

}

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

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

class _StreamSection extends StatelessWidget {
Expand Down
2 changes: 1 addition & 1 deletion lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,7 @@ class _MarkAsReadWidgetState extends State<MarkAsReadWidget> {
void _handlePress(BuildContext context) async {
if (!context.mounted) return;
setState(() => _loading = true);
await markNarrowAsRead(context, widget.narrow);
await ZulipAction.markNarrowAsRead(context, widget.narrow);
setState(() => _loading = false);
}

Expand Down
Loading