From 9335b3ec6e508639f488498cd84c894afdb9dcfc Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 21 Dec 2021 11:58:16 -0800 Subject: [PATCH 1/4] license: Bring THIRDPARTY file up to date 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 e5409c578 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 30e4d19cc in 2020-08. --- docs/THIRDPARTY | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/THIRDPARTY b/docs/THIRDPARTY index 2f1815cf2ae..2d153b6efd5 100644 --- a/docs/THIRDPARTY +++ b/docs/THIRDPARTY @@ -17,16 +17,22 @@ Comment: Kandra Labs of any inaccurate information or errors found in these notices. Files: * -Copyright: 2016-2018 Kandra Labs, Inc., and contributors +Copyright: 2016-2021 Kandra Labs, Inc., and contributors License: Apache-2 Files: src/redux-persist-migrate/index.js -Copyright: 2016-2017 Zack Story and contributors -License: MIT +Copyright: 2016-2017 Zack Story and contributors, 2018-2021 Kandra Labs, Inc., + and contributors +License: Apache-2 +Comment: + Upstream is licensed as MIT. -Files: vendor/intl/* -Copyright: 2013 Andy Earnshaw -License: MIT +Files: src/third/redux-persist/* +Copyright: 2015-2017 Zack Story, 2020-2021 Kandra Labs, Inc., and contributors +License: Apache-2 +Comment: + Upstream is licensed as MIT. Forked from v4.10.2, at + https://github.com/rt2zz/redux-persist/tree/f4a6e86c666 . Files: static/img/app-store-badge.* Copyright: 2018 Apple Inc. From d7d91dcfb63eae31cc39526337649743699da6a7 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 10 Dec 2021 15:02:42 -0800 Subject: [PATCH 2/4] keyboard-avoiding ios: Begin forking RN's KeyboardAvoidingView 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., 70eca0716, which took a lot of painstaking investigation), but we want to fix #5162 as soon as we can. For a not-quite-perfect attempt at reimplementing it from scratch in our style (and with #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. --- .eslintrc.yaml | 20 ++ .flowconfig | 3 + .prettierignore | 4 + docs/THIRDPARTY | 7 + .../react-native/KeyboardAvoidingView.js | 232 ++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 src/third/react-native/KeyboardAvoidingView.js diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 4c4e6756b94..a14f9b4cb18 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -359,3 +359,23 @@ overrides: # a few variables already declared in upper scope; no bugs known # to result from this no-shadow: off + + # + # ================================================================ + # Third-party code: react-native. + # + # We leave this code in the style we received it in. + - files: ['src/third/react-native/**'] + rules: + strict: off + import/order: off + import/first: off + object-curly-spacing: off + react/state-in-constructor: off + no-unused-vars: off + no-case-declarations: off + react/jsx-closing-bracket-location: off + + # We'll fix these right away: + import/no-unresolved: off + import/extensions: off diff --git a/.flowconfig b/.flowconfig index 778651d8389..eeebbadd00e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -22,6 +22,9 @@ node_modules/react-native/Libraries/polyfills/.* .*/node_modules/flow-coverage-report/.* .*/node_modules/@snyk/.* +; We'll un-ignore this very soon. +.*/third/react-native/KeyboardAvoidingView.js + ; As of v4.1.1, this package only has TypeScript. That's fine as far as ; running the code goes -- Metro transpiles it -- but this line causes Flow ; to stop looking for types in that TypeScript, and then go and look in our diff --git a/.prettierignore b/.prettierignore index da481691148..595a854ea03 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,7 @@ flow-typed/@react-navigation # These are purely type definitions, no runtime code. Most of them # are third-party code, too, so naturally don't match our style. **/flow-typed/** + +# Third-party code: react-native. We leave this code in the style we +# received it in. +src/third/react-native diff --git a/docs/THIRDPARTY b/docs/THIRDPARTY index 2d153b6efd5..31bf9cc10c5 100644 --- a/docs/THIRDPARTY +++ b/docs/THIRDPARTY @@ -27,6 +27,13 @@ License: Apache-2 Comment: Upstream is licensed as MIT. +Files: src/third/react-native/* +Copyright: Facebook, Inc., and its affiliates +License: MIT +Comment: + Changes we make here are designed to be sent upstream. So even when making + changes, we keep our versions on the MIT license, the same as upstream. + Files: src/third/redux-persist/* Copyright: 2015-2017 Zack Story, 2020-2021 Kandra Labs, Inc., and contributors License: Apache-2 diff --git a/src/third/react-native/KeyboardAvoidingView.js b/src/third/react-native/KeyboardAvoidingView.js new file mode 100644 index 00000000000..2d9adb6c05b --- /dev/null +++ b/src/third/react-native/KeyboardAvoidingView.js @@ -0,0 +1,232 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * docs/THIRDPARTY file from the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const Keyboard = require('./Keyboard'); +const LayoutAnimation = require('../../LayoutAnimation/LayoutAnimation'); +const Platform = require('../../Utilities/Platform'); +const React = require('react'); +const StyleSheet = require('../../StyleSheet/StyleSheet'); +const View = require('../View/View'); + +import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; +import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; +import type { + ViewProps, + ViewLayout, + ViewLayoutEvent, +} from '../View/ViewPropTypes'; +import type {KeyboardEvent} from './Keyboard'; + +type Props = $ReadOnly<{| + ...ViewProps, + + /** + * Specify how to react to the presence of the keyboard. + */ + behavior?: ?('height' | 'position' | 'padding'), + + /** + * Style of the content container when `behavior` is 'position'. + */ + contentContainerStyle?: ?ViewStyleProp, + + /** + * Controls whether this `KeyboardAvoidingView` instance should take effect. + * This is useful when more than one is on the screen. Defaults to true. + */ + enabled: ?boolean, + + /** + * Distance between the top of the user screen and the React Native view. This + * may be non-zero in some cases. Defaults to 0. + */ + keyboardVerticalOffset: number, +|}>; + +type State = {| + bottom: number, +|}; + +/** + * View that moves out of the way when the keyboard appears by automatically + * adjusting its height, position, or bottom padding. + */ +class KeyboardAvoidingView extends React.Component { + static defaultProps: {|enabled: boolean, keyboardVerticalOffset: number|} = { + enabled: true, + keyboardVerticalOffset: 0, + }; + + _frame: ?ViewLayout = null; + _keyboardEvent: ?KeyboardEvent = null; + _subscriptions: Array = []; + viewRef: {current: React.ElementRef | null, ...}; + _initialFrameHeight: number = 0; + + constructor(props: Props) { + super(props); + this.state = {bottom: 0}; + this.viewRef = React.createRef(); + } + + _relativeKeyboardHeight(keyboardFrame): number { + const frame = this._frame; + if (!frame || !keyboardFrame) { + return 0; + } + + const keyboardY = keyboardFrame.screenY - this.props.keyboardVerticalOffset; + + // Calculate the displacement needed for the view such that it + // no longer overlaps with the keyboard + return Math.max(frame.y + frame.height - keyboardY, 0); + } + + _onKeyboardChange = (event: ?KeyboardEvent) => { + this._keyboardEvent = event; + this._updateBottomIfNecesarry(); + }; + + _onLayout = (event: ViewLayoutEvent) => { + const wasFrameNull = this._frame == null; + this._frame = event.nativeEvent.layout; + if (!this._initialFrameHeight) { + // save the initial frame height, before the keyboard is visible + this._initialFrameHeight = this._frame.height; + } + + if (wasFrameNull) { + this._updateBottomIfNecesarry(); + } + }; + + _updateBottomIfNecesarry = () => { + if (this._keyboardEvent == null) { + this.setState({bottom: 0}); + return; + } + + const {duration, easing, endCoordinates} = this._keyboardEvent; + const height = this._relativeKeyboardHeight(endCoordinates); + + if (this.state.bottom === height) { + return; + } + + if (duration && easing) { + LayoutAnimation.configureNext({ + // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m + duration: duration > 10 ? duration : 10, + update: { + duration: duration > 10 ? duration : 10, + type: LayoutAnimation.Types[easing] || 'keyboard', + }, + }); + } + this.setState({bottom: height}); + }; + + componentDidMount(): void { + if (Platform.OS === 'ios') { + this._subscriptions = [ + Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange), + ]; + } else { + this._subscriptions = [ + Keyboard.addListener('keyboardDidHide', this._onKeyboardChange), + Keyboard.addListener('keyboardDidShow', this._onKeyboardChange), + ]; + } + } + + componentWillUnmount(): void { + this._subscriptions.forEach(subscription => { + subscription.remove(); + }); + } + + render(): React.Node { + const { + behavior, + children, + contentContainerStyle, + enabled, + keyboardVerticalOffset, + style, + ...props + } = this.props; + const bottomHeight = enabled ? this.state.bottom : 0; + switch (behavior) { + case 'height': + let heightStyle; + if (this._frame != null && this.state.bottom > 0) { + // Note that we only apply a height change when there is keyboard present, + // i.e. this.state.bottom is greater than 0. If we remove that condition, + // this.frame.height will never go back to its original value. + // When height changes, we need to disable flex. + heightStyle = { + height: this._initialFrameHeight - bottomHeight, + flex: 0, + }; + } + return ( + + {children} + + ); + + case 'position': + return ( + + + {children} + + + ); + + case 'padding': + return ( + + {children} + + ); + + default: + return ( + + {children} + + ); + } + } +} + +module.exports = KeyboardAvoidingView; From 6e6502c95eaf592aaaaffff67c462b731dc666cb Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 10 Dec 2021 15:32:58 -0800 Subject: [PATCH 3/4] keyboard-avoiding ios [nfc]: Start using forked KeyboardAvoidingView Just make the minimal changes necessary to get type-checking working (with a few suppressions, though) and to have the imports make sense. --- .eslintrc.yaml | 4 --- .flowconfig | 3 --- src/common/KeyboardAvoider.js | 4 ++- .../react-native/KeyboardAvoidingView.js | 27 +++++++++---------- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index a14f9b4cb18..21038810cb8 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -375,7 +375,3 @@ overrides: no-unused-vars: off no-case-declarations: off react/jsx-closing-bracket-location: off - - # We'll fix these right away: - import/no-unresolved: off - import/extensions: off diff --git a/.flowconfig b/.flowconfig index eeebbadd00e..778651d8389 100644 --- a/.flowconfig +++ b/.flowconfig @@ -22,9 +22,6 @@ node_modules/react-native/Libraries/polyfills/.* .*/node_modules/flow-coverage-report/.* .*/node_modules/@snyk/.* -; We'll un-ignore this very soon. -.*/third/react-native/KeyboardAvoidingView.js - ; As of v4.1.1, this package only has TypeScript. That's fine as far as ; running the code goes -- Metro transpiles it -- but this line causes Flow ; to stop looking for types in that TypeScript, and then go and look in our diff --git a/src/common/KeyboardAvoider.js b/src/common/KeyboardAvoider.js index f88bfc52d90..f5195eaeaf1 100644 --- a/src/common/KeyboardAvoider.js +++ b/src/common/KeyboardAvoider.js @@ -1,9 +1,11 @@ /* @flow strict-local */ import React, { PureComponent } from 'react'; import type { Node } from 'react'; -import { KeyboardAvoidingView, Platform, View } from 'react-native'; +import { Platform, View } from 'react-native'; import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet'; +import KeyboardAvoidingView from '../third/react-native/KeyboardAvoidingView'; + type Props = $ReadOnly<{| behavior?: ?('height' | 'position' | 'padding'), children: Node, diff --git a/src/third/react-native/KeyboardAvoidingView.js b/src/third/react-native/KeyboardAvoidingView.js index 2d9adb6c05b..4e63eb1dcaa 100644 --- a/src/third/react-native/KeyboardAvoidingView.js +++ b/src/third/react-native/KeyboardAvoidingView.js @@ -5,26 +5,22 @@ * docs/THIRDPARTY file from the root directory of this source tree. * * @format - * @flow + * @flow strict-local */ 'use strict'; -const Keyboard = require('./Keyboard'); -const LayoutAnimation = require('../../LayoutAnimation/LayoutAnimation'); -const Platform = require('../../Utilities/Platform'); -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); -const View = require('../View/View'); - -import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; -import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; +import React from 'react'; +import type { ElementRef, Node } from 'react'; +import { Keyboard, LayoutAnimation, Platform, StyleSheet, View } from 'react-native'; +import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type { EventSubscription } from 'react-native/Libraries/vendor/emitter/EventEmitter'; +import type { KeyboardEvent } from 'react-native/Libraries/Components/Keyboard/Keyboard'; import type { ViewProps, ViewLayout, ViewLayoutEvent, -} from '../View/ViewPropTypes'; -import type {KeyboardEvent} from './Keyboard'; +} from 'react-native/Libraries/Components/View/ViewPropTypes'; type Props = $ReadOnly<{| ...ViewProps, @@ -69,7 +65,8 @@ class KeyboardAvoidingView extends React.Component { _frame: ?ViewLayout = null; _keyboardEvent: ?KeyboardEvent = null; _subscriptions: Array = []; - viewRef: {current: React.ElementRef | null, ...}; + // $FlowFixMe[unclear-type] + viewRef: {current: ElementRef | null, ...}; _initialFrameHeight: number = 0; constructor(props: Props) { @@ -122,6 +119,7 @@ class KeyboardAvoidingView extends React.Component { return; } + // $FlowFixMe[sketchy-number-and] if (duration && easing) { LayoutAnimation.configureNext({ // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m @@ -154,7 +152,7 @@ class KeyboardAvoidingView extends React.Component { }); } - render(): React.Node { + render(): Node { const { behavior, children, @@ -164,6 +162,7 @@ class KeyboardAvoidingView extends React.Component { style, ...props } = this.props; + // $FlowFixMe[sketchy-null-bool] const bottomHeight = enabled ? this.state.bottom : 0; switch (behavior) { case 'height': From 3b8caa505bf92e28b776749d54324c251b968661 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Fri, 10 Dec 2021 15:46:31 -0800 Subject: [PATCH 4/4] ios: Fix app-unusable bug with "Prefer Cross-Fade Transitions" setting Fixes: #5162 Co-authored-by: Greg Price --- src/third/react-native/KeyboardAvoidingView.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/third/react-native/KeyboardAvoidingView.js b/src/third/react-native/KeyboardAvoidingView.js index 4e63eb1dcaa..53b4d15b350 100644 --- a/src/third/react-native/KeyboardAvoidingView.js +++ b/src/third/react-native/KeyboardAvoidingView.js @@ -81,6 +81,20 @@ class KeyboardAvoidingView extends React.Component { 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 on iOS + // 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. + return 0; + } + const keyboardY = keyboardFrame.screenY - this.props.keyboardVerticalOffset; // Calculate the displacement needed for the view such that it