Skip to content

Conversation

chrisbobbe
Copy link
Contributor

Some preparation for #3804.

Copy link
Member

@gnprice gnprice 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 direction makes sense to me. Some comments below -- notably I think we should be able to simplify the "wait until the store is hydrated" part. Which IIUC runs through these three commits:
844646a messagesActions: Don't let doNarrow cancel an expected navigation (2/2).
dfac22a AppEventHandlers: Make handleInitialShare wait for rehydration.
3a90d90 AppEventHandlers: Use handleInitialCare correctly.


componentDidMount() {
/**
* Do one-off tasks that require persisted data to have loaded.
Copy link
Member

Choose a reason for hiding this comment

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

So, remove the existing check for whether the Redux store is
hydrated.

But that's not *quite* enough: there's one case in the status quo in
which this check is a band-aid for a crash. In the status quo, if
the app is completely closed and you receive a notification, and tap
it, the app will open and we'll run some JavaScript
(`handleInitialNotification`) to send the user on their way to the
narrow corresponding to the notification. This code runs quite soon
after the app opens; in fact, it doesn't even wait to be sure the
data is rehydrated. It should wait; then, we can avoid the crash
without the band-aid.

Hmm dang -- did you observe this crash empirically?

The existing strategy we have that should prevent this kind of thing is, we use redux-action-buffer to cause all actions to be held up in a queue until a REHYDRATE action comes along. See listMiddleware in src/boot/store.js, and 42adfb3 where we started applying this to thunk actions.

More precisely, I believe all actions other than nav actions (because those are handled by a preceding middleware), and REHYDRATE actions themselves (because that's what we tell redux-action-buffer to look out for), will get enqueued that way. But in particular thunk actions will indeed get held in the queue, because that middleware comes after the createActionBuffer middleware.

I like that strategy fairly well, because it's relatively easy to stick to -- most of the interesting complex things that we do that interact with Redux, if they aren't on some component which isn't going to get shown before a rehydrate, are in a thunk action anyway, which means that for the most part when we don't think about it the right thing happens naturally. Occasionally we've had a bug where we get that wrong (I remember we've seen this at least once), but the fix is easy -- just push the relevant code inside a thunk action creator, and add a dispatch call at its call site -- and that's also nice.

Looking at handleInitialNotification:

export const handleInitialNotification = async (dispatch: Dispatch) => {
  const data = await getInitialNotification();
  dispatch(narrowToNotification(data));
};

it looks like the narrowing happens inside a dispatch, so it ought to be covered by that strategy. If you found empirically that something goes wrong, then let's debug.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm dang -- did you observe this crash empirically?

I did observe a crash while investigating my suspicion of a crash, but I'm not reproducing it right now; I'll try more tomorrow?

The existing strategy we have that should prevent this kind of thing is, we use redux-action-buffer to cause all actions to be held up in a queue until a REHYDRATE action comes along.

True, but I was specifically thinking to prepare for the future when navigation actions aren't dispatched through Redux and redux-action-buffer will have no power over them. But you're right; now I think more closely about it, being in a thunk action creator should do the trick (and it seems to in my testing just now).

Copy link
Contributor Author

@chrisbobbe chrisbobbe Oct 2, 2020

Choose a reason for hiding this comment

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

Hmm dang -- did you observe this crash empirically?

I did observe a crash while investigating my suspicion of a crash, but I'm not reproducing it right now; I'll try more tomorrow?

OK, I think I've solved this mystery—I was logged out when I observed the crash. If I set it up so that handleInitialNotification dispatches doNarrow unconditionally (I had it do so before await getInitialNotification()), it does crash if the user is logged out; I get the "no server data" message. No real surprise there.

But my test case case wasn't representative—narrowToNotification will handle checking to see that the account the notification is meant for is logged in, so there is no crash to worry about here.

Copy link
Member

Choose a reason for hiding this comment

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

Great -- glad that's diagnosed.

Comment on lines 33 to 37
// Since we've put this screen in a stack-nav route config, it gets the
// `navigation` prop for free.
navigation: NavigationStackProp<{| ...NavigationStateRoute, params: {| userId: number |} |}>,
Copy link
Member

Choose a reason for hiding this comment

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

The main example in the TypeScript doc is the following:

```
type Props = {
  navigation: NavigationStackProp<{ userId: string }>;
};
```

We find `NavigationStackProp` is a good thing to use, but
`{ userId: string }` is bad for a couple of reasons:

- It's off by a level of nesting; surely they mean to describe
  `navigation.state.params`. To do that, the type needs to look
  something like `{ params: { userId: string } }`.

- It leaves out all the other things on `navigation.state` including
  `key` and `routeName`, which might be nice to have someday.
  Experimentally, these are conveniently filled in by saying
  `NavigationStackProp<NavigationStateRoute>` [3].

Yeah, that's odd!

I wondered if this might be a difference between the TS definitions and the Flow libdef, in what they have NavigationStackProp take as a type parameter. But AFAICT it's equally wrong there -- the first type parameter is called State and is used for the state property, not state.params. (Though there is a second type parameter for the latter.)

Copy link
Contributor Author

@chrisbobbe chrisbobbe Oct 1, 2020

Choose a reason for hiding this comment

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

Yeah, it's kind of all over the place.

Comment on lines 15 to 16
// Since we've put this screen in a stack-nav route config, it gets the
// `navigation` prop for free.
Copy link
Member

Choose a reason for hiding this comment

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

IIUC the precise logic is: because the only way we invoke this component is as a screen in such a route config, it will always get that prop.

Or I guess, equally correct and broader: because that's the only non-type-checked way we invoke it.

Typically I wouldn't expect us to use one of these components in any other way. But as a way of helping confirm that, I did this quick search: rg -w '[A-Z]\w+Screen' src/ to find all references to things that fit the naming pattern common to almost all these components. Other than their own definitions, including react-redux connect calls, the only ones I found referred to anywhere other than AppNavigator were:

  • CompatibilityScreen
  • ChooseRecipientsScreen

And indeed those don't appear in this commit, so that checks out.

I repeated the search on the components that appear in this commit and don't fit that naming pattern: rg -we SmartUrlInput -e MainScreenWithTabs -e MessageReactionList src/

That actually found one reference that doesn't line up! Did you mean to include SmartUrlInput?

Copy link
Contributor Author

@chrisbobbe chrisbobbe Oct 1, 2020

Choose a reason for hiding this comment

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

Ah—SmartUrlInput should probably go in its own commit. It's a reusable component that expects...a navigation prop, to be passed down in the normal React way (i.e., it's not plugged into a navigator), and it shouldn't care whether that navigation prop comes from a stack navigator, drawer navigator, etc. I landed on NavigationScreenProp<NavigationState>, without much consideration, from reading react-navigation/react-navigation#3643 (comment), and it could be wrong.

Comment on lines +59 to +64
navigation: NavigationTabProp<{|
...NavigationStateRoute,
params: {| sharedData: SharedData |},
|}>,
Copy link
Member

Choose a reason for hiding this comment

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

Hmm -- for some reason it seems like this type annotation is not working (it's resulting in any types), whereas in at least some spot-checking the ones from the previous commit with NavigationStackProp are working.

Specifically, if I go and do something silly like

   state = (() => {
-    const { sharedData } = this.props.navigation.state.params;
+    const { sharedData, asdfasdf } = this.props.navigation.state.params;

then I don't get any type error at the attempt to pull an asdfasdf property from params.

Also, hovering on the identifier navigation in that line I just quoted shows "any(implicit)" as the type, and ditto for the subsequent state or params. (That's the symptom I first noticed; I tried the asdfasdf thing just to confirm that it wasn't some IDE glitch.)

Perhaps relatedly, if in VS Code I hit F12 on NavigationTabProp to try to go to its definition, it doesn't find it -- it can only take me to the import type line at the top of this file. Whereas it does work fine for NavigationStackProp in a file like AccountDetailsScreen.js. There does seem to be a libdef in flow-typed/, so not sure what the issue is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a hunch that I'll look into tomorrow—we've got two libdefs for react-navigation-tabs in flow-typed/npm, and one of them is a stub where everything is any.

Copy link
Contributor Author

@chrisbobbe chrisbobbe Oct 1, 2020

Choose a reason for hiding this comment

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

Aha...that's it. Yeah, getting rid of that stub libdef (which we don't want anyway; I don't have reasons to suspect the real one is irredeemably bad) reveals lots of Flow errors that seem productive to address.

Copy link
Member

@gnprice gnprice Oct 3, 2020

Choose a reason for hiding this comment

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

Aha...that's it. Yeah, getting rid of that stub libdef (which we don't want anyway; I don't have reasons to suspect the real one is irredeemably bad) reveals lots of Flow errors that seem productive to address.

Aha! Good catch.

chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 2, 2020
…2/2).

In the status quo, in all cases where `doNarrow` cancels a
navigation, it's a bug -- the user's attempt to navigate is being
silently thwarted, which causes confusion.

So, remove the existing check for whether the Redux store is
hydrated.

As Greg points out [1], this removal is fine because the act of
navigating (to a screen that will crash if the store is not
hydrated) happens in a thunk action (`doNarrow`) that will be held
up in the queue managed by the middleware from `redux-action-buffer`
until rehydration is complete.

We're confident that a thunk action will be buffered like this
because the thunk middleware comes after the `redux-action-buffer`
middleware in `listMiddleware`; see 42adfb3 for more detail on
that design.

The actual plain-object navigation action (created by `StackActions`
in navActions.js), if it were dispatched directly, may not be
buffered [2], because the `react-navigation-redux-helpers`
middleware comes before the `redux-action-buffer` middleware. But
we're safe as long as the code that dispatches it is inside
`doNarrow` or another thunk action creator.

[1] zulip#4273 (comment)

[2] This didn't stand up to some quick testing, but some caution
    wouldn't be harmful. In any case, when we eliminate the
    `react-navigation-redux-helpers` middleware and separate the
    navigation-action dispatch from Redux entirely,
    `redux-action-buffer` will have no control over when the
    navigation happens.
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 2, 2020
As an ordinary, reusable component that wraps something as
multipurpose as the `Input` component, `SmartUrlInput` is meant to
get the `navigation` prop in the normal React way. This is in
contrast to the many screen components that are invoked by passing
them to `create*Navigator` as part of the route config (and in no
other places), and that therefore get their `navigation` prop "for
free".

There's no need to make `SmartUrlInput` too picky about the shape of
the `navigation` prop it receives -- it shouldn't obviously care
that it's the kind of `navigation` prop that you get from a stack
navigator, as opposed to a drawer navigator, etc.

I've found a way to spell that --
`NavigationScreenProp<NavigationState>` -- and it seems to work [1].
In particular, it seems to work nicely with the pattern we'll take
in the next commit of annotating our "screen" components with the
`navigation` prop. So, in this commit, also adopt that pattern for
all the screens that currently use `SmartUrlInput`. Currently,
there's just one, `RealmScreen`.

[1] zulip#4273 (comment)
@chrisbobbe chrisbobbe force-pushed the pr-prep-decouple-nav branch from 05be167 to c2de8bc Compare October 2, 2020 20:52
@chrisbobbe
Copy link
Contributor Author

Thanks for the review! Just pushed some changes and responded to your comments above.

type Props = $ReadOnly<{|
navigation: NavigationScreenProp<{
params: ?{|
params: {|
Copy link
Contributor Author

Choose a reason for hiding this comment

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

e192254 RealmScreen: Assume navigation.state.params will exist.

Oops! This commit is wrong; please don't merge until I've pushed a fix. I forgot about reaching RealmScreen through navReducer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, done.

chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 3, 2020
As an ordinary, reusable component that wraps something as
multipurpose as the `Input` component, `SmartUrlInput` is meant to
get the `navigation` prop in the normal React way. This is in
contrast to the many screen components that are invoked by passing
them to `create*Navigator` as part of the route config (and in no
other places), and that therefore get their `navigation` prop "for
free".

There's no need to make `SmartUrlInput` too picky about the shape of
the `navigation` prop it receives -- it shouldn't obviously care
that it's the kind of `navigation` prop that you get from a stack
navigator, as opposed to a drawer navigator, etc.

I've found a way to spell that --
`NavigationScreenProp<NavigationState>` -- and it seems to work [1].
In particular, it seems to work nicely with the pattern we'll take
in the next commit of annotating our "screen" components with the
`navigation` prop. So, in this commit, also adopt that pattern for
all the screens that currently use `SmartUrlInput`. Currently,
there's just one, `RealmScreen`.

[1] zulip#4273 (comment)
@chrisbobbe chrisbobbe force-pushed the pr-prep-decouple-nav branch from c2de8bc to c2b927e Compare October 3, 2020 00:37
Comment on lines 15 to 17
* As a thunk action creator, this is expected to be buffered by
* `redux-action-buffer` so that it doesn't navigate before the store
* has rehydrated.
Copy link
Member

Choose a reason for hiding this comment

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

One quibble remaining on this commit: this bit of comment doesn't really belong here. In a couple of ways, I guess:

  • It doesn't say anything that's part of the interface, something that's actionable from the perspective of when you're using this function. So it doesn't belong in the jsdoc, though it could as an implementation comment.

  • Even as an implementation comment, the state of affairs it's describing is really common to almost all our thunk actions creators, not distinctive to this one.

    In seeing it the reader naturally wonders, wait, why is this a thing we have to worry about here in particular? What's happening here that brings it up? The answer is really just that there used to be a getIsHydrated check here -- now that there no longer is, there's nothing in particular to say about it at this spot. But that means this remark is really naturally about the change that removed that check, not about the code after that change, so it belongs just in the commit message.

Or from another angle: this commit is basically completing a cleanup that ideally I'd have done right after 42adfb3 as part of the same series. In order to really clean up completely, this code should end up with no lasting complications from that wrinkle at all.

Comment on lines +59 to +64
navigation: NavigationTabProp<{|
...NavigationStateRoute,
params: {| sharedData: SharedData |},
|}>,
Copy link
Member

@gnprice gnprice Oct 3, 2020

Choose a reason for hiding this comment

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

Aha...that's it. Yeah, getting rid of that stub libdef (which we don't want anyway; I don't have reasons to suspect the real one is irredeemably bad) reveals lots of Flow errors that seem productive to address.

Aha! Good catch.

Copy link
Member

@gnprice gnprice 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 revisions!

This all LGTM. A few comments below (and above, thanks GitHub) on small things; please merge at will after going through those.

type Props = $ReadOnly<{|
navigation: NavigationScreenProp<{
params: ?{|
params?: {|
Copy link
Member

Choose a reason for hiding this comment

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

Why optional at all? I think the logic in your previous version (at 6f6a3ee) was sound for why it shouldn't simply go missing; and following that logic we make params non-optional on other screens too.

Copy link
Member

Choose a reason for hiding this comment

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

Ah I see, you answered that here 🙂 : #4273 (comment)

I think this calls for a comment saying why it's optional, in any case -- since that is out of the ordinary compared to how all, I think, of our other screens work.

... Then I went reading this code some more to see how it handles the case where params is missing. OK, effectively realm gets defaulted to '', but initial? It seems to default to false. So when is it ever true -- is that just dead code? ... No, it's not. The answer is this hack in navReducer.js:

export const getStateForRoute = (route: string): NavigationState => {
  // TODO: this is kind of a hack!  Refactor to a better way.
  //  * Perhaps pass `initial: true` unconditionally in this initialization
  //    code, to all routes?  Then that'd just be part of the interface of
  //    making a screen work as an initial screen.
  //  * Alternatively, we could replace the whole system of `ModalNavBar`,
  //    `canGoBack`, etc., with our own "navigation view" that would be more
  //    directly integrated into the navigation framework:
  //      https://reactnavigation.org/docs/en/2.x/navigation-views.html
  const params = route === 'realm' ? { initial: true } : undefined;

So in fact in navReducer when we navigate to this screen, we also pass a params object (which I guess means we always do.) It has a property initial: true, and no params.

I guess now this extra definitely calls for a comment here 😛 Which really I should have added at the same time as adding that hack: 7a04a56

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I've already got plans to get rid of that hack soon, as part of the process :)

*/
defaultDomain: string,
navigation: NavigationScreenProp<mixed>,
navigation: NavigationScreenProp<NavigationState>,
Copy link
Member

Choose a reason for hiding this comment

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

Not a big deal, but I suspect you can make this change after the "Tell all screens on AppNavigator" commit (and include the RealmScreen change in that one.) If you do, then I think the story to tell in this one's commit message becomes simpler, because no foreshadowing is necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I get an error if I leave this as mixed with the RealmScreen change applied:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/start/RealmScreen.js:121:10

Cannot create SmartUrlInput element because in property navigation:
 • Either cannot create exact type from mixed [1].
 • Or property getParam is missing in object type [2] but exists in NavigationScreenProp [3].

        src/start/RealmScreen.js
        118│         shouldShowLoadingBanner={false}
        119│       >
        120│         <Label text="Enter your Zulip server URL:" />
        121│         <SmartUrlInput
        122│           style={styles.input}
        123│           navigation={navigation}
        124│           defaultProtocol="https://"
        125│           defaultOrganization="your-org"
        126│           defaultDomain="zulipchat.com"
        127│           defaultValue={initialRealm}
        128│           onChangeText={this.handleRealmChange}
        129│           onSubmitEditing={this.tryRealm}
        130│           enablesReturnKeyAutomatically
        131│         />
        132│         {error !== null ? (
        133│           <ErrorMsg error={error} />
        134│         ) : (

        flow-typed/npm/react-navigation-stack_v1.x.x.js
    [2] 595│     & {
        596│         pop: (n?: number, params?: {| immediate?: boolean |}) => boolean,
        597│         popToTop: (params?: {| immediate?: boolean |}) => boolean,
        598│         push: (
        599│           routeName: string,
        600│           params?: NavigationParams,
        601│           action?: NavigationNavigateAction
        602│         ) => boolean,
        603│         replace: (
        604│           routeName: string,
        605│           params?: NavigationParams,
        606│           action?: NavigationNavigateAction
        607│         ) => boolean,
        608│         reset: (actions: NavigationAction[], index: number) => boolean,
        609│         ...
        610│       };

        src/common/SmartUrlInput.js
 [3][1]  52│   navigation: NavigationScreenProp<mixed>,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I could do a temporary $FlowFixMe, but I wonder if that would mean roughly the same amount of explanation that's already in this commit?

Copy link
Member

Choose a reason for hiding this comment

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

Huh. That's a bit puzzling to me: it feels like it should be perfectly fine, and also when I look at the actual type definitions it seems like it should be accepted. In particular, the new type NavigationScreenProp<NavigationState> should be a subtype of the old type NavigationScreenProp<mixed> -- NavigationState is a subtype of mixed because everything is, and NavigationScreenProp is declared in the react-navigation libdef as covariant (with a +) in its type parameter:

  declare export type NavigationScreenProp<+S> = {

so S a subtype of T should make NavigationScreenProp<S> a subtype of NavigationScreenProp<T>.

But not worth trying to debug further, since you've already made a fine workaround.

chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 5, 2020
…2/2).

In the status quo, in all cases where `doNarrow` cancels a
navigation, it's a bug -- the user's attempt to navigate is being
silently thwarted, which causes confusion.

So, remove the existing check for whether the Redux store is
hydrated.

As Greg points out [1], this removal is fine because the act of
navigating (to a screen that will crash if the store is not
hydrated) happens in a thunk action (`doNarrow`) that will be held
up in the queue managed by the middleware from `redux-action-buffer`
until rehydration is complete.

We're confident that a thunk action will be buffered like this
because the thunk middleware comes after the `redux-action-buffer`
middleware in `listMiddleware`; see 42adfb3 for more detail on
that design.

The actual plain-object navigation action (created by `StackActions`
in navActions.js), if it were dispatched directly, may not be
buffered [2], because the `react-navigation-redux-helpers`
middleware comes before the `redux-action-buffer` middleware. But
we're safe as long as the code that dispatches it is inside
`doNarrow` or another thunk action creator.

[1] zulip#4273 (comment)

[2] This didn't stand up to some quick testing, but some caution
    wouldn't be harmful. In any case, when we eliminate the
    `react-navigation-redux-helpers` middleware and separate the
    navigation-action dispatch from Redux entirely,
    `redux-action-buffer` will have no control over when the
    navigation happens.
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 5, 2020
As an ordinary, reusable component that wraps something as
multipurpose as the `Input` component, `SmartUrlInput` is meant to
get the `navigation` prop in the normal React way. This is in
contrast to the many screen components that are invoked by passing
them to `create*Navigator` as part of the route config (and in no
other places), and that therefore get their `navigation` prop "for
free".

There's no need to make `SmartUrlInput` too picky about the shape of
the `navigation` prop it receives -- it shouldn't obviously care
that it's the kind of `navigation` prop that you get from a stack
navigator, as opposed to a drawer navigator, etc.

I've found a way to spell that --
`NavigationScreenProp<NavigationState>` -- and it seems to work [1].
In particular, it seems to work nicely with the pattern we'll take
in the next commit of annotating our "screen" components with the
`navigation` prop. So, in this commit, also adopt that pattern for
all the screens that currently use `SmartUrlInput`. Currently,
there's just one, `RealmScreen`.

[1] zulip#4273 (comment)
@chrisbobbe chrisbobbe force-pushed the pr-prep-decouple-nav branch from c2b927e to beba5ca Compare October 5, 2020 22:31
@chrisbobbe
Copy link
Contributor Author

chrisbobbe commented Oct 5, 2020

Not a big deal, but I suspect you can make this change after the "Tell all screens on AppNavigator" commit (and include the RealmScreen change in that one.) If you do, then I think the story to tell in this one's commit message becomes simpler, because no foreshadowing is necessary.

Since you said it's not a big deal, I'll go ahead as-is, if that's OK; see the error in #4273 (comment) that I got when I tried the suggested adjustment.

I've addressed everything else, so I'll go ahead and merge.

We use the default export from this file (calling it `tabsOptions`
in all cases`) for both `createBottomTabNavigator` and
`createMaterialTopTabNavigator`.

Both of these two options were actually removed from
`createBottomTabNavigator`'s config in react-navigation v2 [1], with
much controversy [2]. So neither of them has been doing anything for
`MainTabs`, our only use of `createBottomTabNavigator`, for quite a
while. When we start using a proper libdef in an upcoming commit, we
would get errors about passing these two options to
`createBottomTabNavigator`.

The `swipeEnabled` option remains available for
`createMaterialTopTabNavigator`. We have three uses of
`createMaterialTopTabNavigator`, and they all have `swipeEnabled`
set to `true` (we did this in ad0b5f3) so we don't even have to
think about what the default value is, and this is NFC for those.

`animationEnabled` isn't documented for
`createMaterialTopTabNavigator`, so its removal should also be NFC.
It gets a mention in the v3 doc [3] but not the v4 doc [4].

[1] https://reactnavigation.org/blog/2018/05/07/react-navigation-2.0#tab-navigator-split-into-separate-components
[2] react-navigation/react-navigation#4146
[3] https://reactnavigation.org/docs/3.x/material-top-tab-navigator
[4] https://reactnavigation.org/docs/4.x/material-top-tab-navigator
There are differences in the config options between
`createBottomTabNavigator` and `createMaterialTopTabNavigator`,
which Flow will soon start telling us about, when we start using an
actual libdef.

So, be specific about which options we give which type of navigator.

Testing on iOS and Android shows that this doesn't obviously changed
anything. It shouldn't, anyway, because the status quo is that we've
been handing `createBottomTabNavigator` some options that (if the
docs are to be trusted) it has no idea how to handle, and plausibly
just drops on the floor.

And in particular, the removal of the zulip#2065 workaround
(`borderWidth: 0` and `elevation: 0`, targeting material top tabs)
from the bottom-tab-nav config doesn't seem to give any adverse
effects.
And leave in place the actual, meaningful libdef, so that that one
will prevail.

Now, we get proper type-checking for our calls of
`createMaterialTopTabNavigator` and `createBottomTabNavigator`.

This means a return of the kind of `$FlowFixMe`s [1] we removed in
0d2066f, at a time when there wasn't a suitable libdef for
react-navigation-tabs.

[1] Some thoughts on this were written in a36814e, but see
    zulip#4114 (comment)
    for more recent concerns about typing our component classes with
    a static `navigationOptions` property.

    I'm inclined to not revisit these suppressions until we're on
    React Navigation v5, which has quite a different,
    component-based API, and a particular guide for type-checking
    the route config and the screen components in a unified way;
    that's at https://reactnavigation.org/docs/typescript/.
… so.

And remove DO_NARROW.

We greatly reduced the responsibility of the DO_NARROW action in
55361d8, leaving only this site in our reducers.

This last remaining site in `fetchingReducer` resembles a
garbage-collection mechanism, to clear out the `fetching` state so
it can't grow to be too big. But, as Greg says,

"""
Yeah -- I think that isn't really even essential to have at all. The
`fetching` state is a tiny bit of metadata for each narrow, and
whenever we have it we'll have much more data at the same narrow in
`state.narrows`.

The logic there also doesn't totally make sense to me: when we
navigate to a new narrow, we forget all about the fetching state of
other narrows? Basically if that ever results in dropping an entry
where one of the values was `true`, then that seems like a bug to
me. Possibly latent and not visible to the user.
"""
…1/2).

As Greg points out in discussion [1], it's not great for `doNarrow`
to silently not navigate when the user expects to navigate.

In the case of an invalid narrow, allow the navigation, but make
sure `ChatScreen` gracefully handles it by showing an error state to
the user. That view naturally looks a lot like the `FetchError` view
added in 353cd3a for showing an error in fetching data.

A few tests that have an appropriately narrow focus to confirm the
now-absent behavior of `doNarrow` (checking that the narrow is
valid) are now wrong, so, remove them. A natural question is whether
these tests were giving us more coverage than we already have in
`isNarrowValid`'s tests, and it appears that they're not. In
particular, the two removed tests seem to map neatly onto two of
`isNarrowValid`'s tests, as follows:

(in messageActions-test.js -> in narrowsSelectors-test.js)

'if new narrow stream is not valid, do nothing' ->
  'narrowing to a non-existing stream is invalid'

'if new narrow user is deactivated, do nothing' ->
  'narrowing to a PM with non-existing user is not valid'

[1] https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/decouple.20nav.20from.20redux.20%28.23M3804%29/near/1027854
…2/2).

In the status quo, in all cases where `doNarrow` cancels a
navigation, it's a bug -- the user's attempt to navigate is being
silently thwarted, which causes confusion.

So, remove the existing check for whether the Redux store is
hydrated.

As Greg points out [1], this removal is fine because the act of
navigating (to a screen that will crash if the store is not
hydrated) happens in a thunk action (`doNarrow`) that will be held
up in the queue managed by the middleware from `redux-action-buffer`
until rehydration is complete.

We're confident that a thunk action will be buffered like this
because the thunk middleware comes after the `redux-action-buffer`
middleware in `listMiddleware`; see 42adfb3 for more detail on
that design.

The actual plain-object navigation action (created by `StackActions`
in navActions.js), if it were dispatched directly, may not be
buffered [2], because the `react-navigation-redux-helpers`
middleware comes before the `redux-action-buffer` middleware. But
we're safe as long as the code that dispatches it is inside
`doNarrow` or another thunk action creator.

[1] zulip#4273 (comment)

[2] This didn't stand up to some quick testing, but some caution
    wouldn't be harmful. In any case, when we eliminate the
    `react-navigation-redux-helpers` middleware and separate the
    navigation-action dispatch from Redux entirely,
    `redux-action-buffer` will have no control over when the
    navigation happens.
Like `handleInitialNotification`, `handleInitialShare` isn't a
special thunk action creator to be passed to `dispatch`; rather,
it's a plain action that takes `dispatch` as its only argument. So,
use it that way.
… work.

`createSelector` from `reselect` can be useful when we're dealing
with a lot more data than we are here, e.g., users, messages, etc.
Make this action creator's name match the type of action it creates,
`ACCOUNT_SWITCH`. Good for searchability.
I'm not aware that `params` will ever be null. If we thought it
might be null, we would use the more transparent `| null`.

Also, add a TODO to stop using a hack; we plan to do this later in
the process for zulip#3804.
As an ordinary, reusable component that wraps something as
multipurpose as the `Input` component, `SmartUrlInput` is meant to
get the `navigation` prop in the normal React way. This is in
contrast to the many screen components that are invoked by passing
them to `create*Navigator` as part of the route config (and in no
other places), and that therefore get their `navigation` prop "for
free".

There's no need to make `SmartUrlInput` too picky about the shape of
the `navigation` prop it receives -- it shouldn't obviously care
that it's the kind of `navigation` prop that you get from a stack
navigator, as opposed to a drawer navigator, etc.

I've found a way to spell that --
`NavigationScreenProp<NavigationState>` -- and it seems to work [1].
In particular, it seems to work nicely with the pattern we'll take
in the next commit of annotating our "screen" components with the
`navigation` prop. So, in this commit, also adopt that pattern for
all the screens that currently use `SmartUrlInput`. Currently,
there's just one, `RealmScreen`.

[1] zulip#4273 (comment)
…prop.

This has been done on an as-needed basis in the past, but in a few
different ways, and it would be good to be consistent.

With zulip#3804 approaching, it makes sense to proactively furnish all
these components with the knowledge that they have the `navigation`
prop if they ever need it.

In doing so, we're making an unchecked assumption: that the
components will in fact be passed the `navigation` prop, in the
shape we say it will take. It's unchecked because of the
`$FlowFixMe` from aaaf847 on the route config passed to
`createStackNavigator` [0].

-----

There is a doc for doing this in TypeScript [1], but it's clearly
wrong about how to handle it in Flow (and I believe it's also wrong
about TypeScript in ways that would matter to us if we were using
TypeScript).

In particular, `NavigationStackScreenProps` is marketed (under "Type
checking all props for a screen") as something you can spread in
your `Props` type to get `theme` and `screenProps` along with
`navigation`. Sounds great.

But it doesn't exist in the Flow libdef, and I haven't found a clear
alternative. In zulip#4127, a demo PR branch that informed 17f73d8 --
in particular,

f482827 [FOR DISCUSSION] One way to get `sharedData` into
  ShareToStream, ShareToPm

-- I seemed [2] to have favored something called
`NavigationNavigatorProps` for this purpose. But it has at least two
points against it:

1. I don't see documentation that we're supposed to use it, or how.
2. It's not tailored (or conveniently tailorable) to screens that
   get put on a particular type of navigator (stack, drawer, tabs,
   etc.)

So, be conservative and just type the `navigation` prop, and in a
way that says we know it comes from a stack navigator.

The main example in the TypeScript doc is the following:

```
type Props = {
  navigation: NavigationStackProp<{ userId: string }>;
};
```

We find `NavigationStackProp` is a good thing to use [3], and it
exists in the Flow libdef, but `{ userId: string }` is bad for a
couple of reasons:

- It's off by a level of nesting; surely they mean to describe
  `navigation.state.params`. To do that, the type needs to look
  something like `{ params: { userId: string } }`. This is also the
  case in the TypeScript.

- It leaves out all the other things on `navigation.state` including
  `key` and `routeName`, which might be nice to have someday.
  Experimentally, these are conveniently filled in by saying
  `NavigationStackProp<NavigationStateRoute>` [4].

So, account for those deficiencies straightforwardly.

[0] Initial thoughts on this were written in a36814e, but see
    zulip#4114 (comment)
    for more recent concerns about typing our component classes with
    a static `navigationOptions` property.

    I'm inclined to not revisit these suppressions until we're on
    React Navigation v5, which has quite a different,
    component-based API, and a particular guide for type-checking
    the route config and the screen components in a unified way;
    that's at https://reactnavigation.org/docs/typescript/.

[1] https://reactnavigation.org/docs/4.x/typescript/

[2] Possibly following a train of thought from
    https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/react-navigation.20types/near/878262.

[3] In one or two places, we've been using `NavigationScreenProp` --
    the doc for upgrading from v3 (which we did in f3b6c1f) says
    to replace that with `NavigationStackProp` for stack navigators.
    This didn't catch my eye at the time because it's under the
    "TypeScript" heading and we don't use TypeScript. That doc is at
    https://reactnavigation.org/docs/4.x/upgrading-from-3.x/#typescript.

[4] Like at
    react-navigation/react-navigation#3643 (comment),
    but with `NavigationStackProp` instead of
    `NavigationScreenProp`.
Just like in the previous commit, but here, we include the knowledge
that `navigation` comes from a tab navigator rather than a stack
navigator.
@chrisbobbe chrisbobbe force-pushed the pr-prep-decouple-nav branch from beba5ca to 90f23b7 Compare October 5, 2020 22:48
@chrisbobbe chrisbobbe merged commit 90f23b7 into zulip:master Oct 5, 2020
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 6, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 13, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 13, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 13, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 13, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 16, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 20, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
chrisbobbe added a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 21, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
gnprice pushed a commit to chrisbobbe/zulip-mobile that referenced this pull request Oct 22, 2020
In the previous commit, we removed our last call site of
`getStateWithRoute` where 'realm' was passed for `route`. Therefore,
this conditional can disappear.

See more about this hack at
zulip#4273 (comment).
@chrisbobbe chrisbobbe deleted the pr-prep-decouple-nav branch November 5, 2021 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants