-
-
Notifications
You must be signed in to change notification settings - Fork 662
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
Clean up navigation logic (3/x). #4443
Conversation
(I'll look at the actual content here in a bit, but:
this is a classic example of the kind of phrasing that trips up GitHub's "this is supposed to fix that" detector 😉. Should reword to either not say "fix", or squeeze a word in between "fix" and the issue number.) |
Ah, right; thanks for catching that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @chrisbobbe! Glad to see all these things straightened out. Some comments below.
My goal for that scheme is this:
This plan sounds good to me.
src/common/ZulipStatusBar.js
Outdated
<> | ||
<View style={style} /> | ||
<StatusBar |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React Native's `StatusBar` component is unusual in that its `render`
method always returns null [1]. The component isn't meant to
participate in the spatial layout of the UI elements. Instead, the
API works more like this: Mount a `StatusBar` component in order to
tell something new to the native status bar API; e.g., what colors
and animation you want to use.
Interesting, thanks for sorting this out!
src/main/MainTabsScreen.js
Outdated
<View | ||
style={{ | ||
height: insets.top, | ||
backgroundColor: DEFAULT_TITLE_BACKGROUND_COLOR, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value of this constant is `'transparent'`, which is the default
background color of `View`s anyway.
It looks like after this change a further simplification is possible: I believe it's the case that DEFAULT_TITLE_BACKGROUND_COLOR
is only ever used as a sentinel value! That is, it signifies to our own code "there's nothing here, just use your default behavior", and we have conditionals that test for equality to it... but I believe we never pass it as an actual style prop, or otherwise to any code outside our own.
If so, that means that we could replace the constant's initializer with undefined
, or for that matter {}
or anything that makes a fresh unique object. And probably better -- we could just replace every use of it with undefined
, and augment the relative types with | void
to make that possibility explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, interesting! I think I see just one place where we pass it to code that's not ours, at this commit:
src/chat/ChatScreen
const titleBackgroundColor = useSelector(state => getTitleBackgroundColor(state, narrow));
// …
return (
<ActionSheetProvider>
<View style={[componentStyles.screen, { backgroundColor }]}>
<KeyboardAvoider style={styles.flexed} behavior="padding">
{orientation === 'PORTRAIT' && (
<View
style={{
height: insets.top,
backgroundColor: titleBackgroundColor,
}}
/>
)}
getTitleBackgroundColor
will return DEFAULT_TITLE_BACKGROUND_COLOR
if the narrow is not a stream or topic narrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right—but passing undefined
would be just the same as passing 'transparent' there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My new revision makes DEFAULT_TITLE_BACKGROUND_COLOR
go away. I've also renamed getTitleBackgroundColor
to getStreamColorForNarrow
, but I've kept the part of its interface that says it accepts narrows other than stream or topic narrows (for which it returns undefined
). The react-hooks/rules-of-hooks
rule doesn't like it if I conditionally use a hook, as in
const backgroundColor = isStreamNarrow(narrow)
? useSelector(state => getStreamColorForNarrow(state, narrow))
: '…';
React Hook "useSelector" is called conditionally. React Hooks must be called in the exact same order in every component render. (eslintreact-hooks/rules-of-hooks)
src/chat/ChatScreen.js
Outdated
const insets = useSafeAreaInsets(); | ||
|
||
return ( | ||
<ActionSheetProvider> | ||
<View style={[componentStyles.screen, { backgroundColor }]}> | ||
<KeyboardAvoider style={styles.flexed} behavior="padding"> | ||
{orientation === 'PORTRAIT' && ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of a device that has a nonzero top inset in landscape
mode, so it's possible that this commit is NFC. Still, we might as
well (1) handle that case, so we don't have to think about it, and
(2) remove something that can cause confusion.
Hmm, maybe a foldable? Or some odd-shaped tablet?
In any case I definitely agree with this reasoning.
return ( | ||
<View |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(The default
algorithm used to display the diff in `ChatNavBar` gives a lot of
noise; `git diff -w` should reduce that quite a lot.)
It will, but perhaps this is a good time to plug a handy Git feature! This is one I relatively recently discovered and enabled in my gitconfig:
# Use color to identify code that was moved around verbatim.
[diff]
# Enable the feature.
colorMoved = zebra
# Supercharge it to do the equivalent of `git diff -b`.
# …
colorMovedWS = ignore-space-change
(And git diff -b
is a close relative of -w
; they're pretty similar but I tend to find -b
is closer to what I'm really looking for in principle.)
So here's how the main part of that diff comes out with my normal git usp
, without going back and adding any other flags:
The "moved" colors let me see immediately that the code in each of those chunks didn't change beyond getting indented. Then I can match up the chunks by eye, and pretty quickly isolate what's changed. Not as simple as the git diff -b
or -w
view, but simple enough that I usually don't feel the need anymore to go reach for that one.
(The default colors are different from those and I think less helpful; those colors are also in my posted gitconfig.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, interesting!
Not as simple as the
git diff -b
or-w
view, but simple enough that I usually don't feel the need anymore to go reach for that one.
It is quite a lot more lines than git diff -b
. I'm not sure how long it'll take until I see this output as simpler, but I'll give it a go and find out! 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, the output of git diff -b
is definitely simpler than this!
I just mean that this is simpler than reading the plain diff (and having to look closely to spot the few bits that changed); and it's enough simpler, giving me enough of what I'd get from git diff -b
, that when I'm in the middle of a review I typically don't feel the need to go reach for git diff -b
.
(And I don't just have -b
or -w
on all the time, because I do want to know about the changes those ignore; when they're relevant, I want to both see those changes, and see the changes with those simplified away, just separately.)
<ScrollView style={styles.optionWrapper}> | ||
<ModalNavBar canGoBack={false} title="Settings" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh gosh this was inside the ScrollView! That never made any sense.
Which... oh, I see, this was quite noticeable on iOS.
On Android it could only come up if there was more there than fit vertically. I can make that happen by cranking up the system "Display size" setting plus going into landscape; but otherwise it's pretty unlikely, because we only have a few items on this settings screen.
But iOS has the animation where you can tug the scrollable content around even when there's no scrolling to be done, and the "Settings" header would move right along.
src/nav/ModalNavBar.js
Outdated
<> | ||
<View style={{ height: insets.top }} /> | ||
<View style={{ borderBottomWidth: 1, backgroundColor, paddingTop: insets.top }}> | ||
<View | ||
style={[ | ||
{ | ||
borderColor: 'hsla(0, 0%, 50%, 0.25)', | ||
flexDirection: 'row', | ||
height: NAVBAR_SIZE, | ||
alignItems: 'center', | ||
borderBottomWidth: 1, | ||
backgroundColor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, is this quite right? The borderColor
is getting separated from the borderBottomWidth
.
For that matter: is there a reason not to collapse these into a single wrapper View? I don't think I'm spotting a way that we're using the distinction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the next commit turns the outer one into a SafeAreaView
. But that's supposed to be just like a View
with one extra feature, so I think the same question applies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a reason not to collapse these into a single wrapper View? I don't think I'm spotting a way that we're using the distinction.
The existing View
sets an explicit height of NAVBAR_SIZE
(58), so the part that holds the content gets shrunk. Though I guess we could get rid of that explicit height; we recently removed one in #4442.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the situation is similar with ModalSearchNavBar
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, because the padding comes out of the height
.
We could also handle that directly, I think, by adding the desired padding to the height when combining these two View
s:
paddingTop: insets.top,
height: insets.top + NAVBAR_SIZE,
But removing NAVBAR_SIZE sounds good in any case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also handle that directly, I think, by adding the desired padding to the height when combining these two
View
s:paddingTop: insets.top, height: insets.top + NAVBAR_SIZE,
Oh, but this gets stickier when converting to SafeAreaView
, doesn't it.
Anyway, happy to remove NAVBAR_SIZE
🙂
This is pretty clearly what was intended. Soon, though, we'll simplify things by removing the variable -- at this point, it's just being used as a sentinel value [1]. [1] zulip#4443 (comment)
We're only using this as a sentinel value [1], so `undefined` works just as well and is less to think about. [1] zulip#4443 (comment)
f02fe66
to
f2d629d
Compare
Thanks for the review! Revision pushed. |
src/nav/ModalNavBar.js
Outdated
<> | ||
<View style={{ height: insets.top }} /> | ||
<View style={{ borderBottomWidth: 1, backgroundColor, paddingTop: insets.top }}> | ||
<View | ||
style={[ | ||
{ | ||
borderColor: 'hsla(0, 0%, 50%, 0.25)', | ||
flexDirection: 'row', | ||
height: NAVBAR_SIZE, | ||
alignItems: 'center', | ||
borderBottomWidth: 1, | ||
backgroundColor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also handle that directly, I think, by adding the desired padding to the height when combining these two
View
s:paddingTop: insets.top, height: insets.top + NAVBAR_SIZE,
Oh, but this gets stickier when converting to SafeAreaView
, doesn't it.
Anyway, happy to remove NAVBAR_SIZE
🙂
There was a problem hiding this 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! I like the new further cleanup. Small comments above and below.
src/title/titleSelectors.js
Outdated
/** | ||
* Background color to use for the app bar in narrow `narrow`. | ||
* The stream's color for the given stream or topic narrow. | ||
* | ||
* If `narrow` is a stream or topic narrow, this is based on the stream color. | ||
* Otherwise, it takes a default value. | ||
* Gives undefined for narrows that are not stream or topic narrows. | ||
*/ | ||
export const getTitleBackgroundColor = (state: GlobalState, narrow: Narrow) => { | ||
export const getStreamColorForNarrow = (state: GlobalState, narrow: Narrow) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With its new clearer name, I think this also is best moved to a different file: subscriptionSelectors.js
, where it will fit right in.
That also lets us remove titleSelectors.js
, which never made as much sense as the selectors files like subscriptionSelectors.js
that are clearly about a particular part of the app's data model.
src/common/ZulipStatusBar.js
Outdated
@@ -46,7 +45,7 @@ type Props = $ReadOnly<{ | |||
class ZulipStatusBar extends PureComponent<Props> { | |||
static defaultProps = { | |||
hidden: false, | |||
backgroundColor: DEFAULT_TITLE_BACKGROUND_COLOR, | |||
backgroundColor: undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can also be dropped (probably in a small followup commit, like you've done already for similar cleanups.)
React Native's `StatusBar` component is unusual in that its `render` method always returns null [1]. The component isn't meant to participate in the spatial layout of the UI elements. Instead, the API works more like this: Mount a `StatusBar` component in order to tell something new to the native status bar API; e.g., what colors and animation you want to use. This commit is therefore NFC: the `StatusBar` still gets mounted in all cases where it was getting mounted before, and nothing changes in the spatial layout of the UI elements. The `<View style={style} />` is doing an important job: it covers the "safe area" at the top of the screen (which includes the status bar), so that our content doesn't accidentally overlap with it. But `ZulipStatusBar` already has a very different job, which is to configure the status bar in kind of subtle ways, and depending on the platform. I'd like to pull the safe-area handling out of `ZulipStatusBar` to give it one less thing to worry about; that will happen in the rest of this series of commits. [1] https://github.com/facebook/react-native/blob/v0.63.4/Libraries/Components/StatusBar/StatusBar.js#L497
We'll use this soon.
Now, `ZulipStatusBar` doesn't have to worry about handling safe area views, and we can tailor that logic more specifically to each of the callers.
Simplify some trivial expressions.
The value of this constant is `'transparent'`, which is the default background color of `View`s anyway.
In each of these instances, the `View` will be rendered at the top of the screen -- regardless of the device's orientation. (I.e., if it's in a portrait orientation, it'll be on a short side at the top; if it's in a landscape orientation, it'll be on a long side at the top.) The `View` is meant to guard against a potential unsafe area at the top. In principle, that isn't less necessary when we're in a landscape orientation. I can't think of a device that has a nonzero top inset in landscape mode, so it's possible that this commit is NFC. Still, we might as well (1) handle that case, so we don't have to think about it, and (2) remove something that can cause confusion.
As noted in a recent commit, the `ZulipStatusBar` component no longer has any effect on the spatial layout of the UI, so we're free to do this.
The nav bar is always present, for `ChatScreen`, and always at the top. So it's harmless to have the nav bar component also take control of handling the upper safe-area inset. (The default algorithm used to display the diff in `ChatNavBar` gives a lot of noise; `git diff -w` should reduce that quite a lot.) Soon, I'd like to start reversing our avoidance of React Navigation's screen-header APIs; i.e., it'll make sense for the `<Stack.Screen` with `component={ChatScreen}` to use `ChatNavBar` (or something much like it) in its `header` prop [1], instead of sticking it at the top of the screen component, where the screen's content belongs. When we do that, `ChatScreen` will properly describe only the content of the screen, below the header, and it'll be too far down the screen for top-safe-area handling to be done there. [1] https://reactnavigation.org/docs/stack-navigator/#header
react-native-safe-area-context recommends [1] using its `SafeAreaView` component, which applies the insets natively, to avoid flickering when you rotate the screen. [1] https://github.com/th3rdwave/react-native-safe-area-context
The contents of this header extend all the way to the left and the right of the screen. When in landscape mode, the left and right insets can be nonzero. We shouldn't add padding for the bottom inset because the header isn't at the bottom of the screen.
- None of the other tabs have a header like this, so it looks a bit odd and inconsistent. - It duplicates what's being said by the selected gear icon in the bottom tab bar. - It takes up space. See also discussion at https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/Stack.20nav.20for.20settings.20tab/near/1107707.
As noted in a recent commit, the `ZulipStatusBar` component no longer has any effect on the spatial layout of the UI, so we're free to do this.
Like we did in a recent commit, moving a similar `View` from `ChatScreen` into `ChatNavBar`.
This limits our flexibility for gracefully handling the user's system font-size preference.
Now, `backgroundColor` extends up through the top inset.
react-native-safe-area-context recommends [1] using its `SafeAreaView` component, which applies the insets natively, to avoid flickering when you rotate the screen. [1] https://github.com/th3rdwave/react-native-safe-area-context
For landscape mode.
This limits our flexibility for gracefully handling the user's system font-size preference.
Now, `backgroundColor` extends up through the top inset.
For landscape mode.
This should be equivalent, and it'll help with a simplification we're about to do.
This is pretty clearly what was intended. Soon, though, we'll simplify things by removing the variable -- at this point, it's just being used as a sentinel value [1]. [1] zulip#4443 (comment)
We're only using this as a sentinel value [1], so `undefined` works just as well and is less to think about. [1] zulip#4443 (comment)
Some callers pass a `string` type; some don't pass anything, and one passes a `string | void` type. So `backgroundColor?: string | void` seems the most correct, even though we could probably get away with removing the `| void`.
Based on the rename of `getStreamColorForNarrow` (previously `getTitleBackgroundColor`) in a recent commit.
…tors. As Greg points out [1], this "never made as much sense as the selectors files like `subscriptionSelectors.js` that are clearly about a particular part of the app's data model." [1] zulip#4443 (comment)
f2d629d
to
0a218e0
Compare
Thanks for the review! Revision pushed. |
All looks good, thanks! Merged. |
This is pretty clearly what was intended. Soon, though, we'll simplify things by removing the variable -- at this point, it's just being used as a sentinel value [1]. [1] zulip#4443 (comment)
We're only using this as a sentinel value [1], so `undefined` works just as well and is less to think about. [1] zulip#4443 (comment)
…tors. As Greg points out [1], this "never made as much sense as the selectors files like `subscriptionSelectors.js` that are clearly about a particular part of the app's data model." [1] zulip#4443 (comment)
These app bars -- all our app bars outside the message list, which uses ChatNavBar -- didn't have anything to actually specify how tall they should be. When the back button is present, it serves as a strut to keep the app bar at least as tall as the button is, which gets us pretty close: just a fraction of a pixel off in my testing on an Android 11 emulator. (In fact I don't understand why it isn't exactly right: `NavButtonGeneral` sets the height to NAVBAR_SIZE.) But when the back button is not present, the app bar would collapse to the height of the text, which is typically much shorter. Fix both cases by simply specifying the intended height, as a minimum. We actually used to have explicit heights like these, as simply `height: NAVBAR_SIZE`, and removed them a little while ago in zulip#4443 in commits 635b13a and a9ab3fc. The stated reason was to better handle large font sizes; it also simplified rearranging things for getting these `SafeAreaView`s in place. But those have settled down, and by using `minHeight` there's no trouble if the user has a very large font-size setting. (Also I don't think we noticed at the time that this had a large visible effect in the no-back-button case; if we had, we'd have done something about it then.)
These app bars -- all our app bars outside the message list, which uses ChatNavBar -- didn't have anything to actually specify how tall they should be. When the back button is present, it serves as a strut to keep the app bar at least as tall as the button is, which gets us pretty close: just a fraction of a pixel off in my testing on an Android 11 emulator. (In fact I don't understand why it isn't exactly right: `NavButtonGeneral` sets the height to NAVBAR_SIZE.) But when the back button is not present, the app bar would collapse to the height of the text, which is typically much shorter. Fix both cases by simply specifying the intended height, as a minimum. We actually used to have explicit heights like these, as simply `height: NAVBAR_SIZE`, and removed them a little while ago in zulip#4443 in commits 635b13a and a9ab3fc. The stated reason was to better handle large font sizes; it also simplified rearranging things for getting these `SafeAreaView`s in place. But those have settled down, and by using `minHeight` there's no trouble if the user has a very large font-size setting. (Also I don't think we noticed at the time that this had a large visible effect in the no-back-button case; if we had, we'd have done something about it then.) In the case of `ChatNavBar`, we do specify the height, and ideally we'd say `minHeight` instead there too. But that causes hilariously wrong results, presumably due to a bug somewhere else in our layout. For the present, just leave a TODO comment there.
Following #4428 and #4440, this PR takes the job of handling the top inset away from
ZulipStatusBar
.It doesn't yet establish a consistent scheme for how we should handle safe areas (for #3066 and put a ratchet on a fix for it), but it brings us closer to being able to do that, and I aim to do that in a future PR. 🙂
My goal for that scheme is this:
header
prop to eitherChatNavBar
,ModalNavBar
, etc., can pretty much be dropped in here after this PR.) Or,header
prop to help make consistency across our headers more natural than it is now.)MainTabsScreen
:MainTabsScreen
itself is a screen on a stack navigator,AppNavigator
. It'll get the latter treatment; just a simpleView
that covers the top inset.MainTabsScreen
aren't configurable with aheader
prop. They fill the entire area between the bottom tab bar andMainTabsScreen
's trivial space-covering header (just mentioned). So they just need to worry about their horizontal padding.MainTabsScreen
's header, and (2) the header for each screen on such a stack navigator. (Likely meaning handling the top inset twice, which is one too many times.) But we're not currently planning to use this pattern.Linking React Navigation's doc on handling safe areas.