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

Clarify action sheet's "page context"; fix small bugs #1044

Merged
merged 10 commits into from
Nov 7, 2024

Conversation

gnprice
Copy link
Member

@gnprice gnprice commented Nov 5, 2024

This was prompted by my comment at #1041 (comment) (/cc @PIG208). But then as I got into this code I found some other small cleanups to make, and also spotted a couple of bugs to fix.

Selected commit messages

action_sheet [nfc]: Rename pageContext field on button widgets

Most of the ways we use this field (specifically, all the ways we
use it other than in findMessageListPage) aren't related to it
being the message list in particular.

When we add action sheets other than the message action sheet,
they'll still need a page context. So prepare the way for those
by giving this a name that can generalize.


action_sheet [nfc]: Avoid passing footgun context to onPressed methods

The usual Flutter idiom, which we use across the rest of the app,
would call for using the parameter called context wherever a
BuildContext is required: for getting the store or localizations,
or passing to a method like showErrorDialog, and so on.

That idiom doesn't work here, because that context belongs to the
action sheet itself, which gets dismissed at the start of the method.
Worse, it will sometimes seem to work, nondeterministically: the
context only gets unmounted after the animation to dismiss the action
sheet is complete, which takes something like 200ms. So the
temptation to use it has been a recurring source of small bugs.

Fix the problem by not passing the action sheet's context to these
methods at all.


action_sheet: Mark as unread in current narrow, vs. from when action sheet opened

The narrow of a given MessageListPage can change over time,
in particular if viewing a topic that gets moved.

If it does, the mark-unread action should follow along. Otherwise
we'd have a frustrating race where mark-unread just didn't work
if, for example, someone happened to resolve the topic while you
were in the middle of trying to mark as unread.

Fixes: #1045

@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Nov 5, 2024
@PIG208 PIG208 self-assigned this Nov 5, 2024
Copy link
Member

@PIG208 PIG208 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 PR! Left some small comments. Otherwise this looks good to me.

@@ -315,8 +324,7 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
// This will be null only if the compose box disappeared after the
// message action sheet opened, and before "Quote and reply" was pressed.
// Currently a compose box can't ever disappear, so this is impossible.
ComposeBoxController composeBoxController =
MessageListPage.ancestorOf(messageListContext).composeBoxController!;
var composeBoxController = findMessageListPage().composeBoxController!;
Copy link
Member

Choose a reason for hiding this comment

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

Is var used for fitting this in one line?

Copy link
Member Author

Choose a reason for hiding this comment

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

More that specifying the type feels redundant. It'd look like this:

Well, the alternative is:

    ComposeBoxController composeBoxController =
      findMessageListPage().composeBoxController!;

(or the same with findMessageListPage() moved before the newline).

Our usual practice is to say final and let the type be inferred. Here it's not final because it gets updated below; so var is the choice that corresponds to that.

