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

Conversation

chimnayajith
Copy link

Pull Request

Description

Adds a channel action sheet with a "Mark channel as read" option, accessible via long-press from both the inbox and subscription list pages. The action sheet is triggered by long-pressing on a channel header.

Related Issues

Screenshots

Additional Information

Currently the action sheet only appears in subscription list when the channel has unread messages, since the sheet doesn't have any default buttons.

@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Feb 4, 2025
Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

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

Thanks! Comments below.

@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch 3 times, most recently from 0a8811a to 8a4937e Compare February 5, 2025 09:10
@chimnayajith
Copy link
Author

@chrisbobbe I’ve addressed all the requested changes. Please take a look. Thank you!

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

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

Thanks for the revision! Comments below.

Comment on lines 336 to 340
@override
Future<void> onLongPress() async {
// TODO(#1272) action sheet for DM conversation
return;
}
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

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see; trying this myself, this is a little tricky. Try this change (squashed into the same commit):

diff --git lib/widgets/inbox.dart lib/widgets/inbox.dart
index 551c14bdd..8c8217b6e 100644
--- lib/widgets/inbox.dart
+++ lib/widgets/inbox.dart
@@ -255,7 +255,6 @@ abstract class _HeaderItem extends StatelessWidget {
   }
 
   Future<void> onRowTap();
-  Future<void> onLongPress();
 
   @override
   Widget build(BuildContext context) {
@@ -273,7 +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: onLongPress,
+        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,
@@ -332,12 +333,6 @@ 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;
-  }
 }
 
 class _AllDmsSection extends StatelessWidget {
@@ -439,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({

@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch 4 times, most recently from 06ed689 to 93d3199 Compare February 6, 2025 16:48
@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch 3 times, most recently from 5768d19 to da7af54 Compare February 7, 2025 19:00
Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

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

Thanks, this is getting closer!

One tip that would be helpful for me as I review: when you use GitHub's feature for marking review comments as resolved, it makes it harder for me to go back through each one and check the new revision against it, because I have to click "show resolved" on each one. When I'm responding to a review, I like to just put a 👍 on a comment after I've addressed it, which helps me keep track without causing that problem. 🙂

Also see my reply to a question above: #1317 (comment)

Comment on lines 35 to 86
Future<void> unregisterToken(GlobalStore globalStore, int accountId) async {
Future<void> unregisterToken(GlobalStore globalStore, int accountId) async {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: remove unwanted spacing change

}
}

final didPass = await updateMessageFlagsStartingFromAnchor(
Copy link
Collaborator

Choose a reason for hiding this comment

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

updateMessageFlagsStartingFromAnchor also gives UI feedback, so it also belongs in the new ZulipAction class.

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

Choose a reason for hiding this comment

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

I see; trying this myself, this is a little tricky. Try this change (squashed into the same commit):

diff --git lib/widgets/inbox.dart lib/widgets/inbox.dart
index 551c14bdd..8c8217b6e 100644
--- lib/widgets/inbox.dart
+++ lib/widgets/inbox.dart
@@ -255,7 +255,6 @@ abstract class _HeaderItem extends StatelessWidget {
   }
 
   Future<void> onRowTap();
-  Future<void> onLongPress();
 
   @override
   Widget build(BuildContext context) {
@@ -273,7 +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: onLongPress,
+        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,
@@ -332,12 +333,6 @@ 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;
-  }
 }
 
 class _AllDmsSection extends StatelessWidget {
@@ -439,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({

Comment on lines +210 to +212
@override
void onPressed() async {
final narrow = ChannelNarrow(streamId);
await ZulipAction.markNarrowAsRead(pageContext, narrow);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see; the reasoning for ZulipAction, in its dartdoc, makes sense.

I see that ZulipAction.markNarrowAsRead takes care of giving UI feedback, so I agree a try / catch isn't needed here.

It still doesn't give one specific kind of feedback I was hoping for, though 🙂:

#1317 (comment)

Let's have special handling for if e is ZulipApiException, like other buttons that make API requests

Could you make that adjustment, in a separate commit, for both values of useLegacy? For useLegacy false, that means adjusting updateMessageFlagsStartingFromAnchor, which is where the UI-feedback code is in that case.

Comment on lines 1094 to 1114
Future<void> prepare({UnreadMessagesSnapshot? unreadMsgs}) async {
final stream = eg.stream();
someChannel = stream;
addTearDown(testBinding.reset);

unreadMsgs ??= eg.unreadMsgs(channels: [
eg.unreadChannelMsgs(
streamId: stream.streamId,
topic: 'topic',
unreadMessageIds: [1],
),
]);

await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
realmUsers: [eg.selfUser, eg.otherUser],
streams: [someChannel],
subscriptions: [eg.subscription(someChannel)],
unreadMsgs: unreadMsgs));
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
connection = store.connection as FakeApiConnection;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please read through the indentation in this file and make it consistent with the project style; this part (and at least one other part) is currently hard to read.

Future<void> showFromInbox(WidgetTester tester) async {
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: const HomePage()));
await tester.pumpAndSettle();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we replace this with await tester.pump(); and remove the await tester.pump(); below? The 'topic action sheet' tests are doing fine that way.

Comment on lines 1128 to 1238
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: const SubscriptionListPageBody()));
await tester.pumpAndSettle();
check(find.byType(SubscriptionListPageBody)).findsOne();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: const SubscriptionListPageBody()));
await tester.pumpAndSettle();
check(find.byType(SubscriptionListPageBody)).findsOne();
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: const HomePage()));
await tester.pump();
await tester.tap(find.byIcon(ZulipIcons.hash_italic));
await tester.pump();
check(find.byType(SubscriptionListPageBody)).findsOne();

