Clean up navigation logic (4/x): Tidy up our use of ZulipStatusBar a bit more.#4468
Clean up navigation logic (4/x): Tidy up our use of ZulipStatusBar a bit more.#4468chrisbobbe merged 17 commits intozulip:masterfrom
ZulipStatusBar a bit more.#4468Conversation
I made these two commits before I quite figured out what was going on with the |
0052b73 to
0992609
Compare
| type Props = $ReadOnly<{| | ||
| navigation: AppNavigationProp<'chat'>, | ||
| route: RouteProp<'chat', {| narrow: Narrow |}>, | ||
| route: RouteProp<'chat', {| narrow: Narrow, editMessage: EditMessage | null |}>, |
There was a problem hiding this comment.
ChatScreen: Store `editMessage` as a nav param, not React state.
The fact that `ChatScreen`'s header changes with this bit of state
is a clue that it really wants to be a route param. When we start
using the `header` option [1] for the header, instead of smushing
the header in with the rest of the screen's content, it'll be
convenient to grab `editMessage` from the route params.
[1] https://reactnavigation.org/docs/stack-navigator/#header
We could aim to reduce the contents of editMessage, probably to just the ID of the message being edited, along the lines of what React Navigation recommends should go in these params.
I think the biggest drawback to storing the whole original content and topic equally applies in the status quo (i.e., where editMessage is stored in React state): we should be getting model data from react-redux, in a way that ensures that the UI live-updates. So, perhaps a fix can be done as a followup, or we can add a TODO. (Er, or actually it might be quite easy to do in this PR; I've just assumed it might be a bit complicated because of how tricky it was to work out what editMessage does in the first place.)
The article gives a couple of other good reasons, but some of them (like gracefully handling deep-linking) don't yet really apply to us. But I think their guidance that "You can also think of the route object like a URL" makes sense. For this screen, I'm thinking of the analogy of a URL that has something like ?msg-edit-id=12345.
There was a problem hiding this comment.
In my draft that moves ModalSearchNavBar to the header, the search term is stored in the nav state. This seems fine; I'm thinking of the analogy of a URL that has something like ?q=foo.
Also, from the doc about what should go in params:
Some examples of what should be in params are:
- [...]
- Params for sorting, filtering data etc. when you have a list of items, e.g. navigation.navigate('Feeds', { sortBy: 'latest' })
There was a problem hiding this comment.
In my draft that moves
ModalSearchNavBarto the header
While at first I thought we might not want to keep the search bar in the header/nav bar, I think it's actually a fine place to put it, at least on Android.
From an Android doc:
The key functions of the app bar are as follows:
- [...]
- Access to important actions in a predictable way, such as search.
There was a problem hiding this comment.
We could aim to reduce the contents of
editMessage, probably to just the ID of the message being edited
I think the thing that makes that tricky is mainly just the message content. If you look at where setEditMessage gets called (after getting renamed to startEditMessage along the way >_> -- this whole edit-message path is not a model of good software design), we have to fetch the raw Markdown content from the server at that point:
const { raw_content } = await api.getRawMessageContent(auth, message.id);
startEditMessage({
id: message.id,
content: raw_content,
topic: message.subject,
});We don't have any cause to keep that information beyond the scope of one edit-message operation, so I think it is best kept as local state of one form or another. If we want the message ID in the nav state, I think the least awkward thing may be for the content and topic to go there along with it, as this PR currently does.
There was a problem hiding this comment.
While at first I thought we might not want to keep the search bar in the header/nav bar, I think it's actually a fine place to put it, at least on Android.
Yep, this is pretty classic in Material Design and on Android.
It's not really that foreign on iOS either, I think. Open up Safari, or Photos, and start searching -- the search box is at the top in basically an equivalent of the app bar.
There was a problem hiding this comment.
I think the thing that makes that tricky is mainly just the message content.
Ahh—right, you're right. I saw that but forgot about it when commenting.
| /** Only call this via `doNarrow`. See there for details. */ | ||
| export const navigateToChat = (narrow: Narrow): GenericNavigationAction => | ||
| StackActions.push('chat', { narrow }); | ||
| StackActions.push('chat', { narrow, editMessage: null }); |
There was a problem hiding this comment.
editMessage: null could go here, or it could go in AppNavigator where we define the screen (doc):
<Stack.Screen name="chat" component={ChatScreen} initialParams={{ editMessage: null }} />
There was a problem hiding this comment.
I think I prefer it being here rather than on the navigator. Ideally it really belongs with the component definition itself -- it's basically a default value for an optional parameter -- and this feels a bit closer to the component definition than the navigator does.
0992609 to
14a481e
Compare
|
Just rebased and resolved some conflicts. |
gnprice
left a comment
There was a problem hiding this comment.
Thanks @chrisbobbe ! Sorry for the delayed review.
Some comments below. Generally this looks good except I'm not convinced the final commit is a helpful direction. (And a couple of the prep commits may not be needed if we leave that one out.)
Specifically: this way of specifying the header seems rather more complicated than just invoking something like ChatNavBar at the top of the component's own rendered tree. Does this buy us something that's worth the complexity?
I'm looking back at #4443 (comment) , and I see this at the endpoint:
Each screen component on a stack navigator is responsible for keeping important content out of the right, bottom, and left insets. It doesn't have to worry about the top inset because its header does that, as mentioned above.
I think that means we'll still want to be handling right, bottom, and left insets somewhere generic like Screen for the sake of screens that don't have particular other needs, as well as handling all four sides in screen-specific ways where needed.
Given that, I don't see that we necessarily benefit from handling one of the four sides in a different way. If anything, it's probably better to handle all four together, or close by each other -- it helps in spotting gaps and understanding how things fit together.
... For example. Looking at Screen, it strikes me that we handle the bottom, and then the top (via Modal(Search)?NavBar), but don't handle the left and right. That seems like it's probably the cause of this symptom: #3066 (comment) (Assuming that symptom is still present -- my test iPhone XR is out of battery, just started it charging so I can confirm.) I guess not that exact symptom because those screenshots are from MainTabsScreen, but I'd predict from that code that we have that symptom when Screen is involved too.
| * `StatusBar` renders `null` every time. Therefore, don't look to | ||
| * `ZulipStatusBar`'s position in the hierarchy of `View`s to affect | ||
| * the layout in any way. | ||
| * | ||
| * That being said, hiding and un-hiding the status bar (by | ||
| * interacting with the native API via RN's `StatusBar`) can change | ||
| * the size of the top inset. E.g., on an iPhone without the "notch", | ||
| * the top inset grows to accommodate a visible status bar, and | ||
| * shrinks to give more room to the app's content when the status bar | ||
| * is hidden. |
There was a problem hiding this comment.
Hmm, good point!
As written, this is a bit hard to follow because it's in terms of the underlying StatusBar. That happens to be part of the implementation of this component, but this jsdoc should describe the interface, so it should be understandable without consulting the implementation.
Quite possibly the interface should now be in part something like "This renders to an RN StatusBar with appropriate props, and nothing else." That wasn't always how this component worked, but I think probably that was the wrong design and this one (which you introduced in a previous PR in this series) is a better one. If the jsdoc says that, then I think that provides the needed background for this material to make sense too.
| const [editMessage, setEditMessage] = React.useState<EditMessage | null>(null); | ||
| const { editMessage } = route.params; | ||
| const setEditMessage = (value: EditMessage | null) => | ||
| navigation.setParams({ editMessage: value }); |
| /** Only call this via `doNarrow`. See there for details. */ | ||
| export const navigateToChat = (narrow: Narrow): GenericNavigationAction => | ||
| StackActions.push('chat', { narrow }); | ||
| StackActions.push('chat', { narrow, editMessage: null }); |
There was a problem hiding this comment.
I think I prefer it being here rather than on the navigator. Ideally it really belongs with the component definition itself -- it's basically a default value for an optional parameter -- and this feels a bit closer to the component definition than the navigator does.
| type Props = $ReadOnly<{| | ||
| navigation: AppNavigationProp<'chat'>, | ||
| route: RouteProp<'chat', {| narrow: Narrow |}>, | ||
| route: RouteProp<'chat', {| narrow: Narrow, editMessage: EditMessage | null |}>, |
There was a problem hiding this comment.
We could aim to reduce the contents of
editMessage, probably to just the ID of the message being edited
I think the thing that makes that tricky is mainly just the message content. If you look at where setEditMessage gets called (after getting renamed to startEditMessage along the way >_> -- this whole edit-message path is not a model of good software design), we have to fetch the raw Markdown content from the server at that point:
const { raw_content } = await api.getRawMessageContent(auth, message.id);
startEditMessage({
id: message.id,
content: raw_content,
topic: message.subject,
});We don't have any cause to keep that information beyond the scope of one edit-message operation, so I think it is best kept as local state of one form or another. If we want the message ID in the nav state, I think the least awkward thing may be for the content and topic to go there along with it, as this PR currently does.
| type Props = $ReadOnly<{| | ||
| navigation: AppNavigationProp<'chat'>, | ||
| route: RouteProp<'chat', {| narrow: Narrow |}>, | ||
| route: RouteProp<'chat', {| narrow: Narrow, editMessage: EditMessage | null |}>, |
There was a problem hiding this comment.
While at first I thought we might not want to keep the search bar in the header/nav bar, I think it's actually a fine place to put it, at least on Android.
Yep, this is pretty classic in Material Design and on Android.
It's not really that foreign on iOS either, I think. Open up Safari, or Photos, and start searching -- the search box is at the top in basically an equivalent of the app bar.
| // How much the top of `KeyboardAvoidingView`'s layout *parent* | ||
| // is displaced downward from the top of the screen. | ||
| // | ||
| // If this isn't set correctly, the keyboard will hide some of | ||
| // the bottom of the screen (an area whose height is what this | ||
| // value should have been set to). | ||
| // | ||
| // I think `KeyboardAvoidingView`'s implementation mistakes `x` | ||
| // and `y` from `View#onLayout` to be a `View`'s position | ||
| // relative to the top left of the screen. In reality, I'm | ||
| // pretty sure they represent a `View`'s position relative to | ||
| // its parent: | ||
| // https://github.com/facebook/react-native-website/issues/2056#issuecomment-773618381 | ||
| // | ||
| // But at least `KeyboardAvoidingView` exposes this prop, which | ||
| // we can use to balance the equation. |
There was a problem hiding this comment.
This looks like a helpful explanation of what this prop means, for reference when using this component. I.e., it's part of explaining this component's interface.
To help make that clear and make it discoverable for that purpose, let's put it where the prop appears in the Props type definition, and format it as jsdoc. When it's tucked inside the render method like this, it looks like it's an implementation comment, and would be easy to miss when scanning this component for interface documentation.
There was a problem hiding this comment.
it looks like it's an implementation comment
It sort of is an implementation comment, in that it explains a weirdness in some code that it calls as part of doing its job, right? If for some strange reason (😬) that weirdness were common (or easily obtained) knowledge, the component's jsdoc might do OK to just say "passes some props straight through to KeyboardAvoidingView", I suppose? I guess if it were well-known, the implementation comment wouldn't be needed either, hmm.
I agree that this is easy to miss where it currently is, so it'll be smart to propagate it to the jsdoc where it'll be surer to be found.
There was a problem hiding this comment.
Yeah, as written it does both. But this contains some key information that the reader who's simply using this component will need to know -- so that information is part of the interface and belongs in the jsdoc.
I think one way to write this would be:
- Take the first two paragraphs and put them in jsdoc. Also s/KeyboardAvoidingView/KeyboardAvoider/ there.
- I think that covers all the information needed for simply using this component.
- Then leave the remaining paragraphs formatted as an implementation comment -- they're really information that's not needed for using the component, but is there to explain how the implementation works for the sake of someone modifying or debugging it.
If for some strange reason () that weirdness were common (or easily obtained) knowledge, the component's jsdoc might do OK to just say "passes some props straight through to
KeyboardAvoidingView", I suppose? I guess if it were well-known, the implementation comment wouldn't be needed either, hmm.
Yep and yep 🙂
In other circumstances, where we're just passing some props through but they are reasonably well documented (or at any rate we don't have any thoughts to add to what's in their docs), it can indeed make sense for our interface to just say that:
zulip-mobile/src/common/Input.js
Lines 28 to 42 in 0735ff1
| const { editMessage, narrow } = params; | ||
| return <ChatNavBar editMessage={editMessage} narrow={narrow} />; |
There was a problem hiding this comment.
Hmm -- it seems like a bit of a step backward for the details of this definition to appear here at the navigator, rather than next to the ChatScreen component. In particular it makes it rather indirect to be looking at ChatScreen and work out where the header is coming from.
I think one pretty reasonable solution would be to put this code over in ChatScreen.js, either as a separately exported function or perhaps better as a property on ChatScreen. Like ChatScreen.header = (params: RouteParamsOf<typeof ChatScreen>) => { … };.
Even then, though, that still seems more complicated than just invoking ChatNavBar at the top of the component's own rendered tree. Does this buy us something that's worth the complexity?
(further discussion in main thread)
I know you've got a lot of stuff on your list. 🙂 Thanks for the review! It's useful to see your skepticism about React Navigation's magic here; certainly they don't get everything right. I think we agree right away that React Navigation's default header wouldn't work out of the box, but it might be helpful to see it, for some context, if you haven't already. On default.react.nav.header.mp4On iOS, the animation looks consistent with The default header doesn't work for a few reasons (here's an incomplete list, and this is just from looking on iOS):
Still, these and other issues should be fixable by customizing the header: using various options like Ah, looking back, I agree that my comments about handling safe-area insets don't read as an argument for setting the header React Navigation's way. I think I'd assumed that we'd be interested in doing so (with So, I owe some points in favor of picking that 🙂:
I'd understand if these considerations don't convince you that the trouble (including the trouble with Flow I mentioned in the description) is worth it, but I wanted to put them out there to help inform the decision, since I didn't do so before. 🙂 |
|
Thanks for all those details! Replied in chat. |
14a481e to
1353198
Compare
header for ChatNavBar.ZulipStatusBar a bit more.
|
Thanks for the review! Revision pushed, with the |
gnprice
left a comment
There was a problem hiding this comment.
Thanks @chrisbobbe ! This looks great -- small comments below. Please merge at will after taking a look through those.
|
|
||
| return ( | ||
| <> | ||
| <ZulipStatusBar /> |
As recommended by react-native-safe-area-context to avoid flickering when the orientation changes.
As recommended by react-native-safe-area-context to avoid flickering when the orientation changes.
Not for any effect on the layout (there is none), but because it makes sense for this header component to choose the background color of the status bar. That's because - the status bar lives in the top inset, and - this header component is already in charge of making space for the top inset, giving it a color of its choosing
Like we did in the previous commit.
Soon, we'll set the `hidden` prop based on the `headerFooterVisible` state.
The fact that `ChatScreen`'s header changes with this bit of state is a clue that it really wants to be a route param.
And explain something confusing about it that's easy to run into.
And only invoke it when we mean to depart from those defaults.
This departure from a normal parent-child `View` layout hierarchy in a component is an imperfect way to emphasize that `ZulipStatusBar` isn't meant to participate in that hierarchy at all; its presence/absence with particular props is just a signal to the native status bar API about how the status bar should behave. Possibly we should make a HOC `withStatusBarSettings` that encapsulates details like this.
1353198 to
c1fae32
Compare
Done, thanks for the reviews! |
I've removed the work toward using React Navigation's
headeroption in this revision, and changed the title.Toward: #3066.
This PR is the next in the series started by #4428, #4440, and #4443.
Here, we use the
headeroption—for the first time!—forChatNavBar, as part of the safe-area-handling pattern I described at #4443 (comment).I'm a bit disappointed to see that Flow isn't tracking what route we're on when we use the
headeroption, so I have to suppress it when it says it's suspicious about grabbing specific route params to give toChatNavBar. I'm not sure how easy this would be to fix. With React Navigation v5's component-based API, I'm not sure if it's a case where we'd need to ask more from the React types than is reasonable.Also, on iOS, we start showing and hiding the status bar as the lightbox header/footer slide in and out, as Greg mentioned wanting to do at #4440 (comment).
I think I worked out something odd in how RN's
KeyboardAvoidingViewworks, and I made a note on that.