/// don't call this in a build method.
MessageListPageState findMessageListPage() {
assert(messageListContext.mounted,
"findMessageListPage should be called only when messageListContext is known to still be mounted");
Copy link
Member

Choose a reason for hiding this comment

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

nit: let's use '' for consistency

anchor: message.id,
foundNewest: true,
foundOldest: true,
foundAnchor: true,
Copy link
Member

Choose a reason for hiding this comment

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

Does the test rely on foundAnchor: true or anchor: message.id? If not, eg.newestResult suffices.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, newestResult would be an improvement. (Though it doesn't currently live in eg; maybe it should, with a longer name in that case.)

Adding a separate prep commit to switch to that.

..method.equals('POST')
..url.path.equals('/api/v1/messages/flags/narrow')
..bodyFields['narrow'].equals(
jsonEncode(TopicNarrow(newStream.streamId, newTopic).apiEncode()));
Copy link
Member

Choose a reason for hiding this comment

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

nit: indentation

Copy link
Member Author

Choose a reason for hiding this comment

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

This looks right to me. Do you mean indenting it like this?

          ..bodyFields['narrow'].equals(
            jsonEncode(TopicNarrow(newStream.streamId, newTopic).apiEncode()));

That'd make bodyFields and jsonEncode appear to be at the same level, which isn't desired.

FWIW dartfmt agrees with my indentation (though it moves jsonEncode( to before the newline). I checked with a quick Ctrl+Alt+L after selecting the surrounding few lines.

const topic = 'old topic';
final message = eg.streamMessage(flags: [MessageFlag.read],
stream: stream, topic: topic);
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
Copy link
Member

Choose a reason for hiding this comment

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

nit: it might be good to wrap this line because narrow is particularly interesting to this test

stream: stream, topic: topic);
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
// Get the action sheet fully deployed while the old narrow applies.
await tester.pumpAndSettle();
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, the test passes and would fail as expected without the fix, if I remove this. I see that setupToMessageActionSheet calls tester.pump with a 250ms delay. Maybe that's sufficient?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll expand the comment. Does this version explain why this call is here?

        // Get the action sheet fully deployed while the old narrow applies.
        // (This way we maximize the range of potential bugs this test can catch,
        // by giving the code maximum opportunity to latch onto the old topic.)
        await tester.pumpAndSettle();

// it doesn't matter anyway: [MessageStoreImpl.reconcileMessages] will
// keep the version updated by the event. If that somehow changes in
// some future refactor, it'll cause this test to fail.
connection.prepare(json: _getMessagesResult(message).toJson());
Copy link
Member

Choose a reason for hiding this comment

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

For tests where we need to prepare the new messages, I typically used eg.updateMessageEventMoveTo with the message that has the new channel and topic, but I can see that we are in favor of eg.updateMessageEventMoveFrom here because in this context — moving away from the current narrow — the "move from" variant feels a bit more natural to use.

@gnprice
Copy link
Member Author

gnprice commented Nov 7, 2024

Thanks @PIG208 for the detailed review! Pushed a revision, and replied to some subthreads above; PTAL.

@PIG208
Copy link
Member

PIG208 commented Nov 7, 2024

LGTM. Thanks for the update!

@gnprice
Copy link
Member Author

gnprice commented Nov 7, 2024

Thanks! Merging.

Most of the ways we use this field (specifically, all the ways we
use it other than in findMessageListPage) aren't related to it
being the message list in particular.

When we add action sheets other than the message action sheet,
they'll still need a page context.  So prepare the way for those
by giving this a name that can generalize.
And add a bit of docs clarifying the expected practice.
…sheet

We closed issue zulip#24 because it was apparently resolved by an OS
update.  There's still a milder issue; mention that instead.
The usual Flutter idiom, which we use across the rest of the app,
would call for using the parameter called `context` wherever a
BuildContext is required: for getting the store or localizations,
or passing to a method like showErrorDialog, and so on.

That idiom doesn't work here, because that context belongs to the
action sheet itself, which gets dismissed at the start of the method.
Worse, it will sometimes seem to work, nondeterministically: the
context only gets unmounted after the animation to dismiss the action
sheet is complete, which takes something like 200ms.  So the
temptation to use it has been a recurring source of small bugs.

Fix the problem by not passing the action sheet's context to these
methods at all.
…sheet opened

The narrow of a given MessageListPage can change over time,
in particular if viewing a topic that gets moved.

If it does, the mark-unread action should follow along.  Otherwise
we'd have a frustrating race where mark-unread just didn't work
if, for example, someone happened to resolve the topic while you
were in the middle of trying to mark as unread.

Fixes: zulip#1045
This is a nice small bonus from renaming the parameter that was
`messageListContext` to the shorter name `pageContext`.

Some of these don't quite fit in 80 columns.  But the details of
the parameter list are boring (because they're all the same),
so it's OK that some of them go a little over.
These tests are all already relying on the detail that the
setupToMessageActionSheet helper uses `eg.selfAccount` for the
account.  Better to encapsulate that detail, and have them rely
on it giving the test cases a reference to the store directly.
Their callers always give them the shared store prepared by the
setup helper setupToMessageActionSheet.  Have these helpers just
use that directly.

If in the future we end up having a use case for customizing this
behavior, it'll probably be most convenient to have them accept
a connection instead of a store, anyway.
@gnprice gnprice merged commit cdce570 into zulip:main Nov 7, 2024
1 check passed
@gnprice gnprice deleted the pr-action-sheet branch November 7, 2024 17:52
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.

Mark-unread can fail if topic was concurrently moved
2 participants