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

ios: Fix app-unusable bug with "Prefer Cross-Fade Transitions" setting #5163

Merged
merged 4 commits into from
Dec 21, 2021

Conversation

chrisbobbe
Copy link
Contributor

@chrisbobbe chrisbobbe commented Dec 9, 2021

Fork React Native's KeyboardAvoidingView and apply a fix.

Fixes: #5162

@chrisbobbe chrisbobbe added a-iOS a-a11y Accessibility labels Dec 9, 2021
@chrisbobbe chrisbobbe changed the title KeyboardAvoider: Fix app-unusable bug with "Prefer Cross-Fade Transitions" setting ios: Fix app-unusable bug with "Prefer Cross-Fade Transitions" setting Dec 9, 2021
@chrisbobbe chrisbobbe force-pushed the pr-keyboard-avoiding-fix branch 3 times, most recently from 7fc6250 to 5bce956 Compare December 9, 2021 09:48
@chrisbobbe chrisbobbe added the severe: crash The app quits, or stops responding. label Dec 9, 2021
@chrisbobbe chrisbobbe marked this pull request as draft December 9, 2021 22:38
@chrisbobbe
Copy link
Contributor Author

chrisbobbe commented Dec 9, 2021

Oops, working out a bug; will un-mark-as-draft when finished 🙂 Done!

Greg and I discussed a while in the office and landed on forking KeyboardAvoidingView.

@chrisbobbe chrisbobbe force-pushed the pr-keyboard-avoiding-fix branch 2 times, most recently from 1cc9fe9 to 40f8118 Compare December 14, 2021 19:26
@chrisbobbe chrisbobbe marked this pull request as ready for review December 14, 2021 19:26
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 @chrisbobbe for getting to the bottom of this! This code looks good; comments below on a comment, and on some license metadata.