To be more representative of the real app.

await tester.pumpAndSettle();
check(find.byType(SubscriptionListPageBody)).findsOne();

await tester.pump();
Copy link
Collaborator

Choose a reason for hiding this comment

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

(I think this pump is unnecessary, as in showFromInbox.)

Comment on lines 1209 to 1210
connection.prepare(httpStatus: 400, json: {
'result': 'error', 'code': 'BAD_REQUEST', 'msg': ''});
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about, like in the topic action sheet test:

connection.prepare(exception: http.ClientException('Oops'));

which is shorter.

Comment on lines 1214 to 1215
await tester.pump(); //
await tester.pumpAndSettle(); // Wait for dialog animation
Copy link
Collaborator

Choose a reason for hiding this comment

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

The await tester.pump(); // looks unnecessary, comparing with the corresponding topic action sheet test.

@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch 3 times, most recently from 9b825ee to 2a5b01d Compare February 11, 2025 15:18
@chimnayajith
Copy link
Author

Thanks for the review @chrisbobbe .Pushed a new revision, PTAL.

I'm a bit unsure if my revision on #1317 (comment) is correct.

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

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

Thanks for the revision!

When trying out the UI on an iPhone, I noticed some more places that we might want to offer this action sheet from, and I started a CZO discussion: https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/channel.20action.20sheet/near/2092028

I believe your next revision (for my comments below) doesn't need to be blocked on waiting for a conclusion there.

@@ -20,6 +20,162 @@ import '../notifications/receive.dart';
import 'dialog.dart';
import 'store.dart';

/// High-level operations that combine API calls with UI feedback.
Copy link
Collaborator

Choose a reason for hiding this comment

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

actions [nfc]: Introduce ZulipAction for markNarrowAsRead & updateMessageFlagsStartingFromAnchor

commit-message nit: summary line looks too long (longer than 76 columns), and body text should be wrapped to 68 (you can configure your text editor to help with this).