Comment on lines 84 to 96
if (keyboardFrame.height === 0) {
// Empirically, in default environments, the keyboard animation is
// slide-in and slide-out. The off-screen keyboard frame is a
// downward translation of the on-screen frame, to just off the
// screen, with no change in height or width.
//
// Empirically, if the UIAccessibilityPrefersCrossFadeTransitions
// setting is set (see
// https://developer.apple.com/documentation/uikit/3584818-uiaccessibilitypreferscrossfadet?language=objc)
// ), then the keyboard fades in and out instead of sliding. In the
// off-screen state, the keyboard-frame values (`height`, `width`,
// `screenX`, and `screenY`) all take the special value of zero.
//
// So, recognize that special representation as a keyboard-closed
// state. (We choose `height` for the conditional because it seems
// least likely to collide with some valid non-off-screen state.)
// See zulip/zulip-mobile#5162 and facebook/react-native#29974.
return 0;
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this can explain much more directly why this logic is correct. In particular the correctness of it doesn't depend on those empirical observations (always a bit concerning of a thing to have to rely on) or on very much detail at all.

Then the explanation of why it's needed comes after that. The possibility that it's redundant is lower-stakes than the possibility that it's incorrect, so the reader can be more relaxed about it.

Here's one version:

Suggested change
if (keyboardFrame.height === 0) {
// Empirically, in default environments, the keyboard animation is
// slide-in and slide-out. The off-screen keyboard frame is a
// downward translation of the on-screen frame, to just off the
// screen, with no change in height or width.
//
// Empirically, if the UIAccessibilityPrefersCrossFadeTransitions
// setting is set (see
// https://developer.apple.com/documentation/uikit/3584818-uiaccessibilitypreferscrossfadet?language=objc)
// ), then the keyboard fades in and out instead of sliding. In the
// off-screen state, the keyboard-frame values (`height`, `width`,
// `screenX`, and `screenY`) all take the special value of zero.
//
// So, recognize that special representation as a keyboard-closed
// state. (We choose `height` for the conditional because it seems
// least likely to collide with some valid non-off-screen state.)
// See zulip/zulip-mobile#5162 and facebook/react-native#29974.
return 0;
}
if (keyboardFrame.height === 0) {
// The keyboard is hidden, and occupies no height.
//
// This condition is needed because in some circumstances when the
// keyboard is hidden we get both `screenY` and `height` (as well as
// `screenX` and `width`) of zero. In particular, this happens when
// the UIAccessibilityPrefersCrossFadeTransitions setting is true,
// i.e. when the user has enabled both "Reduce Motion" and "Prefer
// Cross-Fade Transitions" under Settings > Accessibility > Motion.
// See zulip/zulip-mobile#5162 and facebook/react-native#29974.
}

(Another thing there is that I think saying how to find the setting in the UI is more helpful than that API docs page; we don't want to have any code looking for that setting in any case, at least not in connection with this logic. The page is easy to find via Google given the identifier, anyway.)

Copy link
Member

Choose a reason for hiding this comment

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

Oh, and this component is written to be cross-platform, right? We only use it on iOS, but the conditional in componentDidMount is meant to operate on Android too.

So this description of the relevant circumstance should mention iOS. Adding "on iOS" after "In particular, this happens" should do it.

Comment on lines 2 to 6
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
Copy link
Member

Choose a reason for hiding this comment

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

This should be updated; for one thing, with this file in a different source tree, there is a LICENSE file in the root directory but it's a different license.

Relatedly, we'll want to add an entry in docs/THIRDPARTY.

I can make those edits before merging.

Copy link
Member

Choose a reason for hiding this comment

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

In the commit message, let's mention the exact RN version, and path in its tree, that this file is copied from. That makes it easy to compare, and also to look for upstream changes in the future.

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 can make those edits before merging.

Cool, thanks!

@chrisbobbe
Copy link
Contributor Author

chrisbobbe commented Dec 16, 2021

Thanks! Revision pushed.

This should be updated; for one thing, with this file in a different source tree, there is a LICENSE file in the root directory but it's a different license.

Relatedly, we'll want to add an entry in docs/THIRDPARTY.

I can make those edits before merging.

Great, thank you!

gnprice and others added 4 commits December 21, 2021 12:35
It's 2021, and we've kept making changes, so update the copyright
years for our own work.

We've also made a number of changes to the redux-persist-migrate
code.  Like all our software, we make that work available under
the Apache License, Version 2.0 ("Apache-2").  Because the MIT
license (which the upstream version of that code is provided
under) is compatible with Apache-2, the combination is available
under simply the same Apache-2 license.

There's also some more third-party code for which we forgot to
update this file: in src/third/redux-persist/, added as e5409c5
in 2020-05.

 * The upstream for that code has the MIT license, with a
   copyright notice "Copyright (c) 2015-present Zack Story".

 * The last upstream commit (github:rt2zz/redux-persist@v4.10.2)
   for that code is dated 2017, so "present" really means 2017.

 * Because again the MIT license is compatible with our Apache-2
   license, the new versions with our changes are under the same
   Apache-2 license as the rest of our code.

Finally, we deleted vendor/intl/, in 30e4d19 in 2020-08.
From
  node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js
with v0.64.2 of `react-native`, adjusting only the license pointer
in the copyright header.

We'll make the fork usable soon, not in this pure copy-paste commit.

We don't like this implementation (see, e.g., 70eca07, which took
a lot of painstaking investigation), but we want to fix zulip#5162 as
soon as we can.

For a not-quite-perfect attempt at reimplementing it from scratch in
our style (and with zulip#5162 fixed), see my branch
`reimplement-keyboard-avoiding-view` on GitHub.

We can't easily make a jank-free solution in a normal React Native
way because we can't have perfect information about the layout on
every frame. React Native exposes it to us by consuming event
listeners and providing asynchronous query functions, and we end up
having to learn about different aspects of the layout at different
times.

The best hope for a jank-free solution is to use native iOS APIs. If
we're feeling adventurous and we find the time, we should really try
hard to make React Native play along with iOS's
`keyboardLayoutGuide` API (iOS 15+).

The first four minutes of this video should be really useful
background on the `keyboardLayoutGuide` feature:
  https://developer.apple.com/videos/play/wwdc2021/10259/

It works within iOS's "Auto Layout" system:
  https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html

But then we'd have to go and

(a) Make a native component:
      https://reactnative.dev/docs/native-components-ios

(b) Figure out how to make React Native's propagate-from-JavaScript
    layout system (Yoga) not fight with the native iOS layout
    system, including "Auto Layout" and `keyboardLayoutGuide`.
    Possibly we can pick up some clues from
    `react-native-safe-area-context` for this?

From another angle, since the jank is regularly seen on screen
orientation changes between portrait and landscape, we could
consider just unsupporting the landscape orientation. On very common
device types, like phones, it's just not as easy to use the app in,
especially for composing messages.
Just make the minimal changes necessary to get type-checking working
(with a few suppressions, though) and to have the imports make
sense.
@gnprice gnprice merged commit 3b8caa5 into zulip:main Dec 21, 2021
@gnprice
Copy link
Member

gnprice commented Dec 21, 2021

OK, and done! Thanks again for fixing this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a-a11y Accessibility a-iOS severe: crash The app quits, or stops responding.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ios: App unusable if "Prefer Cross-Fade Transitions" a11y setting is set
2 participants