Comment on lines 110 to 220
await updateMessageFlagsStartingFromAnchor(
await ZulipAction.updateMessageFlagsStartingFromAnchor(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah: since markNarrowAsUnreadFromMessage calls a ZulipAction method, that means it also gives UI feedback and should move to ZulipAction too.

Comment on lines 167 to 169
void showChannelActionSheet(BuildContext context, {
required int streamId,
}) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

One useful change was merged after you started working on this PR 🙂: please see the top of showTopicActionSheet in main, and use PageRoot.contextOf(context) the way we do there.

@@ -163,6 +163,58 @@ class ActionSheetCancelButton extends StatelessWidget {
}
}

/// Show a sheet of actions you can take on a channel.
void showChannelActionSheet(BuildContext context, {
required int streamId,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's use the modern term channelId instead of streamId. (There are still several places in the existing code that haven't migrated yet; we'll address those separately.)

Comment on lines +1332 to +1380
checkErrorDialog(tester,
expectedTitle: "Mark as read failed");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might as well also do a checkRequest here too.

@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch from 2a5b01d to e5af07a Compare February 15, 2025 10:31
@chimnayajith
Copy link
Author

@chrisbobbe Pushed the revision. Please take a look.

Also implemented opening the channel action sheet from the places mentioned in the CZO discussion.

@chrisbobbe
Copy link
Collaborator

I notice that the touch target in the app bar has gotten much smaller, which will be particularly problematic for short topic or stream names. Screenshots from an iPhone simulator, using "Select widget mode" in the "Flutter inspector" tab of the Flutter dev tools:

Before After, channel part After, topic part
image image image

Let's have the GestureDetectors occupy all the available horizontal space:

Proposed, channel part Proposed, topic part
image image

To do that, I suggest:

  • Remove the SizedBox with width: double.infinity`
  • Set crossAxisAlignment: CrossAxisAlignment.stretch for the Column
  • Wrap _buildStreamRow and _buildTopicRow each with an Align having the appropriate alignment. Can be a new local variable:
            final alignment = willCenterTitle
              ? Alignment.center
              : AlignmentDirectional.centerStart;

So like this:

      case TopicNarrow(:var streamId, :var topic):
        final store = PerAccountStoreWidget.of(context);
        final stream = store.streams[streamId];
        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)))]);

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

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

Thanks! Here's the rest of a review of this revision, and see also my comment above about touch targets: #1317 (comment)

Comment on lines 398 to 404
return GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () {
showChannelActionSheet(context, channelId: streamId);
},
child: _buildStreamRow(context, stream: stream),
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: no newline before final closing ).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also this needs test coverage.

Comment on lines 414 to 420
GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () {
showChannelActionSheet(context, channelId: streamId);
},
child: _buildStreamRow(context, stream: stream),
),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same comment about final closing ) and test coverage.

@@ -1083,6 +1093,7 @@ class StreamMessageRecipientHeader extends StatelessWidget {
onTap: () => Navigator.push(context,
MessageListPage.buildRoute(context: context,
narrow: ChannelNarrow(message.streamId))),
onLongPress: () => showChannelActionSheet(context, channelId: message.streamId),
Copy link
Collaborator

Choose a reason for hiding this comment

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

This needs test coverage.

Comment on lines 176 to 181
optionButtons.add(
MarkChannelAsReadButton(
streamId: channelId,
pageContext: pageContext,
),
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
optionButtons.add(
MarkChannelAsReadButton(
streamId: channelId,
pageContext: pageContext,
),
);
optionButtons.add(
MarkChannelAsReadButton(pageContext: pageContext, streamId: channelId));

(context first, and fewer lines of code)

@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch 2 times, most recently from fe82a57 to 85bd888 Compare February 19, 2025 15:32
@chimnayajith
Copy link
Author

@chrisbobbe Pushed the revision. Please take a look.

@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch 2 times, most recently from 04d8daa to 680f54f Compare February 19, 2025 16:38
@chrisbobbe
Copy link
Collaborator

It turns out that the ZulipAction class was merged separately as #1366; could you rebase past that please?

@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch from 680f54f to 391bc6e Compare February 20, 2025 00:01
@chimnayajith
Copy link
Author

I've rebased past it and pushed the updated changes. Let me know if anything else is needed!

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

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

Thanks! Small comments below.

final unreadCount = store.unreads.countInChannelNarrow(channelId);
if (unreadCount > 0) {
optionButtons.add(
MarkChannelAsReadButton(pageContext: pageContext,streamId: channelId));
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: space after comma

@@ -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),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I've noticed an existing bug with the gesture handling here. I filed it as #1368; no need to fix it in this PR (but feel free to claim the issue if you'd like to work on it as a followup).

Comment on lines 1250 to 1251
final effectiveChannel = channel ?? someChannel;
final effectiveMessages = messages ?? [someMessage];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually I think we can just do

Suggested change
final effectiveChannel = channel ?? someChannel;
final effectiveMessages = messages ?? [someMessage];
channel ??= someChannel;
messages ??= [someMessage];

(here and in other added tests)

See Greg's comment here: #1029 (comment)

checkButtons();
});

testWidgets('show channel action sheet from recipient header stream row', (tester) async {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
testWidgets('show channel action sheet from recipient header stream row', (tester) async {
testWidgets('show from recipient header', (tester) async {

I think this is clear enough from context, and is fewer words.

Comment on lines 1374 to 1379
// Prepare error response
connection.prepare(httpException: http.ClientException('Oops'));

// Tap and wait for dialog
await tester.tap(findButtonForLabel('Mark channel as read'));
await tester.pumpAndSettle(); // Wait for dialog animation
Copy link
Collaborator

Choose a reason for hiding this comment

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

The code comments don't really add anything that isn't already clear; let's remove them.

@chimnayajith chimnayajith force-pushed the 1226-mark-stream-as-read branch from 391bc6e to 622ac32 Compare February 21, 2025 17:52
@chimnayajith
Copy link
Author

@chrisbobbe Pushed a revision. PTAL!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maintainer review PR ready for review by Zulip maintainers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Offer "Mark channel as read" in channel action sheet
3 participants