diff --git a/.buckconfig b/.buckconfig index dc22ddc157f629..0740647479601e 100644 --- a/.buckconfig +++ b/.buckconfig @@ -11,4 +11,4 @@ jcenter = https://jcenter.bintray.com/ [alias] - rntester = //RNTester/android/app:app + rntester = //packages/rn-tester/android/app:app diff --git a/.circleci/Dockerfiles/scripts/run-ci-e2e-tests.sh b/.circleci/Dockerfiles/scripts/run-ci-e2e-tests.sh index 4c12de632141fd..c930937a593021 100755 --- a/.circleci/Dockerfiles/scripts/run-ci-e2e-tests.sh +++ b/.circleci/Dockerfiles/scripts/run-ci-e2e-tests.sh @@ -67,11 +67,6 @@ while :; do shift ;; - --tvos) - RUN_IOS=1 - shift - ;; - *) break esac diff --git a/.circleci/config.yml b/.circleci/config.yml index 705cb827733d16..9f469a546236e7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,7 +50,7 @@ executors: reactnativeios: <<: *defaults macos: - xcode: &_XCODE_VERSION "11.3.1" + xcode: &_XCODE_VERSION "11.6.0" # ------------------------- # COMMANDS @@ -133,13 +133,13 @@ commands: steps: - restore_cache: keys: - - v3-brew + - v4-brew - steps: << parameters.steps >> - save_cache: paths: - /usr/local/Homebrew - ~/Library/Caches/Homebrew - key: v3-brew + key: v4-brew with_rntester_pods_cache_span: parameters: diff --git a/.eslintrc b/.eslintrc index 47460cc9582009..2c5a51053d8221 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,15 +5,20 @@ "./packages/eslint-config-react-native-community/index.js" ], + "plugins": [ + "@react-native/eslint-plugin-codegen" + ], + "overrides": [ { "files": [ "Libraries/**/*.js", ], - rules: { - '@react-native-community/no-haste-imports': 2, - '@react-native-community/error-subclass-name': 2, - '@react-native-community/platform-colors': 2, + "rules": { + "@react-native-community/no-haste-imports": 2, + "@react-native-community/error-subclass-name": 2, + "@react-native-community/platform-colors": 2, + "@react-native/codegen/react-native-modules": 2 } }, { @@ -40,8 +45,8 @@ ], "env": { "jasmine": true, - "jest": true, - }, - }, - ], + "jest": true + } + } + ] } diff --git a/.flowconfig b/.flowconfig index 0cdac64d51d3cd..edd3fc461862b4 100644 --- a/.flowconfig +++ b/.flowconfig @@ -75,4 +75,4 @@ untyped-import untyped-type-import [version] -^0.132.0 +^0.134.0 diff --git a/.flowconfig.android b/.flowconfig.android index 4093d3710700ed..63d4ec5bcecaaa 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -75,4 +75,4 @@ untyped-import untyped-type-import [version] -^0.132.0 +^0.134.0 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 8f9372e008985a..00000000000000 --- a/.gitattributes +++ /dev/null @@ -1,6 +0,0 @@ -# Force LF line endings for Bash scripts. On Windows the rest of the source -# files will typically have CR+LF endings (Git default on Windows), but Bash -# scripts need to have LF endings to work (under Cygwin), thus override to force -# that. -gradlew text eol=lf -*.sh text eol=lf diff --git a/.gitignore b/.gitignore index ad00da2eefa7e0..382e8aec6fa49f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ project.xcworkspace # Gradle /build/ +/packages/react-native-codegen/android/build/ +/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/build +/packages/rn-tester/android/app/.cxx/ /packages/rn-tester/android/app/build/ /packages/rn-tester/android/app/gradle/ /packages/rn-tester/android/app/gradlew diff --git a/.prettierrc b/.prettierrc index 20374fd919f060..bc951b8e09ab9e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,6 @@ "singleQuote": true, "trailingComma": "all", "bracketSpacing": false, - "jsxBracketSameLine": true + "jsxBracketSameLine": true, + "arrowParens": "avoid" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8b28345822a18..5aad570ec84190 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,12 +72,12 @@ We recommend referring to the [CONTRIBUTING](https://github.com/facebook/react-n ## Contributing Code -Code-level contributions to React Native generally come in the form of [pull requests](https://help.github.com/en/articles/about-pull-requests). These are done by forking the repo and making changes locally. Directly in the repo, there is the [`RNTester` app](/RNTester) that you can install on your device (or simulators) and use to test the changes you're making to React Native sources. +Code-level contributions to React Native generally come in the form of [pull requests](https://help.github.com/en/articles/about-pull-requests). These are done by forking the repo and making changes locally. Directly in the repo, there is the [`rn-tester` app](/packages/rn-tester) that you can install on your device (or simulators) and use to test the changes you're making to React Native sources. The process of proposing a change to React Native can be summarized as follows: 1. Fork the React Native repository and create your branch from `master`. -2. Make the desired changes to React Native sources. Use the `RNTester` app to test them out. +2. Make the desired changes to React Native sources. Use the `packages/rn-tester` app to test them out. 3. If you've added code that should be tested, add tests. 4. If you've changed APIs, update the documentation, which lives in [another repo](https://github.com/facebook/react-native-website/). 5. Ensure the test suite passes, either locally or on CI once you opened a pull request. diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index 9852791cdf67a7..1fc86a0fd8872c 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -59,7 +59,7 @@ const ActionSheetIOS = { 'Options must be a valid object', ); invariant(typeof callback === 'function', 'Must provide a valid callback'); - invariant(RCTActionSheetManager, "ActionSheetManager does't exist"); + invariant(RCTActionSheetManager, "ActionSheetManager doesn't exist"); const {tintColor, destructiveButtonIndex, ...remainingOptions} = options; let destructiveButtonIndices = null; @@ -125,7 +125,7 @@ const ActionSheetIOS = { typeof successCallback === 'function', 'Must provide a valid successCallback', ); - invariant(RCTActionSheetManager, "ActionSheetManager does't exist"); + invariant(RCTActionSheetManager, "ActionSheetManager doesn't exist"); RCTActionSheetManager.showShareActionSheetWithOptions( {...options, tintColor: processColor(options.tintColor)}, failureCallback, diff --git a/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec b/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec index ef883132395026..1fc93c03536570 100644 --- a/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec +++ b/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec @@ -24,10 +24,10 @@ Pod::Spec.new do |s| s.documentation_url = "https://reactnative.dev/docs/actionsheetios" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "*.{m}" - s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" + s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" s.header_dir = "RCTActionSheet" s.dependency "React-Core/RCTActionSheetHeaders", version diff --git a/Libraries/Alert/NativeAlertManager.js b/Libraries/Alert/NativeAlertManager.js index 055a742b20340b..e3bfb8211bced6 100644 --- a/Libraries/Alert/NativeAlertManager.js +++ b/Libraries/Alert/NativeAlertManager.js @@ -26,6 +26,7 @@ export type Args = {| export interface Spec extends TurboModule { +alertWithArgs: ( + // eslint-disable-next-line @react-native/codegen/react-native-modules args: Args, callback: (id: number, value: string) => void, ) => void; diff --git a/Libraries/Animated/NativeAnimatedModule.js b/Libraries/Animated/NativeAnimatedModule.js index a42e7ba5b12bed..c16597018d1485 100644 --- a/Libraries/Animated/NativeAnimatedModule.js +++ b/Libraries/Animated/NativeAnimatedModule.js @@ -30,7 +30,7 @@ export type AnimatingNodeConfig = Object; export interface Spec extends TurboModule { +startOperationBatch: () => void; +finishOperationBatch: () => void; - + // eslint-disable-next-line @react-native/codegen/react-native-modules +createAnimatedNode: (tag: number, config: AnimatedNodeConfig) => void; +getValue: (tag: number, saveValueCallback: SaveValueCallback) => void; +startListeningToAnimatedNodeValue: (tag: number) => void; diff --git a/Libraries/Animated/NativeAnimatedTurboModule.js b/Libraries/Animated/NativeAnimatedTurboModule.js index ac6c50b45069ac..e5d5bcbeebd84f 100644 --- a/Libraries/Animated/NativeAnimatedTurboModule.js +++ b/Libraries/Animated/NativeAnimatedTurboModule.js @@ -30,7 +30,7 @@ export type AnimatingNodeConfig = Object; export interface Spec extends TurboModule { +startOperationBatch: () => void; +finishOperationBatch: () => void; - + // eslint-disable-next-line @react-native/codegen/react-native-modules +createAnimatedNode: (tag: number, config: AnimatedNodeConfig) => void; +getValue: (tag: number, saveValueCallback: SaveValueCallback) => void; +startListeningToAnimatedNodeValue: (tag: number) => void; diff --git a/Libraries/Animated/createAnimatedComponent.js b/Libraries/Animated/createAnimatedComponent.js index d5dc89b7f81f8d..ccc789c8037b46 100644 --- a/Libraries/Animated/createAnimatedComponent.js +++ b/Libraries/Animated/createAnimatedComponent.js @@ -224,7 +224,8 @@ function createAnimatedComponent( style={mergedStyle} ref={this._setComponentRef} nativeID={ - this._isFabric() ? 'animatedComponent' : undefined + props.nativeID ?? + (this._isFabric() ? 'animatedComponent' : undefined) } /* TODO: T68258846. */ // The native driver updates views directly through the UI thread so we // have to make sure the view doesn't get optimized away because it cannot diff --git a/Libraries/Animated/nodes/AnimatedDivision.js b/Libraries/Animated/nodes/AnimatedDivision.js index 437b013b525c8f..fad47ae87d1242 100644 --- a/Libraries/Animated/nodes/AnimatedDivision.js +++ b/Libraries/Animated/nodes/AnimatedDivision.js @@ -24,7 +24,7 @@ class AnimatedDivision extends AnimatedWithChildren { constructor(a: AnimatedNode | number, b: AnimatedNode | number) { super(); - if (b === 0) { + if (b === 0 || (b instanceof AnimatedNode && b.__getValue() === 0)) { console.error('Detected potential division by zero in AnimatedDivision'); } this._a = typeof a === 'number' ? new AnimatedValue(a) : a; diff --git a/Libraries/Blob/React-RCTBlob.podspec b/Libraries/Blob/React-RCTBlob.podspec index 3d98764429054a..9d691b100ab9fa 100644 --- a/Libraries/Blob/React-RCTBlob.podspec +++ b/Libraries/Blob/React-RCTBlob.podspec @@ -26,10 +26,10 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source - s.source_files = "*.{m,mm}" + s.source_files = "*.{h,m,mm}" s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" s.header_dir = "RCTBlob" s.pod_target_xcconfig = { diff --git a/Libraries/Components/AppleTV/TVEventHandler.js b/Libraries/Components/AppleTV/TVEventHandler.js deleted file mode 100644 index d3f475a1fdb777..00000000000000 --- a/Libraries/Components/AppleTV/TVEventHandler.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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. - * - * @format - * @flow - */ - -'use strict'; - -import NativeEventEmitter from '../../EventEmitter/NativeEventEmitter'; -import Platform from '../../Utilities/Platform'; -import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; -import NativeTVNavigationEventEmitter from './NativeTVNavigationEventEmitter'; - -class TVEventHandler { - __nativeTVNavigationEventListener: ?EventSubscription = null; - __nativeTVNavigationEventEmitter: ?NativeEventEmitter = null; - - enable(component: ?any, callback: Function): void { - if (Platform.OS === 'ios' && !NativeTVNavigationEventEmitter) { - return; - } - - this.__nativeTVNavigationEventEmitter = new NativeEventEmitter( - NativeTVNavigationEventEmitter, - ); - this.__nativeTVNavigationEventListener = this.__nativeTVNavigationEventEmitter.addListener( - 'onHWKeyEvent', - data => { - if (callback) { - callback(component, data); - } - }, - ); - } - - disable(): void { - if (this.__nativeTVNavigationEventListener) { - this.__nativeTVNavigationEventListener.remove(); - delete this.__nativeTVNavigationEventListener; - } - if (this.__nativeTVNavigationEventEmitter) { - delete this.__nativeTVNavigationEventEmitter; - } - } -} - -module.exports = TVEventHandler; diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index c43fde3eecedca..a6692c92bd425f 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -130,6 +130,11 @@ type Props = $ReadOnly<{| * Used only for documentation or testing (e.g. snapshot testing). */ testOnly_pressed?: ?boolean, + + /** + * Duration to wait after press down before calling `onPressIn`. + */ + unstable_pressDelay?: ?number, |}>; /** @@ -152,6 +157,7 @@ function Pressable(props: Props, forwardedRef): React.Node { pressRetentionOffset, style, testOnly_pressed, + unstable_pressDelay, ...restProps } = props; @@ -164,6 +170,14 @@ function Pressable(props: Props, forwardedRef): React.Node { const hitSlop = normalizeRect(props.hitSlop); + const restPropsWithDefaults: React.ElementConfig = { + ...restProps, + ...android_rippleConfig?.viewProps, + accessible: accessible !== false, + focusable: focusable !== false, + hitSlop, + }; + const config = useMemo( () => ({ disabled, @@ -171,6 +185,7 @@ function Pressable(props: Props, forwardedRef): React.Node { pressRectOffset: pressRetentionOffset, android_disableSound, delayLongPress, + delayPressIn: unstable_pressDelay, onLongPress, onPress, onPressIn(event: PressEvent): void { @@ -205,18 +220,15 @@ function Pressable(props: Props, forwardedRef): React.Node { onPressOut, pressRetentionOffset, setPressed, + unstable_pressDelay, ], ); const eventHandlers = usePressability(config); return ( diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index a0957a66cafb2d..7aef247b62c869 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -223,6 +223,7 @@ const ScrollResponderMixin = { // and a new touch starts with a non-textinput target (in which case the // first tap should be sent to the scroll view and dismiss the keyboard, // then the second tap goes to the actual interior view) + const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput(); const {keyboardShouldPersistTaps} = this.props; const keyboardNeverPersistTaps = !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never'; @@ -239,7 +240,7 @@ const ScrollResponderMixin = { if ( keyboardNeverPersistTaps && - this.keyboardWillOpenTo !== null && + currentlyFocusedTextInput != null && e.target != null && !TextInputState.isTextInput(e.target) ) { diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 4da263df2e56a6..a069a24c733b87 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -1206,13 +1206,12 @@ class ScrollView extends React.Component { if (refreshControl) { if (Platform.OS === 'ios') { // On iOS the RefreshControl is a child of the ScrollView. - // tvOS lacks native support for RefreshControl, so don't include it in that case return ( /* $FlowFixMe(>=0.117.0 site=react_native_fb) This comment suppresses * an error found when Flow v0.117 was deployed. To see the error, * delete this comment and run Flow. */ - {Platform.isTV ? null : refreshControl} + {refreshControl} {contentContainer} ); diff --git a/Libraries/Components/StatusBar/NativeStatusBarManagerAndroid.js b/Libraries/Components/StatusBar/NativeStatusBarManagerAndroid.js index 5b00b2a4c218a6..723ac602abb559 100644 --- a/Libraries/Components/StatusBar/NativeStatusBarManagerAndroid.js +++ b/Libraries/Components/StatusBar/NativeStatusBarManagerAndroid.js @@ -26,6 +26,7 @@ export interface Spec extends TurboModule { * - 'default' * - 'dark-content' */ + // eslint-disable-next-line @react-native/codegen/react-native-modules +setStyle: (statusBarStyle?: ?string) => void; +setHidden: (hidden: boolean) => void; } diff --git a/Libraries/Components/StatusBar/NativeStatusBarManagerIOS.js b/Libraries/Components/StatusBar/NativeStatusBarManagerIOS.js index b5da2851bf2436..86251253640059 100644 --- a/Libraries/Components/StatusBar/NativeStatusBarManagerIOS.js +++ b/Libraries/Components/StatusBar/NativeStatusBarManagerIOS.js @@ -31,6 +31,7 @@ export interface Spec extends TurboModule { * - 'dark-content' * - 'light-content' */ + // eslint-disable-next-line @react-native/codegen/react-native-modules +setStyle: (statusBarStyle?: ?string, animated: boolean) => void; /** * - withAnimation can be: 'none' | 'fade' | 'slide' diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index b510773d822cfc..a46efe02c63e99 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -487,8 +487,6 @@ export type Props = $ReadOnly<{| * * - `visible-password` * - * On Android devices manufactured by Xiaomi with Android Q, 'email-address' - * type will be replaced in native by 'default' to prevent a system related crash. */ keyboardType?: ?KeyboardType, @@ -573,6 +571,16 @@ export type Props = $ReadOnly<{| */ onEndEditing?: ?(e: EditingEvent) => mixed, + /** + * Called when a touch is engaged. + */ + onPressIn?: ?(event: PressEvent) => mixed, + + /** + * Called when a touch is released. + */ + onPressOut?: ?(event: PressEvent) => mixed, + /** * Callback that is called when the text input selection is changed. * This will be called with @@ -685,7 +693,12 @@ export type Props = $ReadOnly<{| /** * If `true`, caret is hidden. The default value is `false`. - * This property is supported only for single-line TextInput component on iOS. + * + * On Android devices manufactured by Xiaomi with Android Q, + * when keyboardType equals 'email-address'this will be set + * in native to 'true' to prevent a system related crash. This + * will cause cursor to be diabled as a side-effect. + * */ caretHidden?: ?boolean, @@ -1132,6 +1145,8 @@ function InternalTextInput(props: Props): React.Node { boolean, - onBlur: (event: BlurEvent) => mixed, - onFocus: (event: FocusEvent) => mixed, - onPress: (event: PressEvent) => mixed, -|}>; - -export default class TVTouchable { - _tvEventHandler: TVEventHandler; - - constructor(component: any, config: TVTouchableConfig) { - invariant(Platform.isTV, 'TVTouchable: Requires `Platform.isTV`.'); - this._tvEventHandler = new TVEventHandler(); - this._tvEventHandler.enable(component, (_, tvData) => { - tvData.dispatchConfig = {}; - if (ReactNative.findNodeHandle(component) === tvData.tag) { - if (tvData.eventType === 'focus') { - config.onFocus(tvData); - } else if (tvData.eventType === 'blur') { - config.onBlur(tvData); - } else if (tvData.eventType === 'select') { - if (!config.getDisabled()) { - config.onPress(tvData); - } - } - } - }); - } - - destroy(): void { - this._tvEventHandler.disable(); - } -} diff --git a/Libraries/Components/Touchable/Touchable.js b/Libraries/Components/Touchable/Touchable.js index 8bfa1cb75eeb26..8b232d13d467be 100644 --- a/Libraries/Components/Touchable/Touchable.js +++ b/Libraries/Components/Touchable/Touchable.js @@ -16,7 +16,6 @@ const Position = require('./Position'); const React = require('react'); const ReactNative = require('../../Renderer/shims/ReactNative'); const StyleSheet = require('../../StyleSheet/StyleSheet'); -const TVEventHandler = require('../AppleTV/TVEventHandler'); const UIManager = require('../../ReactNative/UIManager'); const View = require('../View/View'); const SoundManager = require('../Sound/SoundManager'); @@ -370,33 +369,12 @@ const TouchableMixin = { if (!Platform.isTV) { return; } - - this._tvEventHandler = new TVEventHandler(); - this._tvEventHandler.enable(this, function(cmp, evt) { - const myTag = ReactNative.findNodeHandle(cmp); - evt.dispatchConfig = {}; - if (myTag === evt.tag) { - if (evt.eventType === 'focus') { - cmp.touchableHandleFocus(evt); - } else if (evt.eventType === 'blur') { - cmp.touchableHandleBlur(evt); - } else if (evt.eventType === 'select' && Platform.OS !== 'android') { - cmp.touchableHandlePress && - !cmp.props.disabled && - cmp.touchableHandlePress(evt); - } - } - }); }, /** * Clear all timeouts on unmount */ componentWillUnmount: function() { - if (this._tvEventHandler) { - this._tvEventHandler.disable(); - delete this._tvEventHandler; - } this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout); this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout); this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 3a89c55d142d1b..49a9a35f078d64 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -15,7 +15,6 @@ import Pressability, { } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; -import TVTouchable from './TVTouchable'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import {Animated, Platform} from 'react-native'; import * as React from 'react'; @@ -38,8 +37,6 @@ type State = $ReadOnly<{| |}>; class TouchableBounce extends React.Component { - _tvTouchable: ?TVTouchable; - state: State = { pressability: new Pressability(this._createPressabilityConfig()), scale: new Animated.Value(1), @@ -72,11 +69,7 @@ class TouchableBounce extends React.Component { this.props.onFocus(event); } }, - onLongPress: event => { - if (this.props.onLongPress != null) { - this.props.onLongPress(event); - } - }, + onLongPress: this.props.onLongPress, onPress: event => { const {onPressAnimationComplete, onPressWithCompletion} = this.props; const releaseBounciness = this.props.releaseBounciness ?? 10; @@ -176,39 +169,11 @@ class TouchableBounce extends React.Component { ); } - componentDidMount(): void { - if (Platform.isTV) { - this._tvTouchable = new TVTouchable(this, { - getDisabled: () => this.props.disabled === true, - onBlur: event => { - if (this.props.onBlur != null) { - this.props.onBlur(event); - } - }, - onFocus: event => { - if (this.props.onFocus != null) { - this.props.onFocus(event); - } - }, - onPress: event => { - if (this.props.onPress != null) { - this.props.onPress(event); - } - }, - }); - } - } - componentDidUpdate(prevProps: Props, prevState: State) { this.state.pressability.configure(this._createPressabilityConfig()); } componentWillUnmount(): void { - if (Platform.isTV) { - if (this._tvTouchable != null) { - this._tvTouchable.destroy(); - } - } this.state.pressability.reset(); } } diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 350ea535e11fc3..327d455e283b6c 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -16,7 +16,6 @@ import Pressability, { import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import StyleSheet, {type ViewStyleProp} from '../../StyleSheet/StyleSheet'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; -import TVTouchable from './TVTouchable'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import Platform from '../../Utilities/Platform'; import View from '../../Components/View/View'; @@ -158,7 +157,6 @@ type State = $ReadOnly<{| class TouchableHighlight extends React.Component { _hideTimeout: ?TimeoutID; _isMounted: boolean = false; - _tvTouchable: ?TVTouchable; state: State = { pressability: new Pressability(this._createPressabilityConfig()), @@ -193,11 +191,7 @@ class TouchableHighlight extends React.Component { this.props.onFocus(event); } }, - onLongPress: event => { - if (this.props.onLongPress != null) { - this.props.onLongPress(event); - } - }, + onLongPress: this.props.onLongPress, onPress: event => { if (this._hideTimeout != null) { clearTimeout(this._hideTimeout); @@ -339,26 +333,6 @@ class TouchableHighlight extends React.Component { componentDidMount(): void { this._isMounted = true; - if (Platform.isTV) { - this._tvTouchable = new TVTouchable(this, { - getDisabled: () => this.props.disabled === true, - onBlur: event => { - if (this.props.onBlur != null) { - this.props.onBlur(event); - } - }, - onFocus: event => { - if (this.props.onFocus != null) { - this.props.onFocus(event); - } - }, - onPress: event => { - if (this.props.onPress != null) { - this.props.onPress(event); - } - }, - }); - } } componentDidUpdate(prevProps: Props, prevState: State) { @@ -370,11 +344,6 @@ class TouchableHighlight extends React.Component { if (this._hideTimeout != null) { clearTimeout(this._hideTimeout); } - if (Platform.isTV) { - if (this._tvTouchable != null) { - this._tvTouchable.destroy(); - } - } this.state.pressability.reset(); } } diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.js b/Libraries/Components/Touchable/TouchableNativeFeedback.js index dda6c4bf371093..907f9670537f52 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -14,7 +14,6 @@ import Pressability, { type PressabilityConfig, } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; -import TVTouchable from './TVTouchable'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import {Commands} from 'react-native/Libraries/Components/View/ViewNativeComponent'; import ReactNative from 'react-native/Libraries/Renderer/shims/ReactNative'; @@ -164,8 +163,6 @@ class TouchableNativeFeedback extends React.Component { static canUseNativeForeground: () => boolean = () => Platform.OS === 'android' && Platform.Version >= 23; - _tvTouchable: ?TVTouchable; - state: State = { pressability: new Pressability(this._createPressabilityConfig()), }; @@ -301,39 +298,11 @@ class TouchableNativeFeedback extends React.Component { ); } - componentDidMount(): void { - if (Platform.isTV) { - this._tvTouchable = new TVTouchable(this, { - getDisabled: () => this.props.disabled === true, - onBlur: event => { - if (this.props.onBlur != null) { - this.props.onBlur(event); - } - }, - onFocus: event => { - if (this.props.onFocus != null) { - this.props.onFocus(event); - } - }, - onPress: event => { - if (this.props.onPress != null) { - this.props.onPress(event); - } - }, - }); - } - } - componentDidUpdate(prevProps: Props, prevState: State) { this.state.pressability.configure(this._createPressabilityConfig()); } componentWillUnmount(): void { - if (Platform.isTV) { - if (this._tvTouchable != null) { - this._tvTouchable.destroy(); - } - } this.state.pressability.reset(); } } diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index f1d09510e03cb8..8023030a36f962 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -14,7 +14,6 @@ import Pressability, { type PressabilityConfig, } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; -import TVTouchable from './TVTouchable'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import Animated from 'react-native/Libraries/Animated/Animated'; import Easing from 'react-native/Libraries/Animated/Easing'; @@ -132,8 +131,6 @@ type State = $ReadOnly<{| * */ class TouchableOpacity extends React.Component { - _tvTouchable: ?TVTouchable; - state: State = { anim: new Animated.Value(this._getChildStyleOpacityWithDefault()), pressability: new Pressability(this._createPressabilityConfig()), @@ -258,29 +255,6 @@ class TouchableOpacity extends React.Component { ); } - componentDidMount(): void { - if (Platform.isTV) { - this._tvTouchable = new TVTouchable(this, { - getDisabled: () => this.props.disabled === true, - onBlur: event => { - if (this.props.onBlur != null) { - this.props.onBlur(event); - } - }, - onFocus: event => { - if (this.props.onFocus != null) { - this.props.onFocus(event); - } - }, - onPress: event => { - if (this.props.onPress != null) { - this.props.onPress(event); - } - }, - }); - } - } - componentDidUpdate(prevProps: Props, prevState: State) { this.state.pressability.configure(this._createPressabilityConfig()); if (this.props.disabled !== prevProps.disabled) { @@ -289,11 +263,6 @@ class TouchableOpacity extends React.Component { } componentWillUnmount(): void { - if (Platform.isTV) { - if (this._tvTouchable != null) { - this._tvTouchable.destroy(); - } - } this.state.pressability.reset(); } } diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 0cb20d0dbad078..d50b1b0bcb1a4d 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -14,7 +14,6 @@ import Pressability, { type PressabilityConfig, } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; -import TVTouchable from './TVTouchable'; import type { AccessibilityActionEvent, AccessibilityActionInfo, @@ -94,8 +93,6 @@ const PASSTHROUGH_PROPS = [ ]; class TouchableWithoutFeedback extends React.Component { - _tvTouchable: ?TVTouchable; - state: State = { pressability: new Pressability(createPressabilityConfig(this.props)), }; @@ -134,39 +131,11 @@ class TouchableWithoutFeedback extends React.Component { return React.cloneElement(element, elementProps, ...children); } - componentDidMount(): void { - if (Platform.isTV) { - this._tvTouchable = new TVTouchable(this, { - getDisabled: () => this.props.disabled === true, - onBlur: event => { - if (this.props.onBlur != null) { - this.props.onBlur(event); - } - }, - onFocus: event => { - if (this.props.onFocus != null) { - this.props.onFocus(event); - } - }, - onPress: event => { - if (this.props.onPress != null) { - this.props.onPress(event); - } - }, - }); - } - } - componentDidUpdate(): void { this.state.pressability.configure(createPressabilityConfig(this.props)); } componentWillUnmount(): void { - if (Platform.isTV) { - if (this._tvTouchable != null) { - this._tvTouchable.destroy(); - } - } this.state.pressability.reset(); } } diff --git a/Libraries/Core/NativeExceptionsManager.js b/Libraries/Core/NativeExceptionsManager.js index 10b7b77e08e816..28192d72780a75 100644 --- a/Libraries/Core/NativeExceptionsManager.js +++ b/Libraries/Core/NativeExceptionsManager.js @@ -47,6 +47,7 @@ export interface Spec extends TurboModule { stack: Array, exceptionId: number, ) => void; + // eslint-disable-next-line @react-native/codegen/react-native-modules +reportException?: (data: ExceptionData) => void; +updateExceptionMessage: ( message: string, diff --git a/Libraries/DeprecatedPropTypes/DeprecatedTVViewPropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedTVViewPropTypes.js deleted file mode 100644 index 22628f0e469446..00000000000000 --- a/Libraries/DeprecatedPropTypes/DeprecatedTVViewPropTypes.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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. - * - * @format - * @flow strict-local - */ - -'use strict'; - -const PropTypes = require('prop-types'); - -const DeprecatedTVViewPropTypes = { - hasTVPreferredFocus: PropTypes.bool, - tvParallaxShiftDistanceX: PropTypes.number, - tvParallaxShiftDistanceY: PropTypes.number, - tvParallaxTiltAngle: PropTypes.number, - tvParallaxMagnification: PropTypes.number, -}; - -module.exports = DeprecatedTVViewPropTypes; diff --git a/Libraries/EventEmitter/NativeEventEmitter.js b/Libraries/EventEmitter/NativeEventEmitter.js index 23557f71a4b9d0..e04bc5f95d06c5 100644 --- a/Libraries/EventEmitter/NativeEventEmitter.js +++ b/Libraries/EventEmitter/NativeEventEmitter.js @@ -50,7 +50,7 @@ export default class NativeEventEmitter extends EventEmitter { removeAllListeners(eventType: string) { invariant(eventType, 'eventType argument is required.'); - const count = this.listeners(eventType).length; + const count = this.listenerCount(eventType); if (this._nativeModule != null) { this._nativeModule.removeListeners(count); } diff --git a/Libraries/FBLazyVector/FBLazyVector.podspec b/Libraries/FBLazyVector/FBLazyVector.podspec index 439c326f9de31f..925860e7e9f242 100644 --- a/Libraries/FBLazyVector/FBLazyVector.podspec +++ b/Libraries/FBLazyVector/FBLazyVector.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "**/*.{c,h,m,mm,cpp}" s.header_dir = "FBLazyVector" diff --git a/Libraries/FBReactNativeSpec/BUCK b/Libraries/FBReactNativeSpec/BUCK index 3ab64ac8a7a14a..96f09f1d31c205 100644 --- a/Libraries/FBReactNativeSpec/BUCK +++ b/Libraries/FBReactNativeSpec/BUCK @@ -20,6 +20,6 @@ fb_apple_library( deps = [ "//xplat/js/react-native-github:RCTTypeSafety", "//xplat/js/react-native-github/Libraries/RCTRequired:RCTRequired", - react_native_xplat_target_apple("turbomodule/core:core"), + react_native_xplat_target_apple("react/nativemodule/core:core"), ], ) diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec.podspec b/Libraries/FBReactNativeSpec/FBReactNativeSpec.podspec index bd2a78e45cccbb..de5e03607754d2 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec.podspec +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "**/*.{c,h,m,mm,cpp}" diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h index 996488e462573a..6a2b084f362e9c 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h @@ -1916,6 +1916,8 @@ namespace JS { RCTRequired Model; NSString *ServerHost; RCTRequired uiMode; + RCTRequired Brand; + RCTRequired Manufacturer; }; /** Initialize with a set of values */ @@ -3397,6 +3399,10 @@ inline JS::NativePlatformConstantsAndroid::Constants::Builder::Builder(const Inp d[@"ServerHost"] = ServerHost; auto uiMode = i.uiMode.get(); d[@"uiMode"] = uiMode; + auto Brand = i.Brand.get(); + d[@"Brand"] = Brand; + auto Manufacturer = i.Manufacturer.get(); + d[@"Manufacturer"] = Manufacturer; return d; }) {} inline JS::NativePlatformConstantsAndroid::Constants::Builder::Builder(Constants i) : _factory(^{ diff --git a/Libraries/Image/NativeImageEditor.js b/Libraries/Image/NativeImageEditor.js index c6fdc63f7d4812..738483b15842f1 100644 --- a/Libraries/Image/NativeImageEditor.js +++ b/Libraries/Image/NativeImageEditor.js @@ -42,6 +42,7 @@ export interface Spec extends TurboModule { +getConstants: () => {||}; +cropImage: ( uri: string, + // eslint-disable-next-line @react-native/codegen/react-native-modules cropData: Options, successCallback: (uri: string) => void, errorCallback: (error: string) => void, diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index ca9f8bd9928836..db660691353909 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -15,8 +15,9 @@ #import #import #import +#import -@interface RCTImageLoader : NSObject +@interface RCTImageLoader : NSObject - (instancetype)init; - (instancetype)initWithRedirectDelegate:(id)redirectDelegate NS_DESIGNATED_INITIALIZER; - (instancetype)initWithRedirectDelegate:(id)redirectDelegate diff --git a/Libraries/Image/RCTImageLoader.mm b/Libraries/Image/RCTImageLoader.mm index 635f89d22deb7a..0ea5a4a0c988f9 100644 --- a/Libraries/Image/RCTImageLoader.mm +++ b/Libraries/Image/RCTImageLoader.mm @@ -247,7 +247,7 @@ - (void)setImageCache:(id)cache _decoders = [_bridge modulesConformingToProtocol:@protocol(RCTImageDataDecoder)]; } - _decoders = [[_bridge modulesConformingToProtocol:@protocol(RCTImageDataDecoder)] sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { + _decoders = [_decoders sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { float priorityA = [a respondsToSelector:@selector(decoderPriority)] ? [a decoderPriority] : 0; float priorityB = [b respondsToSelector:@selector(decoderPriority)] ? [b decoderPriority] : 0; if (priorityA > priorityB) { @@ -375,7 +375,9 @@ - (nullable RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLReques attribution:{} progressBlock:progressBlock partialLoadBlock:partialLoadBlock - completionBlock:completionBlock]; + completionBlock:^(NSError *error, UIImage *image, id metadata) { + completionBlock(error, image); + }]; return ^{ [request cancel]; }; @@ -457,7 +459,7 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req attribution:(const ImageURLLoaderAttribution &)attribution progressBlock:(RCTImageLoaderProgressBlock)progressHandler partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler - completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response))completionBlock + completionBlock:(void (^)(NSError *error, id imageOrData, id imageMetadata, BOOL cacheResult, NSURLResponse *response))completionBlock { { NSMutableURLRequest *mutableRequest = [request mutableCopy]; @@ -491,7 +493,7 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req __block NSLock *cancelLoadLock = [NSLock new]; NSString *requestId = [NSString stringWithFormat:@"%@-%llu",[[NSUUID UUID] UUIDString], monotonicTimeGetCurrentNanoseconds()]; - void (^completionHandler)(NSError *, id, NSURLResponse *) = ^(NSError *error, id imageOrData, NSURLResponse *response) { + void (^completionHandler)(NSError *, id, id, NSURLResponse *) = ^(NSError *error, id imageOrData, id imageMetadata, NSURLResponse *response) { [cancelLoadLock lock]; cancelLoad = nil; [cancelLoadLock unlock]; @@ -503,11 +505,11 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req // expecting it, and may do expensive post-processing in the callback dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if (!std::atomic_load(cancelled.get())) { - completionBlock(error, imageOrData, cacheResult, response); + completionBlock(error, imageOrData, imageMetadata, cacheResult, response); } }); } else if (!std::atomic_load(cancelled.get())) { - completionBlock(error, imageOrData, cacheResult, response); + completionBlock(error, imageOrData, imageMetadata, cacheResult, response); } }; @@ -524,8 +526,8 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req attribution:attributionCopy progressHandler:progressHandler partialLoadHandler:partialLoadHandler - completionHandler:^(NSError *error, UIImage *image) { - completionHandler(error, image, nil); + completionHandler:^(NSError *error, UIImage *image, id metadata) { + completionHandler(error, image, metadata, nil); }]; } RCTImageLoaderCancellationBlock cb = [loadHandler loadImageForURL:request.URL @@ -535,7 +537,7 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req progressHandler:progressHandler partialLoadHandler:partialLoadHandler completionHandler:^(NSError *error, UIImage *image) { - completionHandler(error, image, nil); + completionHandler(error, image, nil, nil); }]; return [[RCTImageURLLoaderRequest alloc] initWithRequestId:nil imageURL:request.URL cancellationBlock:cb]; } @@ -564,8 +566,8 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req attribution:attributionCopy progressHandler:progressHandler partialLoadHandler:partialLoadHandler - completionHandler:^(NSError *error, UIImage *image) { - completionHandler(error, image, nil); + completionHandler:^(NSError *error, UIImage *image, id metadata) { + completionHandler(error, image, metadata, nil); }]; cancelLoadLocal = loaderRequest.cancellationBlock; } else { @@ -576,7 +578,7 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req progressHandler:progressHandler partialLoadHandler:partialLoadHandler completionHandler:^(NSError *error, UIImage *image) { - completionHandler(error, image, nil); + completionHandler(error, image, nil, nil); }]; } [cancelLoadLock lock]; @@ -592,12 +594,14 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req } if (image) { - completionHandler(nil, image, nil); + completionHandler(nil, image, nil, nil); } else { // Use networking module to load image dispatch_block_t cancelLoadLocal = [strongSelf _loadURLRequest:request progressBlock:progressHandler - completionBlock:completionHandler]; + completionBlock:^(NSError *error, id imageOrData, NSURLResponse *response) { + completionHandler(error, imageOrData, nil, response); + }]; [cancelLoadLock lock]; cancelLoad = cancelLoadLocal; [cancelLoadLock unlock]; @@ -746,7 +750,7 @@ - (RCTImageURLLoaderRequest *)loadImageWithURLRequest:(NSURLRequest *)imageURLRe attribution:(const ImageURLLoaderAttribution &)attribution progressBlock:(RCTImageLoaderProgressBlock)progressBlock partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock + completionBlock:(RCTImageLoaderCompletionBlockWithMetadata)completionBlock { auto cancelled = std::make_shared>(0); __block dispatch_block_t cancelLoad = nil; @@ -766,7 +770,7 @@ - (RCTImageURLLoaderRequest *)loadImageWithURLRequest:(NSURLRequest *)imageURLRe }; __weak RCTImageLoader *weakSelf = self; - void (^completionHandler)(NSError *, id, BOOL, NSURLResponse *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response) { + void (^completionHandler)(NSError *, id, id, BOOL, NSURLResponse *) = ^(NSError *error, id imageOrData, id imageMetadata, BOOL cacheResult, NSURLResponse *response) { __typeof(self) strongSelf = weakSelf; if (std::atomic_load(cancelled.get()) || !strongSelf) { return; @@ -776,7 +780,7 @@ - (RCTImageURLLoaderRequest *)loadImageWithURLRequest:(NSURLRequest *)imageURLRe [cancelLoadLock lock]; cancelLoad = nil; [cancelLoadLock unlock]; - completionBlock(error, imageOrData); + completionBlock(error, imageOrData, imageMetadata); return; } @@ -793,7 +797,7 @@ - (RCTImageURLLoaderRequest *)loadImageWithURLRequest:(NSURLRequest *)imageURLRe [cancelLoadLock lock]; cancelLoad = nil; [cancelLoadLock unlock]; - completionBlock(error_, image); + completionBlock(error_, image, nil); }; dispatch_block_t cancelLoadLocal = [strongSelf decodeImageData:imageOrData size:size @@ -819,6 +823,14 @@ - (RCTImageURLLoaderRequest *)loadImageWithURLRequest:(NSURLRequest *)imageURLRe return [[RCTImageURLLoaderRequest alloc] initWithRequestId:loaderRequest.requestId imageURL:imageURLRequest.URL cancellationBlock:cancellationBlock]; } +- (NSString *)loaderModuleNameForRequestUrl:(NSURL *)url { + id loadHandler = [self imageURLLoaderForURL:url]; + if ([loadHandler respondsToSelector:@selector(loaderModuleNameForRequestUrl:)]) { + return [(id)loadHandler loaderModuleNameForRequestUrl:url]; + } + return nil; +} + - (void)trackURLImageContentDidSetForRequest:(RCTImageURLLoaderRequest *)loaderRequest { if (!loaderRequest) { @@ -979,7 +991,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data - (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest block:(void(^)(NSError *error, CGSize size))callback { - void (^completion)(NSError *, id, BOOL, NSURLResponse *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response) { + void (^completion)(NSError *, id, id, BOOL, NSURLResponse *) = ^(NSError *error, id imageOrData, id imageMetadata, BOOL cacheResult, NSURLResponse *response) { CGSize size; if ([imageOrData isKindOfClass:[NSData class]]) { NSDictionary *meta = RCTGetImageMetadata(imageOrData); diff --git a/React/CoreModules/RCTTVNavigationEventEmitter.h b/Libraries/Image/RCTImageLoaderInstrumentableProtocol.h similarity index 52% rename from React/CoreModules/RCTTVNavigationEventEmitter.h rename to Libraries/Image/RCTImageLoaderInstrumentableProtocol.h index b3bf1d43bd07a3..a803aa58daa417 100644 --- a/React/CoreModules/RCTTVNavigationEventEmitter.h +++ b/Libraries/Image/RCTImageLoaderInstrumentableProtocol.h @@ -5,10 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -#import +@protocol RCTImageLoaderInstrumentableProtocol -RCT_EXTERN NSString *const RCTTVNavigationEventNotification; - -@interface RCTTVNavigationEventEmitter : RCTEventEmitter +/** +* Image instrumentation - get name of the image loader module +*/ +- (NSString *)loaderModuleNameForRequestUrl:(NSURL *)url; @end diff --git a/Libraries/Image/RCTImageLoaderProtocol.h b/Libraries/Image/RCTImageLoaderProtocol.h index 366fb7b57ceb6a..f1bb2689607e9d 100644 --- a/Libraries/Image/RCTImageLoaderProtocol.h +++ b/Libraries/Image/RCTImageLoaderProtocol.h @@ -14,6 +14,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + /** * If available, RCTImageRedirectProtocol is invoked before loading an asset. * Implementation should return either a new URL or nil when redirection is @@ -132,3 +134,5 @@ typedef NS_ENUM(NSUInteger, RCTImageLoaderPriority) { - (void)setImageCache:(id)cache; @end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h b/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h index a862667623cb3a..bd26221531b8a4 100644 --- a/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h +++ b/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h @@ -9,13 +9,14 @@ #import #import +#import RCT_EXTERN BOOL RCTImageLoadingInstrumentationEnabled(void); RCT_EXTERN BOOL RCTImageLoadingPerfInstrumentationEnabled(void); RCT_EXTERN void RCTEnableImageLoadingInstrumentation(BOOL enabled); RCT_EXTERN void RCTEnableImageLoadingPerfInstrumentation(BOOL enabled); -@protocol RCTImageLoaderWithAttributionProtocol +@protocol RCTImageLoaderWithAttributionProtocol // TODO (T61325135): Remove C++ checks #ifdef __cplusplus @@ -32,7 +33,7 @@ RCT_EXTERN void RCTEnableImageLoadingPerfInstrumentation(BOOL enabled); attribution:(const facebook::react::ImageURLLoaderAttribution &)attribution progressBlock:(RCTImageLoaderProgressBlock)progressBlock partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock - completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; + completionBlock:(RCTImageLoaderCompletionBlockWithMetadata)completionBlock; #endif /** diff --git a/Libraries/Image/RCTImageURLLoader.h b/Libraries/Image/RCTImageURLLoader.h index d950b46f745f70..e49ed1118f326f 100644 --- a/Libraries/Image/RCTImageURLLoader.h +++ b/Libraries/Image/RCTImageURLLoader.h @@ -10,9 +10,14 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + typedef void (^RCTImageLoaderProgressBlock)(int64_t progress, int64_t total); typedef void (^RCTImageLoaderPartialLoadBlock)(UIImage *image); -typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, UIImage *image); +typedef void (^RCTImageLoaderCompletionBlock)(NSError * _Nullable error, UIImage * _Nullable image); +// Metadata is passed as a id in an additional parameter because there are forks of RN without this parameter, +// and the complexity of RCTImageLoader would make using protocols here difficult to typecheck. +typedef void (^RCTImageLoaderCompletionBlockWithMetadata)(NSError * _Nullable error, UIImage * _Nullable image, id _Nullable metadata); typedef dispatch_block_t RCTImageLoaderCancellationBlock; /** @@ -71,3 +76,5 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock; - (BOOL)shouldCacheLoadedImages; @end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/Image/RCTImageURLLoaderWithAttribution.h b/Libraries/Image/RCTImageURLLoaderWithAttribution.h index e5e8ca8484473b..b05ebfba8adfdf 100644 --- a/Libraries/Image/RCTImageURLLoaderWithAttribution.h +++ b/Libraries/Image/RCTImageURLLoaderWithAttribution.h @@ -7,6 +7,7 @@ #import #import +#import // TODO (T61325135): Remove C++ checks #ifdef __cplusplus @@ -38,7 +39,7 @@ struct ImageURLLoaderAttribution { * Same as the RCTImageURLLoader interface, but allows passing in optional `attribution` information. * This is useful for per-app logging and other instrumentation. */ -@protocol RCTImageURLLoaderWithAttribution +@protocol RCTImageURLLoaderWithAttribution // TODO (T61325135): Remove C++ checks #ifdef __cplusplus @@ -55,7 +56,7 @@ struct ImageURLLoaderAttribution { attribution:(const facebook::react::ImageURLLoaderAttribution &)attribution progressHandler:(RCTImageLoaderProgressBlock)progressHandler partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler - completionHandler:(RCTImageLoaderCompletionBlock)completionHandler; + completionHandler:(RCTImageLoaderCompletionBlockWithMetadata)completionHandler; #endif /** diff --git a/Libraries/Image/RCTImageView.mm b/Libraries/Image/RCTImageView.mm index c0b23f78f5ee0e..2b5fce147f653b 100644 --- a/Libraries/Image/RCTImageView.mm +++ b/Libraries/Image/RCTImageView.mm @@ -332,7 +332,7 @@ - (void)reloadImage imageScale = source.scale; } - RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *loadedImage) { + RCTImageLoaderCompletionBlockWithMetadata completionHandler = ^(NSError *error, UIImage *loadedImage, id metadata) { [weakSelf imageLoaderLoadedImage:loadedImage error:error forImageSource:source partial:NO]; }; diff --git a/Libraries/Image/React-RCTImage.podspec b/Libraries/Image/React-RCTImage.podspec index d0de0bd722e4e1..2678065c862b1e 100644 --- a/Libraries/Image/React-RCTImage.podspec +++ b/Libraries/Image/React-RCTImage.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.documentation_url = "https://reactnative.dev/docs/image" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "*.{m,mm}" diff --git a/Libraries/Inspector/PerformanceOverlay.js b/Libraries/Inspector/PerformanceOverlay.js index a7307bda9174a6..128abb43099330 100644 --- a/Libraries/Inspector/PerformanceOverlay.js +++ b/Libraries/Inspector/PerformanceOverlay.js @@ -22,7 +22,7 @@ class PerformanceOverlay extends React.Component<{...}> { const items = []; for (const key in perfLogs) { - if (perfLogs[key].totalTime) { + if (perfLogs[key]?.totalTime) { const unit = key === 'BundleSize' ? 'b' : 'ms'; items.push( diff --git a/Libraries/Interaction/TaskQueue.js b/Libraries/Interaction/TaskQueue.js index 965953f64fb8d0..3fad71ae0a0d7c 100644 --- a/Libraries/Interaction/TaskQueue.js +++ b/Libraries/Interaction/TaskQueue.js @@ -156,6 +156,7 @@ class TaskQueue { // happens once it is fully processed. this._queueStack.push({tasks: [], popable: false}); const stackIdx = this._queueStack.length - 1; + const stackItem = this._queueStack[stackIdx]; DEBUG && infoLog('TaskQueue: push new queue: ', {stackIdx}); DEBUG && infoLog('TaskQueue: exec gen task ' + task.name); task @@ -166,7 +167,7 @@ class TaskQueue { stackIdx, queueStackSize: this._queueStack.length, }); - this._queueStack[stackIdx].popable = true; + stackItem.popable = true; this.hasTasksToProcess() && this._onMoreTasks(); }) .catch(ex => { diff --git a/Libraries/Interaction/__tests__/TaskQueue-test.js b/Libraries/Interaction/__tests__/TaskQueue-test.js index 5055fa2dde853b..aeed06ca0aeae6 100644 --- a/Libraries/Interaction/__tests__/TaskQueue-test.js +++ b/Libraries/Interaction/__tests__/TaskQueue-test.js @@ -150,4 +150,20 @@ describe('TaskQueue', () => { expect(task1).not.toBeCalled(); expect(taskQueue.hasTasksToProcess()).toBe(false); }); + + it('should not crash when task is cancelled between being started and resolved', () => { + const task1 = jest.fn(() => { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1); + }); + }); + + taskQueue.enqueue({gen: task1, name: 'gen1'}); + taskQueue.processNext(); + taskQueue.cancelTasks([task1]); + + jest.runAllTimers(); + }); }); diff --git a/Libraries/LinkingIOS/React-RCTLinking.podspec b/Libraries/LinkingIOS/React-RCTLinking.podspec index 5f07abebc676a3..fe2c5d33079dec 100644 --- a/Libraries/LinkingIOS/React-RCTLinking.podspec +++ b/Libraries/LinkingIOS/React-RCTLinking.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.documentation_url = "https://reactnative.dev/docs/linking" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "*.{m,mm}" diff --git a/Libraries/LogBox/Data/parseLogBoxLog.js b/Libraries/LogBox/Data/parseLogBoxLog.js index c382ca301b37de..e9021e9b4192de 100644 --- a/Libraries/LogBox/Data/parseLogBoxLog.js +++ b/Libraries/LogBox/Data/parseLogBoxLog.js @@ -131,8 +131,8 @@ export function parseInterpolation( } function isComponentStack(consoleArgument: string) { - const isOldComponentStackFormat = /\s{4}in/.test(consoleArgument); - const isNewComponentStackFormat = /\s{4}at/.test(consoleArgument); + const isOldComponentStackFormat = / {4}in/.test(consoleArgument); + const isNewComponentStackFormat = / {4}at/.test(consoleArgument); const isNewJSCComponentStackFormat = /@.*\n/.test(consoleArgument); return ( diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm b/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm index 35adf39289aa07..33fe87ad4d4a10 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.mm @@ -46,6 +46,7 @@ - (instancetype)init - (void)invalidate { + [super invalidate]; [_nodesManager stopAnimationLoop]; [self.bridge.eventDispatcher removeDispatchObserver:self]; [self.bridge.uiManager.observerCoordinator removeObserver:self]; diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm b/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm index 6fc594872f874b..45213380d81d7a 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm +++ b/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm @@ -47,6 +47,7 @@ - (instancetype)init - (void)invalidate { + [super invalidate]; [_nodesManager stopAnimationLoop]; [self.bridge.eventDispatcher removeDispatchObserver:self]; [self.bridge.uiManager.observerCoordinator removeObserver:self]; diff --git a/Libraries/NativeAnimation/React-RCTAnimation.podspec b/Libraries/NativeAnimation/React-RCTAnimation.podspec index 01f605c9822f46..cf09cbff7c0c05 100644 --- a/Libraries/NativeAnimation/React-RCTAnimation.podspec +++ b/Libraries/NativeAnimation/React-RCTAnimation.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "{Drivers/*,Nodes/*,*}.{m,mm}" diff --git a/Libraries/NativeModules/specs/NativeDialogManagerAndroid.js b/Libraries/NativeModules/specs/NativeDialogManagerAndroid.js index fcc476b3419c33..809f488f0a1212 100644 --- a/Libraries/NativeModules/specs/NativeDialogManagerAndroid.js +++ b/Libraries/NativeModules/specs/NativeDialogManagerAndroid.js @@ -40,6 +40,7 @@ export interface Spec extends TurboModule { +buttonNeutral: DialogButtonKey, |}; +showAlert: ( + // eslint-disable-next-line @react-native/codegen/react-native-modules config: DialogOptions, onError: (error: string) => void, onAction: (action: DialogAction, buttonKey?: DialogButtonKey) => void, diff --git a/Libraries/Network/RCTNetworking.mm b/Libraries/Network/RCTNetworking.mm index b857c43a9086e4..c4dbf8a7bf3364 100644 --- a/Libraries/Network/RCTNetworking.mm +++ b/Libraries/Network/RCTNetworking.mm @@ -168,6 +168,8 @@ - (instancetype)initWithHandlersProvider:(NSArray> * (^ - (void)invalidate { + [super invalidate]; + for (NSNumber *requestID in _tasksByRequestID) { [_tasksByRequestID[requestID] cancel]; } @@ -680,7 +682,7 @@ - (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request completionBlo @"timeout": @(query.timeout()), @"withCredentials": @(query.withCredentials()), }; - + // TODO: buildRequest returns a cancellation block, but there's currently // no way to invoke it, if, for example the request is cancelled while // loading a large file to build the request body diff --git a/Libraries/Network/React-RCTNetwork.podspec b/Libraries/Network/React-RCTNetwork.podspec index e21afd21a309d7..e868267d51d079 100644 --- a/Libraries/Network/React-RCTNetwork.podspec +++ b/Libraries/Network/React-RCTNetwork.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "*.{m,mm}" diff --git a/Libraries/Network/XMLHttpRequest.js b/Libraries/Network/XMLHttpRequest.js index 27df314c5244eb..be38c07ca21902 100644 --- a/Libraries/Network/XMLHttpRequest.js +++ b/Libraries/Network/XMLHttpRequest.js @@ -10,9 +10,11 @@ 'use strict'; +import type {IPerformanceLogger} from '../Utilities/createPerformanceLogger'; + const BlobManager = require('../Blob/BlobManager'); const EventTarget = require('event-target-shim'); -const GlobalPerformanceLogger = require('react-native/Libraries/Utilities/GlobalPerformanceLogger'); +const GlobalPerformanceLogger = require('../Utilities/GlobalPerformanceLogger'); const RCTNetworking = require('./RCTNetworking'); const base64 = require('base64-js'); @@ -141,6 +143,7 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): any) { _timedOut: boolean = false; _trackingName: string = 'unknown'; _incrementalEvents: boolean = false; + _performanceLogger: IPerformanceLogger = GlobalPerformanceLogger; static setInterceptor(interceptor: ?XHRInterceptor) { XMLHttpRequest._interceptor = interceptor; @@ -302,7 +305,7 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): any) { ): void { if (requestId === this._requestId) { this._perfKey != null && - GlobalPerformanceLogger.stopTimespan(this._perfKey); + this._performanceLogger.stopTimespan(this._perfKey); this.status = status; this.setResponseHeaders(responseHeaders); this.setReadyState(this.HEADERS_RECEIVED); @@ -447,6 +450,14 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): any) { return this; } + /** + * Custom extension for setting a custom performance logger + */ + setPerformanceLogger(performanceLogger: IPerformanceLogger): XMLHttpRequest { + this._performanceLogger = performanceLogger; + return this; + } + open(method: string, url: string, async: ?boolean): void { /* Other optional arguments are not supported yet */ if (this.readyState !== this.UNSENT) { @@ -519,7 +530,7 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): any) { const friendlyName = this._trackingName !== 'unknown' ? this._trackingName : this._url; this._perfKey = 'network_XMLHttpRequest_' + String(friendlyName); - GlobalPerformanceLogger.startTimespan(this._perfKey); + this._performanceLogger.startTimespan(this._perfKey); invariant( this._method, 'XMLHttpRequest method needs to be defined (%s).', diff --git a/Libraries/Network/__tests__/XMLHttpRequest-test.js b/Libraries/Network/__tests__/XMLHttpRequest-test.js index f7f1bfcf32d56f..c36977f1bc7945 100644 --- a/Libraries/Network/__tests__/XMLHttpRequest-test.js +++ b/Libraries/Network/__tests__/XMLHttpRequest-test.js @@ -9,8 +9,13 @@ */ 'use strict'; + jest.unmock('../../Utilities/Platform'); +jest.mock('../../Utilities/GlobalPerformanceLogger'); + const Platform = require('../../Utilities/Platform'); +const GlobalPerformanceLogger = require('../../Utilities/GlobalPerformanceLogger'); +const createPerformanceLogger = require('../../Utilities/createPerformanceLogger'); let requestId = 1; function setRequestId(id) { @@ -71,6 +76,8 @@ describe('XMLHttpRequest', function() { xhr.addEventListener('load', handleLoad); xhr.addEventListener('loadend', handleLoadEnd); xhr.addEventListener('readystatechange', handleReadyStateChange); + + jest.clearAllMocks(); }); afterEach(() => { @@ -238,4 +245,52 @@ describe('XMLHttpRequest', function() { 'Content-Type: text/plain; charset=utf-8\r\n' + 'Content-Length: 32', ); }); + + it('should log to GlobalPerformanceLogger if a custom performance logger is not set', () => { + xhr.open('GET', 'blabla'); + xhr.send(); + + expect(GlobalPerformanceLogger.startTimespan).toHaveBeenCalledWith( + 'network_XMLHttpRequest_blabla', + ); + expect(GlobalPerformanceLogger.stopTimespan).not.toHaveBeenCalled(); + + setRequestId(8); + xhr.__didReceiveResponse(requestId, 200, { + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Length': '32', + }); + + expect(GlobalPerformanceLogger.stopTimespan).toHaveBeenCalledWith( + 'network_XMLHttpRequest_blabla', + ); + }); + + it('should log to a custom performance logger if set', () => { + const performanceLogger = createPerformanceLogger(); + jest.spyOn(performanceLogger, 'startTimespan'); + jest.spyOn(performanceLogger, 'stopTimespan'); + + xhr.setPerformanceLogger(performanceLogger); + + xhr.open('GET', 'blabla'); + xhr.send(); + + expect(performanceLogger.startTimespan).toHaveBeenCalledWith( + 'network_XMLHttpRequest_blabla', + ); + expect(GlobalPerformanceLogger.startTimespan).not.toHaveBeenCalled(); + expect(performanceLogger.stopTimespan).not.toHaveBeenCalled(); + + setRequestId(9); + xhr.__didReceiveResponse(requestId, 200, { + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Length': '32', + }); + + expect(performanceLogger.stopTimespan).toHaveBeenCalledWith( + 'network_XMLHttpRequest_blabla', + ); + expect(GlobalPerformanceLogger.stopTimespan).not.toHaveBeenCalled(); + }); }); diff --git a/Libraries/PermissionsAndroid/NativePermissionsAndroid.js b/Libraries/PermissionsAndroid/NativePermissionsAndroid.js index 2e834fc725cb42..2b53284da78b98 100644 --- a/Libraries/PermissionsAndroid/NativePermissionsAndroid.js +++ b/Libraries/PermissionsAndroid/NativePermissionsAndroid.js @@ -47,6 +47,7 @@ export type PermissionType = */ export interface Spec extends TurboModule { + // eslint-disable-next-line @react-native/codegen/react-native-modules +checkPermission: (permission: PermissionType) => Promise; +requestPermission: (permission: PermissionType) => Promise; +shouldShowRequestPermissionRationale: ( diff --git a/Libraries/Pressability/Pressability.js b/Libraries/Pressability/Pressability.js index 31d32166804bed..ba6ba885dff9ad 100644 --- a/Libraries/Pressability/Pressability.js +++ b/Libraries/Pressability/Pressability.js @@ -276,8 +276,7 @@ const isPressInSignal = signal => const isTerminalSignal = signal => signal === 'RESPONDER_TERMINATED' || signal === 'RESPONDER_RELEASE'; -const DEFAULT_LONG_PRESS_DELAY_MS = 370; // 500 - 130 -const DEFAULT_PRESS_DELAY_MS = 130; +const DEFAULT_LONG_PRESS_DELAY_MS = 500; const DEFAULT_PRESS_RECT_OFFSETS = { bottom: 30, left: 20, @@ -472,12 +471,7 @@ export default class Pressability { this._touchState = 'NOT_RESPONDER'; this._receiveSignal('RESPONDER_GRANT', event); - const delayPressIn = normalizeDelay( - this._config.delayPressIn, - 0, - DEFAULT_PRESS_DELAY_MS, - ); - + const delayPressIn = normalizeDelay(this._config.delayPressIn); if (delayPressIn > 0) { this._pressDelayTimeout = setTimeout(() => { this._receiveSignal('DELAY', event); @@ -489,7 +483,7 @@ export default class Pressability { const delayLongPress = normalizeDelay( this._config.delayLongPress, 10, - DEFAULT_LONG_PRESS_DELAY_MS, + DEFAULT_LONG_PRESS_DELAY_MS - delayPressIn, ); this._longPressDelayTimeout = setTimeout(() => { this._handleLongPress(event); @@ -685,6 +679,11 @@ export default class Pressability { } if (isPressInSignal(prevState) && signal === 'RESPONDER_RELEASE') { + // If we never activated (due to delays), activate and deactivate now. + if (!isNextActive && !isPrevActive) { + this._activate(event); + this._deactivate(event); + } const {onLongPress, onPress, android_disableSound} = this._config; if (onPress != null) { const isPressCanceledByLongPress = @@ -692,11 +691,6 @@ export default class Pressability { prevState === 'RESPONDER_ACTIVE_LONG_PRESS_IN' && this._shouldLongPressCancelPress(); if (!isPressCanceledByLongPress) { - // If we never activated (due to delays), activate and deactivate now. - if (!isNextActive && !isPrevActive) { - this._activate(event); - this._deactivate(event); - } if (Platform.OS === 'android' && android_disableSound !== true) { SoundManager.playTouchSound(); } diff --git a/Libraries/Pressability/__tests__/Pressability-test.js b/Libraries/Pressability/__tests__/Pressability-test.js index 52c35d00c1faae..d584d215b7cc3e 100644 --- a/Libraries/Pressability/__tests__/Pressability-test.js +++ b/Libraries/Pressability/__tests__/Pressability-test.js @@ -355,7 +355,7 @@ describe('Pressability', () => { expect(config.onLongPress).toBeCalled(); }); - it('is called if pressed for 370ms after the press delay', () => { + it('is called if pressed for 500ms after press started', () => { const {config, handlers} = createMockPressability({ delayPressIn: 100, }); @@ -364,7 +364,7 @@ describe('Pressability', () => { handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); handlers.onResponderMove(createMockPressEvent('onResponderMove')); - jest.advanceTimersByTime(469); + jest.advanceTimersByTime(499); expect(config.onLongPress).not.toBeCalled(); jest.advanceTimersByTime(1); expect(config.onLongPress).toBeCalled(); @@ -393,7 +393,7 @@ describe('Pressability', () => { handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); handlers.onResponderMove(createMockPressEvent('onResponderMove')); - jest.advanceTimersByTime(139); + jest.advanceTimersByTime(9); expect(config.onLongPress).not.toBeCalled(); jest.advanceTimersByTime(1); expect(config.onLongPress).toBeCalled(); @@ -460,7 +460,13 @@ describe('Pressability', () => { const {config, handlers} = createMockPressability(); handlers.onStartShouldSetResponder(); - handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); + handlers.onResponderGrant( + createMockPressEvent({ + registrationName: 'onResponderGrant', + pageX: 0, + pageY: 0, + }), + ); handlers.onResponderMove( createMockPressEvent({ registrationName: 'onResponderMove', @@ -475,7 +481,13 @@ describe('Pressability', () => { // Subsequent long touch gesture should not carry over previous state. handlers.onStartShouldSetResponder(); - handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); + handlers.onResponderGrant( + createMockPressEvent({ + registrationName: 'onResponderGrant', + pageX: 7, + pageY: 8, + }), + ); handlers.onResponderMove( // NOTE: Delta from (0, 0) is ~10.6 > 10, but should not matter. createMockPressEvent({ @@ -522,7 +534,7 @@ describe('Pressability', () => { expect(config.onPressIn).toBeCalled(); }); - it('is called after the default delay by default', () => { + it('is called immediately by default', () => { const {config, handlers} = createMockPressability({ delayPressIn: null, }); @@ -531,24 +543,6 @@ describe('Pressability', () => { handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); handlers.onResponderMove(createMockPressEvent('onResponderMove')); - jest.advanceTimersByTime(129); - expect(config.onPressIn).not.toBeCalled(); - jest.advanceTimersByTime(1); - expect(config.onPressIn).toBeCalled(); - }); - - it('falls back to the default delay if `delayPressIn` is omitted', () => { - const {config, handlers} = createMockPressability({ - delayPressIn: null, - }); - - handlers.onStartShouldSetResponder(); - handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); - handlers.onResponderMove(createMockPressEvent('onResponderMove')); - - jest.advanceTimersByTime(129); - expect(config.onPressIn).not.toBeCalled(); - jest.advanceTimersByTime(1); expect(config.onPressIn).toBeCalled(); }); @@ -582,7 +576,9 @@ describe('Pressability', () => { describe('onPressOut', () => { it('is called after `onResponderRelease` before `delayPressIn`', () => { - const {config, handlers} = createMockPressability(); + const {config, handlers} = createMockPressability({ + delayPressIn: Number.EPSILON, + }); handlers.onStartShouldSetResponder(); handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); @@ -596,7 +592,9 @@ describe('Pressability', () => { }); it('is called after `onResponderRelease` after `delayPressIn`', () => { - const {config, handlers} = createMockPressability(); + const {config, handlers} = createMockPressability({ + delayPressIn: Number.EPSILON, + }); handlers.onStartShouldSetResponder(); handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); @@ -611,7 +609,9 @@ describe('Pressability', () => { }); it('is not called after `onResponderTerminate` before `delayPressIn`', () => { - const {config, handlers} = createMockPressability(); + const {config, handlers} = createMockPressability({ + delayPressIn: Number.EPSILON, + }); handlers.onStartShouldSetResponder(); handlers.onResponderGrant(createMockPressEvent('onResponderGrant')); diff --git a/Libraries/Pressability/usePressability.js b/Libraries/Pressability/usePressability.js index 3538524239387b..9ed4cc22197437 100644 --- a/Libraries/Pressability/usePressability.js +++ b/Libraries/Pressability/usePressability.js @@ -16,11 +16,16 @@ import Pressability, { } from './Pressability'; import {useEffect, useRef} from 'react'; +/** + * Creates a persistent instance of `Pressability` that automatically configures + * itself and resets. Accepts null `config` to support lazy initialization. Once + * initialized, will not un-initialize until the component has been unmounted. + */ export default function usePressability( - config: PressabilityConfig, -): EventHandlers { + config: ?PressabilityConfig, +): ?EventHandlers { const pressabilityRef = useRef(null); - if (pressabilityRef.current == null) { + if (config != null && pressabilityRef.current == null) { pressabilityRef.current = new Pressability(config); } const pressability = pressabilityRef.current; @@ -28,16 +33,20 @@ export default function usePressability( // On the initial mount, this is a no-op. On updates, `pressability` will be // re-configured to use the new configuration. useEffect(() => { - pressability.configure(config); + if (config != null && pressability != null) { + pressability.configure(config); + } }, [config, pressability]); // On unmount, reset pending state and timers inside `pressability`. This is // a separate effect because we do not want to reset when `config` changes. useEffect(() => { - return () => { - pressability.reset(); - }; + if (pressability != null) { + return () => { + pressability.reset(); + }; + } }, [pressability]); - return pressability.getEventHandlers(); + return pressability == null ? null : pressability.getEventHandlers(); } diff --git a/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js b/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js index 2e2044262eefaa..941ba0a3c77f99 100644 --- a/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js +++ b/Libraries/PushNotificationIOS/NativePushNotificationManagerIOS.js @@ -32,7 +32,7 @@ type Notification = {| |}; export interface Spec extends TurboModule { - +getConstants: () => {...}; + +getConstants: () => {||}; +onFinishRemoteNotification: ( notificationId: string, /** @@ -52,6 +52,7 @@ export interface Spec extends TurboModule { |}) => Promise; +abandonPermissions: () => void; +checkPermissions: (callback: (permissions: Permissions) => void) => void; + // eslint-disable-next-line @react-native/codegen/react-native-modules +presentLocalNotification: (notification: Notification) => void; +scheduleLocalNotification: (notification: Notification) => void; +cancelAllLocalNotifications: () => void; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index c4281361e6d673..e486252c704b4a 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -13,7 +13,7 @@ extern NSString *const RCTRemoteNotificationReceived; typedef void (^RCTRemoteNotificationCallback)(UIBackgroundFetchResult result); -#if !TARGET_OS_TV && !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_UIKITFORMAC + (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; + (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; + (void)didReceiveRemoteNotification:(NSDictionary *)notification; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index 95eec384e9a9a6..02fb7e4214bdcb 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -25,7 +25,7 @@ static NSString *const kErrorUnableToRequestPermissions = @"E_UNABLE_TO_REQUEST_PERMISSIONS"; -#if !TARGET_OS_TV +#if !TARGET_OS_UIKITFORMAC @implementation RCTConvert (NSCalendarUnit) RCT_ENUM_CONVERTER(NSCalendarUnit, @@ -79,11 +79,11 @@ + (UILocalNotification *)UILocalNotification:(id)json #else @interface RCTPushNotificationManager () @end -#endif //TARGET_OS_TV / TARGET_OS_UIKITFORMAC +#endif // TARGET_OS_UIKITFORMAC @implementation RCTPushNotificationManager -#if !TARGET_OS_TV && !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_UIKITFORMAC static NSDictionary *RCTFormatLocalNotification(UILocalNotification *notification) { @@ -128,7 +128,7 @@ @implementation RCTPushNotificationManager return formattedNotification; } -#endif //TARGET_OS_TV / TARGET_OS_UIKITFORMAC +#endif // TARGET_OS_UIKITFORMAC RCT_EXPORT_MODULE() @@ -137,7 +137,7 @@ - (dispatch_queue_t)methodQueue return dispatch_get_main_queue(); } -#if !TARGET_OS_TV && !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_UIKITFORMAC - (void)startObserving { [[NSNotificationCenter defaultCenter] addObserver:self @@ -472,7 +472,7 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification }]; } -#else //TARGET_OS_TV / TARGET_OS_UIKITFORMAC +#else // TARGET_OS_UIKITFORMAC RCT_EXPORT_METHOD(onFinishRemoteNotification:(NSString *)notificationId fetchResult:(NSString *)fetchResult) { @@ -557,7 +557,7 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification return @[]; } -#endif //TARGET_OS_TV / TARGET_OS_UIKITFORMAC +#endif // TARGET_OS_UIKITFORMAC - (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { diff --git a/Libraries/PushNotificationIOS/React-RCTPushNotification.podspec b/Libraries/PushNotificationIOS/React-RCTPushNotification.podspec index a2c915316eb7a0..f1f734adabd1a4 100644 --- a/Libraries/PushNotificationIOS/React-RCTPushNotification.podspec +++ b/Libraries/PushNotificationIOS/React-RCTPushNotification.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.documentation_url = "https://reactnative.dev/docs/pushnotificationios" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "*.{m,mm}" diff --git a/Libraries/RCTRequired/RCTRequired.podspec b/Libraries/RCTRequired/RCTRequired.podspec index 3c55234fbd3e50..45d537db606814 100644 --- a/Libraries/RCTRequired/RCTRequired.podspec +++ b/Libraries/RCTRequired/RCTRequired.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "**/*.{c,h,m,mm,cpp}" s.header_dir = "RCTRequired" diff --git a/Libraries/ReactNative/renderApplication.js b/Libraries/ReactNative/renderApplication.js index 7d7b3b4409b1f7..a757414252dffb 100644 --- a/Libraries/ReactNative/renderApplication.js +++ b/Libraries/ReactNative/renderApplication.js @@ -33,9 +33,10 @@ function renderApplication( ) { invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag); + const performanceLogger = scopedPerformanceLogger ?? GlobalPerformanceLogger; + const renderable = ( - + ( ); - GlobalPerformanceLogger.startTimespan('renderApplication_React_render'); + performanceLogger.startTimespan('renderApplication_React_render'); if (fabric) { require('../Renderer/shims/ReactFabric').render(renderable, rootTag); } else { require('../Renderer/shims/ReactNative').render(renderable, rootTag); } - GlobalPerformanceLogger.stopTimespan('renderApplication_React_render'); + performanceLogger.stopTimespan('renderApplication_React_render'); } module.exports = renderApplication; diff --git a/Libraries/Settings/React-RCTSettings.podspec b/Libraries/Settings/React-RCTSettings.podspec index 45c92ba557b623..c9790ff0e8e29d 100644 --- a/Libraries/Settings/React-RCTSettings.podspec +++ b/Libraries/Settings/React-RCTSettings.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.documentation_url = "https://reactnative.dev/docs/settings" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "*.{m,mm}" diff --git a/Libraries/Share/NativeShareModule.js b/Libraries/Share/NativeShareModule.js index 08d313d76d7d6c..e6d8800725b30e 100644 --- a/Libraries/Share/NativeShareModule.js +++ b/Libraries/Share/NativeShareModule.js @@ -17,6 +17,7 @@ export interface Spec extends TurboModule { +getConstants: () => {||}; +share: ( content: {|title?: string, message?: string|}, + // eslint-disable-next-line @react-native/codegen/react-native-modules dialogTitle?: string, ) => Promise<{|action: string|}>; } diff --git a/Libraries/Text/React-RCTText.podspec b/Libraries/Text/React-RCTText.podspec index 6f94706d54e3bc..af8a8f6ec70156 100644 --- a/Libraries/Text/React-RCTText.podspec +++ b/Libraries/Text/React-RCTText.podspec @@ -24,7 +24,7 @@ Pod::Spec.new do |s| s.documentation_url = "https://reactnative.dev/docs/text" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "**/*.{h,m}" s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index 6ff8c14c267d92..681922ea13fdde 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -10,14 +10,13 @@ 'use strict'; +import {NativeText, NativeVirtualText} from './TextNativeComponent'; + const DeprecatedTextPropTypes = require('../DeprecatedPropTypes/DeprecatedTextPropTypes'); const React = require('react'); -const ReactNativeViewAttributes = require('../Components/View/ReactNativeViewAttributes'); const TextAncestor = require('./TextAncestor'); const Touchable = require('../Components/Touchable/Touchable'); -const UIManager = require('../ReactNative/UIManager'); -const createReactNativeComponentClass = require('../Renderer/shims/createReactNativeComponentClass'); const nullthrows = require('nullthrows'); const processColor = require('../StyleSheet/processColor'); @@ -27,7 +26,7 @@ import type {PressRetentionOffset, TextProps} from './TextProps'; type ResponseHandlers = $ReadOnly<{| onStartShouldSetResponder: () => boolean, - onResponderGrant: (event: PressEvent, dispatchID: string) => void, + onResponderGrant: (event: PressEvent) => void, onResponderMove: (event: PressEvent) => void, onResponderRelease: (event: PressEvent) => void, onResponderTerminate: (event: PressEvent) => void, @@ -36,7 +35,7 @@ type ResponseHandlers = $ReadOnly<{| type Props = $ReadOnly<{| ...TextProps, - forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>, + forwardedRef: ?React.Ref, |}>; type State = {| @@ -51,36 +50,6 @@ type State = {| const PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; -const viewConfig = { - validAttributes: { - ...ReactNativeViewAttributes.UIView, - isHighlighted: true, - numberOfLines: true, - ellipsizeMode: true, - allowFontScaling: true, - maxFontSizeMultiplier: true, - disabled: true, - selectable: true, - selectionColor: true, - adjustsFontSizeToFit: true, - minimumFontScale: true, - textBreakStrategy: true, - onTextLayout: true, - onInlineViewLayout: true, - dataDetectorType: true, - android_hyphenationFrequency: true, - }, - directEventTypes: { - topTextLayout: { - registrationName: 'onTextLayout', - }, - topInlineViewLayout: { - registrationName: 'onInlineViewLayout', - }, - }, - uiViewClassName: 'RCTText', -}; - /** * A React component for displaying text. * @@ -98,10 +67,7 @@ class TouchableText extends React.Component { touchableHandleActivePressOut: ?() => void; touchableHandleLongPress: ?(event: PressEvent) => void; touchableHandlePress: ?(event: PressEvent) => void; - touchableHandleResponderGrant: ?( - event: PressEvent, - dispatchID: string, - ) => void; + touchableHandleResponderGrant: ?(event: PressEvent) => void; touchableHandleResponderMove: ?(event: PressEvent) => void; touchableHandleResponderRelease: ?(event: PressEvent) => void; touchableHandleResponderTerminate: ?(event: PressEvent) => void; @@ -125,21 +91,19 @@ class TouchableText extends React.Component { : null; } - static viewConfig = viewConfig; - render(): React.Node { - let props = this.props; - if (isTouchable(props)) { + let {forwardedRef, selectionColor, ...props} = this.props; + if (isTouchable(this.props)) { props = { ...props, ...this.state.responseHandlers, isHighlighted: this.state.isHighlighted, }; } - if (props.selectionColor != null) { + if (selectionColor != null) { props = { ...props, - selectionColor: processColor(props.selectionColor), + selectionColor: processColor(selectionColor), }; } if (__DEV__) { @@ -154,10 +118,17 @@ class TouchableText extends React.Component { {hasTextAncestor => hasTextAncestor ? ( - + // $FlowFixMe[prop-missing] For the `onClick` workaround. + ) : ( - + ) } @@ -179,10 +150,10 @@ class TouchableText extends React.Component { } return shouldSetResponder; }, - onResponderGrant: (event: PressEvent, dispatchID: string): void => { - nullthrows(this.touchableHandleResponderGrant)(event, dispatchID); + onResponderGrant: (event: PressEvent): void => { + nullthrows(this.touchableHandleResponderGrant)(event); if (this.props.onResponderGrant != null) { - this.props.onResponderGrant.call(this, event, dispatchID); + this.props.onResponderGrant.call(this, event); } }, onResponderMove: (event: PressEvent): void => { @@ -260,26 +231,9 @@ const isTouchable = (props: Props): boolean => props.onLongPress != null || props.onStartShouldSetResponder != null; -const RCTText = createReactNativeComponentClass( - viewConfig.uiViewClassName, - () => viewConfig, -); - -const RCTVirtualText = - UIManager.getViewManagerConfig('RCTVirtualText') == null - ? RCTText - : createReactNativeComponentClass('RCTVirtualText', () => ({ - validAttributes: { - ...ReactNativeViewAttributes.UIView, - isHighlighted: true, - maxFontSizeMultiplier: true, - }, - uiViewClassName: 'RCTVirtualText', - })); - const Text = ( props: TextProps, - forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>, + forwardedRef: ?React.Ref, ) => { return ; }; diff --git a/Libraries/Text/Text/RCTTextView.m b/Libraries/Text/Text/RCTTextView.m index 0c13d038cb3502..bda5f8d078f428 100644 --- a/Libraries/Text/Text/RCTTextView.m +++ b/Libraries/Text/Text/RCTTextView.m @@ -227,7 +227,7 @@ - (void)disableContextMenu - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) -#if !TARGET_OS_TV && !TARGET_OS_UIKITFORMAC +#if !TARGET_OS_UIKITFORMAC UIMenuController *menuController = [UIMenuController sharedMenuController]; if (menuController.isMenuVisible) { @@ -259,7 +259,6 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender - (void)copy:(id)sender { -#if !TARGET_OS_TV NSAttributedString *attributedText = _textStorage; NSMutableDictionary *item = [NSMutableDictionary new]; @@ -276,7 +275,6 @@ - (void)copy:(id)sender UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.items = @[item]; -#endif } @end diff --git a/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.m b/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.m index 6f9696029b049f..7a661b183fa008 100644 --- a/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.m +++ b/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.m @@ -19,8 +19,6 @@ - (UIView *)view #pragma mark - Multiline (aka TextView) specific properties -#if !TARGET_OS_TV RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) -#endif @end diff --git a/Libraries/Text/TextInput/Multiline/RCTUITextView.m b/Libraries/Text/TextInput/Multiline/RCTUITextView.m index 88d3183f4df345..a417cc11751099 100644 --- a/Libraries/Text/TextInput/Multiline/RCTUITextView.m +++ b/Libraries/Text/TextInput/Multiline/RCTUITextView.m @@ -51,15 +51,22 @@ - (instancetype)initWithFrame:(CGRect)frame self.textColor = [UIColor blackColor]; // This line actually removes 5pt (default value) left and right padding in UITextView. self.textContainer.lineFragmentPadding = 0; -#if !TARGET_OS_TV self.scrollsToTop = NO; -#endif self.scrollEnabled = YES; } return self; } +- (void)setDelegate:(id)delegate { + // Delegate is set inside `[RCTBackedTextViewDelegateAdapter initWithTextView]` and + // it cannot be changed from outside. + if (super.delegate) { + return; + } + [super setDelegate:delegate]; +} + #pragma mark - Accessibility - (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index 9edad515f00861..01ab7f19d12267 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -245,7 +245,7 @@ - (void)setTextContentType:(NSString *)type }; #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ - if (@available(iOS 11.0, tvOS 11.0, *)) { + if (@available(iOS 11.0, *)) { NSDictionary * iOS11extras = @{@"username": UITextContentTypeUsername, @"password": UITextContentTypePassword}; @@ -257,7 +257,7 @@ - (void)setTextContentType:(NSString *)type #endif #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000 /* __IPHONE_12_0 */ - if (@available(iOS 12.0, tvOS 12.0, *)) { + if (@available(iOS 12.0, *)) { NSDictionary * iOS12extras = @{@"newPassword": UITextContentTypeNewPassword, @"oneTimeCode": UITextContentTypeOneTimeCode}; @@ -572,7 +572,6 @@ - (void)didSetProps:(NSArray *)changedProps - (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID { - #if !TARGET_OS_TV __weak RCTBaseTextInputView *weakSelf = self; [_bridge.uiManager rootViewForReactTag:self.reactTag withCompletion:^(UIView *rootView) { RCTBaseTextInputView *strongSelf = weakSelf; @@ -585,12 +584,10 @@ - (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID } } }]; - #endif /* !TARGET_OS_TV */ } - (void)setDefaultInputAccessoryView { - #if !TARGET_OS_TV UIView *textInputView = self.backedTextInputView; UIKeyboardType keyboardType = textInputView.keyboardType; @@ -629,7 +626,6 @@ - (void)setDefaultInputAccessoryView textInputView.inputAccessoryView = nil; } [self reloadInputViewsIfNecessary]; - #endif /* !TARGET_OS_TV */ } - (void)reloadInputViewsIfNecessary diff --git a/Libraries/Text/TextInput/RCTInputAccessoryViewContent.m b/Libraries/Text/TextInput/RCTInputAccessoryViewContent.m index de77e47bf2d0d7..35db4d308733c4 100644 --- a/Libraries/Text/TextInput/RCTInputAccessoryViewContent.m +++ b/Libraries/Text/TextInput/RCTInputAccessoryViewContent.m @@ -30,7 +30,7 @@ - (instancetype)init _heightConstraint = [_safeAreaContainer.heightAnchor constraintEqualToConstant:0]; _heightConstraint.active = YES; - if (@available(iOS 11.0, tvOS 11.0, *)) { + if (@available(iOS 11.0, *)) { [_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor].active = YES; [_safeAreaContainer.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor].active = YES; [_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leadingAnchor].active = YES; diff --git a/Libraries/Text/TextNativeComponent.js b/Libraries/Text/TextNativeComponent.js new file mode 100644 index 00000000000000..eafc7ddf8d1483 --- /dev/null +++ b/Libraries/Text/TextNativeComponent.js @@ -0,0 +1,68 @@ +/** + * 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. + * + * @flow + * @format + */ + +'use strict'; + +import ReactNativeViewAttributes from '../Components/View/ReactNativeViewAttributes'; +import UIManager from '../ReactNative/UIManager'; +import {type HostComponent} from '../Renderer/shims/ReactNativeTypes'; +import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; +import {type ProcessedColorValue} from '../StyleSheet/processColor'; +import {type TextProps} from './TextProps'; + +type NativeTextProps = $ReadOnly<{ + ...TextProps, + isHighlighted?: ?boolean, + selectionColor?: ?ProcessedColorValue, +}>; + +export const NativeText: HostComponent = (createReactNativeComponentClass( + 'RCTText', + () => ({ + validAttributes: { + ...ReactNativeViewAttributes.UIView, + isHighlighted: true, + numberOfLines: true, + ellipsizeMode: true, + allowFontScaling: true, + maxFontSizeMultiplier: true, + disabled: true, + selectable: true, + selectionColor: true, + adjustsFontSizeToFit: true, + minimumFontScale: true, + textBreakStrategy: true, + onTextLayout: true, + onInlineViewLayout: true, + dataDetectorType: true, + }, + directEventTypes: { + topTextLayout: { + registrationName: 'onTextLayout', + }, + topInlineViewLayout: { + registrationName: 'onInlineViewLayout', + }, + }, + uiViewClassName: 'RCTText', + }), +): any); + +export const NativeVirtualText: HostComponent = + UIManager.getViewManagerConfig('RCTVirtualText') == null + ? NativeText + : (createReactNativeComponentClass('RCTVirtualText', () => ({ + validAttributes: { + ...ReactNativeViewAttributes.UIView, + isHighlighted: true, + maxFontSizeMultiplier: true, + }, + uiViewClassName: 'RCTVirtualText', + })): any); diff --git a/Libraries/Text/TextProps.js b/Libraries/Text/TextProps.js index 4b48bc57066a4d..9555e576618303 100644 --- a/Libraries/Text/TextProps.js +++ b/Libraries/Text/TextProps.js @@ -122,7 +122,7 @@ export type TextProps = $ReadOnly<{| * See https://reactnative.dev/docs/text.html#onpress */ onPress?: ?(event: PressEvent) => mixed, - onResponderGrant?: ?(event: PressEvent, dispatchID: string) => void, + onResponderGrant?: ?(event: PressEvent) => void, onResponderMove?: ?(event: PressEvent) => void, onResponderRelease?: ?(event: PressEvent) => void, onResponderTerminate?: ?(event: PressEvent) => void, diff --git a/Libraries/TurboModule/samples/NativeSampleTurboModule.js b/Libraries/TurboModule/samples/NativeSampleTurboModule.js index f9c4c6ef53e393..5af0c1f77c690e 100644 --- a/Libraries/TurboModule/samples/NativeSampleTurboModule.js +++ b/Libraries/TurboModule/samples/NativeSampleTurboModule.js @@ -26,6 +26,7 @@ export interface Spec extends TurboModule { +getString: (arg: string) => string; +getArray: (arg: Array) => Array; +getObject: (arg: Object) => Object; + // eslint-disable-next-line @react-native/codegen/react-native-modules +getRootTag: (arg: RootTag) => RootTag; +getValue: (x: number, y: string, z: Object) => Object; +getValueWithCallback: (callback: (value: string) => void) => void; diff --git a/Libraries/TypeSafety/RCTTypeSafety.podspec b/Libraries/TypeSafety/RCTTypeSafety.podspec index 1dd04a8c29a6e8..96d90e6949b1de 100644 --- a/Libraries/TypeSafety/RCTTypeSafety.podspec +++ b/Libraries/TypeSafety/RCTTypeSafety.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags s.source = source s.source_files = "**/*.{c,h,m,mm,cpp}" diff --git a/Libraries/Utilities/BackHandler.android.js b/Libraries/Utilities/BackHandler.android.js index e762c1635f93f7..efb07ec5918ea6 100644 --- a/Libraries/Utilities/BackHandler.android.js +++ b/Libraries/Utilities/BackHandler.android.js @@ -35,10 +35,6 @@ RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() { * Android: Detect hardware back button presses, and programmatically invoke the default back button * functionality to exit the app if there are no listeners or if none of the listeners return true. * - * tvOS: Detect presses of the menu button on the TV remote. (Still to be implemented: - * programmatically disable menu button handling - * functionality to exit the app if there are no listeners or if none of the listeners return true.) - * * iOS: Not applicable. * * The event subscriptions are called in reverse order (i.e. last registered subscription first), @@ -82,8 +78,7 @@ const BackHandler: TBackHandler = { /** * Adds an event handler. Supported events: * - * - `hardwareBackPress`: Fires when the Android hardware back button is pressed or when the - * tvOS menu button is pressed. + * - `hardwareBackPress`: Fires when the Android hardware back button is pressed. */ addEventListener: function( eventName: BackPressEventName, diff --git a/Libraries/Utilities/BackHandler.ios.js b/Libraries/Utilities/BackHandler.ios.js index 1407a57e95ff0d..2e1e5c4cd2a7ac 100644 --- a/Libraries/Utilities/BackHandler.ios.js +++ b/Libraries/Utilities/BackHandler.ios.js @@ -4,52 +4,18 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict-local * @format + * @flow */ -// On Apple TV, this implements back navigation using the TV remote's menu button. -// On iOS, this just implements a stub. - 'use strict'; -const Platform = require('./Platform'); -const TVEventHandler = require('../Components/AppleTV/TVEventHandler'); +module.exports = require('../Components/UnimplementedViews/UnimplementedView'); type BackPressEventName = 'backPress' | 'hardwareBackPress'; function emptyFunction(): void {} -/** - * Detect hardware button presses for back navigation. - * - * Android: Detect hardware back button presses, and programmatically invoke the default back button - * functionality to exit the app if there are no listeners or if none of the listeners return true. - * - * tvOS: Detect presses of the menu button on the TV remote. (Still to be implemented: - * programmatically disable menu button handling - * functionality to exit the app if there are no listeners or if none of the listeners return true.) - * - * iOS: Not applicable. - * - * The event subscriptions are called in reverse order (i.e. last registered subscription first), - * and if one subscription returns true then subscriptions registered earlier will not be called. - * - * Example: - * - * ```javascript - * BackHandler.addEventListener('hardwareBackPress', function() { - * // this.onMainScreen and this.goBack are just examples, you need to use your own implementation here - * // Typically you would use the navigator here to go to the last state. - * - * if (!this.onMainScreen()) { - * this.goBack(); - * return true; - * } - * return false; - * }); - * ``` - */ type TBackHandler = {| +exitApp: () => void, +addEventListener: ( @@ -62,65 +28,14 @@ type TBackHandler = {| ) => void, |}; -let BackHandler: TBackHandler; - -if (Platform.isTV) { - const _tvEventHandler = new TVEventHandler(); - const _backPressSubscriptions = new Set(); - - _tvEventHandler.enable(this, function(cmp, evt) { - if (evt && evt.eventType === 'menu') { - let invokeDefault = true; - const subscriptions = Array.from( - _backPressSubscriptions.values(), - ).reverse(); - - for (let i = 0; i < subscriptions.length; ++i) { - if (subscriptions[i]()) { - invokeDefault = false; - break; - } - } - - if (invokeDefault) { - BackHandler.exitApp(); - } - } - }); - - BackHandler = { - exitApp: emptyFunction, - - addEventListener: function( - eventName: BackPressEventName, - handler: () => ?boolean, - ): {remove: () => void, ...} { - _backPressSubscriptions.add(handler); - return { - remove: () => BackHandler.removeEventListener(eventName, handler), - }; - }, - - removeEventListener: function( - eventName: BackPressEventName, - handler: () => ?boolean, - ): void { - _backPressSubscriptions.delete(handler); - }, - }; -} else { - BackHandler = { - exitApp: emptyFunction, - addEventListener(_eventName: BackPressEventName, _handler: () => ?boolean) { - return { - remove: emptyFunction, - }; - }, - removeEventListener( - _eventName: BackPressEventName, - _handler: () => ?boolean, - ) {}, - }; -} +let BackHandler: TBackHandler = { + exitApp: emptyFunction, + addEventListener(_eventName: BackPressEventName, _handler: Function) { + return { + remove: emptyFunction, + }; + }, + removeEventListener(_eventName: BackPressEventName, _handler: Function) {}, +}; module.exports = BackHandler; diff --git a/Libraries/Utilities/NativePlatformConstantsAndroid.js b/Libraries/Utilities/NativePlatformConstantsAndroid.js index c12339267e5081..993b8091ddb487 100644 --- a/Libraries/Utilities/NativePlatformConstantsAndroid.js +++ b/Libraries/Utilities/NativePlatformConstantsAndroid.js @@ -29,6 +29,8 @@ export interface Spec extends TurboModule { Model: string, ServerHost?: string, uiMode: string, + Brand: string, + Manufacturer: string, |}; +getAndroidID: () => string; } diff --git a/Libraries/Utilities/Platform.android.js b/Libraries/Utilities/Platform.android.js index ece5fe511ad288..ab3bc9b345c5b2 100644 --- a/Libraries/Utilities/Platform.android.js +++ b/Libraries/Utilities/Platform.android.js @@ -42,6 +42,8 @@ const Platform = { Model: string, ServerHost?: string, uiMode: string, + Brand: string, + Manufacturer: string, |} { if (this.__constants == null) { this.__constants = NativePlatformConstantsAndroid.getConstants(); diff --git a/Libraries/Utilities/__tests__/PerformanceLogger-test.js b/Libraries/Utilities/__tests__/PerformanceLogger-test.js index 9af931852397ab..d26cbede535bef 100644 --- a/Libraries/Utilities/__tests__/PerformanceLogger-test.js +++ b/Libraries/Utilities/__tests__/PerformanceLogger-test.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local */ 'use strict'; @@ -14,30 +15,64 @@ import createPerformanceLogger from '../createPerformanceLogger'; import type {IPerformanceLogger} from '../createPerformanceLogger'; const TIMESPAN_1 = ''; -const TIMESPAN_2 = ''; -const TIMESPAN_2_DURATION = 123; const EXTRA_KEY = ''; const EXTRA_VALUE = ''; const EXTRA_VALUE_2 = ''; const POINT = ''; const POINT_TIMESTAMP = 99; const POINT_TIMESTAMP_2 = 999; +const POINT_ANNOTATION_1 = {extra: 'value1'}; +const POINT_ANNOTATION_2 = {extra: 'value2'}; describe('PerformanceLogger', () => { beforeEach(() => { GlobalPerformanceLogger.clear(); }); + describe('close() ', () => { + let perfLogger; + beforeEach(() => { + perfLogger = createPerformanceLogger(); + }); + + it('does not markPoint', () => { + perfLogger.close(); + perfLogger.markPoint(POINT, POINT_TIMESTAMP); + expect(perfLogger.getPoints()).toEqual({}); + }); + it('does not startTimespan', () => { + perfLogger.close(); + perfLogger.startTimespan(TIMESPAN_1); + expect(perfLogger.getTimespans()).toEqual({}); + }); + it('does not setExtra', () => { + perfLogger.close(); + perfLogger.setExtra('extra', 'an extra value'); + expect(perfLogger.getTimespans()).toEqual({}); + }); - it('starts & stops and adds a timespan', () => { + it('does not stopTimespan', () => { + perfLogger.startTimespan(TIMESPAN_1); + perfLogger.close(); + let timespan = perfLogger.getTimespans()[TIMESPAN_1]; + expect(timespan?.endTime).toBeUndefined(); + expect(timespan?.totalTime).toBeUndefined(); + perfLogger.stopTimespan(TIMESPAN_1); + timespan = perfLogger.getTimespans()[TIMESPAN_1]; + expect(timespan?.endTime).toBeUndefined(); + expect(timespan?.totalTime).toBeUndefined(); + }); + }); + + it('starts & stops a timespan', () => { let perfLogger = createPerformanceLogger(); perfLogger.startTimespan(TIMESPAN_1); perfLogger.stopTimespan(TIMESPAN_1); - perfLogger.addTimeAnnotation(TIMESPAN_2, TIMESPAN_2_DURATION); expect(perfLogger.hasTimespan(TIMESPAN_1)).toBe(true); - expect(perfLogger.hasTimespan(TIMESPAN_2)).toBe(true); - expect(perfLogger.getTimespans()[TIMESPAN_2].totalTime).toBe( - TIMESPAN_2_DURATION, - ); + expect(perfLogger.getTimespans()[TIMESPAN_1]).toEqual({ + startTime: expect.any(Number), + endTime: expect.any(Number), + totalTime: expect.any(Number), + }); }); it('does not override a timespan', () => { @@ -46,18 +81,14 @@ describe('PerformanceLogger', () => { let old = perfLogger.getTimespans()[TIMESPAN_1]; perfLogger.startTimespan(TIMESPAN_1); expect(perfLogger.getTimespans()[TIMESPAN_1]).toBe(old); - perfLogger.addTimeAnnotation(TIMESPAN_1, 1); - expect(perfLogger.getTimespans()[TIMESPAN_1]).toBe(old); }); it('adds a timespan with start and end timestamps', () => { let perfLogger = createPerformanceLogger(); const startTime = 0; const endTime = 100; - const description = 'description'; - perfLogger.addTimespan(TIMESPAN_1, startTime, endTime, description); + perfLogger.addTimespan(TIMESPAN_1, startTime, endTime); expect(perfLogger.getTimespans()[TIMESPAN_1]).toEqual({ - description, startTime, endTime, totalTime: endTime - startTime, @@ -69,7 +100,7 @@ describe('PerformanceLogger', () => { perfLogger.startTimespan(TIMESPAN_1); perfLogger.stopTimespan(TIMESPAN_1); const existing = perfLogger.getTimespans()[TIMESPAN_1]; - perfLogger.addTimespan(TIMESPAN_1, 0, 100, 'overriding'); + perfLogger.addTimespan(TIMESPAN_1, 0, 100); expect(perfLogger.getTimespans()[TIMESPAN_1]).toEqual(existing); }); @@ -172,4 +203,24 @@ describe('PerformanceLogger', () => { checkLogger(localPerformanceLogger2, true); checkLogger(GlobalPerformanceLogger, true); }); + + it('records extras for a timespan', () => { + let perfLogger = createPerformanceLogger(); + perfLogger.startTimespan(TIMESPAN_1, POINT_ANNOTATION_1); + perfLogger.stopTimespan(TIMESPAN_1, POINT_ANNOTATION_2); + expect(perfLogger.getTimespans()[TIMESPAN_1]?.startExtras).toEqual( + POINT_ANNOTATION_1, + ); + expect(perfLogger.getTimespans()[TIMESPAN_1]?.endExtras).toEqual( + POINT_ANNOTATION_2, + ); + }); + + it('records extras for a point', () => { + let perfLogger = createPerformanceLogger(); + perfLogger.markPoint(POINT, POINT_TIMESTAMP, POINT_ANNOTATION_1); + + expect(Object.keys(perfLogger.getPointExtras())).toEqual([POINT]); + expect(perfLogger.getPointExtras()[POINT]).toEqual(POINT_ANNOTATION_1); + }); }); diff --git a/Libraries/Utilities/createPerformanceLogger.js b/Libraries/Utilities/createPerformanceLogger.js index 1cb2df6348f365..2e8753f9ec0877 100644 --- a/Libraries/Utilities/createPerformanceLogger.js +++ b/Libraries/Utilities/createPerformanceLogger.js @@ -13,245 +13,294 @@ const Systrace = require('../Performance/Systrace'); const infoLog = require('./infoLog'); -const performanceNow = +const performanceNow: () => number = global.nativeQPLTimestamp ?? global.performance.now.bind(global.performance); type Timespan = { - description?: string, - totalTime?: number, - startTime?: number, + startTime: number, endTime?: number, - ... + totalTime?: number, + startExtras?: Extras, + endExtras?: Extras, }; -export type IPerformanceLogger = { - addTimeAnnotation(string, number, string | void): void, - addTimespan(string, number, number, string | void): void, - startTimespan(string, string | void): void, - stopTimespan(string, options?: {update?: boolean}): void, - clear(): void, - clearCompleted(): void, - currentTimestamp(): number, - getTimespans(): {[key: string]: Timespan, ...}, - hasTimespan(string): boolean, - setExtra(string, mixed): void, - getExtras(): {[key: string]: mixed, ...}, - removeExtra(string): ?mixed, - markPoint(string, number | void): void, - getPoints(): {[key: string]: number, ...}, - logEverything(): void, - ... -}; +// Extra values should be serializable primitives +type ExtraValue = number | string | boolean; + +type Extras = {[key: string]: ExtraValue}; + +export interface IPerformanceLogger { + addTimespan( + key: string, + startTime: number, + endTime: number, + startExtras?: Extras, + endExtras?: Extras, + ): void; + clear(): void; + clearCompleted(): void; + close(): void; + currentTimestamp(): number; + getExtras(): {[key: string]: ?ExtraValue, ...}; + getPoints(): {[key: string]: ?number, ...}; + getPointExtras(): {[key: string]: ?Extras, ...}; + getTimespans(): {[key: string]: ?Timespan, ...}; + hasTimespan(key: string): boolean; + isClosed(): boolean; + logEverything(): void; + markPoint(key: string, timestamp?: number, extras?: Extras): void; + removeExtra(key: string): ?ExtraValue; + setExtra(key: string, value: ExtraValue): void; + startTimespan(key: string, extras?: Extras): void; + stopTimespan(key: string, extras?: Extras): void; +} const _cookies: {[key: string]: number, ...} = {}; const PRINT_TO_CONSOLE: false = false; // Type as false to prevent accidentally committing `true`; -/** - * This function creates performance loggers that can be used to collect and log - * various performance data such as timespans, points and extras. - * The loggers need to have minimal overhead since they're used in production. - */ -function createPerformanceLogger(): IPerformanceLogger { - const result: IPerformanceLogger & { - _timespans: {[key: string]: Timespan, ...}, - _extras: {[key: string]: mixed, ...}, - _points: {[key: string]: number, ...}, - ... - } = { - _timespans: {}, - _extras: {}, - _points: {}, - - addTimeAnnotation(key: string, durationInMs: number, description?: string) { - if (this._timespans[key]) { - if (PRINT_TO_CONSOLE && __DEV__) { - infoLog( - 'PerformanceLogger: Attempting to add a timespan that already exists ', - key, - ); - } - return; +class PerformanceLogger implements IPerformanceLogger { + _timespans: {[key: string]: ?Timespan} = {}; + _extras: {[key: string]: ?ExtraValue} = {}; + _points: {[key: string]: ?number} = {}; + _pointExtras: {[key: string]: ?Extras, ...} = {}; + _closed: boolean = false; + + addTimespan( + key: string, + startTime: number, + endTime: number, + startExtras?: Extras, + endExtras?: Extras, + ) { + if (this._closed) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog('PerformanceLogger: addTimespan - has closed ignoring: ', key); } + return; + } + if (this._timespans[key]) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog( + 'PerformanceLogger: Attempting to add a timespan that already exists ', + key, + ); + } + return; + } - this._timespans[key] = { - description: description, - totalTime: durationInMs, - }; - }, - - addTimespan( - key: string, - startTime: number, - endTime: number, - description?: string, - ) { - if (this._timespans[key]) { - if (PRINT_TO_CONSOLE && __DEV__) { - infoLog( - 'PerformanceLogger: Attempting to add a timespan that already exists ', - key, - ); - } - return; + this._timespans[key] = { + startTime, + endTime, + totalTime: endTime - (startTime || 0), + startExtras, + endExtras, + }; + } + + clear() { + this._timespans = {}; + this._extras = {}; + this._points = {}; + if (PRINT_TO_CONSOLE) { + infoLog('PerformanceLogger.js', 'clear'); + } + } + + clearCompleted() { + for (const key in this._timespans) { + if (this._timespans[key]?.totalTime != null) { + delete this._timespans[key]; } + } + this._extras = {}; + this._points = {}; + if (PRINT_TO_CONSOLE) { + infoLog('PerformanceLogger.js', 'clearCompleted'); + } + } + + close() { + this._closed = true; + } + + currentTimestamp() { + return performanceNow(); + } + + getExtras() { + return this._extras; + } - this._timespans[key] = { - description, - startTime, - endTime, - totalTime: endTime - (startTime || 0), - }; - }, - - startTimespan(key: string, description?: string) { - if (this._timespans[key]) { - if (PRINT_TO_CONSOLE && __DEV__) { - infoLog( - 'PerformanceLogger: Attempting to start a timespan that already exists ', - key, - ); + getPoints() { + return this._points; + } + + getPointExtras() { + return this._pointExtras; + } + + getTimespans() { + return this._timespans; + } + + hasTimespan(key: string) { + return !!this._timespans[key]; + } + + isClosed() { + return this._closed; + } + + logEverything() { + if (PRINT_TO_CONSOLE) { + // log timespans + for (const key in this._timespans) { + if (this._timespans[key]?.totalTime != null) { + infoLog(key + ': ' + this._timespans[key].totalTime + 'ms'); } - return; } - this._timespans[key] = { - description: description, - startTime: performanceNow(), - }; - _cookies[key] = Systrace.beginAsyncEvent(key); - if (PRINT_TO_CONSOLE) { - infoLog('PerformanceLogger.js', 'start: ' + key); - } - }, - - stopTimespan(key: string, options?: {update?: boolean}) { - const timespan = this._timespans[key]; - if (!timespan || !timespan.startTime) { - if (PRINT_TO_CONSOLE && __DEV__) { - infoLog( - 'PerformanceLogger: Attempting to end a timespan that has not started ', - key, - ); + // log extras + infoLog(this._extras); + + // log points + for (const key in this._points) { + if (this._points[key] != null) { + infoLog(key + ': ' + this._points[key] + 'ms'); } - return; } - if (timespan.endTime && !options?.update) { - if (PRINT_TO_CONSOLE && __DEV__) { - infoLog( - 'PerformanceLogger: Attempting to end a timespan that has already ended ', - key, - ); - } - return; + } + } + + markPoint(key: string, timestamp?: number, extras?: Extras) { + if (this._closed) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog('PerformanceLogger: markPoint - has closed ignoring: ', key); + } + return; + } + if (this._points[key] != null) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog( + 'PerformanceLogger: Attempting to mark a point that has been already logged ', + key, + ); } + return; + } + this._points[key] = timestamp ?? performanceNow(); + if (extras) { + this._pointExtras[key] = extras; + } + } + + removeExtra(key: string): ?ExtraValue { + const value = this._extras[key]; + delete this._extras[key]; + return value; + } - timespan.endTime = performanceNow(); - timespan.totalTime = timespan.endTime - (timespan.startTime || 0); - if (PRINT_TO_CONSOLE) { - infoLog('PerformanceLogger.js', 'end: ' + key); + setExtra(key: string, value: ExtraValue) { + if (this._closed) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog('PerformanceLogger: setExtra - has closed ignoring: ', key); } + return; + } - if (_cookies[key] != null) { - Systrace.endAsyncEvent(key, _cookies[key]); - delete _cookies[key]; + if (this._extras.hasOwnProperty(key)) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog( + 'PerformanceLogger: Attempting to set an extra that already exists ', + {key, currentValue: this._extras[key], attemptedValue: value}, + ); } - }, - - clear() { - this._timespans = {}; - this._extras = {}; - this._points = {}; - if (PRINT_TO_CONSOLE) { - infoLog('PerformanceLogger.js', 'clear'); + return; + } + this._extras[key] = value; + } + + startTimespan(key: string, extras?: Extras) { + if (this._closed) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog( + 'PerformanceLogger: startTimespan - has closed ignoring: ', + key, + ); } - }, + return; + } - clearCompleted() { - for (const key in this._timespans) { - if (this._timespans[key].totalTime) { - delete this._timespans[key]; - } + if (this._timespans[key]) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog( + 'PerformanceLogger: Attempting to start a timespan that already exists ', + key, + ); } - this._extras = {}; - this._points = {}; - if (PRINT_TO_CONSOLE) { - infoLog('PerformanceLogger.js', 'clearCompleted'); + return; + } + + this._timespans[key] = { + startTime: performanceNow(), + startExtras: extras, + }; + _cookies[key] = Systrace.beginAsyncEvent(key); + if (PRINT_TO_CONSOLE) { + infoLog('PerformanceLogger.js', 'start: ' + key); + } + } + + stopTimespan(key: string, extras?: Extras) { + if (this._closed) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog('PerformanceLogger: stopTimespan - has closed ignoring: ', key); } - }, - - currentTimestamp() { - return performanceNow(); - }, - - getTimespans() { - return this._timespans; - }, - - hasTimespan(key: string) { - return !!this._timespans[key]; - }, - - setExtra(key: string, value: mixed) { - if (this._extras[key]) { - if (PRINT_TO_CONSOLE && __DEV__) { - infoLog( - 'PerformanceLogger: Attempting to set an extra that already exists ', - {key, currentValue: this._extras[key], attemptedValue: value}, - ); - } - return; + return; + } + + const timespan = this._timespans[key]; + if (!timespan || timespan.startTime == null) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog( + 'PerformanceLogger: Attempting to end a timespan that has not started ', + key, + ); } - this._extras[key] = value; - }, - - getExtras() { - return this._extras; - }, - - removeExtra(key: string): ?mixed { - const value = this._extras[key]; - delete this._extras[key]; - return value; - }, - - markPoint(key: string, timestamp?: number) { - if (this._points[key]) { - if (PRINT_TO_CONSOLE && __DEV__) { - infoLog( - 'PerformanceLogger: Attempting to mark a point that has been already logged ', - key, - ); - } - return; + return; + } + if (timespan.endTime != null) { + if (PRINT_TO_CONSOLE && __DEV__) { + infoLog( + 'PerformanceLogger: Attempting to end a timespan that has already ended ', + key, + ); } - this._points[key] = timestamp ?? performanceNow(); - }, - - getPoints() { - return this._points; - }, - - logEverything() { - if (PRINT_TO_CONSOLE) { - // log timespans - for (const key in this._timespans) { - if (this._timespans[key].totalTime) { - infoLog(key + ': ' + this._timespans[key].totalTime + 'ms'); - } - } + return; + } - // log extras - infoLog(this._extras); + timespan.endExtras = extras; + timespan.endTime = performanceNow(); + timespan.totalTime = timespan.endTime - (timespan.startTime || 0); + if (PRINT_TO_CONSOLE) { + infoLog('PerformanceLogger.js', 'end: ' + key); + } - // log points - for (const key in this._points) { - infoLog(key + ': ' + this._points[key] + 'ms'); - } - } - }, - }; - return result; + if (_cookies[key] != null) { + Systrace.endAsyncEvent(key, _cookies[key]); + delete _cookies[key]; + } + } +} + +/** + * This function creates performance loggers that can be used to collect and log + * various performance data such as timespans, points and extras. + * The loggers need to have minimal overhead since they're used in production. + */ +function createPerformanceLogger(): IPerformanceLogger { + return new PerformanceLogger(); } module.exports = createPerformanceLogger; diff --git a/Libraries/Vibration/React-RCTVibration.podspec b/Libraries/Vibration/React-RCTVibration.podspec index c08abe6407cc3a..87e0659e4d1c4f 100644 --- a/Libraries/Vibration/React-RCTVibration.podspec +++ b/Libraries/Vibration/React-RCTVibration.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.documentation_url = "https://reactnative.dev/docs/vibration" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "*.{m,mm}" diff --git a/Libraries/vendor/emitter/_EventEmitter.js b/Libraries/vendor/emitter/_EventEmitter.js index 8ec88d27d02246..150c2538916698 100644 --- a/Libraries/vendor/emitter/_EventEmitter.js +++ b/Libraries/vendor/emitter/_EventEmitter.js @@ -93,23 +93,21 @@ class EventEmitter { } /** - * Returns an array of listeners that are currently registered for the given + * Returns the number of listeners that are currently registered for the given * event. * * @param {string} eventType - Name of the event to query - * @returns {array} + * @returns {number} */ - listeners(eventType: string): [EmitterSubscription] { + listenerCount(eventType: string): number { const subscriptions = this._subscriber.getSubscriptionsForType(eventType); return subscriptions - ? subscriptions - // We filter out missing entries because the array is sparse. - // "callbackfn is called only for elements of the array which actually - // exist; it is not called for missing elements of the array." - // https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.filter - .filter(sparseFilterPredicate) - .map(subscription => subscription.listener) - : []; + ? // We filter out missing entries because the array is sparse. + // "callbackfn is called only for elements of the array which actually + // exist; it is not called for missing elements of the array." + // https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.filter + subscriptions.filter(sparseFilterPredicate).length + : 0; } /** diff --git a/React-Core.podspec b/React-Core.podspec index 7a004607739d7f..5c69ed70f06778 100644 --- a/React-Core.podspec +++ b/React-Core.podspec @@ -41,14 +41,14 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.resource_bundle = { "AccessibilityResources" => ["React/AccessibilityResources/*.lproj"]} s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags s.header_dir = "React" s.framework = "JavaScriptCore" s.library = "stdc++" - s.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/boost-for-react-native\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/RCT-Folly\"" } + s.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/boost-for-react-native\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/RCT-Folly\"", "DEFINES_MODULE" => "YES" } s.user_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Headers/Private/React-Core\""} s.default_subspec = "Default" @@ -59,13 +59,6 @@ Pod::Spec.new do |s| "React/Fabric/**/*", "React/Tests/**/*", "React/Inspector/**/*" - ss.ios.exclude_files = "React/**/RCTTV*.*" - ss.tvos.exclude_files = "React/Modules/RCTClipboard*", - "React/Views/RCTDatePicker*", - "React/Views/RCTPicker*", - "React/Views/RCTRefreshControl*", - "React/Views/RCTSlider*", - "React/Views/RCTSwitch*", ss.private_header_files = "React/Cxx*/*.h" end diff --git a/React.podspec b/React.podspec index da482ff0154c7e..a6511d2d1aeadb 100644 --- a/React.podspec +++ b/React.podspec @@ -36,7 +36,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" s.cocoapods_version = ">= 1.2.0" diff --git a/React/Base/RCTAssert.h b/React/Base/RCTAssert.h index 6b5109ed74bb81..9f0e9af2dec37c 100644 --- a/React/Base/RCTAssert.h +++ b/React/Base/RCTAssert.h @@ -64,6 +64,11 @@ RCT_EXTERN NSString *const RCTJSStackTraceKey; */ RCT_EXTERN NSString *const RCTJSRawStackTraceKey; +/** + * Objective-C stack trace string provided as part of an NSError's userInfo + */ +RCT_EXTERN NSString *const RCTObjCStackTraceKey; + /** * Name of fatal exceptions generated by RCTFatal */ diff --git a/React/Base/RCTAssert.m b/React/Base/RCTAssert.m index b454fae65b92f0..087900d1979565 100644 --- a/React/Base/RCTAssert.m +++ b/React/Base/RCTAssert.m @@ -11,6 +11,7 @@ NSString *const RCTErrorDomain = @"RCTErrorDomain"; NSString *const RCTJSStackTraceKey = @"RCTJSStackTraceKey"; NSString *const RCTJSRawStackTraceKey = @"RCTJSRawStackTraceKey"; +NSString *const RCTObjCStackTraceKey = @"RCTObjCStackTraceKey"; NSString *const RCTFatalExceptionName = @"RCTFatalException"; NSString *const RCTUntruncatedMessageKey = @"RCTUntruncatedMessageKey"; diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index c83c40fa9d0d9e..5981e1faa0ae0a 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -157,6 +157,10 @@ RCT_EXTERN void RCTEnableTurboModule(BOOL enabled); RCT_EXTERN BOOL RCTTurboModuleEagerInitEnabled(void); RCT_EXTERN void RCTEnableTurboModuleEagerInit(BOOL enabled); +// Turn on TurboModule shared mutex initialization +RCT_EXTERN BOOL RCTTurboModuleSharedMutexInitEnabled(void); +RCT_EXTERN void RCTEnableTurboModuleSharedMutexInit(BOOL enabled); + /** * Async batched bridge used to communicate with the JavaScript application. */ diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index a30a273df32f26..8cca1a00b9d749 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -125,6 +125,17 @@ void RCTEnableTurboModuleEagerInit(BOOL enabled) turboModuleEagerInitEnabled = enabled; } +static BOOL turboModuleSharedMutexInitEnabled = NO; +BOOL RCTTurboModuleSharedMutexInitEnabled(void) +{ + return turboModuleSharedMutexInitEnabled; +} + +void RCTEnableTurboModuleSharedMutexInit(BOOL enabled) +{ + turboModuleSharedMutexInitEnabled = enabled; +} + @interface RCTBridge () @end @@ -207,7 +218,24 @@ - (void)setRCTTurboModuleRegistry:(id)turboModuleRegistr - (void)didReceiveReloadCommand { - [self reloadWithReason:@"Command"]; +#if RCT_ENABLE_INSPECTOR + // Disable debugger to resume the JsVM & avoid thread locks while reloading + [RCTInspectorDevServerHelper disableDebugger]; +#endif + + [[NSNotificationCenter defaultCenter] postNotificationName:RCTBridgeWillReloadNotification object:self userInfo:nil]; + + /** + * Any thread + */ + dispatch_async(dispatch_get_main_queue(), ^{ + // WARNING: Invalidation is async, so it may not finish before re-setting up the bridge, + // causing some issues. TODO: revisit this post-Fabric/TurboModule. + [self invalidate]; + // Reload is a special case, do not preserve launchOptions and treat reload as a fresh start + self->_launchOptions = nil; + [self setUp]; + }); } - (NSArray *)moduleClasses @@ -258,7 +286,7 @@ - (BOOL)moduleIsInitialized:(Class)moduleClass */ - (void)reload { - [self reloadWithReason:@"Unknown from bridge"]; + RCTTriggerReloadCommandListeners(@"Unknown from bridge"); } /** @@ -266,24 +294,7 @@ - (void)reload */ - (void)reloadWithReason:(NSString *)reason { -#if RCT_ENABLE_INSPECTOR - // Disable debugger to resume the JsVM & avoid thread locks while reloading - [RCTInspectorDevServerHelper disableDebugger]; -#endif - - [[NSNotificationCenter defaultCenter] postNotificationName:RCTBridgeWillReloadNotification object:self userInfo:nil]; - - /** - * Any thread - */ - dispatch_async(dispatch_get_main_queue(), ^{ - // WARNING: Invalidation is async, so it may not finish before re-setting up the bridge, - // causing some issues. TODO: revisit this post-Fabric/TurboModule. - [self invalidate]; - // Reload is a special case, do not preserve launchOptions and treat reload as a fresh start - self->_launchOptions = nil; - [self setUp]; - }); + RCTTriggerReloadCommandListeners(reason); } - (void)onFastRefresh diff --git a/React/Base/RCTConstants.h b/React/Base/RCTConstants.h index 64b3117e18adcc..17e3cbabd7860f 100644 --- a/React/Base/RCTConstants.h +++ b/React/Base/RCTConstants.h @@ -22,3 +22,9 @@ RCT_EXTERN void RCTExperimentSetOnDemandViewMounting(BOOL value); */ RCT_EXTERN BOOL RCTExperimentGetSyncPerformanceFlag(void); RCT_EXTERN void RCTExperimentSetSyncPerformanceFlag(BOOL value); + +/* + * It's an experimental feature that improves performance of hit-testing. + */ +RCT_EXTERN BOOL RCTExperimentGetOptimizedHitTesting(void); +RCT_EXTERN void RCTExperimentSetOptimizedHitTesting(BOOL value); diff --git a/React/Base/RCTConstants.m b/React/Base/RCTConstants.m index cbc0c75e90f473..58c11fea497475 100644 --- a/React/Base/RCTConstants.m +++ b/React/Base/RCTConstants.m @@ -10,6 +10,9 @@ NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification"; NSString *const RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey = @"traitCollection"; +/* + * On-demand view mounting + */ static BOOL RCTExperimentOnDemandViewMounting = NO; BOOL RCTExperimentGetOnDemandViewMounting() @@ -22,6 +25,9 @@ void RCTExperimentSetOnDemandViewMounting(BOOL value) RCTExperimentOnDemandViewMounting = value; } +/* + * Sync performance flag + */ static BOOL RCTExperimentSyncPerformanceFlag = NO; BOOL RCTExperimentGetSyncPerformanceFlag() @@ -33,3 +39,18 @@ void RCTExperimentSetSyncPerformanceFlag(BOOL value) { RCTExperimentSyncPerformanceFlag = value; } + +/* + * Optimized hit-testing + */ +static BOOL RCTExperimentOptimizedHitTesting = NO; + +BOOL RCTExperimentGetOptimizedHitTesting() +{ + return RCTExperimentOptimizedHitTesting; +} + +void RCTExperimentSetOptimizedHitTesting(BOOL value) +{ + RCTExperimentOptimizedHitTesting = value; +} diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index f3588b38ecb503..7ef3b233893ae9 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -416,7 +416,6 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC return type; } -#if !TARGET_OS_TV RCT_MULTI_ENUM_CONVERTER( UIDataDetectorTypes, (@{ @@ -446,8 +445,6 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC WKDataDetectorTypePhoneNumber, unsignedLongLongValue) -#endif // !TARGET_OS_TV - RCT_ENUM_CONVERTER( UIKeyboardAppearance, (@{ @@ -500,7 +497,6 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC UIViewContentModeScaleAspectFill, integerValue) -#if !TARGET_OS_TV RCT_ENUM_CONVERTER( UIBarStyle, (@{ @@ -511,7 +507,6 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC }), UIBarStyleDefault, integerValue) -#endif static void convertCGStruct(const char *type, NSArray *fields, CGFloat *result, id json) { diff --git a/React/Base/RCTJSStackFrame.h b/React/Base/RCTJSStackFrame.h index 43592ec18d0c8e..1a66e29e58740e 100644 --- a/React/Base/RCTJSStackFrame.h +++ b/React/Base/RCTJSStackFrame.h @@ -13,13 +13,13 @@ @property (nonatomic, copy, readonly) NSString *file; @property (nonatomic, readonly) NSInteger lineNumber; @property (nonatomic, readonly) NSInteger column; -@property (nonatomic, readonly) NSInteger collapse; +@property (nonatomic, readonly) BOOL collapse; - (instancetype)initWithMethodName:(NSString *)methodName file:(NSString *)file lineNumber:(NSInteger)lineNumber column:(NSInteger)column - collapse:(NSInteger)collapse; + collapse:(BOOL)collapse; - (NSDictionary *)toDictionary; + (instancetype)stackFrameWithLine:(NSString *)line; diff --git a/React/Base/RCTJSStackFrame.m b/React/Base/RCTJSStackFrame.m index 2458626f349dd7..1e3d5498eaf777 100644 --- a/React/Base/RCTJSStackFrame.m +++ b/React/Base/RCTJSStackFrame.m @@ -57,7 +57,7 @@ - (instancetype)initWithMethodName:(NSString *)methodName file:(NSString *)file lineNumber:(NSInteger)lineNumber column:(NSInteger)column - collapse:(NSInteger)collapse + collapse:(BOOL)collapse { if (self = [super init]) { _methodName = methodName; @@ -100,7 +100,7 @@ + (instancetype)stackFrameWithLine:(NSString *)line file:file lineNumber:[lineNumber integerValue] column:[column integerValue] - collapse:@NO]; + collapse:NO]; } + (instancetype)stackFrameWithDictionary:(NSDictionary *)dict @@ -109,7 +109,7 @@ + (instancetype)stackFrameWithDictionary:(NSDictionary *)dict file:dict[@"file"] lineNumber:[RCTNilIfNull(dict[@"lineNumber"]) integerValue] column:[RCTNilIfNull(dict[@"column"]) integerValue] - collapse:[RCTNilIfNull(dict[@"collapse"]) integerValue]]; + collapse:[RCTNilIfNull(dict[@"collapse"]) boolValue]]; } + (NSArray *)stackFramesWithLines:(NSString *)lines diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 220a8b321f09cf..7e4ccb2a6a153b 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -113,7 +113,7 @@ + (void)initialize - (void)handleKeyUIEventSwizzle:(UIEvent *)event { NSString *modifiedInput = nil; - UIKeyModifierFlags *modifierFlags = nil; + UIKeyModifierFlags modifierFlags = 0; BOOL isKeyDown = NO; if ([event respondsToSelector:@selector(_modifiedInput)]) { diff --git a/React/Base/RCTModuleData.h b/React/Base/RCTModuleData.h index 04d6271eeedd5b..5ff81532eb34b6 100644 --- a/React/Base/RCTModuleData.h +++ b/React/Base/RCTModuleData.h @@ -97,4 +97,4 @@ typedef id (^RCTBridgeModuleProvider)(void); @end RCT_EXTERN void RCTSetIsMainQueueExecutionOfConstantsToExportDisabled(BOOL val); -RCT_EXTERN BOOL RCTIsMainQueueExecutionOfConstantsToExportDisabled(); +RCT_EXTERN BOOL RCTIsMainQueueExecutionOfConstantsToExportDisabled(void); diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 013c7506850fd2..d9fbc37b38dfae 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -29,11 +29,6 @@ #import "RCTView.h" #import "UIView+React.h" -#if TARGET_OS_TV -#import "RCTTVNavigationEventEmitter.h" -#import "RCTTVRemoteHandler.h" -#endif - NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification"; @interface RCTUIManager (RCTRootView) @@ -89,13 +84,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge name:RCTContentDidAppearNotification object:self]; -#if TARGET_OS_TV - self.tvRemoteHandler = [RCTTVRemoteHandler new]; - for (NSString *key in [self.tvRemoteHandler.tvRemoteGestureRecognizers allKeys]) { - [self addGestureRecognizer:self.tvRemoteHandler.tvRemoteGestureRecognizers[key]]; - } -#endif - [self showLoadingView]; // Immediately schedule the application to be started. @@ -121,16 +109,6 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder) -#if TARGET_OS_TV -- (UIView *)preferredFocusedView -{ - if (self.reactPreferredFocusedView) { - return self.reactPreferredFocusedView; - } - return [super preferredFocusedView]; -} -#endif - #pragma mark - passThroughTouches - (BOOL)passThroughTouches diff --git a/React/Base/RCTRootViewInternal.h b/React/Base/RCTRootViewInternal.h index c6a21c9ab1cb2f..8e28dd453570c1 100644 --- a/React/Base/RCTRootViewInternal.h +++ b/React/Base/RCTRootViewInternal.h @@ -7,8 +7,6 @@ #import -@class RCTTVRemoteHandler; - /** * The interface provides a set of functions that allow other internal framework * classes to change the RCTRootViews's internal state. @@ -21,14 +19,6 @@ */ @property (readwrite, nonatomic, assign) CGSize intrinsicContentSize; -/** - * TV remote gesture recognizers - */ -#if TARGET_OS_TV -@property (nonatomic, strong) RCTTVRemoteHandler *tvRemoteHandler; -@property (nonatomic, strong) UIView *reactPreferredFocusedView; -#endif - - (void)contentViewInvalidated; @end diff --git a/React/Base/RCTTVRemoteHandler.h b/React/Base/RCTTVRemoteHandler.h deleted file mode 100644 index 10d56906901677..00000000000000 --- a/React/Base/RCTTVRemoteHandler.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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. - */ - -#import - -extern NSString *const RCTTVRemoteEventMenu; -extern NSString *const RCTTVRemoteEventPlayPause; -extern NSString *const RCTTVRemoteEventSelect; - -extern NSString *const RCTTVRemoteEventLongPlayPause; -extern NSString *const RCTTVRemoteEventLongSelect; - -extern NSString *const RCTTVRemoteEventLeft; -extern NSString *const RCTTVRemoteEventRight; -extern NSString *const RCTTVRemoteEventUp; -extern NSString *const RCTTVRemoteEventDown; - -extern NSString *const RCTTVRemoteEventSwipeLeft; -extern NSString *const RCTTVRemoteEventSwipeRight; -extern NSString *const RCTTVRemoteEventSwipeUp; -extern NSString *const RCTTVRemoteEventSwipeDown; - -@interface RCTTVRemoteHandler : NSObject - -@property (nonatomic, copy, readonly) NSDictionary *tvRemoteGestureRecognizers; - -@end diff --git a/React/Base/RCTTVRemoteHandler.m b/React/Base/RCTTVRemoteHandler.m deleted file mode 100644 index aadbacec4b7eb5..00000000000000 --- a/React/Base/RCTTVRemoteHandler.m +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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. - */ - -#import "RCTTVRemoteHandler.h" - -#import - -#import "RCTAssert.h" -#import "RCTBridge.h" -#import "RCTEventDispatcher.h" -#import "RCTLog.h" -#import "RCTRootView.h" -#import "RCTTVNavigationEventEmitter.h" -#import "RCTUIManager.h" -#import "RCTUtils.h" -#import "RCTView.h" -#import "UIView+React.h" - -#if __has_include("RCTDevMenu.h") -#import "RCTDevMenu.h" -#endif - -NSString *const RCTTVRemoteEventMenu = @"menu"; -NSString *const RCTTVRemoteEventPlayPause = @"playPause"; -NSString *const RCTTVRemoteEventSelect = @"select"; - -NSString *const RCTTVRemoteEventLongPlayPause = @"longPlayPause"; -NSString *const RCTTVRemoteEventLongSelect = @"longSelect"; - -NSString *const RCTTVRemoteEventLeft = @"left"; -NSString *const RCTTVRemoteEventRight = @"right"; -NSString *const RCTTVRemoteEventUp = @"up"; -NSString *const RCTTVRemoteEventDown = @"down"; - -NSString *const RCTTVRemoteEventSwipeLeft = @"swipeLeft"; -NSString *const RCTTVRemoteEventSwipeRight = @"swipeRight"; -NSString *const RCTTVRemoteEventSwipeUp = @"swipeUp"; -NSString *const RCTTVRemoteEventSwipeDown = @"swipeDown"; - -@implementation RCTTVRemoteHandler { - NSMutableDictionary *_tvRemoteGestureRecognizers; -} - -- (instancetype)init -{ - if ((self = [super init])) { - _tvRemoteGestureRecognizers = [NSMutableDictionary dictionary]; - - // Recognizers for Apple TV remote buttons - - // Play/Pause - [self addTapGestureRecognizerWithSelector:@selector(playPausePressed:) - pressType:UIPressTypePlayPause - name:RCTTVRemoteEventPlayPause]; - - // Menu - [self addTapGestureRecognizerWithSelector:@selector(menuPressed:) - pressType:UIPressTypeMenu - name:RCTTVRemoteEventMenu]; - - // Select - [self addTapGestureRecognizerWithSelector:@selector(selectPressed:) - pressType:UIPressTypeSelect - name:RCTTVRemoteEventSelect]; - - // Up - [self addTapGestureRecognizerWithSelector:@selector(tappedUp:) - pressType:UIPressTypeUpArrow - name:RCTTVRemoteEventUp]; - - // Down - [self addTapGestureRecognizerWithSelector:@selector(tappedDown:) - pressType:UIPressTypeDownArrow - name:RCTTVRemoteEventDown]; - - // Left - [self addTapGestureRecognizerWithSelector:@selector(tappedLeft:) - pressType:UIPressTypeLeftArrow - name:RCTTVRemoteEventLeft]; - - // Right - [self addTapGestureRecognizerWithSelector:@selector(tappedRight:) - pressType:UIPressTypeRightArrow - name:RCTTVRemoteEventRight]; - - // Recognizers for long button presses - // We don't intercept long menu press -- that's used by the system to go to the home screen - - [self addLongPressGestureRecognizerWithSelector:@selector(longPlayPausePressed:) - pressType:UIPressTypePlayPause - name:RCTTVRemoteEventLongPlayPause]; - - [self addLongPressGestureRecognizerWithSelector:@selector(longSelectPressed:) - pressType:UIPressTypeSelect - name:RCTTVRemoteEventLongSelect]; - - // Recognizers for Apple TV remote trackpad swipes - - // Up - [self addSwipeGestureRecognizerWithSelector:@selector(swipedUp:) - direction:UISwipeGestureRecognizerDirectionUp - name:RCTTVRemoteEventSwipeUp]; - - // Down - [self addSwipeGestureRecognizerWithSelector:@selector(swipedDown:) - direction:UISwipeGestureRecognizerDirectionDown - name:RCTTVRemoteEventSwipeDown]; - - // Left - [self addSwipeGestureRecognizerWithSelector:@selector(swipedLeft:) - direction:UISwipeGestureRecognizerDirectionLeft - name:RCTTVRemoteEventSwipeLeft]; - - // Right - [self addSwipeGestureRecognizerWithSelector:@selector(swipedRight:) - direction:UISwipeGestureRecognizerDirectionRight - name:RCTTVRemoteEventSwipeRight]; - } - - return self; -} - -- (void)playPausePressed:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventPlayPause toView:r.view]; -} - -- (void)menuPressed:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventMenu toView:r.view]; -} - -- (void)selectPressed:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventSelect toView:r.view]; -} - -- (void)longPlayPausePressed:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventLongPlayPause toView:r.view]; - -#if __has_include("RCTDevMenu.h") && RCT_DEV - // If shake to show is enabled on device, use long play/pause event to show dev menu - [[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil]; -#endif -} - -- (void)longSelectPressed:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventLongSelect toView:r.view]; -} - -- (void)swipedUp:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventSwipeUp toView:r.view]; -} - -- (void)swipedDown:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventSwipeDown toView:r.view]; -} - -- (void)swipedLeft:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventSwipeLeft toView:r.view]; -} - -- (void)swipedRight:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventSwipeRight toView:r.view]; -} - -- (void)tappedUp:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventUp toView:r.view]; -} - -- (void)tappedDown:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventDown toView:r.view]; -} - -- (void)tappedLeft:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventLeft toView:r.view]; -} - -- (void)tappedRight:(UIGestureRecognizer *)r -{ - [self sendAppleTVEvent:RCTTVRemoteEventRight toView:r.view]; -} - -#pragma mark - - -- (void)addLongPressGestureRecognizerWithSelector:(nonnull SEL)selector - pressType:(UIPressType)pressType - name:(NSString *)name -{ - UILongPressGestureRecognizer *recognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:selector]; - recognizer.allowedPressTypes = @[ @(pressType) ]; - - _tvRemoteGestureRecognizers[name] = recognizer; -} - -- (void)addTapGestureRecognizerWithSelector:(nonnull SEL)selector pressType:(UIPressType)pressType name:(NSString *)name -{ - UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:selector]; - recognizer.allowedPressTypes = @[ @(pressType) ]; - - _tvRemoteGestureRecognizers[name] = recognizer; -} - -- (void)addSwipeGestureRecognizerWithSelector:(nonnull SEL)selector - direction:(UISwipeGestureRecognizerDirection)direction - name:(NSString *)name -{ - UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:selector]; - recognizer.direction = direction; - - _tvRemoteGestureRecognizers[name] = recognizer; -} - -- (void)sendAppleTVEvent:(NSString *)eventType toView:(__unused UIView *)v -{ - [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification - object:@{@"eventType" : eventType}]; -} - -@end diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 1270e471d28d57..5643035b55ea17 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -98,6 +98,9 @@ RCT_EXTERN BOOL RCTForceTouchAvailable(void); // Create an NSError in the RCTErrorDomain RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message); +// Creates an NSError from given an NSException +RCT_EXTERN NSError *RCTErrorWithNSException(NSException *exception); + // Convert nil values to NSNull, and vice-versa #define RCTNullIfNil(value) ((value) ?: (id)kCFNull) #define RCTNilIfNull(value) \ diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 13547d5852c697..1d26c256557a28 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -581,6 +581,16 @@ BOOL RCTForceTouchAvailable(void) return [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; } +NSError *RCTErrorWithNSException(NSException *exception) +{ + NSString *message = [NSString stringWithFormat:@"NSException: %@; trace: %@.", + exception, + [[exception callStackSymbols] componentsJoinedByString:@";"]]; + NSDictionary *errorInfo = + @{NSLocalizedDescriptionKey : message, RCTObjCStackTraceKey : [exception callStackSymbols]}; + return [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; +} + double RCTZeroIfNaN(double value) { return isnan(value) || isinf(value) ? 0 : value; diff --git a/React/CoreModules/BUCK b/React/CoreModules/BUCK index 2f9902478139d5..45b4b45f0daf1b 100644 --- a/React/CoreModules/BUCK +++ b/React/CoreModules/BUCK @@ -104,9 +104,6 @@ rn_apple_library( ) + react_module_plugin_providers( name = "LogBox", native_class_func = "RCTLogBoxCls", - ) + react_module_plugin_providers( - name = "TVNavigationEventEmitter", - native_class_func = "RCTTVNavigationEventEmitterCls", ) + react_module_plugin_providers( name = "WebSocketExecutor", native_class_func = "RCTWebSocketExecutorCls", diff --git a/React/CoreModules/CoreModulesPlugins.h b/React/CoreModules/CoreModulesPlugins.h index 509eb5af2f8dbc..b8fa8e9f0767ed 100644 --- a/React/CoreModules/CoreModulesPlugins.h +++ b/React/CoreModules/CoreModulesPlugins.h @@ -49,7 +49,6 @@ Class RCTDevMenuCls(void) __attribute__((used)); Class RCTDevSettingsCls(void) __attribute__((used)); Class RCTRedBoxCls(void) __attribute__((used)); Class RCTLogBoxCls(void) __attribute__((used)); -Class RCTTVNavigationEventEmitterCls(void) __attribute__((used)); Class RCTWebSocketExecutorCls(void) __attribute__((used)); Class RCTWebSocketModuleCls(void) __attribute__((used)); Class RCTDevLoadingViewCls(void) __attribute__((used)); diff --git a/React/CoreModules/CoreModulesPlugins.mm b/React/CoreModules/CoreModulesPlugins.mm index ced0cbd0797bbe..de47e923f813d7 100644 --- a/React/CoreModules/CoreModulesPlugins.mm +++ b/React/CoreModules/CoreModulesPlugins.mm @@ -38,7 +38,6 @@ Class RCTCoreModulesClassProvider(const char *name) { {"DevSettings", RCTDevSettingsCls}, {"RedBox", RCTRedBoxCls}, {"LogBox", RCTLogBoxCls}, - {"TVNavigationEventEmitter", RCTTVNavigationEventEmitterCls}, {"WebSocketExecutor", RCTWebSocketExecutorCls}, {"WebSocketModule", RCTWebSocketModuleCls}, {"DevLoadingView", RCTDevLoadingViewCls}, diff --git a/React/CoreModules/RCTAppState.h b/React/CoreModules/RCTAppState.h index 6e0f19c3356f65..0921f70e44723b 100644 --- a/React/CoreModules/RCTAppState.h +++ b/React/CoreModules/RCTAppState.h @@ -7,6 +7,6 @@ #import -@interface RCTAppState : RCTEventEmitter +@interface RCTAppState : RCTEventEmitter @end diff --git a/React/CoreModules/RCTAppState.mm b/React/CoreModules/RCTAppState.mm index 66a1e72693f2f6..b0dc10bce5ad6d 100644 --- a/React/CoreModules/RCTAppState.mm +++ b/React/CoreModules/RCTAppState.mm @@ -99,11 +99,6 @@ - (void)stopObserving [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)invalidate -{ - [self stopObserving]; -} - #pragma mark - App Notification Methods - (void)handleMemoryWarning diff --git a/React/CoreModules/RCTAsyncLocalStorage.mm b/React/CoreModules/RCTAsyncLocalStorage.mm index 00b66634f03edd..ecbb05976a0168 100644 --- a/React/CoreModules/RCTAsyncLocalStorage.mm +++ b/React/CoreModules/RCTAsyncLocalStorage.mm @@ -76,11 +76,7 @@ static void RCTAppendError(NSDictionary *error, NSMutableArray * static NSString *storageDirectory = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ -#if TARGET_OS_TV - storageDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; -#else storageDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; -#endif storageDirectory = [storageDirectory stringByAppendingPathComponent:RCTStorageDirectory]; }); return storageDirectory; @@ -232,10 +228,6 @@ - (NSDictionary *)_ensureSetup { RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); -#if TARGET_OS_TV - RCTLogWarn(@"Persistent storage is not supported on tvOS, your data may be removed at any point."); -#endif - NSError *error = nil; if (!RCTHasCreatedStorageDirectory) { [[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory() diff --git a/React/CoreModules/RCTDevSettings.mm b/React/CoreModules/RCTDevSettings.mm index b84d4e3114233e..4bf03630201fe0 100644 --- a/React/CoreModules/RCTDevSettings.mm +++ b/React/CoreModules/RCTDevSettings.mm @@ -202,6 +202,7 @@ - (dispatch_queue_t)methodQueue - (void)invalidate { + [super invalidate]; #if ENABLE_PACKAGER_CONNECTION [[RCTPackagerConnection sharedPackagerConnection] removeHandler:_reloadToken]; #endif @@ -405,17 +406,18 @@ - (void)addHandler:(id)handler forPackagerMethod:(NSStr - (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL { - if (bundleURL && !bundleURL.fileURL) { // isHotLoadingAvailable check + if (bundleURL && !bundleURL.fileURL) { NSString *const path = [bundleURL.path substringFromIndex:1]; // Strip initial slash. NSString *const host = bundleURL.host; NSNumber *const port = bundleURL.port; + BOOL isHotLoadingEnabled = self.isHotLoadingEnabled; if (self.bridge) { [self.bridge enqueueJSCall:@"HMRClient" method:@"setup" - args:@[ @"ios", path, host, RCTNullIfNil(port), @(YES) ] + args:@[ @"ios", path, host, RCTNullIfNil(port), @(isHotLoadingEnabled) ] completion:NULL]; } else { - self.invokeJS(@"HMRClient", @"setup", @[ @"ios", path, host, RCTNullIfNil(port), @(YES) ]); + self.invokeJS(@"HMRClient", @"setup", @[ @"ios", path, host, RCTNullIfNil(port), @(isHotLoadingEnabled) ]); } } } diff --git a/React/CoreModules/RCTDeviceInfo.mm b/React/CoreModules/RCTDeviceInfo.mm index eb5f8217946238..e3ac433991568c 100644 --- a/React/CoreModules/RCTDeviceInfo.mm +++ b/React/CoreModules/RCTDeviceInfo.mm @@ -23,10 +23,8 @@ @interface RCTDeviceInfo () @end @implementation RCTDeviceInfo { -#if !TARGET_OS_TV UIInterfaceOrientation _currentInterfaceOrientation; NSDictionary *_currentInterfaceDimensions; -#endif } @synthesize bridge = _bridge; @@ -51,7 +49,7 @@ - (void)setBridge:(RCTBridge *)bridge selector:@selector(didReceiveNewContentSizeMultiplier) name:RCTAccessibilityManagerDidUpdateMultiplierNotification object:_bridge.accessibilityManager]; -#if !TARGET_OS_TV + _currentInterfaceOrientation = [RCTSharedApplication() statusBarOrientation]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -70,8 +68,6 @@ - (void)setBridge:(RCTBridge *)bridge selector:@selector(interfaceFrameDidChange) name:RCTUserInterfaceStyleDidChangeNotification object:nil]; - -#endif } static BOOL RCTIsIPhoneX() @@ -149,8 +145,6 @@ - (void)didReceiveNewContentSizeMultiplier }); } -#if !TARGET_OS_TV - - (void)interfaceOrientationDidChange { __weak __typeof(self) weakSelf = self; @@ -199,8 +193,6 @@ - (void)_interfaceFrameDidChange _currentInterfaceDimensions = nextInterfaceDimensions; } -#endif // TARGET_OS_TV - - (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params { return std::make_shared(params); diff --git a/React/CoreModules/RCTKeyboardObserver.mm b/React/CoreModules/RCTKeyboardObserver.mm index b00f08c21cabb6..93c853f7acc423 100644 --- a/React/CoreModules/RCTKeyboardObserver.mm +++ b/React/CoreModules/RCTKeyboardObserver.mm @@ -23,8 +23,6 @@ @implementation RCTKeyboardObserver - (void)startObserving { -#if !TARGET_OS_TV - NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; #define ADD_KEYBOARD_HANDLER(NAME, SELECTOR) [nc addObserver:self selector:@selector(SELECTOR:) name:NAME object:nil] @@ -37,8 +35,6 @@ - (void)startObserving ADD_KEYBOARD_HANDLER(UIKeyboardDidChangeFrameNotification, keyboardDidChangeFrame); #undef ADD_KEYBOARD_HANDLER - -#endif } - (NSArray *)supportedEvents @@ -113,9 +109,6 @@ -(void)EVENT : (NSNotification *)notification static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification) { -#if TARGET_OS_TV - return @{}; -#else NSDictionary *userInfo = notification.userInfo; CGRect beginFrame = [userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; @@ -131,7 +124,6 @@ -(void)EVENT : (NSNotification *)notification @"easing" : RCTAnimationNameForCurve(curve), @"isEventFromThisApp" : isLocalUserInfoKey == 1 ? @YES : @NO, }; -#endif } Class RCTKeyboardObserverCls(void) diff --git a/React/CoreModules/RCTRedBox.mm b/React/CoreModules/RCTRedBox.mm index 4325dc139eca47..9fec829b0157e2 100644 --- a/React/CoreModules/RCTRedBox.mm +++ b/React/CoreModules/RCTRedBox.mm @@ -104,10 +104,8 @@ - (instancetype)initWithFrame:(CGRect)frame _stackTraceTableView.delegate = self; _stackTraceTableView.dataSource = self; _stackTraceTableView.backgroundColor = [UIColor clearColor]; -#if !TARGET_OS_TV _stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3]; _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; -#endif _stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite; [rootView addSubview:_stackTraceTableView]; @@ -292,10 +290,8 @@ - (void)copyStack [fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]]; } } -#if !TARGET_OS_TV UIPasteboard *pb = [UIPasteboard generalPasteboard]; [pb setString:fullStackTrace]; -#endif } - (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame diff --git a/React/CoreModules/RCTStatusBarManager.h b/React/CoreModules/RCTStatusBarManager.h index dead2d86a85803..db02b9176a694b 100644 --- a/React/CoreModules/RCTStatusBarManager.h +++ b/React/CoreModules/RCTStatusBarManager.h @@ -12,10 +12,8 @@ @interface RCTConvert (UIStatusBar) -#if !TARGET_OS_TV + (UIStatusBarStyle)UIStatusBarStyle:(id)json; + (UIStatusBarAnimation)UIStatusBarAnimation:(id)json; -#endif @end diff --git a/React/CoreModules/RCTStatusBarManager.mm b/React/CoreModules/RCTStatusBarManager.mm index 3e62b9df5052c7..4104a7081512c0 100644 --- a/React/CoreModules/RCTStatusBarManager.mm +++ b/React/CoreModules/RCTStatusBarManager.mm @@ -12,7 +12,6 @@ #import #import -#if !TARGET_OS_TV #import @implementation RCTConvert (UIStatusBar) @@ -58,15 +57,10 @@ + (UIStatusBarStyle)UIStatusBarStyle:(id)json RCT_DYNAMIC integerValue); @end -#endif - -#if !TARGET_OS_TV @interface RCTStatusBarManager () @end -#endif - @implementation RCTStatusBarManager static BOOL RCTViewControllerBasedStatusBarAppearance() @@ -94,8 +88,6 @@ + (BOOL)requiresMainQueueSetup return @[ @"statusBarFrameDidChange", @"statusBarFrameWillChange" ]; } -#if !TARGET_OS_TV - - (void)startObserving { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; @@ -207,8 +199,6 @@ - (void)applicationWillChangeStatusBarFrame:(NSNotification *)notification return std::make_shared(params); } -#endif // TARGET_OS_TV - @end Class RCTStatusBarManagerCls(void) diff --git a/React/CoreModules/RCTTVNavigationEventEmitter.mm b/React/CoreModules/RCTTVNavigationEventEmitter.mm deleted file mode 100644 index 58fc1ead297cfb..00000000000000 --- a/React/CoreModules/RCTTVNavigationEventEmitter.mm +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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. - */ - -#import "RCTTVNavigationEventEmitter.h" - -#import -#import "CoreModulesPlugins.h" - -NSString *const RCTTVNavigationEventNotification = @"RCTTVNavigationEventNotification"; - -static NSString *const TVNavigationEventName = @"onHWKeyEvent"; - -@interface RCTTVNavigationEventEmitter () -@end - -@implementation RCTTVNavigationEventEmitter - -RCT_EXPORT_MODULE() - -+ (BOOL)requiresMainQueueSetup -{ - return NO; -} - -- (instancetype)init -{ - if (self = [super init]) { - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleTVNavigationEventNotification:) - name:RCTTVNavigationEventNotification - object:nil]; - } - return self; -} - -- (NSArray *)supportedEvents -{ - return @[ TVNavigationEventName ]; -} - -- (void)handleTVNavigationEventNotification:(NSNotification *)notif -{ - if (self.bridge) { - [self sendEventWithName:TVNavigationEventName body:notif.object]; - } -} - -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params -{ - return std::make_shared(params); -} - -@end - -Class RCTTVNavigationEventEmitterCls(void) -{ - return RCTTVNavigationEventEmitter.class; -} diff --git a/React/CoreModules/RCTWebSocketModule.mm b/React/CoreModules/RCTWebSocketModule.mm index e26423d245438b..3bc43d797f65d6 100644 --- a/React/CoreModules/RCTWebSocketModule.mm +++ b/React/CoreModules/RCTWebSocketModule.mm @@ -53,6 +53,8 @@ - (NSArray *)supportedEvents - (void)invalidate { + [super invalidate]; + _contentHandlers = nil; for (RCTSRWebSocket *socket in _sockets.allValues) { socket.delegate = nil; diff --git a/React/CoreModules/React-CoreModules.podspec b/React/CoreModules/React-CoreModules.podspec index 7d5a617b28a7ee..dee7239471187d 100644 --- a/React/CoreModules/React-CoreModules.podspec +++ b/React/CoreModules/React-CoreModules.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "**/*.{c,m,mm,cpp}" diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index 171005a4f02cf8..594b1cbeec6536 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -1119,26 +1119,6 @@ - (void)setUp { } -- (void)reload -{ - if (!_valid) { - RCTLogWarn( - @"Attempting to reload bridge before it's valid: %@. Try restarting the development server if connected.", - self); - } - RCTTriggerReloadCommandListeners(@"Unknown from cxx bridge"); -} - -- (void)reloadWithReason:(NSString *)reason -{ - if (!_valid) { - RCTLogWarn( - @"Attempting to reload bridge before it's valid: %@. Try restarting the development server if connected.", - self); - } - RCTTriggerReloadCommandListeners(reason); -} - - (Class)executorClass { return _parentBridge.executorClass; diff --git a/React/CxxModule/RCTCxxUtils.mm b/React/CxxModule/RCTCxxUtils.mm index a25328fac75371..b81a8da54ae947 100644 --- a/React/CxxModule/RCTCxxUtils.mm +++ b/React/CxxModule/RCTCxxUtils.mm @@ -74,8 +74,7 @@ func(); return nil; } @catch (NSException *exception) { - NSString *message = [NSString stringWithFormat:@"Exception '%@' was thrown from JS thread", exception]; - return RCTErrorWithMessage(message); + return RCTErrorWithNSException(exception); } @catch (id exception) { // This will catch any other ObjC exception, but no C++ exceptions return RCTErrorWithMessage(@"non-std ObjC Exception"); diff --git a/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.mm index dd79102526d9e5..7b5a56999ed8be 100644 --- a/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ActivityIndicator/RCTActivityIndicatorViewComponentView.mm @@ -7,6 +7,8 @@ #import "RCTActivityIndicatorViewComponentView.h" +#import + #import #import #import @@ -50,7 +52,7 @@ - (instancetype)initWithFrame:(CGRect)frame } else { [_activityIndicatorView stopAnimating]; } - _activityIndicatorView.color = [UIColor colorWithCGColor:defaultProps->color.get()]; + _activityIndicatorView.color = RCTUIColorFromSharedColor(defaultProps->color); _activityIndicatorView.hidesWhenStopped = defaultProps->hidesWhenStopped; _activityIndicatorView.activityIndicatorViewStyle = convertActivityIndicatorViewStyle(defaultProps->size); @@ -73,8 +75,8 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & } } - if (oldViewProps.color.get() != newViewProps.color.get()) { - _activityIndicatorView.color = [UIColor colorWithCGColor:newViewProps.color.get()]; + if (oldViewProps.color != newViewProps.color) { + _activityIndicatorView.color = RCTUIColorFromSharedColor(newViewProps.color); } // TODO: This prop should be deprecated. diff --git a/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.h b/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.h index c8db70c6b80002..8f40efe885c947 100644 --- a/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.h +++ b/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.h @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#import #import NS_ASSUME_NONNULL_BEGIN @@ -12,7 +13,10 @@ NS_ASSUME_NONNULL_BEGIN /** * UIView class for root component. */ -@interface RCTImageComponentView : RCTViewComponentView +@interface RCTImageComponentView : RCTViewComponentView { + @protected + UIImageView *_imageView; +} @end diff --git a/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm b/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm index 0de4b95a59235a..ce8e55c0229fb6 100644 --- a/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm @@ -9,7 +9,6 @@ #import #import -#import #import #import #import @@ -21,11 +20,10 @@ using namespace facebook::react; -@interface RCTImageComponentView () +@interface RCTImageComponentView () @end @implementation RCTImageComponentView { - UIImageView *_imageView; ImageShadowNode::ConcreteStateTeller _stateTeller; ImageResponseObserverCoordinator const *_coordinator; RCTImageResponseObserverProxy _imageResponseObserverProxy; @@ -76,7 +74,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & // `tintColor` if (oldImageProps.tintColor != newImageProps.tintColor) { - _imageView.tintColor = [UIColor colorWithCGColor:newImageProps.tintColor.get()]; + _imageView.tintColor = RCTUIColorFromSharedColor(newImageProps.tintColor); } [super updateProps:props oldProps:oldProps]; @@ -130,7 +128,7 @@ - (void)dealloc #pragma mark - RCTImageResponseDelegate -- (void)didReceiveImage:(UIImage *)image fromObserver:(void const *)observer +- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(void const *)observer { if (!_eventEmitter || !_stateTeller.isValid()) { // Notifications are delivered asynchronously and might arrive after the view is already recycled. diff --git a/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm index d9c4fb231b2493..1d112b51c1c88f 100644 --- a/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm +++ b/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryContentView.mm @@ -24,7 +24,7 @@ - (instancetype)init _heightConstraint = [_safeAreaContainer.heightAnchor constraintEqualToConstant:0]; _heightConstraint.active = YES; - if (@available(iOS 11.0, tvOS 11.0, *)) { + if (@available(iOS 11.0, *)) { [NSLayoutConstraint activateConstraints:@[ [_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor], [_safeAreaContainer.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor], diff --git a/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm b/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm index 2bb5ef0168e930..3262418b17832c 100644 --- a/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm @@ -37,6 +37,17 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *result = [super hitTest:point withEvent:event]; + + if (result == _adapter.paperView) { + return self; + } + + return result; +} + + (NSMutableSet *)supportedViewManagers { static NSMutableSet *supported = [NSMutableSet setWithObjects:@"Picker", @@ -99,7 +110,11 @@ - (void)mountChildComponentView:(UIView *)childCompone - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { - [_viewsToBeUnmounted addObject:childComponentView]; + if (_adapter) { + [_adapter.paperView removeReactSubview:childComponentView]; + } else { + [_viewsToBeUnmounted addObject:childComponentView]; + } } + (ComponentDescriptorProvider)componentDescriptorProvider diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h b/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h index 1cd88f8aa62fff..f794b067c75443 100644 --- a/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h +++ b/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h @@ -15,8 +15,6 @@ @property (nonatomic, weak) id delegate; -#if !TARGET_OS_TV @property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; -#endif @end diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm b/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm index 95f333c2e3f7fe..54061f535d79ee 100644 --- a/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm +++ b/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm @@ -40,7 +40,6 @@ - (void)loadView [_touchHandler attachToView:self.view]; } -#if !TARGET_OS_TV - (UIStatusBarStyle)preferredStatusBarStyle { return [RCTSharedApplication() statusBarStyle]; @@ -89,6 +88,5 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations return _supportedInterfaceOrientations; } #endif // RCT_DEV -#endif // !TARGET_OS_TV @end diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm index b4a8b7b2f5239d..65f2f6ce266300 100644 --- a/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm @@ -19,7 +19,6 @@ using namespace facebook::react; -#if !TARGET_OS_TV static UIInterfaceOrientationMask supportedOrientationsMask(ModalHostViewSupportedOrientationsMask mask) { UIInterfaceOrientationMask supportedOrientations = 0; @@ -54,7 +53,6 @@ static UIInterfaceOrientationMask supportedOrientationsMask(ModalHostViewSupport return supportedOrientations; } -#endif static std::tuple animationConfiguration(ModalHostViewAnimationType const animation) { diff --git a/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm index c54ea89bcac19f..7623c1099f93b0 100644 --- a/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm @@ -34,7 +34,7 @@ - (instancetype)initWithFrame:(CGRect)frame - (UIEdgeInsets)_safeAreaInsets { - if (@available(iOS 11.0, tvOS 11.0, *)) { + if (@available(iOS 11.0, *)) { return self.safeAreaInsets; } diff --git a/React/Fabric/Mounting/ComponentViews/Slider/RCTSliderComponentView.mm b/React/Fabric/Mounting/ComponentViews/Slider/RCTSliderComponentView.mm index fea74e18c146fc..af62366caa3be3 100644 --- a/React/Fabric/Mounting/ComponentViews/Slider/RCTSliderComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/Slider/RCTSliderComponentView.mm @@ -7,8 +7,10 @@ #import "RCTSliderComponentView.h" +#import #import #import + #import #import #import @@ -108,12 +110,6 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & const auto &oldSliderProps = *std::static_pointer_cast(_props); const auto &newSliderProps = *std::static_pointer_cast(props); - // `value` - if (oldSliderProps.value != newSliderProps.value) { - _sliderView.value = newSliderProps.value; - _previousValue = newSliderProps.value; - } - // `minimumValue` if (oldSliderProps.minimumValue != newSliderProps.minimumValue) { _sliderView.minimumValue = newSliderProps.minimumValue; @@ -124,6 +120,12 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _sliderView.maximumValue = newSliderProps.maximumValue; } + // `value` + if (oldSliderProps.value != newSliderProps.value) { + _sliderView.value = newSliderProps.value; + _previousValue = newSliderProps.value; + } + // `disabled` if (oldSliderProps.disabled != newSliderProps.disabled) { _sliderView.enabled = !newSliderProps.disabled; @@ -131,17 +133,17 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & // `thumbTintColor` if (oldSliderProps.thumbTintColor != newSliderProps.thumbTintColor) { - _sliderView.thumbTintColor = [UIColor colorWithCGColor:newSliderProps.thumbTintColor.get()]; + _sliderView.thumbTintColor = RCTUIColorFromSharedColor(newSliderProps.thumbTintColor); } // `minimumTrackTintColor` if (oldSliderProps.minimumTrackTintColor != newSliderProps.minimumTrackTintColor) { - _sliderView.minimumTrackTintColor = [UIColor colorWithCGColor:newSliderProps.minimumTrackTintColor.get()]; + _sliderView.minimumTrackTintColor = RCTUIColorFromSharedColor(newSliderProps.minimumTrackTintColor); } // `maximumTrackTintColor` if (oldSliderProps.maximumTrackTintColor != newSliderProps.maximumTrackTintColor) { - _sliderView.maximumTrackTintColor = [UIColor colorWithCGColor:newSliderProps.maximumTrackTintColor.get()]; + _sliderView.maximumTrackTintColor = RCTUIColorFromSharedColor(newSliderProps.maximumTrackTintColor); } [super updateProps:props oldProps:oldProps]; @@ -298,7 +300,7 @@ - (void)onChange:(UISlider *)sender withContinuous:(BOOL)continuous const auto &props = *std::static_pointer_cast(_props); - if (props.step > 0 && value <= (props.maximumValue - props.minimumValue)) { + if (props.step > 0 && props.step <= (props.maximumValue - props.minimumValue)) { value = MAX( props.minimumValue, MIN(props.maximumValue, props.minimumValue + round((value - props.minimumValue) / props.step) * props.step)); @@ -320,7 +322,7 @@ - (void)onChange:(UISlider *)sender withContinuous:(BOOL)continuous #pragma mark - RCTImageResponseDelegate -- (void)didReceiveImage:(UIImage *)image fromObserver:(void const *)observer +- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(void const *)observer { if (observer == &_trackImageResponseObserverProxy) { self.trackImage = image; diff --git a/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm b/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm index 2538f040476c78..5c89f7cbc0bab0 100644 --- a/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm @@ -7,6 +7,8 @@ #import "RCTSwitchComponentView.h" +#import + #import #import #import @@ -75,17 +77,17 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & // `tintColor` if (oldSwitchProps.tintColor != newSwitchProps.tintColor) { - _switchView.tintColor = [UIColor colorWithCGColor:newSwitchProps.tintColor.get()]; + _switchView.tintColor = RCTUIColorFromSharedColor(newSwitchProps.tintColor); } // `onTintColor if (oldSwitchProps.onTintColor != newSwitchProps.onTintColor) { - _switchView.onTintColor = [UIColor colorWithCGColor:newSwitchProps.onTintColor.get()]; + _switchView.onTintColor = RCTUIColorFromSharedColor(newSwitchProps.onTintColor); } // `thumbTintColor` if (oldSwitchProps.thumbTintColor != newSwitchProps.thumbTintColor) { - _switchView.thumbTintColor = [UIColor colorWithCGColor:newSwitchProps.thumbTintColor.get()]; + _switchView.thumbTintColor = RCTUIColorFromSharedColor(newSwitchProps.thumbTintColor); } [super updateProps:props oldProps:oldProps]; diff --git a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index c574b1752c7ff8..a58d7f288c6d73 100644 --- a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -8,6 +8,7 @@ #import #import +#import #import #import #import diff --git a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index e5c7e6a85375fb..428d8bc630e1b0 100644 --- a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -7,10 +7,12 @@ #import "RCTViewComponentView.h" +#import +#import + #import #import #import -#import #import #import #import @@ -118,7 +120,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & // `shadowColor` if (oldViewProps.shadowColor != newViewProps.shadowColor) { - CGColorRef shadowColor = RCTCGColorRefFromSharedColor(newViewProps.shadowColor); + CGColorRef shadowColor = RCTCreateCGColorRefFromSharedColor(newViewProps.shadowColor); self.layer.shadowColor = shadowColor; CGColorRelease(shadowColor); needsInvalidateLayer = YES; @@ -301,7 +303,13 @@ - (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event BOOL isPointInside = [self pointInside:point withEvent:event]; - if (self.clipsToBounds && !isPointInside) { + BOOL clipsToBounds = self.clipsToBounds; + + if (RCTExperimentGetOptimizedHitTesting()) { + clipsToBounds = clipsToBounds || _layoutMetrics.overflowInset == EdgeInsets{}; + } + + if (clipsToBounds && !isPointInside) { return nil; } @@ -338,12 +346,20 @@ static RCTCornerRadii RCTCornerRadiiFromBorderRadii(BorderRadii borderRadii) .bottomRight = (CGFloat)borderRadii.bottomRight}; } -static RCTBorderColors RCTBorderColorsFromBorderColors(BorderColors borderColors) +static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors borderColors) +{ + return RCTBorderColors{.top = RCTCreateCGColorRefFromSharedColor(borderColors.top), + .left = RCTCreateCGColorRefFromSharedColor(borderColors.left), + .bottom = RCTCreateCGColorRefFromSharedColor(borderColors.bottom), + .right = RCTCreateCGColorRefFromSharedColor(borderColors.right)}; +} + +static void RCTReleaseRCTBorderColors(RCTBorderColors borderColors) { - return RCTBorderColors{.left = RCTCGColorRefUnretainedFromSharedColor(borderColors.left), - .top = RCTCGColorRefUnretainedFromSharedColor(borderColors.top), - .bottom = RCTCGColorRefUnretainedFromSharedColor(borderColors.bottom), - .right = RCTCGColorRefUnretainedFromSharedColor(borderColors.right)}; + CGColorRelease(borderColors.top); + CGColorRelease(borderColors.left); + CGColorRelease(borderColors.bottom); + CGColorRelease(borderColors.right); } static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) @@ -406,7 +422,7 @@ - (void)invalidateLayer } layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; - CGColorRef borderColor = RCTCGColorRefFromSharedColor(borderMetrics.borderColors.left); + CGColorRef borderColor = RCTCreateCGColorRefFromSharedColor(borderMetrics.borderColors.left); layer.borderColor = borderColor; CGColorRelease(borderColor); layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft; @@ -425,15 +441,19 @@ - (void)invalidateLayer layer.borderColor = nil; layer.cornerRadius = 0; + RCTBorderColors borderColors = RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors); + UIImage *image = RCTGetBorderImage( RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left), layer.bounds.size, RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths), - RCTBorderColorsFromBorderColors(borderMetrics.borderColors), + borderColors, _backgroundColor.CGColor, self.clipsToBounds); + RCTReleaseRCTBorderColors(borderColors); + if (image == nil) { _borderLayer.contents = nil; } else { @@ -549,6 +569,11 @@ - (NSString *)accessibilityValue #pragma mark - Accessibility Events +- (BOOL)shouldGroupAccessibilityChildren +{ + return YES; +} + - (NSArray *)accessibilityCustomActions { auto const &accessibilityActions = _props->accessibilityActions; diff --git a/React/Fabric/Mounting/RCTComponentViewFactory.mm b/React/Fabric/Mounting/RCTComponentViewFactory.mm index 88489f2b3cd900..1c857b440c9d95 100644 --- a/React/Fabric/Mounting/RCTComponentViewFactory.mm +++ b/React/Fabric/Mounting/RCTComponentViewFactory.mm @@ -117,6 +117,7 @@ - (RCTComponentViewClassDescriptor)_componentViewClassDescriptorFromClass:(Class - (void)registerComponentViewClass:(Class)componentViewClass { + RCTAssert(componentViewClass, @"RCTComponentViewFactory: Provided `componentViewClass` is `nil`."); std::unique_lock lock(_mutex); auto componentDescriptorProvider = [componentViewClass componentDescriptorProvider]; diff --git a/React/Fabric/Mounting/UIView+ComponentViewProtocol.mm b/React/Fabric/Mounting/UIView+ComponentViewProtocol.mm index 82bfa1a5515111..c0de92d8c21df9 100644 --- a/React/Fabric/Mounting/UIView+ComponentViewProtocol.mm +++ b/React/Fabric/Mounting/UIView+ComponentViewProtocol.mm @@ -30,16 +30,29 @@ + (ComponentDescriptorProvider)componentDescriptorProvider - (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { - RCTAssert(childComponentView.superview == nil, @"Attempt to mount already mounted component view."); + RCTAssert( + childComponentView.superview == nil, + @"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@)", + self, + childComponentView, + @(index)); [self insertSubview:childComponentView atIndex:index]; } - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { - RCTAssert(childComponentView.superview == self, @"Attempt to unmount improperly mounted component view."); RCTAssert( - [self.subviews objectAtIndex:index] == childComponentView, - @"Attempt to unmount improperly mounted component view."); + childComponentView.superview == self, + @"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)", + self, + childComponentView, + @(index)); + RCTAssert( + (self.subviews.count > index) && [self.subviews objectAtIndex:index] == childComponentView, + @"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@)", + self, + childComponentView, + @(index)); [childComponentView removeFromSuperview]; } diff --git a/React/Fabric/RCTConversions.h b/React/Fabric/RCTConversions.h index 29e7aff75d67c2..222e9ba85005b2 100644 --- a/React/Fabric/RCTConversions.h +++ b/React/Fabric/RCTConversions.h @@ -32,23 +32,35 @@ inline NSString *_Nullable RCTNSStringFromStringNilIfEmpty( inline std::string RCTStringFromNSString(NSString *string) { - return string ? std::string([string UTF8String]) : ""; + return std::string{string.UTF8String ?: ""}; } -inline UIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor) +inline UIColor *_Nullable RCTUIColorFromSharedColor(facebook::react::SharedColor const &sharedColor) { - return sharedColor ? [UIColor colorWithCGColor:sharedColor.get()] : nil; -} + if (!sharedColor) { + return nil; + } -inline CF_RETURNS_NOT_RETAINED CGColorRef -RCTCGColorRefUnretainedFromSharedColor(const facebook::react::SharedColor &sharedColor) -{ - return sharedColor ? sharedColor.get() : nil; + if (*facebook::react::clearColor() == *sharedColor) { + return [UIColor clearColor]; + } + + if (*facebook::react::blackColor() == *sharedColor) { + return [UIColor blackColor]; + } + + if (*facebook::react::whiteColor() == *sharedColor) { + return [UIColor whiteColor]; + } + + auto components = facebook::react::colorComponentsFromColor(sharedColor); + return [UIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha]; } -inline CF_RETURNS_RETAINED CGColorRef RCTCGColorRefFromSharedColor(const facebook::react::SharedColor &sharedColor) +inline CF_RETURNS_RETAINED CGColorRef +RCTCreateCGColorRefFromSharedColor(const facebook::react::SharedColor &sharedColor) { - return sharedColor ? CGColorCreateCopy(sharedColor.get()) : nil; + return CGColorRetain(RCTUIColorFromSharedColor(sharedColor).CGColor); } inline CGPoint RCTCGPointFromPoint(const facebook::react::Point &point) diff --git a/React/Fabric/RCTImageResponseDelegate.h b/React/Fabric/RCTImageResponseDelegate.h index eea1fabf953a7d..29f641518043d2 100644 --- a/React/Fabric/RCTImageResponseDelegate.h +++ b/React/Fabric/RCTImageResponseDelegate.h @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol RCTImageResponseDelegate -- (void)didReceiveImage:(UIImage *)image fromObserver:(void const *)observer; +- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(void const *)observer; - (void)didReceiveProgress:(float)progress fromObserver:(void const *)observer; - (void)didReceiveFailureFromObserver:(void const *)observer; diff --git a/React/Fabric/RCTImageResponseObserverProxy.mm b/React/Fabric/RCTImageResponseObserverProxy.mm index 4c74e2ea42f2bd..ffb68fbdef7502 100644 --- a/React/Fabric/RCTImageResponseObserverProxy.mm +++ b/React/Fabric/RCTImageResponseObserverProxy.mm @@ -23,10 +23,11 @@ void RCTImageResponseObserverProxy::didReceiveImage(ImageResponse const &imageResponse) const { UIImage *image = (UIImage *)unwrapManagedObject(imageResponse.getImage()); + id metadata = unwrapManagedObject(imageResponse.getMetadata()); id delegate = delegate_; auto this_ = this; RCTExecuteOnMainQueue(^{ - [delegate didReceiveImage:image fromObserver:this_]; + [delegate didReceiveImage:image metadata:metadata fromObserver:this_]; }); } diff --git a/React/Fabric/RCTSurfacePresenter.mm b/React/Fabric/RCTSurfacePresenter.mm index 79a12af1be5a1a..589375d4f29b84 100644 --- a/React/Fabric/RCTSurfacePresenter.mm +++ b/React/Fabric/RCTSurfacePresenter.mm @@ -35,10 +35,8 @@ #import #import -#import "MainRunLoopEventBeat.h" #import "PlatformRunLoopObserver.h" #import "RCTConversions.h" -#import "RuntimeEventBeat.h" using namespace facebook::react; @@ -330,6 +328,10 @@ - (RCTScheduler *)_createScheduler RCTExperimentSetOnDemandViewMounting(YES); } + if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:optimized_hit_testing_ios")) { + RCTExperimentSetOptimizedHitTesting(YES); + } + auto componentRegistryFactory = [factory = wrapManagedObject(_mountingManager.componentViewRegistry.componentViewFactory)]( EventDispatcher::Weak const &eventDispatcher, ContextContainer::Shared const &contextContainer) { @@ -352,27 +354,17 @@ - (RCTScheduler *)_createScheduler toolbox.backgroundExecutor = RCTGetBackgroundExecutor(); } - if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_run_loop_based_event_beat_ios")) { - toolbox.synchronousEventBeatFactory = [runtimeExecutor](EventBeat::SharedOwnerBox const &ownerBox) { - auto runLoopObserver = - std::make_unique(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner); - return std::make_unique(std::move(runLoopObserver), runtimeExecutor); - }; - - toolbox.asynchronousEventBeatFactory = [runtimeExecutor](EventBeat::SharedOwnerBox const &ownerBox) { - auto runLoopObserver = - std::make_unique(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner); - return std::make_unique(std::move(runLoopObserver), runtimeExecutor); - }; - } else { - toolbox.synchronousEventBeatFactory = [runtimeExecutor](EventBeat::SharedOwnerBox const &ownerBox) { - return std::make_unique(ownerBox, runtimeExecutor); - }; - - toolbox.asynchronousEventBeatFactory = [runtimeExecutor](EventBeat::SharedOwnerBox const &ownerBox) { - return std::make_unique(ownerBox, runtimeExecutor); - }; - } + toolbox.synchronousEventBeatFactory = [runtimeExecutor](EventBeat::SharedOwnerBox const &ownerBox) { + auto runLoopObserver = + std::make_unique(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner); + return std::make_unique(std::move(runLoopObserver), runtimeExecutor); + }; + + toolbox.asynchronousEventBeatFactory = [runtimeExecutor](EventBeat::SharedOwnerBox const &ownerBox) { + auto runLoopObserver = + std::make_unique(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner); + return std::make_unique(std::move(runLoopObserver), runtimeExecutor); + }; RCTScheduler *scheduler = [[RCTScheduler alloc] initWithToolbox:toolbox]; scheduler.delegate = self; diff --git a/React/Fabric/Surface/RCTFabricSurface.mm b/React/Fabric/Surface/RCTFabricSurface.mm index f8ea1c39faeea4..9cd25cd8318dad 100644 --- a/React/Fabric/Surface/RCTFabricSurface.mm +++ b/React/Fabric/Surface/RCTFabricSurface.mm @@ -83,6 +83,7 @@ - (BOOL)stop } [_surfacePresenter unregisterSurface:self]; + [_touchHandler detachFromView:_view]; return YES; } diff --git a/React/Fabric/Utils/MainRunLoopEventBeat.h b/React/Fabric/Utils/MainRunLoopEventBeat.h deleted file mode 100644 index b0b7751f06ce7c..00000000000000 --- a/React/Fabric/Utils/MainRunLoopEventBeat.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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. - */ - -#pragma once - -#include -#include -#include -#include - -namespace facebook { -namespace react { - -/* - * Event beat associated with main run loop cycle. - * The callback is always called on the main thread. - */ -class MainRunLoopEventBeat final : public EventBeat { - public: - MainRunLoopEventBeat( - EventBeat::SharedOwnerBox const &ownerBox, - RuntimeExecutor runtimeExecutor); - ~MainRunLoopEventBeat(); - - void induce() const override; - - private: - void lockExecutorAndBeat() const; - - const RuntimeExecutor runtimeExecutor_; - CFRunLoopObserverRef mainRunLoopObserver_; -}; - -} // namespace react -} // namespace facebook diff --git a/React/Fabric/Utils/MainRunLoopEventBeat.mm b/React/Fabric/Utils/MainRunLoopEventBeat.mm deleted file mode 100644 index 563cebde21d38b..00000000000000 --- a/React/Fabric/Utils/MainRunLoopEventBeat.mm +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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. - */ - -#import "MainRunLoopEventBeat.h" - -#import -#import - -namespace facebook { -namespace react { - -MainRunLoopEventBeat::MainRunLoopEventBeat(EventBeat::SharedOwnerBox const &ownerBox, RuntimeExecutor runtimeExecutor) - : EventBeat(ownerBox), runtimeExecutor_(std::move(runtimeExecutor)) -{ - mainRunLoopObserver_ = CFRunLoopObserverCreateWithHandler( - NULL /* allocator */, - kCFRunLoopBeforeWaiting /* activities */, - true /* repeats */, - 0 /* order */, - ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - if (!this->isRequested_) { - return; - } - - this->lockExecutorAndBeat(); - }); - - assert(mainRunLoopObserver_); - - CFRunLoopAddObserver(CFRunLoopGetMain(), mainRunLoopObserver_, kCFRunLoopCommonModes); -} - -MainRunLoopEventBeat::~MainRunLoopEventBeat() -{ - CFRunLoopRemoveObserver(CFRunLoopGetMain(), mainRunLoopObserver_, kCFRunLoopCommonModes); - CFRelease(mainRunLoopObserver_); -} - -void MainRunLoopEventBeat::induce() const -{ - if (!this->isRequested_) { - return; - } - - RCTExecuteOnMainQueue(^{ - this->lockExecutorAndBeat(); - }); -} - -void MainRunLoopEventBeat::lockExecutorAndBeat() const -{ - auto owner = ownerBox_->owner.lock(); - if (!owner) { - return; - } - - executeSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor_, [this](jsi::Runtime &runtime) { beat(runtime); }); -} - -} // namespace react -} // namespace facebook diff --git a/React/Fabric/Utils/RuntimeEventBeat.h b/React/Fabric/Utils/RuntimeEventBeat.h deleted file mode 100644 index 8bfdd0cdf9c472..00000000000000 --- a/React/Fabric/Utils/RuntimeEventBeat.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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. - */ - -#include -#include -#include -#include - -namespace facebook { -namespace react { - -/* - * Event beat associated with JavaScript runtime. - * The beat is called on `RuntimeExecutor`'s thread induced by the main thread - * event loop. - */ -class RuntimeEventBeat : public EventBeat { - public: - RuntimeEventBeat( - EventBeat::SharedOwnerBox const &ownerBox, - RuntimeExecutor runtimeExecutor); - ~RuntimeEventBeat(); - - void induce() const override; - - private: - const RuntimeExecutor runtimeExecutor_; - CFRunLoopObserverRef mainRunLoopObserver_; - mutable std::atomic isBusy_{false}; -}; - -} // namespace react -} // namespace facebook diff --git a/React/Fabric/Utils/RuntimeEventBeat.mm b/React/Fabric/Utils/RuntimeEventBeat.mm deleted file mode 100644 index 0cd3a3c5bb71a2..00000000000000 --- a/React/Fabric/Utils/RuntimeEventBeat.mm +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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. - */ - -#include "RuntimeEventBeat.h" - -namespace facebook { -namespace react { - -RuntimeEventBeat::RuntimeEventBeat(EventBeat::SharedOwnerBox const &ownerBox, RuntimeExecutor runtimeExecutor) - : EventBeat(ownerBox), runtimeExecutor_(std::move(runtimeExecutor)) -{ - mainRunLoopObserver_ = CFRunLoopObserverCreateWithHandler( - NULL /* allocator */, - kCFRunLoopBeforeWaiting /* activities */, - true /* repeats */, - 0 /* order */, - ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - // Note: We only `induce` beat here; actual beat will be performed on - // a different thread. - this->induce(); - }); - - assert(mainRunLoopObserver_); - - CFRunLoopAddObserver(CFRunLoopGetMain(), mainRunLoopObserver_, kCFRunLoopCommonModes); -} - -RuntimeEventBeat::~RuntimeEventBeat() -{ - CFRunLoopRemoveObserver(CFRunLoopGetMain(), mainRunLoopObserver_, kCFRunLoopCommonModes); - CFRelease(mainRunLoopObserver_); -} - -void RuntimeEventBeat::induce() const -{ - if (!isRequested_ || isBusy_) { - return; - } - - isBusy_ = true; - runtimeExecutor_([this, ownerBox = ownerBox_](jsi::Runtime &runtime) mutable { - auto owner = ownerBox->owner.lock(); - if (!owner) { - return; - } - - this->beat(runtime); - isBusy_ = false; - }); -} - -} // namespace react -} // namespace facebook diff --git a/React/Modules/RCTEventEmitter.h b/React/Modules/RCTEventEmitter.h index 700368c599d016..f04d50f02418e4 100644 --- a/React/Modules/RCTEventEmitter.h +++ b/React/Modules/RCTEventEmitter.h @@ -12,7 +12,7 @@ * RCTEventEmitter is an abstract base class to be used for modules that emit * events to be observed by JS. */ -@interface RCTEventEmitter : NSObject +@interface RCTEventEmitter : NSObject @property (nonatomic, weak) RCTBridge *bridge; @@ -37,6 +37,8 @@ - (void)startObserving; - (void)stopObserving; +- (void)invalidate NS_REQUIRES_SUPER; + - (void)addListener:(NSString *)eventName; - (void)removeListeners:(double)count; diff --git a/React/Modules/RCTEventEmitter.m b/React/Modules/RCTEventEmitter.m index 7717ddbdcac451..6e5a6ff37e7f5d 100644 --- a/React/Modules/RCTEventEmitter.m +++ b/React/Modules/RCTEventEmitter.m @@ -78,7 +78,7 @@ - (void)stopObserving // Does nothing } -- (void)dealloc +- (void)invalidate { if (_listenerCount > 0) { [self stopObserving]; diff --git a/React/Modules/RCTLayoutAnimation.m b/React/Modules/RCTLayoutAnimation.m index 7ad88992d9ec87..7b702c18d80dcb 100644 --- a/React/Modules/RCTLayoutAnimation.m +++ b/React/Modules/RCTLayoutAnimation.m @@ -39,7 +39,6 @@ static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnim // `UIKeyboardWillChangeFrameNotification`s. + (void)initializeStatics { -#if !TARGET_OS_TV static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [[NSNotificationCenter defaultCenter] addObserver:self @@ -47,15 +46,12 @@ + (void)initializeStatics name:UIKeyboardWillChangeFrameNotification object:nil]; }); -#endif } + (void)keyboardWillChangeFrame:(NSNotification *)notification { -#if !TARGET_OS_TV NSDictionary *userInfo = notification.userInfo; _currentKeyboardAnimationCurve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; -#endif } - (instancetype)initWithDuration:(NSTimeInterval)duration diff --git a/React/Modules/RCTRedBoxExtraDataViewController.m b/React/Modules/RCTRedBoxExtraDataViewController.m index 240aa5e9b033ad..9b006ec68c1ef3 100644 --- a/React/Modules/RCTRedBoxExtraDataViewController.m +++ b/React/Modules/RCTRedBoxExtraDataViewController.m @@ -33,10 +33,8 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr self.keyLabel.textColor = [UIColor whiteColor]; self.keyLabel.numberOfLines = 0; -#if !TARGET_OS_TV self.keyLabel.lineBreakMode = NSLineBreakByWordWrapping; self.keyLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:12.0f]; -#endif self.valueLabel = [UILabel new]; [self.contentView addSubview:self.valueLabel]; @@ -48,10 +46,8 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr self.valueLabel.textColor = [UIColor whiteColor]; self.valueLabel.numberOfLines = 0; -#if !TARGET_OS_TV self.valueLabel.lineBreakMode = NSLineBreakByWordWrapping; self.valueLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:12.0f]; -#endif } return self; } @@ -82,9 +78,7 @@ - (instancetype)init _tableView.dataSource = self; _tableView.backgroundColor = [UIColor clearColor]; _tableView.estimatedRowHeight = 200; -#if !TARGET_OS_TV _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; -#endif _tableView.rowHeight = UITableViewAutomaticDimension; _tableView.allowsSelection = NO; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 2079bbb7f6c215..8f43fd3f3b1c04 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -179,12 +179,10 @@ - (void)setBridge:(RCTBridge *)bridge object:[self->_bridge moduleForName:@"AccessibilityManager" lazilyLoadIfNecessary:YES]]; }); -#if !TARGET_OS_TV [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(namedOrientationDidChange) name:UIDeviceOrientationDidChangeNotification object:nil]; -#endif [RCTLayoutAnimation initializeStatics]; } @@ -210,7 +208,6 @@ - (void)didReceiveNewContentSizeMultiplier }); } -#if !TARGET_OS_TV // Names and coordinate system from html5 spec: // https://developer.mozilla.org/en-US/docs/Web/API/Screen.orientation // https://developer.mozilla.org/en-US/docs/Web/API/Screen.lockOrientation @@ -262,7 +259,6 @@ - (void)namedOrientationDidChange [_bridge.eventDispatcher sendDeviceEventWithName:@"namedOrientationDidChange" body:orientationEvent]; #pragma clang diagnostic pop } -#endif - (dispatch_queue_t)methodQueue { diff --git a/React/Modules/RCTUIManagerUtils.m b/React/Modules/RCTUIManagerUtils.m index 54f77f67a6e4d2..7b19bf2cf3534f 100644 --- a/React/Modules/RCTUIManagerUtils.m +++ b/React/Modules/RCTUIManagerUtils.m @@ -7,7 +7,7 @@ #import "RCTUIManagerUtils.h" -#import +#import #import "RCTAssert.h" @@ -98,6 +98,6 @@ void RCTUnsafeExecuteOnUIManagerQueueSync(dispatch_block_t block) NSNumber *RCTAllocateRootViewTag() { // Numbering of these tags goes from 1, 11, 21, 31, ..., 100501, ... - static int64_t rootViewTagCounter = -1; - return @(OSAtomicIncrement64(&rootViewTagCounter) * 10 + 1); + static _Atomic int64_t rootViewTagCounter = 0; + return @(atomic_fetch_add_explicit(&rootViewTagCounter, 1, memory_order_relaxed) * 10 + 1); } diff --git a/React/Profiler/RCTProfile.m b/React/Profiler/RCTProfile.m index 12d42000e3cdf5..233f6e767ba781 100644 --- a/React/Profiler/RCTProfile.m +++ b/React/Profiler/RCTProfile.m @@ -22,6 +22,7 @@ #import "RCTDefines.h" #import "RCTLog.h" #import "RCTModuleData.h" +#import "RCTReloadCommand.h" #import "RCTUIManager.h" #import "RCTUIManagerUtils.h" #import "RCTUtils.h" @@ -378,7 +379,7 @@ + (void)vsync:(CADisplayLink *)displayLink + (void)reload { - [RCTProfilingBridge() reloadWithReason:@"Profiling controls"]; + RCTTriggerReloadCommandListeners(@"Profiling controls"); } + (void)toggle:(UIButton *)target @@ -392,7 +393,6 @@ + (void)toggle:(UIButton *)target RCTProfileEnd(RCTProfilingBridge(), ^(NSString *result) { NSString *outFile = [NSTemporaryDirectory() stringByAppendingString:@"tmp_trace.json"]; [result writeToFile:outFile atomically:YES encoding:NSUTF8StringEncoding error:nil]; -#if !TARGET_OS_TV UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[ [NSURL fileURLWithPath:outFile] ] applicationActivities:nil]; @@ -409,7 +409,6 @@ + (void)toggle:(UIButton *)target animated:YES completion:nil]; }); -#endif }); } else { RCTProfileInit(RCTProfilingBridge()); @@ -746,7 +745,6 @@ void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *data) NSString *message = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; if (message.length) { -#if !TARGET_OS_TV dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Profile" @@ -757,7 +755,6 @@ void RCTProfileSendResult(RCTBridge *bridge, NSString *route, NSData *data) handler:nil]]; [RCTPresentedViewController() presentViewController:alertController animated:YES completion:nil]; }); -#endif } } }]; diff --git a/React/React-RCTFabric.podspec b/React/React-RCTFabric.podspec index 34d1096c413d9e..bca122846f75b0 100644 --- a/React/React-RCTFabric.podspec +++ b/React/React-RCTFabric.podspec @@ -28,7 +28,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "Fabric/**/*.{c,h,m,mm,S,cpp}", "Tests/**/*.{mm}" diff --git a/React/Views/RCTComponentData.h b/React/Views/RCTComponentData.h index b61dc4011e1f22..614ca5f0f94f4c 100644 --- a/React/Views/RCTComponentData.h +++ b/React/Views/RCTComponentData.h @@ -15,6 +15,8 @@ @class RCTShadowView; @class UIView; +NS_ASSUME_NONNULL_BEGIN + @interface RCTComponentData : NSObject @property (nonatomic, readonly) Class managerClass; @@ -23,7 +25,7 @@ - (instancetype)initWithManagerClass:(Class)managerClass bridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; -- (UIView *)createViewWithTag:(NSNumber *)tag rootTag:(NSNumber *)rootTag; +- (UIView *)createViewWithTag:(nullable NSNumber *)tag rootTag:(nullable NSNumber *)rootTag; - (RCTShadowView *)createShadowViewWithTag:(NSNumber *)tag; - (void)setProps:(NSDictionary *)props forView:(id)view; - (void)setProps:(NSDictionary *)props forShadowView:(RCTShadowView *)shadowView; @@ -34,3 +36,5 @@ - (NSDictionary *)viewConfig; @end + +NS_ASSUME_NONNULL_END diff --git a/React/Views/RCTComponentData.m b/React/Views/RCTComponentData.m index f4ecbb1b042764..5a8600cf5b55ab 100644 --- a/React/Views/RCTComponentData.m +++ b/React/Views/RCTComponentData.m @@ -63,16 +63,14 @@ - (RCTViewManager *)manager RCT_NOT_IMPLEMENTED(-(instancetype)init) -- (UIView *)createViewWithTag:(NSNumber *)tag rootTag:(NSNumber *)rootTag +- (UIView *)createViewWithTag:(nullable NSNumber *)tag rootTag:(nullable NSNumber *)rootTag { RCTAssertMainQueue(); UIView *view = [self.manager view]; view.reactTag = tag; view.rootTag = rootTag; -#if !TARGET_OS_TV view.multipleTouchEnabled = YES; -#endif view.userInteractionEnabled = YES; // required for touch handling view.layer.allowsGroupOpacity = YES; // required for touch handling return view; diff --git a/React/Views/RCTDatePicker.m b/React/Views/RCTDatePicker.m index 1a921c556a0918..9fee82b3cfffc2 100644 --- a/React/Views/RCTDatePicker.m +++ b/React/Views/RCTDatePicker.m @@ -7,9 +7,20 @@ #import "RCTDatePicker.h" +#import +#import + #import "RCTUtils.h" #import "UIView+React.h" +#ifndef __IPHONE_14_0 + #define __IPHONE_14_0 140000 +#endif // __IPHONE_14_0 + +#ifndef RCT_IOS_14_0_SDK_OR_LATER + #define RCT_IOS_14_0_SDK_OR_LATER (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0) +#endif // RCT_IOS_14_0_SDK_OR_LATER + @interface RCTDatePicker () @property (nonatomic, copy) RCTBubblingEventBlock onChange; @@ -24,6 +35,12 @@ - (instancetype)initWithFrame:(CGRect)frame if ((self = [super initWithFrame:frame])) { [self addTarget:self action:@selector(didChange) forControlEvents:UIControlEventValueChanged]; _reactMinuteInterval = 1; + +#if RCT_IOS_14_0_SDK_OR_LATER + if (@available(iOS 14, *)) { + self.preferredDatePickerStyle = UIDatePickerStyleWheels; + } +#endif // RCT_IOS_14_0_SDK_OR_LATER } return self; } diff --git a/React/Views/RCTModalHostView.h b/React/Views/RCTModalHostView.h index 4e61886dba54ef..c54c1c69c94413 100644 --- a/React/Views/RCTModalHostView.h +++ b/React/Views/RCTModalHostView.h @@ -13,7 +13,6 @@ @class RCTBridge; @class RCTModalHostViewController; -@class RCTTVRemoteHandler; @protocol RCTModalHostViewInteractor; @@ -32,11 +31,6 @@ @property (nonatomic, copy) NSArray *supportedOrientations; @property (nonatomic, copy) RCTDirectEventBlock onOrientationChange; -#if TARGET_OS_TV -@property (nonatomic, copy) RCTDirectEventBlock onRequestClose; -@property (nonatomic, strong) RCTTVRemoteHandler *tvRemoteHandler; -#endif - - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; @end diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index 6a153301038853..0cfa69a99aab6f 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -16,9 +16,6 @@ #import "RCTUIManager.h" #import "RCTUtils.h" #import "UIView+React.h" -#if TARGET_OS_TV -#import "RCTTVRemoteHandler.h" -#endif @implementation RCTModalHostView { __weak RCTBridge *_bridge; @@ -26,11 +23,7 @@ @implementation RCTModalHostView { RCTModalHostViewController *_modalViewController; RCTTouchHandler *_touchHandler; UIView *_reactSubview; -#if TARGET_OS_TV - UITapGestureRecognizer *_menuButtonGestureRecognizer; -#else UIInterfaceOrientation _lastKnownOrientation; -#endif } RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) @@ -45,12 +38,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; _modalViewController.view = containerView; _touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge]; -#if TARGET_OS_TV - _menuButtonGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(menuButtonPressed:)]; - _menuButtonGestureRecognizer.allowedPressTypes = @[ @(UIPressTypeMenu) ]; - self.tvRemoteHandler = [RCTTVRemoteHandler new]; -#endif _isPresented = NO; __weak typeof(self) weakSelf = self; @@ -62,27 +49,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge return self; } -#if TARGET_OS_TV -- (void)menuButtonPressed:(__unused UIGestureRecognizer *)gestureRecognizer -{ - if (_onRequestClose) { - _onRequestClose(nil); - } -} - -- (void)setOnRequestClose:(RCTDirectEventBlock)onRequestClose -{ - _onRequestClose = onRequestClose; - if (_reactSubview) { - if (_onRequestClose && _menuButtonGestureRecognizer) { - [_reactSubview addGestureRecognizer:_menuButtonGestureRecognizer]; - } else { - [_reactSubview removeGestureRecognizer:_menuButtonGestureRecognizer]; - } - } -} -#endif - - (void)notifyForBoundsChange:(CGRect)newBounds { if (_reactSubview && _isPresented) { @@ -93,7 +59,6 @@ - (void)notifyForBoundsChange:(CGRect)newBounds - (void)notifyForOrientationChange { -#if !TARGET_OS_TV if (!_onOrientationChange) { return; } @@ -110,7 +75,6 @@ - (void)notifyForOrientationChange @"orientation" : isPortrait ? @"portrait" : @"landscape", }; _onOrientationChange(eventPayload); -#endif } - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex @@ -118,16 +82,6 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex RCTAssert(_reactSubview == nil, @"Modal view can only have one subview"); [super insertReactSubview:subview atIndex:atIndex]; [_touchHandler attachToView:subview]; -#if TARGET_OS_TV - for (NSString *key in [self.tvRemoteHandler.tvRemoteGestureRecognizers allKeys]) { - if (![key isEqualToString:RCTTVRemoteEventMenu]) { - [subview addGestureRecognizer:self.tvRemoteHandler.tvRemoteGestureRecognizers[key]]; - } - } - if (_onRequestClose) { - [subview addGestureRecognizer:_menuButtonGestureRecognizer]; - } -#endif [_modalViewController.view insertSubview:subview atIndex:0]; _reactSubview = subview; @@ -139,14 +93,6 @@ - (void)removeReactSubview:(UIView *)subview // Superclass (category) removes the `subview` from actual `superview`. [super removeReactSubview:subview]; [_touchHandler detachFromView:subview]; -#if TARGET_OS_TV - if (_menuButtonGestureRecognizer) { - [subview removeGestureRecognizer:_menuButtonGestureRecognizer]; - } - for (UIGestureRecognizer *gr in self.tvRemoteHandler.tvRemoteGestureRecognizers) { - [subview removeGestureRecognizer:gr]; - } -#endif _reactSubview = nil; } @@ -176,9 +122,8 @@ - (void)didMoveToWindow if (!_isPresented && self.window) { RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller"); -#if !TARGET_OS_TV _modalViewController.supportedInterfaceOrientations = [self supportedOrientationsMask]; -#endif + if ([self.animationType isEqualToString:@"fade"]) { _modalViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; } else if ([self.animationType isEqualToString:@"slide"]) { @@ -228,7 +173,6 @@ - (void)setTransparent:(BOOL)transparent transparent ? UIModalPresentationOverFullScreen : UIModalPresentationFullScreen; } -#if !TARGET_OS_TV - (UIInterfaceOrientationMask)supportedOrientationsMask { if (_supportedOrientations.count == 0) { @@ -255,6 +199,5 @@ - (UIInterfaceOrientationMask)supportedOrientationsMask } return supportedOrientations; } -#endif @end diff --git a/React/Views/RCTModalHostViewController.h b/React/Views/RCTModalHostViewController.h index 0a4a82968075c5..a3f66b6b068336 100644 --- a/React/Views/RCTModalHostViewController.h +++ b/React/Views/RCTModalHostViewController.h @@ -11,8 +11,6 @@ @property (nonatomic, copy) void (^boundsDidChangeBlock)(CGRect newBounds); -#if !TARGET_OS_TV @property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; -#endif @end diff --git a/React/Views/RCTModalHostViewController.m b/React/Views/RCTModalHostViewController.m index 112bc17c4d85bf..00c149d3db8474 100644 --- a/React/Views/RCTModalHostViewController.m +++ b/React/Views/RCTModalHostViewController.m @@ -12,10 +12,8 @@ @implementation RCTModalHostViewController { CGRect _lastViewFrame; -#if !TARGET_OS_TV UIStatusBarStyle _preferredStatusBarStyle; BOOL _preferredStatusBarHidden; -#endif } - (instancetype)init @@ -31,10 +29,8 @@ - (instancetype)init } #endif -#if !TARGET_OS_TV _preferredStatusBarStyle = [RCTSharedApplication() statusBarStyle]; _preferredStatusBarHidden = [RCTSharedApplication() isStatusBarHidden]; -#endif return self; } @@ -49,7 +45,6 @@ - (void)viewDidLayoutSubviews } } -#if !TARGET_OS_TV - (UIStatusBarStyle)preferredStatusBarStyle { return _preferredStatusBarStyle; @@ -78,6 +73,5 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations return _supportedInterfaceOrientations; } #endif // RCT_DEV -#endif // !TARGET_OS_TV @end diff --git a/React/Views/RCTModalHostViewManager.m b/React/Views/RCTModalHostViewManager.m index bafab9dff9f3a9..14c220ba5b90e9 100644 --- a/React/Views/RCTModalHostViewManager.m +++ b/React/Views/RCTModalHostViewManager.m @@ -19,10 +19,8 @@ @implementation RCTConvert (RCTModalHostView) UIModalPresentationStyle, (@{ @"fullScreen" : @(UIModalPresentationFullScreen), -#if !TARGET_OS_TV @"pageSheet" : @(UIModalPresentationPageSheet), @"formSheet" : @(UIModalPresentationFormSheet), -#endif @"overFullScreen" : @(UIModalPresentationOverFullScreen), }), UIModalPresentationFullScreen, @@ -48,6 +46,8 @@ - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex @interface RCTModalHostViewManager () +@property (nonatomic, copy) dispatch_block_t dismissWaitingBlock; + @end @implementation RCTModalHostViewManager { @@ -79,9 +79,16 @@ - (void)presentModalHostView:(RCTModalHostView *)modalHostView if (_presentationBlock) { _presentationBlock([modalHostView reactViewController], viewController, animated, completionBlock); } else { + __weak typeof(self) weakself = self; [[modalHostView reactViewController] presentViewController:viewController animated:animated - completion:completionBlock]; + completion:^{ + !completionBlock ?: completionBlock(); + __strong typeof(weakself) strongself = weakself; + !strongself.dismissWaitingBlock + ?: strongself.dismissWaitingBlock(); + strongself.dismissWaitingBlock = nil; + }]; } } @@ -92,7 +99,13 @@ - (void)dismissModalHostView:(RCTModalHostView *)modalHostView if (_dismissalBlock) { _dismissalBlock([modalHostView reactViewController], viewController, animated, nil); } else { - [viewController.presentingViewController dismissViewControllerAnimated:animated completion:nil]; + self.dismissWaitingBlock = ^{ + [viewController.presentingViewController dismissViewControllerAnimated:animated completion:nil]; + }; + if (viewController.presentingViewController) { + self.dismissWaitingBlock(); + self.dismissWaitingBlock = nil; + } } } @@ -117,8 +130,4 @@ - (void)invalidate RCT_EXPORT_VIEW_PROPERTY(supportedOrientations, NSArray) RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock) -#if TARGET_OS_TV -RCT_EXPORT_VIEW_PROPERTY(onRequestClose, RCTDirectEventBlock) -#endif - @end diff --git a/React/Views/RCTProgressViewManager.m b/React/Views/RCTProgressViewManager.m index 887473cb1aa73b..ed434250101f58 100644 --- a/React/Views/RCTProgressViewManager.m +++ b/React/Views/RCTProgressViewManager.m @@ -15,9 +15,7 @@ @implementation RCTConvert (RCTProgressViewManager) UIProgressViewStyle, (@{ @"default" : @(UIProgressViewStyleDefault), -#if !TARGET_OS_TV @"bar" : @(UIProgressViewStyleBar), -#endif }), UIProgressViewStyleDefault, integerValue) diff --git a/React/Views/RCTTVView.h b/React/Views/RCTTVView.h deleted file mode 100644 index a421e6fc61f173..00000000000000 --- a/React/Views/RCTTVView.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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. - */ - -#import -#import - -#import - -// A RCTView with additional properties and methods for user interaction using the Apple TV focus engine. -@interface RCTTVView : RCTView - -/** - * TV event handlers - */ -@property (nonatomic, assign) BOOL isTVSelectable; // True if this view is TV-focusable - -/** - * Properties for Apple TV focus parallax effects - */ -@property (nonatomic, copy) NSDictionary *tvParallaxProperties; - -/** - * TV preferred focus - */ -@property (nonatomic, assign) BOOL hasTVPreferredFocus; - -@end diff --git a/React/Views/RCTTVView.m b/React/Views/RCTTVView.m deleted file mode 100644 index 6f06b9a1f411d2..00000000000000 --- a/React/Views/RCTTVView.m +++ /dev/null @@ -1,258 +0,0 @@ -/* - * 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. - */ - -#import "RCTTVView.h" - -#import "RCTAutoInsetsProtocol.h" -#import "RCTBorderDrawing.h" -#import "RCTBridge.h" -#import "RCTConvert.h" -#import "RCTEventDispatcher.h" -#import "RCTLog.h" -#import "RCTRootViewInternal.h" -#import "RCTTVNavigationEventEmitter.h" -#import "RCTUtils.h" -#import "RCTView.h" -#import "UIView+React.h" - -@implementation RCTTVView { - UITapGestureRecognizer *_selectRecognizer; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - dispatch_once(&onceToken, ^{ - defaultTVParallaxProperties = @{ - @"enabled" : @YES, - @"shiftDistanceX" : @2.0f, - @"shiftDistanceY" : @2.0f, - @"tiltAngle" : @0.05f, - @"magnification" : @1.0f, - @"pressMagnification" : @1.0f, - @"pressDuration" : @0.3f, - @"pressDelay" : @0.0f - }; - }); - self.tvParallaxProperties = defaultTVParallaxProperties; - } - - return self; -} - -static NSDictionary *defaultTVParallaxProperties = nil; -static dispatch_once_t onceToken; - -- (void)setTvParallaxProperties:(NSDictionary *)tvParallaxProperties -{ - if (_tvParallaxProperties == nil) { - _tvParallaxProperties = [defaultTVParallaxProperties copy]; - return; - } - - NSMutableDictionary *newParallaxProperties = [NSMutableDictionary dictionaryWithDictionary:_tvParallaxProperties]; - for (NSString *k in [defaultTVParallaxProperties allKeys]) { - if (tvParallaxProperties[k]) { - newParallaxProperties[k] = tvParallaxProperties[k]; - } - } - _tvParallaxProperties = [newParallaxProperties copy]; -} - -RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : unused) - -- (void)setIsTVSelectable:(BOOL)isTVSelectable -{ - self->_isTVSelectable = isTVSelectable; - if (isTVSelectable) { - UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleSelect:)]; - recognizer.allowedPressTypes = @[ @(UIPressTypeSelect) ]; - _selectRecognizer = recognizer; - [self addGestureRecognizer:_selectRecognizer]; - } else { - if (_selectRecognizer) { - [self removeGestureRecognizer:_selectRecognizer]; - } - } -} - -- (void)handleSelect:(__unused UIGestureRecognizer *)r -{ - if ([self.tvParallaxProperties[@"enabled"] boolValue] == YES) { - float magnification = [self.tvParallaxProperties[@"magnification"] floatValue]; - float pressMagnification = [self.tvParallaxProperties[@"pressMagnification"] floatValue]; - - // Duration of press animation - float pressDuration = [self.tvParallaxProperties[@"pressDuration"] floatValue]; - - // Delay of press animation - float pressDelay = [self.tvParallaxProperties[@"pressDelay"] floatValue]; - - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:pressDelay]]; - - [UIView animateWithDuration:(pressDuration / 2) - animations:^{ - self.transform = CGAffineTransformMakeScale(pressMagnification, pressMagnification); - } - completion:^(__unused BOOL finished1) { - [UIView animateWithDuration:(pressDuration / 2) - animations:^{ - self.transform = CGAffineTransformMakeScale(magnification, magnification); - } - completion:^(__unused BOOL finished2) { - [[NSNotificationCenter defaultCenter] - postNotificationName:RCTTVNavigationEventNotification - object:@{@"eventType" : @"select", @"tag" : self.reactTag}]; - }]; - }]; - - } else { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification - object:@{@"eventType" : @"select", @"tag" : self.reactTag}]; - } -} - -- (BOOL)isUserInteractionEnabled -{ - return YES; -} - -- (BOOL)canBecomeFocused -{ - return (self.isTVSelectable); -} - -- (void)addParallaxMotionEffects -{ - // Size of shift movements - CGFloat const shiftDistanceX = [self.tvParallaxProperties[@"shiftDistanceX"] floatValue]; - CGFloat const shiftDistanceY = [self.tvParallaxProperties[@"shiftDistanceY"] floatValue]; - - // Make horizontal movements shift the centre left and right - UIInterpolatingMotionEffect *xShift = - [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" - type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; - xShift.minimumRelativeValue = @(shiftDistanceX * -1.0f); - xShift.maximumRelativeValue = @(shiftDistanceX); - - // Make vertical movements shift the centre up and down - UIInterpolatingMotionEffect *yShift = - [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" - type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]; - yShift.minimumRelativeValue = @(shiftDistanceY * -1.0f); - yShift.maximumRelativeValue = @(shiftDistanceY); - - // Size of tilt movements - CGFloat const tiltAngle = [self.tvParallaxProperties[@"tiltAngle"] floatValue]; - - // Now make horizontal movements effect a rotation about the Y axis for side-to-side rotation. - UIInterpolatingMotionEffect *xTilt = - [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"layer.transform" - type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; - - // CATransform3D value for minimumRelativeValue - CATransform3D transMinimumTiltAboutY = CATransform3DIdentity; - transMinimumTiltAboutY.m34 = 1.0 / 500; - transMinimumTiltAboutY = CATransform3DRotate(transMinimumTiltAboutY, tiltAngle * -1.0, 0, 1, 0); - - // CATransform3D value for minimumRelativeValue - CATransform3D transMaximumTiltAboutY = CATransform3DIdentity; - transMaximumTiltAboutY.m34 = 1.0 / 500; - transMaximumTiltAboutY = CATransform3DRotate(transMaximumTiltAboutY, tiltAngle, 0, 1, 0); - - // Set the transform property boundaries for the interpolation - xTilt.minimumRelativeValue = [NSValue valueWithCATransform3D:transMinimumTiltAboutY]; - xTilt.maximumRelativeValue = [NSValue valueWithCATransform3D:transMaximumTiltAboutY]; - - // Now make vertical movements effect a rotation about the X axis for up and down rotation. - UIInterpolatingMotionEffect *yTilt = - [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"layer.transform" - type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]; - - // CATransform3D value for minimumRelativeValue - CATransform3D transMinimumTiltAboutX = CATransform3DIdentity; - transMinimumTiltAboutX.m34 = 1.0 / 500; - transMinimumTiltAboutX = CATransform3DRotate(transMinimumTiltAboutX, tiltAngle * -1.0, 1, 0, 0); - - // CATransform3D value for minimumRelativeValue - CATransform3D transMaximumTiltAboutX = CATransform3DIdentity; - transMaximumTiltAboutX.m34 = 1.0 / 500; - transMaximumTiltAboutX = CATransform3DRotate(transMaximumTiltAboutX, tiltAngle, 1, 0, 0); - - // Set the transform property boundaries for the interpolation - yTilt.minimumRelativeValue = [NSValue valueWithCATransform3D:transMinimumTiltAboutX]; - yTilt.maximumRelativeValue = [NSValue valueWithCATransform3D:transMaximumTiltAboutX]; - - // Add all of the motion effects to this group - self.motionEffects = @[ xShift, yShift, xTilt, yTilt ]; - - float magnification = [self.tvParallaxProperties[@"magnification"] floatValue]; - - [UIView animateWithDuration:0.2 - animations:^{ - self.transform = CGAffineTransformMakeScale(magnification, magnification); - }]; -} - -- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context - withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator -{ - if (context.nextFocusedView == self && self.isTVSelectable) { - [self becomeFirstResponder]; - [coordinator - addCoordinatedAnimations:^(void) { - if ([self.tvParallaxProperties[@"enabled"] boolValue]) { - [self addParallaxMotionEffects]; - } - [[NSNotificationCenter defaultCenter] - postNotificationName:RCTTVNavigationEventNotification - object:@{@"eventType" : @"focus", @"tag" : self.reactTag}]; - } - completion:^(void){ - }]; - } else { - [coordinator - addCoordinatedAnimations:^(void) { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification - object:@{@"eventType" : @"blur", @"tag" : self.reactTag}]; - [UIView animateWithDuration:0.2 - animations:^{ - self.transform = CGAffineTransformMakeScale(1, 1); - }]; - - for (UIMotionEffect *effect in [self.motionEffects copy]) { - [self removeMotionEffect:effect]; - } - } - completion:^(void){ - }]; - [self resignFirstResponder]; - } -} - -- (void)setHasTVPreferredFocus:(BOOL)hasTVPreferredFocus -{ - _hasTVPreferredFocus = hasTVPreferredFocus; - if (hasTVPreferredFocus) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - UIView *rootview = self; - while (![rootview isReactRootView] && rootview != nil) { - rootview = [rootview superview]; - } - if (rootview == nil) - return; - - rootview = [rootview superview]; - - [rootview setNeedsFocusUpdate]; - [rootview updateFocusIfNeeded]; - }); - } -} - -@end diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 8b948eb5eb9ab8..ee6c41fee1cc2b 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -20,10 +20,6 @@ #import "RCTView.h" #import "UIView+React.h" -#if TARGET_OS_TV -#import "RCTTVView.h" -#endif - @implementation RCTConvert (UIAccessibilityTraits) RCT_MULTI_ENUM_CONVERTER( @@ -83,11 +79,7 @@ - (dispatch_queue_t)methodQueue - (UIView *)view { -#if TARGET_OS_TV - return [RCTTVView new]; -#else return [RCTView new]; -#endif } - (RCTShadowView *)shadowView @@ -118,13 +110,6 @@ - (RCTShadowView *)shadowView #pragma mark - View properties -#if TARGET_OS_TV -// TODO: Delete props for Apple TV. -RCT_EXPORT_VIEW_PROPERTY(isTVSelectable, BOOL) -RCT_EXPORT_VIEW_PROPERTY(hasTVPreferredFocus, BOOL) -RCT_EXPORT_VIEW_PROPERTY(tvParallaxProperties, NSDictionary) -#endif - // Accessibility related properties RCT_REMAP_VIEW_PROPERTY(accessible, reactAccessibilityElement.isAccessibilityElement, BOOL) RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSDictionaryArray) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 7e341219a63984..8bdb3bb461d267 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -11,6 +11,7 @@ #import "RCTConvert.h" #import "RCTLog.h" +#import "RCTRefreshControl.h" #import "RCTScrollEvent.h" #import "RCTUIManager.h" #import "RCTUIManagerObserverCoordinator.h" @@ -19,10 +20,6 @@ #import "UIView+Private.h" #import "UIView+React.h" -#if !TARGET_OS_TV -#import "RCTRefreshControl.h" -#endif - /** * Include a custom scroll view subclass because we want to limit certain * default UIKit behaviors such as textFields automatically scrolling @@ -31,10 +28,8 @@ @interface RCTCustomScrollView : UIScrollView @property (nonatomic, assign) BOOL centerContent; -#if !TARGET_OS_TV @property (nonatomic, strong) UIView *customRefreshControl; @property (nonatomic, assign) BOOL pinchGestureEnabled; -#endif @end @@ -52,9 +47,7 @@ - (instancetype)initWithFrame:(CGRect)frame self.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight; } -#if !TARGET_OS_TV _pinchGestureEnabled = YES; -#endif } return self; } @@ -223,7 +216,6 @@ - (void)setFrame:(CGRect)frame } } -#if !TARGET_OS_TV - (void)setCustomRefreshControl:(UIView *)refreshControl { if (_customRefreshControl) { @@ -256,7 +248,6 @@ - (void)didMoveToWindow // in the setter gets overridden when the view loads. self.pinchGestureRecognizer.enabled = _pinchGestureEnabled; } -#endif // TARGET_OS_TV - (BOOL)shouldGroupAccessibilityChildren { @@ -351,15 +342,12 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex { [super insertReactSubview:view atIndex:atIndex]; -#if !TARGET_OS_TV if ([view conformsToProtocol:@protocol(RCTCustomRefreshContolProtocol)]) { [_scrollView setCustomRefreshControl:(UIView *)view]; if (![view isKindOfClass:[UIRefreshControl class]] && [view conformsToProtocol:@protocol(UIScrollViewDelegate)]) { [self addScrollListener:(UIView *)view]; } - } else -#endif - { + } else { RCTAssert( _contentView == nil, @"RCTScrollView may only contain a single subview, the already set subview looks like: %@", @@ -373,16 +361,13 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex - (void)removeReactSubview:(UIView *)subview { [super removeReactSubview:subview]; -#if !TARGET_OS_TV if ([subview conformsToProtocol:@protocol(RCTCustomRefreshContolProtocol)]) { [_scrollView setCustomRefreshControl:nil]; if (![subview isKindOfClass:[UIRefreshControl class]] && [subview conformsToProtocol:@protocol(UIScrollViewDelegate)]) { [self removeScrollListener:(UIView *)subview]; } - } else -#endif - { + } else { RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview"); _contentView = nil; } @@ -994,10 +979,8 @@ -(type)getter \ RCT_SET_AND_PRESERVE_OFFSET(setMaximumZoomScale, maximumZoomScale, CGFloat) RCT_SET_AND_PRESERVE_OFFSET(setMinimumZoomScale, minimumZoomScale, CGFloat) RCT_SET_AND_PRESERVE_OFFSET(setScrollEnabled, isScrollEnabled, BOOL) -#if !TARGET_OS_TV RCT_SET_AND_PRESERVE_OFFSET(setPagingEnabled, isPagingEnabled, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setScrollsToTop, scrollsToTop, BOOL) -#endif RCT_SET_AND_PRESERVE_OFFSET(setShowsHorizontalScrollIndicator, showsHorizontalScrollIndicator, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL) RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat); diff --git a/React/Views/ScrollView/RCTScrollViewManager.m b/React/Views/ScrollView/RCTScrollViewManager.m index a176d1068609ba..086a05450cbd6d 100644 --- a/React/Views/ScrollView/RCTScrollViewManager.m +++ b/React/Views/ScrollView/RCTScrollViewManager.m @@ -78,11 +78,9 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(maximumZoomScale, CGFloat) RCT_EXPORT_VIEW_PROPERTY(minimumZoomScale, CGFloat) RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) -#if !TARGET_OS_TV RCT_EXPORT_VIEW_PROPERTY(pagingEnabled, BOOL) RCT_REMAP_VIEW_PROPERTY(pinchGestureEnabled, scrollView.pinchGestureEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(scrollsToTop, BOOL) -#endif RCT_EXPORT_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) diff --git a/ReactAndroid/Android-prebuilt.mk b/ReactAndroid/Android-prebuilt.mk new file mode 100644 index 00000000000000..4a4fb45ee1ff5b --- /dev/null +++ b/ReactAndroid/Android-prebuilt.mk @@ -0,0 +1,94 @@ +# 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. + +# This configuration provides access to most common React Native prebuilt .so files +# to avoid recompiling each of the libraries outside of ReactAndroid NDK compilation. +# Hosting app's/library's Android.mk can include this Android-prebuilt.mk file to +# get access to those .so to depend on. +# NOTES: +# * Currently, it assumes building React Native from source. +# * Not every .so is listed here (yet). +# * Static libs are not covered here (yet). + +LOCAL_PATH := $(call my-dir) + +REACT_ANDROID_DIR := $(LOCAL_PATH) +# TODO: Find a better way without pointing to ReactAndroid/build dir. +REACT_ANDROID_BUILD_DIR := $(REACT_ANDROID_DIR)/build + +FIRST_PARTY_NDK_DIR := $(REACT_ANDROID_DIR)/src/main/jni/first-party +THIRD_PARTY_NDK_DIR := $(REACT_ANDROID_BUILD_DIR)/third-party-ndk +REACT_ANDROID_SRC_DIR := $(REACT_ANDROID_DIR)/src/main +REACT_COMMON_DIR := $(REACT_ANDROID_DIR)/../ReactCommon +REACT_GENERATED_SRC_DIR := $(REACT_ANDROID_BUILD_DIR)/generated/source +# Note: this only have .so, not .a +REACT_NDK_EXPORT_DIR := $(REACT_ANDROID_BUILD_DIR)/react-ndk/exported + +# fb +include $(CLEAR_VARS) +LOCAL_MODULE := fb +LOCAL_SRC_FILES := $(REACT_NDK_EXPORT_DIR)/$(TARGET_ARCH_ABI)/libfb.so +LOCAL_EXPORT_C_INCLUDES := $(FIRST_PARTY_NDK_DIR)/fb/include +include $(PREBUILT_SHARED_LIBRARY) + +# folly_json +include $(CLEAR_VARS) +LOCAL_MODULE := folly_json +LOCAL_SRC_FILES := $(REACT_NDK_EXPORT_DIR)/$(TARGET_ARCH_ABI)/libfolly_json.so +LOCAL_EXPORT_C_INCLUDES := \ + $(THIRD_PARTY_NDK_DIR)/boost/boost_1_63_0 \ + $(THIRD_PARTY_NDK_DIR)/double-conversion \ + $(THIRD_PARTY_NDK_DIR)/folly \ + $(THIRD_PARTY_NDK_DIR)/glog/exported +# Note: Sync with folly/Android.mk. +FOLLY_FLAGS := \ + -DFOLLY_NO_CONFIG=1 \ + -DFOLLY_HAVE_CLOCK_GETTIME=1 \ + -DFOLLY_HAVE_MEMRCHR=1 \ + -DFOLLY_USE_LIBCPP=1 \ + -DFOLLY_MOBILE=1 \ + -DFOLLY_HAVE_XSI_STRERROR_R=1 +LOCAL_CFLAGS += $(FOLLY_FLAGS) +LOCAL_EXPORT_CPPFLAGS := $(FOLLY_FLAGS) +include $(PREBUILT_SHARED_LIBRARY) + +# folly_futures +include $(CLEAR_VARS) +LOCAL_MODULE := folly_futures +LOCAL_SRC_FILES := $(REACT_NDK_EXPORT_DIR)/$(TARGET_ARCH_ABI)/libfolly_futures.so +LOCAL_SHARED_LIBRARIES := liblibfolly_json +include $(PREBUILT_SHARED_LIBRARY) + +# react_nativemodule_core +include $(CLEAR_VARS) +LOCAL_MODULE := react_nativemodule_core +LOCAL_SRC_FILES := $(REACT_NDK_EXPORT_DIR)/$(TARGET_ARCH_ABI)/libreact_nativemodule_core.so +LOCAL_EXPORT_C_INCLUDES := \ + $(REACT_ANDROID_SRC_DIR)/jni \ + $(REACT_COMMON_DIR)/callinvoker \ + $(REACT_COMMON_DIR)/jsi \ + $(REACT_COMMON_DIR)/react/nativemodule/core \ + $(REACT_COMMON_DIR)/react/nativemodule/core/platform/android +LOCAL_SHARED_LIBRARIES := libfolly_json +include $(PREBUILT_SHARED_LIBRARY) + +# turbomodulejsijni +include $(CLEAR_VARS) +LOCAL_MODULE := turbomodulejsijni +LOCAL_SRC_FILES := $(REACT_NDK_EXPORT_DIR)/$(TARGET_ARCH_ABI)/libturbomodulejsijni.so +LOCAL_EXPORT_C_INCLUDES := \ + $(REACT_ANDROID_SRC_DIR)/java/com/facebook/react/turbomodule/core/jni +include $(PREBUILT_SHARED_LIBRARY) + +# react_codegen_reactandroidspec +include $(CLEAR_VARS) +LOCAL_MODULE := react_codegen_reactandroidspec +LOCAL_SRC_FILES := $(REACT_NDK_EXPORT_DIR)/$(TARGET_ARCH_ABI)/libreact_codegen_reactandroidspec.so +LOCAL_EXPORT_C_INCLUDES := \ + $(REACT_GENERATED_SRC_DIR)/codegen/jni +include $(PREBUILT_SHARED_LIBRARY) + +# fbjni +include $(FIRST_PARTY_NDK_DIR)/fbjni/Android.mk diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 1189ead6e9a4c4..1c0a3a0f9b301a 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -33,6 +33,9 @@ def thirdPartyNdkDir = new File("$buildDir/third-party-ndk") // - glog-0.3.5 def dependenciesPath = System.getenv("REACT_NATIVE_DEPENDENCIES") +// The 'USE_CODEGEN' environment variable will use codegen and compile the output. +def enableCodegen = (System.getenv('USE_CODEGEN') ?: '0').toBoolean() + // The 'USE_FABRIC' environment variable will build Fabric C++ code into the bundle // USE_FABRIC=0 will build RN excluding fabric // USE_FABRIC=1 will build RN including fabric @@ -301,6 +304,7 @@ def getNdkBuildFullPath() { def buildReactNdkLib = tasks.register("buildReactNdkLib", Exec) { dependsOn(prepareJSC, prepareHermes, prepareBoost, prepareDoubleConversion, prepareFolly, prepareGlog, extractAARHeaders, extractJNIFiles) + dependsOn("generateCodegenArtifactsFromSchema"); inputs.dir("$projectDir/../ReactCommon") inputs.dir("src/main/jni") @@ -314,8 +318,10 @@ def buildReactNdkLib = tasks.register("buildReactNdkLib", Exec) { "NDK_LIBS_OUT=$buildDir/react-ndk/all", "THIRD_PARTY_NDK_DIR=$thirdPartyNdkDir", "REACT_COMMON_DIR=$projectDir/../ReactCommon", + "REACT_GENERATED_SRC_DIR=$buildDir/generated/source", "REACT_SRC_DIR=$projectDir/src/main/java/com/facebook/react", "BUILD_FABRIC=$enableFabric", + "USE_CODEGEN=$enableCodegen", "-C", file("src/main/jni/react/jni").absolutePath, "--jobs", project.findProperty("jobs") ?: Runtime.runtime.availableProcessors() ) @@ -471,7 +477,6 @@ dependencies { apply(from: "release.gradle") react { - enableCodegen = System.getenv("USE_CODEGEN") ?: false jsRootDir = file("../Libraries") reactNativeRootDir = file("$rootDir") useJavaGenerator = System.getenv("USE_CODEGEN_JAVAPOET") ?: false diff --git a/ReactAndroid/gradle.properties b/ReactAndroid/gradle.properties index c35e7783e39570..04ef27362cf6fc 100644 --- a/ReactAndroid/gradle.properties +++ b/ReactAndroid/gradle.properties @@ -14,7 +14,7 @@ FEST_ASSERT_CORE_VERSION=2.0M10 ANDROIDX_TEST_VERSION=1.1.0 FRESCO_VERSION=2.0.0 OKHTTP_VERSION=3.12.12 -SO_LOADER_VERSION=0.8.2 +SO_LOADER_VERSION=0.9.0 BOOST_VERSION=1_63_0 DOUBLE_CONVERSION_VERSION=1.1.6 diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java index 3d955e4b4e5fba..920ce021812192 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java @@ -199,6 +199,37 @@ public void run() { assertFalse(reactEditText.isFocused()); } + public void testAccessibilityFocus_notEmpty_selectionSetAtEnd() throws Throwable { + String testId = "textInput1"; + String text = "Testing"; + + final ReactEditText reactEditText = getViewByTestId(testId); + reactEditText.setText(text); + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + reactEditText.clearFocus(); + } + }); + waitForBridgeAndUIIdle(); + assertFalse(reactEditText.isFocused()); + assertEquals(0, reactEditText.getSelectionStart()); + + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + reactEditText.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + reactEditText.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, null); + } + }); + waitForBridgeAndUIIdle(); + assertTrue(reactEditText.isFocused()); + assertEquals(text.length(), reactEditText.getSelectionStart()); + } + private void fireEditorActionAndCheckRecording( final ReactEditText reactEditText, final int actionId) throws Throwable { fireEditorActionAndCheckRecording(reactEditText, actionId, true); diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/BUCK b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/BUCK index 94067e4f20f33d..49f617762e45c9 100644 --- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/BUCK @@ -51,7 +51,7 @@ rn_xplat_cxx_library( react_native_xplat_target("cxxreact:bridge"), ], exported_deps = ([ - react_native_xplat_target("turbomodule/core:core"), + react_native_xplat_target("react/nativemodule/core:core"), ]) + ([ "//xplat/jsi:jsi", ]) if not IS_OSS_BUILD else [], diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativePlatformConstantsAndroidSpec.java b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativePlatformConstantsAndroidSpec.java index c8340538348f63..00d2039ade6a21 100644 --- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativePlatformConstantsAndroidSpec.java +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativePlatformConstantsAndroidSpec.java @@ -41,12 +41,14 @@ public NativePlatformConstantsAndroidSpec(ReactApplicationContext reactContext) Map constants = getTypedExportedConstants(); if (ReactBuildConfig.DEBUG || ReactBuildConfig.IS_INTERNAL_BUILD) { Set obligatoryFlowConstants = new HashSet<>(Arrays.asList( + "Brand", "Serial", "Fingerprint", "uiMode", "Version", "reactNativeVersion", "Model", + "Manufacturer", "isTesting", "Release" )); diff --git a/ReactAndroid/src/main/java/com/facebook/react/MemoryPressureRouter.java b/ReactAndroid/src/main/java/com/facebook/react/MemoryPressureRouter.java index f1b639887c8dd9..41a386442219a9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/MemoryPressureRouter.java +++ b/ReactAndroid/src/main/java/com/facebook/react/MemoryPressureRouter.java @@ -20,7 +20,7 @@ public class MemoryPressureRouter implements ComponentCallbacks2 { private final Set mListeners = Collections.synchronizedSet(new LinkedHashSet()); - MemoryPressureRouter(Context context) { + public MemoryPressureRouter(Context context) { context.getApplicationContext().registerComponentCallbacks(this); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 7807b98c9091ee..d685867afbbf77 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -1151,30 +1151,18 @@ private void attachRootViewToInstance(final ReactRoot reactRoot) { final int rootTag; - if (ReactFeatureFlags.enableFabricStartSurfaceWithLayoutMetrics) { - if (reactRoot.getUIManagerType() == FABRIC) { - rootTag = - uiManager.startSurface( - reactRoot.getRootViewGroup(), - reactRoot.getJSModuleName(), - initialProperties == null - ? new WritableNativeMap() - : Arguments.fromBundle(initialProperties), - reactRoot.getWidthMeasureSpec(), - reactRoot.getHeightMeasureSpec()); - reactRoot.setRootViewTag(rootTag); - reactRoot.setShouldLogContentAppeared(true); - } else { - rootTag = - uiManager.addRootView( - reactRoot.getRootViewGroup(), - initialProperties == null - ? new WritableNativeMap() - : Arguments.fromBundle(initialProperties), - reactRoot.getInitialUITemplate()); - reactRoot.setRootViewTag(rootTag); - reactRoot.runApplication(); - } + if (reactRoot.getUIManagerType() == FABRIC) { + rootTag = + uiManager.startSurface( + reactRoot.getRootViewGroup(), + reactRoot.getJSModuleName(), + initialProperties == null + ? new WritableNativeMap() + : Arguments.fromBundle(initialProperties), + reactRoot.getWidthMeasureSpec(), + reactRoot.getHeightMeasureSpec()); + reactRoot.setRootViewTag(rootTag); + reactRoot.setShouldLogContentAppeared(true); } else { rootTag = uiManager.addRootView( @@ -1184,15 +1172,7 @@ private void attachRootViewToInstance(final ReactRoot reactRoot) { : Arguments.fromBundle(initialProperties), reactRoot.getInitialUITemplate()); reactRoot.setRootViewTag(rootTag); - if (reactRoot.getUIManagerType() == FABRIC) { - // Fabric requires to call updateRootLayoutSpecs before starting JS Application, - // this ensures the root will hace the correct pointScaleFactor. - uiManager.updateRootLayoutSpecs( - rootTag, reactRoot.getWidthMeasureSpec(), reactRoot.getHeightMeasureSpec()); - reactRoot.setShouldLogContentAppeared(true); - } else { - reactRoot.runApplication(); - } + reactRoot.runApplication(); } Systrace.beginAsyncSection( diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 45049cb5a65366..c21bffc720a519 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -14,6 +14,7 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.util.AttributeSet; @@ -291,7 +292,12 @@ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - // No-op since UIManagerModule handles actually laying out children. + // No-op in non-Fabric since UIManagerModule handles actually laying out children. + + // In Fabric, update LayoutSpecs just so we update the offsetX and offsetY. + if (mWasMeasured && getUIManagerType() == FABRIC) { + updateRootLayoutSpecs(mWidthMeasureSpec, mHeightMeasureSpec); + } } @Override @@ -376,8 +382,6 @@ public void startReactApplication( mReactInstanceManager.createReactContextInBackground(); - attachToReactInstanceManager(); - } finally { Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); } @@ -405,6 +409,20 @@ public String getSurfaceID() { return appProperties != null ? appProperties.getString("surfaceID") : null; } + public static Point getViewportOffset(View v) { + int[] locationInWindow = new int[2]; + v.getLocationInWindow(locationInWindow); + + // we need to subtract visibleWindowCoords - to subtract possible window insets, split + // screen or multi window + Rect visibleWindowFrame = new Rect(); + v.getWindowVisibleDisplayFrame(visibleWindowFrame); + locationInWindow[0] -= visibleWindowFrame.left; + locationInWindow[1] -= visibleWindowFrame.top; + + return new Point(locationInWindow[0], locationInWindow[1]); + } + private void updateRootLayoutSpecs(final int widthMeasureSpec, final int heightMeasureSpec) { if (mReactInstanceManager == null) { FLog.w(TAG, "Unable to update root layout specs for uninitialized ReactInstanceManager"); @@ -418,7 +436,17 @@ private void updateRootLayoutSpecs(final int widthMeasureSpec, final int heightM UIManagerHelper.getUIManager(reactApplicationContext, getUIManagerType()); // Ignore calling updateRootLayoutSpecs if UIManager is not properly initialized. if (uiManager != null) { - uiManager.updateRootLayoutSpecs(getRootViewTag(), widthMeasureSpec, heightMeasureSpec); + // In Fabric only, get position of view within screen + int offsetX = 0; + int offsetY = 0; + if (getUIManagerType() == FABRIC) { + Point viewportOffset = getViewportOffset(this); + offsetX = viewportOffset.x; + offsetY = viewportOffset.y; + } + + uiManager.updateRootLayoutSpecs( + getRootViewTag(), widthMeasureSpec, heightMeasureSpec, offsetX, offsetY); } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMarkerConstants.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMarkerConstants.java index dd08b9a8eea480..8c5f38d4d89c06 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMarkerConstants.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactMarkerConstants.java @@ -108,5 +108,8 @@ public enum ReactMarkerConstants { FABRIC_BATCH_EXECUTION_START, FABRIC_BATCH_EXECUTION_END, FABRIC_UPDATE_UI_MAIN_THREAD_START, - FABRIC_UPDATE_UI_MAIN_THREAD_END + FABRIC_UPDATE_UI_MAIN_THREAD_END, + // New markers used by bridgeless RN below this line + REACT_INSTANCE_INIT_START, + REACT_INSTANCE_INIT_END } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java index 0bd6a2c649a15e..74af261f5b47c3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UIManager.java @@ -41,11 +41,12 @@ int startSurface( /** * Updates the layout specs of the RootShadowNode based on the Measure specs received by - * parameters. + * parameters. offsetX and offsetY are the position of the RootView within the screen. */ @UiThread @ThreadConfined(UI) - void updateRootLayoutSpecs(int rootTag, int widthMeasureSpec, int heightMeasureSpec); + void updateRootLayoutSpecs( + int rootTag, int widthMeasureSpec, int heightMeasureSpec, int offsetX, int offsetY); /** * Dispatches the commandId received by parameter to the view associated with the reactTag. The diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 7a6b8a95ae4c41..b83c4c821d9152 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -63,12 +63,12 @@ public class ReactFeatureFlags { /** Feature flag to configure eager initialization of Fabric */ public static boolean eagerInitializeFabric = false; - /** Feature flag to configure initialization of Fabric surfaces. */ - public static boolean enableFabricStartSurfaceWithLayoutMetrics = true; - /** Feature flag to use stopSurface when ReactRootView is unmounted. */ public static boolean enableStopSurfaceOnRootViewUnmount = false; /** Use experimental SetState retry mechanism in view? */ public static boolean enableExperimentalStateUpdateRetry = false; + + /** Enable caching of Spannable objects using equality of ReadableNativeMaps */ + public static boolean enableSpannableCacheByReadableNativeMapEquality = true; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK b/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK index 9ebcd431161f44..a1882a6cdbd5d1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK @@ -39,6 +39,7 @@ rn_android_library( react_native_target("java/com/facebook/react/common:common"), react_native_target("java/com/facebook/react/uimanager:uimanager"), react_native_target("java/com/facebook/react/views/view:view"), + react_native_target("java/com/facebook/react/views/text:text"), react_native_target("java/com/facebook/react/touch:touch"), ], exported_deps = [ diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java index cbe49ce51d2088..ea080bd4e071ed 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java @@ -52,6 +52,8 @@ public native void startSurfaceWithConstraints( float maxWidth, float minHeight, float maxHeight, + float offsetX, + float offsetY, boolean isRTL, boolean doLeftAndRightSwapInRTL); @@ -67,6 +69,8 @@ public native void setConstraints( float maxWidth, float minHeight, float maxHeight, + float offsetX, + float offsetY, boolean isRTL, boolean doLeftAndRightSwapInRTL); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 740809e39ea0b1..3e5b6c37aaba5f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -22,6 +22,7 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Point; import android.os.SystemClock; import android.view.View; import androidx.annotation.AnyThread; @@ -34,7 +35,9 @@ import com.facebook.debug.tags.ReactDebugOverlayTags; import com.facebook.infer.annotation.ThreadConfined; import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.ReactRootView; import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.NativeArray; import com.facebook.react.bridge.NativeMap; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; @@ -72,6 +75,7 @@ import com.facebook.react.fabric.mounting.mountitems.UpdateStateMountItem; import com.facebook.react.modules.core.ReactChoreographer; import com.facebook.react.modules.i18nmanager.I18nUtil; +import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactRoot; import com.facebook.react.uimanager.ReactRootViewTagGenerator; import com.facebook.react.uimanager.StateWrapper; @@ -80,6 +84,7 @@ import com.facebook.react.uimanager.ViewManagerPropertyUpdater; import com.facebook.react.uimanager.ViewManagerRegistry; import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.text.TextLayoutManager; import com.facebook.systrace.Systrace; import java.util.ArrayDeque; import java.util.ArrayList; @@ -230,6 +235,9 @@ public int startSurface( } mMountingManager.addRootView(rootTag, rootView); mReactContextForRootTag.put(rootTag, reactContext); + + Point viewportOffset = ReactRootView.getViewportOffset(rootView); + mBinding.startSurfaceWithConstraints( rootTag, moduleName, @@ -238,6 +246,8 @@ public int startSurface( getMaxSize(widthMeasureSpec), getMinSize(heightMeasureSpec), getMaxSize(heightMeasureSpec), + viewportOffset.x, + viewportOffset.y, I18nUtil.getInstance().isRTL(context), I18nUtil.getInstance().doLeftAndRightSwapInRTL(context)); return rootTag; @@ -436,6 +446,18 @@ private MountItem createBatchMountItem( return new BatchMountItem(rootTag, items, size, commitNumber); } + @DoNotStrip + @SuppressWarnings("unused") + private NativeArray measureLines( + ReadableMap attributedString, ReadableMap paragraphAttributes, float width, float height) { + return (NativeArray) + TextLayoutManager.measureLines( + mReactApplicationContext, + attributedString, + paragraphAttributes, + PixelUtil.toPixelFromDIP(width)); + } + @DoNotStrip @SuppressWarnings("unused") private long measure( @@ -957,7 +979,11 @@ public void setBinding(Binding binding) { @UiThread @ThreadConfined(UI) public void updateRootLayoutSpecs( - final int rootTag, final int widthMeasureSpec, final int heightMeasureSpec) { + final int rootTag, + final int widthMeasureSpec, + final int heightMeasureSpec, + final int offsetX, + final int offsetY) { if (ENABLE_FABRIC_LOGS) { FLog.d(TAG, "Updating Root Layout Specs"); @@ -977,6 +1003,8 @@ public void updateRootLayoutSpecs( getMaxSize(widthMeasureSpec), getMinSize(heightMeasureSpec), getMaxSize(heightMeasureSpec), + offsetX, + offsetY, isRTL, doLeftAndRightSwapInRTL); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp index 0764559764f5cd..134c1c5de27daf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp @@ -101,6 +101,8 @@ void Binding::startSurfaceWithConstraints( jfloat maxWidth, jfloat minHeight, jfloat maxHeight, + jfloat offsetX, + jfloat offsetY, jboolean isRTL, jboolean doLeftAndRightSwapInRTL) { SystraceSection s("FabricUIManagerBinding::startSurfaceWithConstraints"); @@ -123,6 +125,8 @@ void Binding::startSurfaceWithConstraints( Size{maxWidth / pointScaleFactor_, maxHeight / pointScaleFactor_}; LayoutContext context; + context.viewportOffset = + Point{offsetX / pointScaleFactor_, offsetY / pointScaleFactor_}; context.pointScaleFactor = {pointScaleFactor_}; context.swapLeftAndRightInRTL = doLeftAndRightSwapInRTL; LayoutConstraints constraints = {}; @@ -178,6 +182,8 @@ void Binding::setConstraints( jfloat maxWidth, jfloat minHeight, jfloat maxHeight, + jfloat offsetX, + jfloat offsetY, jboolean isRTL, jboolean doLeftAndRightSwapInRTL) { SystraceSection s("FabricUIManagerBinding::setConstraints"); @@ -194,6 +200,8 @@ void Binding::setConstraints( Size{maxWidth / pointScaleFactor_, maxHeight / pointScaleFactor_}; LayoutContext context; + context.viewportOffset = + Point{offsetX / pointScaleFactor_, offsetY / pointScaleFactor_}; context.pointScaleFactor = {pointScaleFactor_}; context.swapLeftAndRightInRTL = doLeftAndRightSwapInRTL; LayoutConstraints constraints = {}; @@ -420,11 +428,12 @@ local_ref createUpdatePaddingMountItem( auto newChildShadowView = mutation.newChildShadowView; if (oldChildShadowView.layoutMetrics.contentInsets == - newChildShadowView.layoutMetrics.contentInsets) { + newChildShadowView.layoutMetrics.contentInsets && + mutation.type != ShadowViewMutation::Type::Insert) { return nullptr; } - static auto updateLayoutInstruction = + static auto updatePaddingInstruction = jni::findClassStatic(Binding::UIManagerJavaDescriptor) ->getMethod(jint, jint, jint, jint, jint)>( "updatePaddingMountItem"); @@ -433,12 +442,12 @@ local_ref createUpdatePaddingMountItem( auto pointScaleFactor = layoutMetrics.pointScaleFactor; auto contentInsets = layoutMetrics.contentInsets; - int left = round(contentInsets.left * pointScaleFactor); - int top = round(contentInsets.top * pointScaleFactor); - int right = round(contentInsets.right * pointScaleFactor); - int bottom = round(contentInsets.bottom * pointScaleFactor); + int left = floor(contentInsets.left * pointScaleFactor); + int top = floor(contentInsets.top * pointScaleFactor); + int right = floor(contentInsets.right * pointScaleFactor); + int bottom = floor(contentInsets.bottom * pointScaleFactor); - return updateLayoutInstruction( + return updatePaddingInstruction( javaUIManager, newChildShadowView.tag, left, top, right, bottom); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h index fe27981709892b..635b74185c0983 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h @@ -48,6 +48,8 @@ class Binding : public jni::HybridClass, jfloat maxWidth, jfloat minHeight, jfloat maxHeight, + jfloat offsetX, + jfloat offsetY, jboolean isRTL, jboolean doLeftAndRightSwapInRTL); @@ -74,6 +76,8 @@ class Binding : public jni::HybridClass, jfloat maxWidth, jfloat minHeight, jfloat maxHeight, + jfloat offsetX, + jfloat offsetY, jboolean isRTL, jboolean doLeftAndRightSwapInRTL); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java index 4629668d4944b4..6ff40bda0ffb18 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java @@ -399,7 +399,7 @@ public void removeViewAt(final int tag, final int parentTag, final int index) { } throw new IllegalStateException( - "Tried to delete view [" + "Tried to remove view [" + tag + "] of parent [" + parentTag @@ -577,12 +577,20 @@ public void deleteView(int reactTag) { return; } - View view = viewState.mView; + // To delete we simply remove the tag from the registry. + // In the past we called dropView here, but we want to rely on either + // (1) the correct set of MountInstructions being sent to the platform + // and/or (2) dropView being called by stopSurface. + // If Views are orphaned at this stage and leaked, it's a problem in + // the differ or LayoutAnimations, not MountingManager. + // Additionally, as documented in `dropView`, we cannot always trust a + // view's children to be up-to-date. + mTagToViewState.remove(reactTag); - if (view != null) { - dropView(view, false); - } else { - mTagToViewState.remove(reactTag); + // For non-root views we notify viewmanager with {@link ViewManager#onDropInstance} + ViewManager viewManager = viewState.mViewManager; + if (!viewState.mIsRoot && viewManager != null) { + viewManager.onDropViewInstance(viewState.mView); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchMountItem.java index d685be1ae02868..fc908e2deedc29 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchMountItem.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/BatchMountItem.java @@ -8,6 +8,7 @@ package com.facebook.react.fabric.mounting.mountitems; import androidx.annotation.NonNull; +import com.facebook.common.logging.FLog; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.react.bridge.ReactMarker; import com.facebook.react.bridge.ReactMarkerConstants; @@ -70,8 +71,20 @@ private void endMarkers() { public void execute(@NonNull MountingManager mountingManager) { beginMarkers("mountViews"); - for (int mountItemIndex = 0; mountItemIndex < mSize; mountItemIndex++) { - mMountItems[mountItemIndex].execute(mountingManager); + int mountItemIndex = 0; + try { + for (; mountItemIndex < mSize; mountItemIndex++) { + mMountItems[mountItemIndex].execute(mountingManager); + } + } catch (RuntimeException e) { + FLog.e( + TAG, + "Caught exception executing mountItem @" + + mountItemIndex + + ": " + + mMountItems[mountItemIndex].toString(), + e); + throw e; } endMarkers(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java index e3a25072156aa6..664b359a27b63b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/DialogModule.java @@ -129,7 +129,8 @@ public AlertFragmentListener(Callback callback) { @Override public void onClick(DialogInterface dialog, int which) { if (!mCallbackConsumed) { - if (getReactApplicationContext().hasActiveCatalystInstance()) { + if (getReactApplicationContext().isBridgeless() + || getReactApplicationContext().hasActiveCatalystInstance()) { mCallback.invoke(ACTION_BUTTON_CLICKED, which); mCallbackConsumed = true; } @@ -139,7 +140,8 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onDismiss(DialogInterface dialog) { if (!mCallbackConsumed) { - if (getReactApplicationContext().hasActiveCatalystInstance()) { + if (getReactApplicationContext().isBridgeless() + || getReactApplicationContext().hasActiveCatalystInstance()) { mCallback.invoke(ACTION_DISMISSED); mCallbackConsumed = true; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java index 3c4be024f518c5..b2355c7ecbf62e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java @@ -70,6 +70,8 @@ public String getName() { constants.put("Serial", Build.SERIAL); constants.put("Fingerprint", Build.FINGERPRINT); constants.put("Model", Build.MODEL); + constants.put("Manufacturer", Build.MANUFACTURER); + constants.put("Brand", Build.BRAND); if (ReactBuildConfig.DEBUG) { constants.put( "ServerHost", diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/TurboModuleManager.java b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/TurboModuleManager.java index 35f393a82e4e90..e6e6fcce69e099 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/TurboModuleManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/TurboModuleManager.java @@ -10,7 +10,6 @@ import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.jni.HybridData; import com.facebook.proguard.annotations.DoNotStrip; @@ -18,7 +17,6 @@ import com.facebook.react.bridge.JSIModule; import com.facebook.react.bridge.JavaScriptContextHolder; import com.facebook.react.bridge.NativeModule; -import com.facebook.react.common.ReactConstants; import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder; import com.facebook.react.turbomodule.core.interfaces.TurboModule; import com.facebook.react.turbomodule.core.interfaces.TurboModuleRegistry; @@ -218,10 +216,6 @@ private TurboModule getModule( * Therefore, we should initialize on the TurboModule now. */ ((NativeModule) turboModule).initialize(); - } else { - FLog.e( - ReactConstants.TAG, - "TurboModuleManager.getModule: TurboModule " + moduleName + " not found in delegate"); } TurboModulePerfLogger.moduleCreateSetUpEnd(moduleName, moduleHolder.getModuleId()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/Android.mk b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/Android.mk index 80ba4096da6480..955cc86a8b10de 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/Android.mk +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/Android.mk @@ -5,6 +5,10 @@ LOCAL_PATH := $(call my-dir) +######################### +### callinvokerholder ### +######################### + include $(CLEAR_VARS) # Header search path for all source files in this module. @@ -15,10 +19,10 @@ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall -LOCAL_STATIC_LIBRARIES = libcallinvoker libreactperfloggerjni - LOCAL_SHARED_LIBRARIES = libfb libfbjni +LOCAL_STATIC_LIBRARIES = libcallinvoker libreactperfloggerjni + # Name of this module. LOCAL_MODULE := callinvokerholder @@ -27,3 +31,31 @@ LOCAL_SRC_FILES := $(LOCAL_PATH)/ReactCommon/CallInvokerHolder.cpp # Build the files in this directory as a shared library include $(BUILD_STATIC_LIBRARY) + +################################## +### react_nativemodule_manager ### +################################## + +include $(CLEAR_VARS) + +# Name of this module. +# TODO: rename to react_nativemodule_manager +LOCAL_MODULE := turbomodulejsijni + +# Header search path for all source files in this module. +LOCAL_C_INCLUDES := $(LOCAL_PATH)/ReactCommon + +# Header search path for modules that depend on this module +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +LOCAL_SHARED_LIBRARIES = libfb libfbjni libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES = libcallinvokerholder libreactperfloggerjni + +# Compile all local c++ files +LOCAL_SRC_FILES := $(LOCAL_PATH)/ReactCommon/TurboModuleManager.cpp $(LOCAL_PATH)/ReactCommon/OnLoad.cpp + +# Build the files in this directory as a shared library +include $(BUILD_SHARED_LIBRARY) diff --git a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/BUCK b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/BUCK index 84258474072b91..934dd73ac5d5c7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/BUCK @@ -34,7 +34,7 @@ rn_xplat_cxx_library( exported_deps = [ ":callinvokerholder", "//xplat/jsi:jsi", - react_native_xplat_target("turbomodule/core:core"), + react_native_xplat_target("react/nativemodule/core:core"), react_native_target("java/com/facebook/react/reactperflogger/jni:jni"), ], ) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index 17480d628e73c3..c5340ae3b7510a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -926,10 +926,14 @@ public void invalidateNodeLayout(int tag) { /** * Updates the styles of the {@link ReactShadowNode} based on the Measure specs received by - * parameters. + * parameters. offsetX and offsetY aren't used in non-Fabric, so they're ignored here. */ public void updateRootLayoutSpecs( - final int rootViewTag, final int widthMeasureSpec, final int heightMeasureSpec) { + final int rootViewTag, + final int widthMeasureSpec, + final int heightMeasureSpec, + int offsetX, + int offsetY) { ReactApplicationContext reactApplicationContext = getReactApplicationContext(); reactApplicationContext.runOnNativeModulesQueueThread( new GuardedRunnable(reactApplicationContext) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index 0578d16548f844..61b1a712332a5d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -21,11 +21,14 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.accessibility.AccessibilityEvent; import android.widget.HorizontalScrollView; import android.widget.OverScroller; import androidx.annotation.Nullable; import androidx.core.text.TextUtilsCompat; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.WritableMap; @@ -104,6 +107,23 @@ public ReactHorizontalScrollView(Context context, @Nullable FpsListener fpsListe mReactBackgroundManager = new ReactViewBackgroundManager(this); mFpsListener = fpsListener; + ViewCompat.setAccessibilityDelegate( + this, + new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setScrollable(mScrollEnabled); + } + + @Override + public void onInitializeAccessibilityNodeInfo( + View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setScrollable(mScrollEnabled); + } + }); + mScroller = getOverScrollerFromParent(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK index cd966af069962c..ac69e03c258350 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK @@ -22,6 +22,7 @@ rn_android_library( react_native_dep("third-party/java/jsr-305:jsr-305"), react_native_target("java/com/facebook/react/bridge:bridge"), react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/config:config"), react_native_target("java/com/facebook/react/module/annotations:annotations"), react_native_target("java/com/facebook/react/uimanager:uimanager"), react_native_target("java/com/facebook/react/uimanager/annotations:annotations"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLineHeightSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLineHeightSpan.java index 9959bea4b828f1..0c8208468badce 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLineHeightSpan.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLineHeightSpan.java @@ -17,7 +17,7 @@ public class CustomLineHeightSpan implements LineHeightSpan, ReactSpan { private final int mHeight; - CustomLineHeightSpan(float height) { + public CustomLineHeightSpan(float height) { this.mHeight = (int) Math.ceil(height); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactClickableSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactClickableSpan.java new file mode 100644 index 00000000000000..37f7000a915b9c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactClickableSpan.java @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package com.facebook.react.views.text; + +import android.text.TextPaint; +import android.text.style.ClickableSpan; +import android.view.View; +import androidx.annotation.NonNull; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.view.ViewGroupClickEvent; + +/** + * This class is used in {@link TextLayoutManager} to linkify and style a span of text with + * accessibilityRole="link". This is needed to make nested Text components accessible. + * + *

For example, if your React component looks like this: + * + *

{@code
+ * 
+ *   Some text with
+ *   a link
+ *   in the middle.
+ * 
+ * }
+ * + * then only one {@link ReactTextView} will be created, for the parent. The child Text component + * does not exist as a native view, and therefore has no accessibility properties. Instead, we have + * to use spans on the parent's {@link ReactTextView} to properly style the child, and to make it + * accessible (TalkBack announces that the text has links available, and the links are exposed in + * the context menu). + */ +class ReactClickableSpan extends ClickableSpan implements ReactSpan { + + private final int mReactTag; + private final int mForegroundColor; + + ReactClickableSpan(int reactTag, int foregroundColor) { + mReactTag = reactTag; + mForegroundColor = foregroundColor; + } + + @Override + public void onClick(@NonNull View view) { + ReactContext context = (ReactContext) view.getContext(); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(context, mReactTag); + if (eventDispatcher != null) { + eventDispatcher.dispatchEvent(new ViewGroupClickEvent(mReactTag)); + } + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setColor(mForegroundColor); + ds.setUnderlineText(false); + } + + public int getReactTag() { + return mReactTag; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index ac84caae1e07d9..5dbd0796e53363 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -144,6 +144,9 @@ public long measure( } } + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.Q) { + layoutWidth = (float) Math.ceil(layoutWidth); + } float layoutHeight = height; if (heightMode != YogaMeasureMode.EXACTLY) { layoutHeight = layout.getLineBottom(lineCount - 1); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java index 31a1139b697a76..7400b1df909e86 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java @@ -11,8 +11,6 @@ import android.text.Layout; import android.text.Spannable; -import androidx.annotation.Nullable; -import com.facebook.react.bridge.ReadableMap; /** * Class that contains the data needed for a text update. Used by both and @@ -33,7 +31,7 @@ public class ReactTextUpdate { private final int mSelectionEnd; private final int mJustificationMode; - public @Nullable ReadableMap mAttributedString = null; + public boolean mContainsMultipleFragments; /** * @deprecated Use a non-deprecated constructor for ReactTextUpdate instead. This one remains @@ -145,14 +143,13 @@ public static ReactTextUpdate buildReactTextUpdateFromState( int textAlign, int textBreakStrategy, int justificationMode, - ReadableMap attributedString) { + boolean containsMultipleFragments) { - ReactTextUpdate textUpdate = + ReactTextUpdate reactTextUpdate = new ReactTextUpdate( text, jsEventCounter, false, textAlign, textBreakStrategy, justificationMode); - - textUpdate.mAttributedString = attributedString; - return textUpdate; + reactTextUpdate.mContainsMultipleFragments = containsMultipleFragments; + return reactTextUpdate; } public Spannable getText() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index 29e3e2969fb879..f99e24019e5236 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -17,6 +17,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; import com.facebook.yoga.YogaDirection; @@ -72,6 +73,9 @@ public class TextAttributeProps { protected boolean mIsLineThroughTextDecorationSet = false; protected boolean mIncludeFontPadding = true; + protected @Nullable ReactAccessibilityDelegate.AccessibilityRole mAccessibilityRole = null; + protected boolean mIsAccessibilityRoleSet = false; + /** * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link * Typeface#NORMAL} or {@link Typeface#BOLD}. @@ -134,6 +138,7 @@ public TextAttributeProps(ReactStylesDiffMap props) { setTextShadowColor(getIntProp(PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR)); setTextTransform(getStringProp(PROP_TEXT_TRANSFORM)); setLayoutDirection(getStringProp(ViewProps.LAYOUT_DIRECTION)); + setAccessibilityRole(getStringProp(ViewProps.ACCESSIBILITY_ROLE)); } public static int getTextAlignment(ReactStylesDiffMap props, boolean isRTL) { @@ -412,6 +417,14 @@ public void setTextTransform(@Nullable String textTransform) { } } + public void setAccessibilityRole(@Nullable String accessibilityRole) { + if (accessibilityRole != null) { + mIsAccessibilityRoleSet = accessibilityRole != null; + mAccessibilityRole = + ReactAccessibilityDelegate.AccessibilityRole.fromValue(accessibilityRole); + } + } + public static int getTextBreakStrategy(@Nullable String textBreakStrategy) { int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY; if (textBreakStrategy != null) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index ce32acc099d816..34ad7c4e9af018 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -26,7 +26,11 @@ import com.facebook.common.logging.FLog; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; import com.facebook.yoga.YogaConstants; @@ -61,6 +65,8 @@ public class TextLayoutManager { private static final String MAXIMUM_NUMBER_OF_LINES_KEY = "maximumNumberOfLines"; private static final LruCache sSpannableCache = new LruCache<>(spannableCacheSize); + private static final LruCache sSpannableCacheV2 = + new LruCache<>(spannableCacheSize); private static final ConcurrentHashMap sTagToSpannableCache = new ConcurrentHashMap<>(); @@ -111,7 +117,12 @@ private static void buildSpannableFromFragment( sb.length(), new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); } else if (end >= start) { - if (textAttributes.mIsColorSet) { + if (ReactAccessibilityDelegate.AccessibilityRole.LINK.equals( + textAttributes.mAccessibilityRole)) { + ops.add( + new SetSpanOperation( + start, end, new ReactClickableSpan(reactTag, textAttributes.mColor))); + } else if (textAttributes.mIsColorSet) { ops.add( new SetSpanOperation( start, end, new ReactForegroundColorSpan(textAttributes.mColor))); @@ -179,20 +190,40 @@ public static Spannable getOrCreateSpannableForText( @Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) { Spannable preparedSpannableText; - String attributedStringPayload = attributedString.toString(); - synchronized (sSpannableCacheLock) { - preparedSpannableText = sSpannableCache.get(attributedStringPayload); - // TODO: T31905686 implement proper equality of attributedStrings - if (preparedSpannableText != null) { - return preparedSpannableText; + String attributedStringPayload = ""; + + boolean cacheByReadableNativeMap = + ReactFeatureFlags.enableSpannableCacheByReadableNativeMapEquality; + // TODO: T74600554 Cleanup this experiment once positive impact is confirmed in production + if (cacheByReadableNativeMap) { + synchronized (sSpannableCacheLock) { + preparedSpannableText = sSpannableCacheV2.get((ReadableNativeMap) attributedString); + if (preparedSpannableText != null) { + return preparedSpannableText; + } + } + } else { + attributedStringPayload = attributedString.toString(); + synchronized (sSpannableCacheLock) { + preparedSpannableText = sSpannableCache.get(attributedStringPayload); + if (preparedSpannableText != null) { + return preparedSpannableText; + } } } preparedSpannableText = createSpannableFromAttributedString( context, attributedString, reactTextViewManagerCallback); - synchronized (sSpannableCacheLock) { - sSpannableCache.put(attributedStringPayload, preparedSpannableText); + + if (cacheByReadableNativeMap) { + synchronized (sSpannableCacheLock) { + sSpannableCacheV2.put((ReadableNativeMap) attributedString, preparedSpannableText); + } + } else { + synchronized (sSpannableCacheLock) { + sSpannableCache.put(attributedStringPayload, preparedSpannableText); + } } return preparedSpannableText; } @@ -227,51 +258,19 @@ private static Spannable createSpannableFromAttributedString( return sb; } - public static long measureText( - Context context, - ReadableMap attributedString, - ReadableMap paragraphAttributes, + private static Layout createLayout( + Spannable text, + BoringLayout.Metrics boring, float width, YogaMeasureMode widthYogaMeasureMode, - float height, - YogaMeasureMode heightYogaMeasureMode, - ReactTextViewManagerCallback reactTextViewManagerCallback, - @Nullable float[] attachmentsPositions) { - - // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic) + boolean includeFontPadding, + int textBreakStrategy) { + Layout layout; + int spanLength = text.length(); + boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0; TextPaint textPaint = sTextPaintInstance; - Spannable text; - if (attributedString.hasKey("cacheId")) { - int cacheId = attributedString.getInt("cacheId"); - if (sTagToSpannableCache.containsKey(cacheId)) { - text = sTagToSpannableCache.get(attributedString.getInt("cacheId")); - } else { - return 0; - } - } else { - text = getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback); - } - - int textBreakStrategy = - TextAttributeProps.getTextBreakStrategy( - paragraphAttributes.getString(TEXT_BREAK_STRATEGY_KEY)); - boolean includeFontPadding = - paragraphAttributes.hasKey(INCLUDE_FONT_PADDING_KEY) - ? paragraphAttributes.getBoolean(INCLUDE_FONT_PADDING_KEY) - : DEFAULT_INCLUDE_FONT_PADDING; - - if (text == null) { - throw new IllegalStateException("Spannable element has not been prepared in onBeforeLayout"); - } - - BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint); float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN; - // technically, width should never be negative, but there is currently a bug in - boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0; - - Layout layout; - int spanLength = text.length(); if (boring == null && (unconstrainedWidth || (!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) { @@ -327,16 +326,70 @@ public static long measureText( 0.f, includeFontPadding); } else { - layout = + StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, spanLength, textPaint, (int) width) .setAlignment(Layout.Alignment.ALIGN_NORMAL) .setLineSpacing(0.f, 1.f) .setIncludePad(includeFontPadding) .setBreakStrategy(textBreakStrategy) - .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) - .build(); + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + builder.setUseLineSpacingFromFallbacks(true); + } + + layout = builder.build(); } } + return layout; + } + + public static long measureText( + Context context, + ReadableMap attributedString, + ReadableMap paragraphAttributes, + float width, + YogaMeasureMode widthYogaMeasureMode, + float height, + YogaMeasureMode heightYogaMeasureMode, + ReactTextViewManagerCallback reactTextViewManagerCallback, + @Nullable float[] attachmentsPositions) { + + // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic) + TextPaint textPaint = sTextPaintInstance; + Spannable text; + if (attributedString.hasKey("cacheId")) { + int cacheId = attributedString.getInt("cacheId"); + if (sTagToSpannableCache.containsKey(cacheId)) { + text = sTagToSpannableCache.get(cacheId); + } else { + return 0; + } + } else { + text = getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback); + } + + int textBreakStrategy = + TextAttributeProps.getTextBreakStrategy( + paragraphAttributes.getString(TEXT_BREAK_STRATEGY_KEY)); + boolean includeFontPadding = + paragraphAttributes.hasKey(INCLUDE_FONT_PADDING_KEY) + ? paragraphAttributes.getBoolean(INCLUDE_FONT_PADDING_KEY) + : DEFAULT_INCLUDE_FONT_PADDING; + + if (text == null) { + throw new IllegalStateException("Spannable element has not been prepared in onBeforeLayout"); + } + + BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint); + float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN; + + // technically, width should never be negative, but there is currently a bug in + boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0; + + Layout layout = + createLayout( + text, boring, width, widthYogaMeasureMode, includeFontPadding, textBreakStrategy); int maximumNumberOfLines = paragraphAttributes.hasKey(MAXIMUM_NUMBER_OF_LINES_KEY) @@ -378,9 +431,9 @@ public static long measureText( // follows a similar logic than used in pre-fabric (see ReactTextView.onLayout method). int attachmentIndex = 0; int lastAttachmentFoundInSpan; - for (int i = 0; i < spanLength; i = lastAttachmentFoundInSpan) { + for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) { lastAttachmentFoundInSpan = - text.nextSpanTransition(i, spanLength, TextInlineViewPlaceholderSpan.class); + text.nextSpanTransition(i, text.length(), TextInlineViewPlaceholderSpan.class); TextInlineViewPlaceholderSpan[] placeholders = text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class); for (TextInlineViewPlaceholderSpan placeholder : placeholders) { @@ -402,7 +455,7 @@ public static long measureText( // There's a bug on Samsung devices where calling getPrimaryHorizontal on // the last offset in the layout will result in an endless loop. Work around // this bug by avoiding getPrimaryHorizontal in that case. - if (start == spanLength - 1) { + if (start == text.length() - 1) { placeholderLeftPosition = isRtlParagraph // Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns incorrect @@ -472,18 +525,41 @@ public static long measureText( return YogaMeasureOutput.make(widthInSP, heightInSP); } + public static WritableArray measureLines( + @NonNull Context context, + ReadableMap attributedString, + ReadableMap paragraphAttributes, + float width) { + TextPaint textPaint = sTextPaintInstance; + Spannable text = getOrCreateSpannableForText(context, attributedString, null); + BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint); + + int textBreakStrategy = + TextAttributeProps.getTextBreakStrategy( + paragraphAttributes.getString(TEXT_BREAK_STRATEGY_KEY)); + boolean includeFontPadding = + paragraphAttributes.hasKey(INCLUDE_FONT_PADDING_KEY) + ? paragraphAttributes.getBoolean(INCLUDE_FONT_PADDING_KEY) + : DEFAULT_INCLUDE_FONT_PADDING; + + Layout layout = + createLayout( + text, boring, width, YogaMeasureMode.EXACTLY, includeFontPadding, textBreakStrategy); + return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context); + } + // TODO T31905686: This class should be private public static class SetSpanOperation { protected int start, end; protected ReactSpan what; - SetSpanOperation(int start, int end, ReactSpan what) { + public SetSpanOperation(int start, int end, ReactSpan what) { this.start = start; this.end = end; this.what = what; } - public void execute(SpannableStringBuilder sb, int priority) { + public void execute(Spannable sb, int priority) { // All spans will automatically extend to the right of the text, but not the left - except // for spans that start at the beginning of the text. int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index c0cb0df07b8fa2..570dd1f2fe07b4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -8,6 +8,7 @@ package com.facebook.react.views.textinput; import static com.facebook.react.uimanager.UIManagerHelper.getReactContext; +import static com.facebook.react.views.text.TextAttributeProps.UNSET; import android.content.Context; import android.graphics.Rect; @@ -17,7 +18,7 @@ import android.os.Bundle; import android.text.Editable; import android.text.InputType; -import android.text.SpannableString; +import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -41,6 +42,10 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.FabricViewStateManager; import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.views.text.CustomLetterSpacingSpan; +import com.facebook.react.views.text.CustomLineHeightSpan; +import com.facebook.react.views.text.CustomStyleSpan; +import com.facebook.react.views.text.ReactAbsoluteSizeSpan; import com.facebook.react.views.text.ReactSpan; import com.facebook.react.views.text.ReactTextUpdate; import com.facebook.react.views.text.ReactTypefaceUtils; @@ -49,6 +54,7 @@ import com.facebook.react.views.text.TextLayoutManager; import com.facebook.react.views.view.ReactViewBackgroundManager; import java.util.ArrayList; +import java.util.List; /** * A wrapper around the EditText that lets us better control what happens when an EditText gets @@ -70,6 +76,7 @@ public class ReactEditText extends AppCompatEditText // *TextChanged events should be triggered. This is less expensive than removing the text // listeners and adding them back again after the text change is completed. protected boolean mIsSettingTextFromJS; + protected boolean mIsSettingTextFromCacheUpdate = false; private int mDefaultGravityHorizontal; private int mDefaultGravityVertical; @@ -145,6 +152,14 @@ public ReactEditText(Context context) { @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (action == AccessibilityNodeInfo.ACTION_CLICK) { + int length = getText().length(); + if (length > 0) { + // For some reason, when you swipe to focus on a text input that already has text in + // it, it clears the selection and resets the cursor to the beginning of the input. + // Since this is not typically (ever?) what you want, let's just explicitly set the + // selection on accessibility click to undo that. + setSelection(length); + } return requestFocusInternal(); } return super.performAccessibilityAction(host, action, args); @@ -317,7 +332,7 @@ public void setSelection(int start, int end) { @Override protected void onSelectionChanged(int selStart, int selEnd) { super.onSelectionChanged(selStart, selEnd); - if (mSelectionWatcher != null && hasFocus()) { + if (!mIsSettingTextFromCacheUpdate && mSelectionWatcher != null && hasFocus()) { mSelectionWatcher.onSelectionChanged(selStart, selEnd); } } @@ -392,21 +407,8 @@ public void setInputType(int type) { // Input type password defaults to monospace font, so we need to re-apply the font super.setTypeface(tf); - int inputType = type; - - // Set InputType to TYPE_CLASS_TEXT (the default one for Android) to fix a crash on Xiaomi - // devices with Android Q. This crash happens when focusing on a email EditText within a - // ScrollView, a prompt will be triggered but the system fail to locate it properly. - // Here is an example post discussing about this issue: - // https://github.com/facebook/react-native/issues/27204 - if (inputType == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS - && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q - && Build.MANUFACTURER.startsWith("Xiaomi")) { - inputType = InputType.TYPE_CLASS_TEXT; - } - - super.setInputType(inputType); - mStagedInputType = inputType; + super.setInputType(type); + mStagedInputType = type; /** * If set forces multiline on input, because of a restriction on Android source that enables @@ -421,7 +423,7 @@ public void setInputType(int type) { // We override the KeyListener so that all keys on the soft input keyboard as well as hardware // keyboards work. Some KeyListeners like DigitsKeyListener will display the keyboard but not // accept all input from it - mKeyListener.setInputType(inputType); + mKeyListener.setInputType(type); setKeyListener(mKeyListener); } @@ -507,7 +509,7 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(reactTextUpdate.getText()); - manageSpans(spannableStringBuilder); + manageSpans(spannableStringBuilder, reactTextUpdate.mContainsMultipleFragments); mContainsImages = reactTextUpdate.containsImages(); // When we update text, we trigger onChangeText code that will @@ -533,10 +535,8 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { } } - // Update cached spans (in Fabric only) - if (this.getFabricViewStateManager() != null) { - TextLayoutManager.setCachedSpannabledForTag(getId(), spannableStringBuilder); - } + // Update cached spans (in Fabric only). + updateCachedSpannable(false); } /** @@ -545,30 +545,42 @@ public void maybeSetText(ReactTextUpdate reactTextUpdate) { * will adapt to the new text, hence why {@link SpannableStringBuilder#replace} never removes * them. */ - private void manageSpans(SpannableStringBuilder spannableStringBuilder) { + private void manageSpans( + SpannableStringBuilder spannableStringBuilder, boolean skipAddSpansForMeasurements) { Object[] spans = getText().getSpans(0, length(), Object.class); for (int spanIdx = 0; spanIdx < spans.length; spanIdx++) { + Object span = spans[spanIdx]; + int spanFlags = getText().getSpanFlags(span); + boolean isExclusiveExclusive = + (spanFlags & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; + // Remove all styling spans we might have previously set - if (spans[spanIdx] instanceof ReactSpan) { - getText().removeSpan(spans[spanIdx]); + if (span instanceof ReactSpan) { + getText().removeSpan(span); } - if ((getText().getSpanFlags(spans[spanIdx]) & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - != Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) { + // We only add spans back for EXCLUSIVE_EXCLUSIVE spans + if (!isExclusiveExclusive) { continue; } - Object span = spans[spanIdx]; - final int spanStart = getText().getSpanStart(spans[spanIdx]); - final int spanEnd = getText().getSpanEnd(spans[spanIdx]); - final int spanFlags = getText().getSpanFlags(spans[spanIdx]); + + final int spanStart = getText().getSpanStart(span); + final int spanEnd = getText().getSpanEnd(span); // Make sure the span is removed from existing text, otherwise the spans we set will be // ignored or it will cover text that has changed. - getText().removeSpan(spans[spanIdx]); + getText().removeSpan(span); if (sameTextForSpan(getText(), spannableStringBuilder, spanStart, spanEnd)) { spannableStringBuilder.setSpan(span, spanStart, spanEnd, spanFlags); } } + + // In Fabric only, apply necessary styles to entire span + // If the Spannable was constructed from multiple fragments, we don't apply any spans that could + // impact the whole Spannable, because that would override "local" styles per-fragment + if (!skipAddSpansForMeasurements) { + addSpansForMeasurement(getText()); + } } private static boolean sameTextForSpan( @@ -587,6 +599,75 @@ private static boolean sameTextForSpan( return true; } + // This is hacked in for Fabric. When we delete non-Fabric code, we might be able to simplify or + // clean this up a bit. + private void addSpansForMeasurement(Spannable spannable) { + if (!mFabricViewStateManager.hasStateWrapper()) { + return; + } + + boolean originalDisableTextDiffing = mDisableTextDiffing; + mDisableTextDiffing = true; + + int start = 0; + int end = spannable.length(); + + // Remove duplicate spans we might add here + Object[] spans = spannable.getSpans(0, length(), Object.class); + for (Object span : spans) { + int spanFlags = spannable.getSpanFlags(span); + boolean isInclusive = + (spanFlags & Spanned.SPAN_INCLUSIVE_INCLUSIVE) == Spanned.SPAN_INCLUSIVE_INCLUSIVE + || (spanFlags & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) == Spanned.SPAN_INCLUSIVE_EXCLUSIVE; + if (isInclusive + && span instanceof ReactSpan + && spannable.getSpanStart(span) == start + && spannable.getSpanEnd(span) == end) { + spannable.removeSpan(span); + } + } + + List ops = new ArrayList<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (!Float.isNaN(mTextAttributes.getLetterSpacing())) { + ops.add( + new TextLayoutManager.SetSpanOperation( + start, end, new CustomLetterSpacingSpan(mTextAttributes.getLetterSpacing()))); + } + } + ops.add( + new TextLayoutManager.SetSpanOperation( + start, end, new ReactAbsoluteSizeSpan((int) mTextAttributes.getEffectiveFontSize()))); + if (mFontStyle != UNSET || mFontWeight != UNSET || mFontFamily != null) { + ops.add( + new TextLayoutManager.SetSpanOperation( + start, + end, + new CustomStyleSpan( + mFontStyle, + mFontWeight, + null, // TODO: do we need to support FontFeatureSettings / fontVariant? + mFontFamily, + getReactContext(ReactEditText.this).getAssets()))); + } + if (!Float.isNaN(mTextAttributes.getEffectiveLineHeight())) { + ops.add( + new TextLayoutManager.SetSpanOperation( + start, end, new CustomLineHeightSpan(mTextAttributes.getEffectiveLineHeight()))); + } + + int priority = 0; + for (TextLayoutManager.SetSpanOperation op : ops) { + // Actual order of calling {@code execute} does NOT matter, + // but the {@code priority} DOES matter. + op.execute(spannable, priority); + priority++; + } + + mDisableTextDiffing = originalDisableTextDiffing; + } + protected boolean showSoftKeyboard() { return mInputMethodManager.showSoftInput(this, 0); } @@ -847,6 +928,59 @@ public FabricViewStateManager getFabricViewStateManager() { return mFabricViewStateManager; } + /** + * Update the cached Spannable used in TextLayoutManager to measure the text in Fabric. This is + * mostly copied from ReactTextInputShadowNode.java (the non-Fabric version) and + * TextLayoutManager.java with some very minor modifications. There's some duplication between + * here and TextLayoutManager, so there might be an opportunity for refactor. + */ + private void updateCachedSpannable(boolean resetStyles) { + // Noops in non-Fabric + if (getFabricViewStateManager() == null) { + return; + } + // If this view doesn't have an ID yet, we don't have a cache key, so bail here + if (getId() == -1) { + return; + } + + if (resetStyles) { + mIsSettingTextFromCacheUpdate = true; + addSpansForMeasurement(getText()); + mIsSettingTextFromCacheUpdate = false; + } + + Editable currentText = getText(); + boolean haveText = currentText != null && currentText.length() > 0; + + SpannableStringBuilder sb = new SpannableStringBuilder(); + + // A note of caution: appending currentText to sb appends all the spans of currentText - not + // copies of the Spans, but the actual span objects. Any modifications to sb after that point + // can modify the spans of sb/currentText, impact the text or spans visible on screen, and + // also call the TextChangeWatcher methods. + if (haveText) { + sb.append(currentText); + } + + // If we don't have text, make sure we have *something* to measure. + // Hint has the same dimensions - the only thing that's different is background or foreground + // color + if (!haveText) { + if (getHint() != null && getHint().length() > 0) { + sb.append(getHint()); + } else { + // Measure something so we have correct height, even if there's no string. + sb.append("I"); + } + + // Make sure that all text styles are applied when we're measurable the hint or "blank" text + addSpansForMeasurement(sb); + } + + TextLayoutManager.setCachedSpannabledForTag(getId(), sb); + } + /** * This class will redirect *TextChanged calls to the listeners only in the case where the text is * changed by the user, and not explicitly set by JS. @@ -854,7 +988,7 @@ public FabricViewStateManager getFabricViewStateManager() { private class TextWatcherDelegator implements TextWatcher { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { - if (!mIsSettingTextFromJS && mListeners != null) { + if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) { for (TextWatcher listener : mListeners) { listener.beforeTextChanged(s, start, count, after); } @@ -863,14 +997,15 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { - if (!mIsSettingTextFromJS && mListeners != null) { - for (TextWatcher listener : mListeners) { - listener.onTextChanged(s, start, before, count); + if (!mIsSettingTextFromCacheUpdate) { + if (!mIsSettingTextFromJS && mListeners != null) { + for (TextWatcher listener : mListeners) { + listener.onTextChanged(s, start, before, count); + } } - } - if (getFabricViewStateManager() != null) { - TextLayoutManager.setCachedSpannabledForTag(getId(), new SpannableString(getText())); + updateCachedSpannable( + !mIsSettingTextFromJS && !mIsSettingTextFromState && start == 0 && before == 0); } onContentSizeChange(); @@ -878,7 +1013,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { @Override public void afterTextChanged(Editable s) { - if (!mIsSettingTextFromJS && mListeners != null) { + if (!mIsSettingTextFromCacheUpdate && !mIsSettingTextFromJS && mListeners != null) { for (TextWatcher listener : mListeners) { listener.afterTextChanged(s); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 5e044b283cfa54..a8894676574b81 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -477,8 +477,17 @@ public void setCursorColor(ReactEditText view, @Nullable Integer color) { } } + private static boolean shouldHideCursorForEmailTextInput() { + String manufacturer = Build.MANUFACTURER.toLowerCase(); + return (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && manufacturer.contains("xiaomi")); + } + @ReactProp(name = "caretHidden", defaultBoolean = false) public void setCaretHidden(ReactEditText view, boolean caretHidden) { + if (view.getStagedInputType() == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + && shouldHideCursorForEmailTextInput()) { + return; + } view.setCursorVisible(!caretHidden); } @@ -750,6 +759,15 @@ public void setKeyboardType(ReactEditText view, @Nullable String keyboardType) { flagsToSet = INPUT_TYPE_KEYBOARD_DECIMAL_PAD; } else if (KEYBOARD_TYPE_EMAIL_ADDRESS.equalsIgnoreCase(keyboardType)) { flagsToSet = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_CLASS_TEXT; + + // Set cursor's visibility to False to fix a crash on some Xiaomi devices with Android Q. This + // crash happens when focusing on a email EditText, during which a prompt will be triggered + // but + // the system fail to locate it properly. Here is an example post discussing about this + // issue: https://github.com/facebook/react-native/issues/27204 + if (shouldHideCursorForEmailTextInput()) { + view.setCursorVisible(false); + } } else if (KEYBOARD_TYPE_PHONE_PAD.equalsIgnoreCase(keyboardType)) { flagsToSet = InputType.TYPE_CLASS_PHONE; } else if (KEYBOARD_TYPE_VISIBLE_PASSWORD.equalsIgnoreCase(keyboardType)) { @@ -913,7 +931,6 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { return; } - // Fabric: update representation of AttributedString if (mEditText.getFabricViewStateManager().hasStateWrapper()) { // Fabric: communicate to C++ layer that text has changed // We need to call `incrementAndGetEventCounter` here explicitly because this @@ -1184,6 +1201,9 @@ public Object updateState( TextLayoutManager.getOrCreateSpannableForText( view.getContext(), attributedString, mReactTextViewManagerCallback); + boolean containsMultipleFragments = + attributedString.getArray("fragments").toArrayList().size() > 1; + int textBreakStrategy = TextAttributeProps.getTextBreakStrategy(paragraphAttributes.getString("textBreakStrategy")); @@ -1193,6 +1213,6 @@ public Object updateState( TextAttributeProps.getTextAlignment(props, TextLayoutManager.isRTL(attributedString)), textBreakStrategy, TextAttributeProps.getJustificationMode(props), - attributedString); + containsMultipleFragments); } } diff --git a/ReactAndroid/src/main/jni/Application.mk b/ReactAndroid/src/main/jni/Application.mk index 84eae80adf4c9d..8bcc6cfa8b8817 100644 --- a/ReactAndroid/src/main/jni/Application.mk +++ b/ReactAndroid/src/main/jni/Application.mk @@ -22,7 +22,7 @@ APP_MK_DIR := $(dir $(lastword $(MAKEFILE_LIST))) # Where are APP_MK_DIR, THIRD_PARTY_NDK_DIR, etc. defined? # The directories inside NDK_MODULE_PATH (ex: APP_MK_DIR, THIRD_PARTY_NDK_DIR, # etc.) are defined inside build.gradle. -NDK_MODULE_PATH := $(APP_MK_DIR)$(HOST_DIRSEP)$(THIRD_PARTY_NDK_DIR)$(HOST_DIRSEP)$(REACT_COMMON_DIR)$(HOST_DIRSEP)$(APP_MK_DIR)first-party$(HOST_DIRSEP)$(REACT_SRC_DIR) +NDK_MODULE_PATH := $(APP_MK_DIR)$(HOST_DIRSEP)$(THIRD_PARTY_NDK_DIR)$(HOST_DIRSEP)$(REACT_COMMON_DIR)$(HOST_DIRSEP)$(APP_MK_DIR)first-party$(HOST_DIRSEP)$(REACT_SRC_DIR)$(HOST_DIRSEP)$(REACT_GENERATED_SRC_DIR) APP_STL := c++_shared diff --git a/ReactAndroid/src/main/jni/react/jni/Android.mk b/ReactAndroid/src/main/jni/react/jni/Android.mk index 70b65082fac834..b4ba8ddce6a245 100644 --- a/ReactAndroid/src/main/jni/react/jni/Android.mk +++ b/ReactAndroid/src/main/jni/react/jni/Android.mk @@ -80,7 +80,7 @@ LOCAL_LDLIBS += -landroid LOCAL_SHARED_LIBRARIES := libreactnativeutilsjni libfolly_json libfb libfbjni libglog_init libyoga # The static libraries (.a files) that this module depends on. -LOCAL_STATIC_LIBRARIES := libreactnative libcallinvokerholder libruntimeexecutor +LOCAL_STATIC_LIBRARIES := libreactnative libruntimeexecutor libcallinvokerholder # Name of this module. # @@ -128,8 +128,11 @@ $(call import-module,callinvoker) $(call import-module,reactperflogger) $(call import-module,hermes) $(call import-module,runtimeexecutor) +$(call import-module,react/nativemodule/core) include $(REACT_SRC_DIR)/reactperflogger/jni/Android.mk +# TODO (T48588859): Restructure this target to align with dir structure: "react/nativemodule/..." +# Note: Update this only when ready to minimize breaking changes. include $(REACT_SRC_DIR)/turbomodule/core/jni/Android.mk ifeq ($(BUILD_FABRIC),true) @@ -145,3 +148,7 @@ include $(REACT_SRC_DIR)/jscexecutor/Android.mk include $(REACT_SRC_DIR)/../hermes/reactexecutor/Android.mk include $(REACT_SRC_DIR)/../hermes/instrumentation/Android.mk include $(REACT_SRC_DIR)/modules/blob/jni/Android.mk + +ifeq ($(USE_CODEGEN),true) + include $(REACT_GENERATED_SRC_DIR)/codegen/jni/Android.mk +endif diff --git a/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp b/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp index 7d5b1acf483f88..659c8235a64bbb 100644 --- a/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp +++ b/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp @@ -92,12 +92,6 @@ CatalystInstanceImpl::initHybrid(jni::alias_ref) { CatalystInstanceImpl::CatalystInstanceImpl() : instance_(std::make_unique()) {} -CatalystInstanceImpl::~CatalystInstanceImpl() { - if (moduleMessageQueue_ != NULL) { - moduleMessageQueue_->quitSynchronous(); - } -} - void CatalystInstanceImpl::registerNatives() { registerHybrid({ makeNativeMethod("initHybrid", CatalystInstanceImpl::initHybrid), diff --git a/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.h b/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.h index dd9990ef090a00..c40a691232c6a2 100644 --- a/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.h +++ b/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.h @@ -37,7 +37,6 @@ class CatalystInstanceImpl : public jni::HybridClass { "Lcom/facebook/react/bridge/CatalystInstanceImpl;"; static jni::local_ref initHybrid(jni::alias_ref); - ~CatalystInstanceImpl() override; static void registerNatives(); diff --git a/ReactAndroid/src/main/libraries/soloader/java/com/facebook/soloader/BUCK b/ReactAndroid/src/main/libraries/soloader/java/com/facebook/soloader/BUCK index d6a57a807c96fe..3c1ec406b6898a 100644 --- a/ReactAndroid/src/main/libraries/soloader/java/com/facebook/soloader/BUCK +++ b/ReactAndroid/src/main/libraries/soloader/java/com/facebook/soloader/BUCK @@ -10,9 +10,9 @@ fb_native.android_library( visibility = ["PUBLIC"], ) -fb_native.android_prebuilt_aar( +fb_native.prebuilt_jar( name = "annotation-binary", - aar = ":annotation-binary-aar", + binary_jar = ":annotation-binary.jar", ) fb_native.prebuilt_jar( @@ -26,19 +26,19 @@ fb_native.android_prebuilt_aar( ) fb_native.remote_file( - name = "annotation-binary-aar", - sha1 = "ae6d46195467467fae746c6225f79ac41e7039e8", - url = "mvn:com.facebook.soloader:annotation:aar:0.8.2", + name = "annotation-binary.jar", + sha1 = "dc58463712cb3e5f03d8ee5ac9743b9ced9afa77", + url = "mvn:com.facebook.soloader:annotation:jar:0.9.0", ) fb_native.remote_file( name = "nativeloader-binary.jar", - sha1 = "86cb3da9384707034355ac1e84e9a8cf6de80f7c", - url = "mvn:com.facebook.soloader:nativeloader:jar:0.8.2", + sha1 = "677c7fbfcc847d7eb6082048d07b10afd4cff898", + url = "mvn:com.facebook.soloader:nativeloader:jar:0.9.0", ) fb_native.remote_file( name = "soloader-binary-aar", - sha1 = "8575dbdec464207a19273bd3c09d758a08fa655c", - url = "mvn:com.facebook.soloader:soloader:aar:0.8.2", + sha1 = "6e138af1dd29ceabf5bace2d24dc4333f304d104", + url = "mvn:com.facebook.soloader:soloader:aar:0.9.0", ) diff --git a/ReactCommon/React-Fabric.podspec b/ReactCommon/React-Fabric.podspec index 1937e45d71b0b6..0bf38ed0c2f62a 100644 --- a/ReactCommon/React-Fabric.podspec +++ b/ReactCommon/React-Fabric.podspec @@ -28,7 +28,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.prepare_command = File.read("../scripts/generate-rncore.sh") s.source_files = "dummyFile.cpp" diff --git a/ReactCommon/ReactCommon.podspec b/ReactCommon/ReactCommon.podspec index bad800f338c274..0c083cf2d5b133 100644 --- a/ReactCommon/ReactCommon.podspec +++ b/ReactCommon/ReactCommon.podspec @@ -28,7 +28,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.header_dir = "ReactCommon" # Use global header_dir for all subspecs for use_frameworks! compatibility s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags @@ -36,6 +36,8 @@ Pod::Spec.new do |s| "USE_HEADERMAP" => "YES", "CLANG_CXX_LANGUAGE_STANDARD" => "c++14" } + # TODO (T48588859): Restructure this target to align with dir structure: "react/nativemodule/..." + # Note: Update this only when ready to minimize breaking changes. s.subspec "turbomodule" do |ss| ss.dependency "React-callinvoker", version ss.dependency "React-perflogger", version @@ -47,13 +49,13 @@ Pod::Spec.new do |s| ss.dependency "glog" ss.subspec "core" do |sss| - sss.source_files = "turbomodule/core/*.{cpp,h}", - "turbomodule/core/platform/ios/*.{mm,cpp,h}" + sss.source_files = "react/nativemodule/core/ReactCommon/**/*.{cpp,h}", + "react/nativemodule/core/platform/ios/**/*.{mm,cpp,h}" end ss.subspec "samples" do |sss| - sss.source_files = "turbomodule/samples/*.{cpp,h}", - "turbomodule/samples/platform/ios/*.{mm,cpp,h}" + sss.source_files = "react/nativemodule/samples/*.{cpp,h}", + "react/nativemodule/samples/platform/ios/**/*.{mm,cpp,h}" sss.dependency "ReactCommon/turbomodule/core", version end end diff --git a/ReactCommon/callinvoker/React-callinvoker.podspec b/ReactCommon/callinvoker/React-callinvoker.podspec index e2a90d30943353..76aec146d61cdb 100644 --- a/ReactCommon/callinvoker/React-callinvoker.podspec +++ b/ReactCommon/callinvoker/React-callinvoker.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "**/*.{cpp,h}" s.header_dir = "ReactCommon" diff --git a/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.cpp b/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.cpp index 5f9f96b9bc48b7..c19a5336582447 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.cpp +++ b/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.cpp @@ -20,12 +20,17 @@ namespace chrome { namespace detail = facebook::hermes::inspector::detail; -AsyncHermesRuntime::AsyncHermesRuntime() - : runtime_(facebook::hermes::makeHermesRuntime()), - executor_( +AsyncHermesRuntime::AsyncHermesRuntime(bool veryLazy) + : executor_( std::make_unique("async-hermes-runtime")) { using namespace std::placeholders; + auto builder = ::hermes::vm::RuntimeConfig::Builder(); + if (veryLazy) { + builder.withCompilationMode(::hermes::vm::ForceLazyCompilation); + } + runtime_ = facebook::hermes::makeHermesRuntime(builder.build()); + runtime_->global().setProperty( *runtime_, "shouldStop", diff --git a/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.h b/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.h index b249746c407622..60a63147fc38b9 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.h +++ b/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.h @@ -27,7 +27,9 @@ namespace chrome { */ class AsyncHermesRuntime { public: - AsyncHermesRuntime(); + // Create a runtime. If veryLazy, configure the runtime to use completely + // lazy compilation. + AsyncHermesRuntime(bool veryLazy = false); ~AsyncHermesRuntime(); std::shared_ptr runtime() { diff --git a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp index b97bf2cccd414d..e4d857aac3f2b1 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp +++ b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp @@ -44,8 +44,8 @@ namespace { // the already-deallocated connection. class TestContext { public: - TestContext(bool waitForDebugger = false) - : conn_(runtime_.runtime(), waitForDebugger) {} + TestContext(bool waitForDebugger = false, bool veryLazy = false) + : runtime_(veryLazy), conn_(runtime_.runtime(), waitForDebugger) {} ~TestContext() { runtime_.wait(); } @@ -860,9 +860,6 @@ TEST(ConnectionTests, testSetLazyBreakpoint) { SyncConnection &conn = context.conn(); int msgId = 1; - facebook::hermes::HermesRuntime::DebugFlags flags{}; - flags.lazy = true; - asyncRuntime.executeScriptAsync( R"( var a = 1 + 2; @@ -879,8 +876,7 @@ TEST(ConnectionTests, testSetLazyBreakpoint) { foo(); )", - "url", - flags); + "url"); send(conn, msgId++); expectExecutionContextCreated(conn); diff --git a/ReactCommon/jsi/Android.mk b/ReactCommon/jsi/Android.mk index d6a547cc413a6e..40c7042c16e375 100644 --- a/ReactCommon/jsi/Android.mk +++ b/ReactCommon/jsi/Android.mk @@ -32,4 +32,8 @@ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) LOCAL_CFLAGS := -fexceptions -frtti -O3 LOCAL_SHARED_LIBRARIES := libfolly_json libjsc glog +ifeq ($(BUILD_FABRIC),true) + LOCAL_CFLAGS += -DRN_FABRIC_ENABLED +endif + include $(BUILD_STATIC_LIBRARY) diff --git a/ReactCommon/jsi/React-jsi.podspec b/ReactCommon/jsi/React-jsi.podspec index 01035d5667e3e4..d250597b12721f 100644 --- a/ReactCommon/jsi/React-jsi.podspec +++ b/ReactCommon/jsi/React-jsi.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "**/*.{cpp,h}" s.exclude_files = "**/test/*" diff --git a/ReactCommon/jsi/jsi/CMakeLists.txt b/ReactCommon/jsi/jsi/CMakeLists.txt index d04af0006af915..121d697fd8e76e 100644 --- a/ReactCommon/jsi/jsi/CMakeLists.txt +++ b/ReactCommon/jsi/jsi/CMakeLists.txt @@ -21,6 +21,9 @@ elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "MSVC") # when they go out of scope due to exceptions. list(APPEND jsi_compile_flags "/EHsc") endif() +if (HERMES_ENABLE_BITCODE) + list(APPEND jsi_compile_flags "-fembed-bitcode") +endif () target_compile_options(jsi PUBLIC ${jsi_compile_flags}) install(DIRECTORY "${PROJECT_SOURCE_DIR}/API/jsi/" DESTINATION include diff --git a/ReactCommon/jsi/jsi/decorator.h b/ReactCommon/jsi/jsi/decorator.h index 46e7414abbecb1..bef9796dc5b8af 100644 --- a/ReactCommon/jsi/jsi/decorator.h +++ b/ReactCommon/jsi/jsi/decorator.h @@ -331,8 +331,8 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { return plain().instrumentation().getHeapInfo(includeExpensive); } - void collectGarbage() override { - plain().instrumentation().collectGarbage(); + void collectGarbage(std::string cause) override { + plain().instrumentation().collectGarbage(std::move(cause)); } void startTrackingHeapObjectStackTraces() override { diff --git a/ReactCommon/jsi/jsi/instrumentation.h b/ReactCommon/jsi/jsi/instrumentation.h index 04c76ce2594d62..b8aa4da02b2366 100644 --- a/ReactCommon/jsi/jsi/instrumentation.h +++ b/ReactCommon/jsi/jsi/instrumentation.h @@ -49,8 +49,10 @@ class JSI_EXPORT Instrumentation { virtual std::unordered_map getHeapInfo( bool includeExpensive) = 0; - /// perform a full garbage collection - virtual void collectGarbage() = 0; + /// Perform a full garbage collection. + /// \param cause The cause of this collection, as it should be reported in + /// logs. + virtual void collectGarbage(std::string cause) = 0; /// Start capturing JS stack-traces for all JS heap allocated objects. These /// can be accessed via \c ::createSnapshotToFile(). diff --git a/ReactCommon/jsi/jsi/jsi.cpp b/ReactCommon/jsi/jsi/jsi.cpp index e4a7e431fca00d..eac4c4b6356a8a 100644 --- a/ReactCommon/jsi/jsi/jsi.cpp +++ b/ReactCommon/jsi/jsi/jsi.cpp @@ -97,7 +97,7 @@ Instrumentation& Runtime::instrumentation() { return std::unordered_map{}; } - void collectGarbage() override {} + void collectGarbage(std::string) override {} void startTrackingHeapObjectStackTraces() override {} void stopTrackingHeapObjectStackTraces() override {} diff --git a/ReactCommon/jsiexecutor/React-jsiexecutor.podspec b/ReactCommon/jsiexecutor/React-jsiexecutor.podspec index f67143d1b0c91d..45429c92bf325d 100644 --- a/ReactCommon/jsiexecutor/React-jsiexecutor.podspec +++ b/ReactCommon/jsiexecutor/React-jsiexecutor.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "jsireact/*.{cpp,h}" s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags diff --git a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp index 0d91ca7778e0b0..c4a3d90625e078 100644 --- a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp +++ b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp @@ -344,7 +344,7 @@ void JSIExecutor::handleMemoryPressure(int pressureLevel) { // collections. LOG(INFO) << "Memory warning (pressure level: " << levelName << ") received by JS VM, running a GC"; - runtime_->instrumentation().collectGarbage(); + runtime_->instrumentation().collectGarbage("memory warning"); break; default: // Use the raw number instead of the name here since the name is diff --git a/ReactCommon/jsinspector/React-jsinspector.podspec b/ReactCommon/jsinspector/React-jsinspector.podspec index 600480fe8e20cd..f87cdeefe3aed5 100644 --- a/ReactCommon/jsinspector/React-jsinspector.podspec +++ b/ReactCommon/jsinspector/React-jsinspector.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "*.{cpp,h}" s.header_dir = 'jsinspector' diff --git a/ReactCommon/turbomodule/.clang-tidy b/ReactCommon/react/nativemodule/.clang-tidy similarity index 100% rename from ReactCommon/turbomodule/.clang-tidy rename to ReactCommon/react/nativemodule/.clang-tidy diff --git a/ReactCommon/react/nativemodule/core/Android.mk b/ReactCommon/react/nativemodule/core/Android.mk new file mode 100644 index 00000000000000..70f7651b4831f4 --- /dev/null +++ b/ReactCommon/react/nativemodule/core/Android.mk @@ -0,0 +1,31 @@ +# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := react_nativemodule_core + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../ $(LOCAL_PATH)/ReactCommon $(LOCAL_PATH)/platform/android/ReactCommon + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/ReactCommon/*.cpp) $(wildcard $(LOCAL_PATH)/platform/android/ReactCommon/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) $(LOCAL_PATH)/platform/android/ + +LOCAL_SHARED_LIBRARIES := libfbjni libfolly_json libreactnativejni + +LOCAL_STATIC_LIBRARIES := libjsi libreactperflogger + +LOCAL_CFLAGS := \ + -DLOG_TAG=\"ReactNative\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,folly) +$(call import-module,jsi) +$(call import-module,reactperflogger) diff --git a/ReactCommon/turbomodule/core/BUCK b/ReactCommon/react/nativemodule/core/BUCK similarity index 93% rename from ReactCommon/turbomodule/core/BUCK rename to ReactCommon/react/nativemodule/core/BUCK index 549ddef0a9e243..1bc3b9950df7ee 100644 --- a/ReactCommon/turbomodule/core/BUCK +++ b/ReactCommon/react/nativemodule/core/BUCK @@ -4,12 +4,12 @@ load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "APPLE", "FBJNI_TARGET", " rn_xplat_cxx_library( name = "core", srcs = glob( - ["*.cpp"], + ["ReactCommon/**/*.cpp"], ), header_namespace = "", exported_headers = subdir_glob( [ - ("", "*.h"), + ("ReactCommon", "*.h"), ], prefix = "ReactCommon", ), @@ -26,13 +26,13 @@ rn_xplat_cxx_library( ], fbandroid_exported_headers = subdir_glob( [ - ("platform/android", "*.h"), + ("platform/android/ReactCommon", "*.h"), ], prefix = "ReactCommon", ), fbandroid_srcs = glob( [ - "platform/android/**/*.cpp", + "platform/android/ReactCommon/*.cpp", ], ), fbobjc_compiler_flags = [ diff --git a/ReactCommon/turbomodule/core/LongLivedObject.cpp b/ReactCommon/react/nativemodule/core/ReactCommon/LongLivedObject.cpp similarity index 100% rename from ReactCommon/turbomodule/core/LongLivedObject.cpp rename to ReactCommon/react/nativemodule/core/ReactCommon/LongLivedObject.cpp diff --git a/ReactCommon/turbomodule/core/LongLivedObject.h b/ReactCommon/react/nativemodule/core/ReactCommon/LongLivedObject.h similarity index 100% rename from ReactCommon/turbomodule/core/LongLivedObject.h rename to ReactCommon/react/nativemodule/core/ReactCommon/LongLivedObject.h diff --git a/ReactCommon/turbomodule/core/TurboCxxModule.cpp b/ReactCommon/react/nativemodule/core/ReactCommon/TurboCxxModule.cpp similarity index 100% rename from ReactCommon/turbomodule/core/TurboCxxModule.cpp rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboCxxModule.cpp diff --git a/ReactCommon/turbomodule/core/TurboCxxModule.h b/ReactCommon/react/nativemodule/core/ReactCommon/TurboCxxModule.h similarity index 100% rename from ReactCommon/turbomodule/core/TurboCxxModule.h rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboCxxModule.h diff --git a/ReactCommon/turbomodule/core/TurboModule.cpp b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp similarity index 100% rename from ReactCommon/turbomodule/core/TurboModule.cpp rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp diff --git a/ReactCommon/turbomodule/core/TurboModule.h b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h similarity index 100% rename from ReactCommon/turbomodule/core/TurboModule.h rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h diff --git a/ReactCommon/turbomodule/core/TurboModuleBinding.cpp b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp similarity index 100% rename from ReactCommon/turbomodule/core/TurboModuleBinding.cpp rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp diff --git a/ReactCommon/turbomodule/core/TurboModuleBinding.h b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.h similarity index 100% rename from ReactCommon/turbomodule/core/TurboModuleBinding.h rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.h diff --git a/ReactCommon/turbomodule/core/TurboModulePerfLogger.cpp b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModulePerfLogger.cpp similarity index 100% rename from ReactCommon/turbomodule/core/TurboModulePerfLogger.cpp rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboModulePerfLogger.cpp diff --git a/ReactCommon/turbomodule/core/TurboModulePerfLogger.h b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModulePerfLogger.h similarity index 100% rename from ReactCommon/turbomodule/core/TurboModulePerfLogger.h rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboModulePerfLogger.h diff --git a/ReactCommon/turbomodule/core/TurboModuleUtils.cpp b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleUtils.cpp similarity index 100% rename from ReactCommon/turbomodule/core/TurboModuleUtils.cpp rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleUtils.cpp diff --git a/ReactCommon/turbomodule/core/TurboModuleUtils.h b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleUtils.h similarity index 100% rename from ReactCommon/turbomodule/core/TurboModuleUtils.h rename to ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleUtils.h diff --git a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp b/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp similarity index 97% rename from ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp rename to ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp index 4b5d8d6e9538b6..82fbc928fb6c1a 100644 --- a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp +++ b/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp @@ -31,6 +31,26 @@ JavaTurboModule::JavaTurboModule(const InitParams ¶ms) instance_(jni::make_global(params.instance)), nativeInvoker_(params.nativeInvoker) {} +JavaTurboModule::~JavaTurboModule() { + /** + * TODO(T75896241): In E2E tests, instance_ is null. Investigate why. Can we + * get rid of this null check? + */ + if (!instance_) { + return; + } + + nativeInvoker_->invokeAsync([instance = std::move(instance_)]() mutable { + /** + * Reset the global NativeModule ref on the NativeModules thread. Why: + * - ~JavaTurboModule() can be called on a non-JVM thread. If we reset the + * global ref in ~JavaTurboModule(), we might access the JVM from a + * non-JVM thread, which will crash the app. + */ + instance.reset(); + }); +} + bool JavaTurboModule::isPromiseAsyncDispatchEnabled_ = false; void JavaTurboModule::enablePromiseAsyncDispatch(bool enable) { isPromiseAsyncDispatchEnabled_ = enable; diff --git a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h b/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h similarity index 98% rename from ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h rename to ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h index 642cacb194a85b..93acda48fcff56 100644 --- a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h +++ b/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h @@ -41,6 +41,7 @@ class JSI_EXPORT JavaTurboModule : public TurboModule { }; JavaTurboModule(const InitParams ¶ms); + virtual ~JavaTurboModule(); jsi::Value invokeJavaMethod( jsi::Runtime &runtime, TurboModuleMethodValueKind valueKind, diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.h b/ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModule.h similarity index 100% rename from ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.h rename to ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModule.h diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm b/ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModule.mm similarity index 97% rename from ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm rename to ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModule.mm index 4e5cdc8a4ee3c6..c8950c5718fe6a 100644 --- a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm +++ b/ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModule.mm @@ -171,7 +171,7 @@ static int32_t getUniqueId() { auto weakWrapper = CallbackWrapper::createWeak(value.getFunction(runtime), runtime, jsInvoker); BOOL __block wrapperWasCalled = NO; - return ^(NSArray *responses) { + RCTResponseSenderBlock callback = ^(NSArray *responses) { if (wrapperWasCalled) { throw std::runtime_error("callback arg cannot be called more than once"); } @@ -194,6 +194,8 @@ static int32_t getUniqueId() wrapperWasCalled = YES; }; + + return [callback copy]; } namespace facebook { @@ -331,12 +333,11 @@ static int32_t getUniqueId() bool wasMethodSync = isMethodSync(returnType); void (^block)() = ^{ - if (!weakModule) { + id strongModule = weakModule; + if (!strongModule) { return; } - id strongModule = weakModule; - if (wasMethodSync) { TurboModulePerfLogger::syncMethodCallExecutionStart(moduleName, methodNameStr.c_str()); } else { @@ -635,10 +636,13 @@ static int32_t getUniqueId() runtime, jsInvoker_, ^(RCTPromiseResolveBlock resolveBlock, RCTPromiseRejectBlock rejectBlock) { - [inv setArgument:(void *)&resolveBlock atIndex:count + 2]; - [inv setArgument:(void *)&rejectBlock atIndex:count + 3]; - [retainedObjectsForInvocation addObject:resolveBlock]; - [retainedObjectsForInvocation addObject:rejectBlock]; + RCTPromiseResolveBlock resolveCopy = [resolveBlock copy]; + RCTPromiseRejectBlock rejectCopy = [rejectBlock copy]; + + [inv setArgument:(void *)&resolveCopy atIndex:count + 2]; + [inv setArgument:(void *)&rejectCopy atIndex:count + 3]; + [retainedObjectsForInvocation addObject:resolveCopy]; + [retainedObjectsForInvocation addObject:rejectCopy]; // The return type becomes void in the ObjC side. performMethodInvocation(runtime, VoidKind, methodName, inv, retainedObjectsForInvocation); }) diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.h b/ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModuleManager.h similarity index 100% rename from ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.h rename to ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModuleManager.h diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm b/ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModuleManager.mm similarity index 92% rename from ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm rename to ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModuleManager.mm index 0d89875a25bb56..223522eb684f4b 100644 --- a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm +++ b/ReactCommon/react/nativemodule/core/platform/ios/RCTTurboModuleManager.mm @@ -11,6 +11,7 @@ #import #import #import +#import #import @@ -168,6 +169,7 @@ @implementation RCTTurboModuleManager { std::mutex _turboModuleManagerDelegateMutex; // Enforce synchronous access to _invalidating and _turboModuleHolders + std::shared_timed_mutex _turboModuleHoldersSharedMutex; std::mutex _turboModuleHoldersMutex; std::atomic _invalidating; } @@ -314,6 +316,33 @@ - (void)notifyAboutTurboModuleSetup:(const char *)name return turboModule; } +- (TurboModuleHolder *)_getOrCreateTurboModuleHolder:(const char *)moduleName +{ + if (RCTTurboModuleSharedMutexInitEnabled()) { + { + std::shared_lock guard(_turboModuleHoldersSharedMutex); + if (_invalidating) { + return nullptr; + } + + auto it = _turboModuleHolders.find(moduleName); + if (it != _turboModuleHolders.end()) { + return &it->second; + } + } + + std::unique_lock guard(_turboModuleHoldersSharedMutex); + return &_turboModuleHolders[moduleName]; + } + + std::lock_guard guard(_turboModuleHoldersMutex); + if (_invalidating) { + return nullptr; + } + + return &_turboModuleHolders[moduleName]; +} + /** * Given a name for a TurboModule, return an ObjC object which is the instance * of that TurboModule ObjC class. If no TurboModule exist with the provided name, @@ -324,15 +353,10 @@ - (void)notifyAboutTurboModuleSetup:(const char *)name */ - (id)provideRCTTurboModule:(const char *)moduleName { - TurboModuleHolder *moduleHolder; - - { - std::lock_guard guard(_turboModuleHoldersMutex); - if (_invalidating) { - return nil; - } + TurboModuleHolder *moduleHolder = [self _getOrCreateTurboModuleHolder:moduleName]; - moduleHolder = &_turboModuleHolders[moduleName]; + if (!moduleHolder) { + return nil; } TurboModulePerfLogger::moduleCreateStart(moduleName, moduleHolder->getModuleId()); @@ -655,22 +679,21 @@ - (void)installJSBindingWithRuntimeExecutor:(facebook::react::RuntimeExecutor)ru return; } - __weak __typeof(self) weakSelf = self; + /** + * We keep TurboModuleManager alive until the JS VM is deleted. + * It is perfectly valid to only use/create TurboModules from JS. + * In such a case, we shouldn't dealloc TurboModuleManager if there + * aren't any strong references to it in ObjC. Hence, we give + * __turboModuleProxy a strong reference to TurboModuleManager. + */ auto turboModuleProvider = - [weakSelf](const std::string &name, const jsi::Value *schema) -> std::shared_ptr { - if (!weakSelf) { - return nullptr; - } - + [self](const std::string &name, const jsi::Value *schema) -> std::shared_ptr { auto moduleName = name.c_str(); TurboModulePerfLogger::moduleJSRequireBeginningStart(moduleName); - - __strong __typeof(self) strongSelf = weakSelf; - - auto moduleWasNotInitialized = ![strongSelf moduleIsInitialized:moduleName]; + auto moduleWasNotInitialized = ![self moduleIsInitialized:moduleName]; if (moduleWasNotInitialized) { - [strongSelf->_bridge.performanceLogger markStartForTag:RCTPLTurboModuleSetup]; + [self->_bridge.performanceLogger markStartForTag:RCTPLTurboModuleSetup]; } /** @@ -678,11 +701,11 @@ - (void)installJSBindingWithRuntimeExecutor:(facebook::react::RuntimeExecutor)ru * Additionally, if a TurboModule with the name `name` isn't found, then we * trigger an assertion failure. */ - auto turboModule = [strongSelf provideTurboModule:moduleName]; + auto turboModule = [self provideTurboModule:moduleName]; - if (moduleWasNotInitialized && [strongSelf moduleIsInitialized:moduleName]) { - [strongSelf->_bridge.performanceLogger markStopForTag:RCTPLTurboModuleSetup]; - [strongSelf notifyAboutTurboModuleSetup:moduleName]; + if (moduleWasNotInitialized && [self moduleIsInitialized:moduleName]) { + [self->_bridge.performanceLogger markStopForTag:RCTPLTurboModuleSetup]; + [self notifyAboutTurboModuleSetup:moduleName]; } if (turboModule) { @@ -719,6 +742,11 @@ - (id)moduleForName:(const char *)moduleName warnOnLookupFailure:(BOOL)warnOnLoo - (BOOL)moduleIsInitialized:(const char *)moduleName { + if (RCTTurboModuleSharedMutexInitEnabled()) { + std::shared_lock guard(_turboModuleHoldersSharedMutex); + return _turboModuleHolders.find(moduleName) != _turboModuleHolders.end(); + } + std::unique_lock guard(_turboModuleHoldersMutex); return _turboModuleHolders.find(moduleName) != _turboModuleHolders.end(); } @@ -750,10 +778,14 @@ - (void)bridgeWillInvalidateModules:(NSNotification *)notification return; } - std::lock_guard guard(_turboModuleHoldersMutex); - // This should halt all insertions into _turboModuleHolders - _invalidating = true; + if (RCTTurboModuleSharedMutexInitEnabled()) { + std::unique_lock guard(_turboModuleHoldersSharedMutex); + _invalidating = true; + } else { + std::lock_guard guard(_turboModuleHoldersMutex); + _invalidating = true; + } } - (void)bridgeDidInvalidateModules:(NSNotification *)notification @@ -807,7 +839,10 @@ - (void)bridgeDidInvalidateModules:(NSNotification *)notification - (void)invalidate { - { + if (RCTTurboModuleSharedMutexInitEnabled()) { + std::unique_lock guard(_turboModuleHoldersSharedMutex); + _invalidating = true; + } else { std::lock_guard guard(_turboModuleHoldersMutex); _invalidating = true; } diff --git a/ReactCommon/turbomodule/samples/BUCK b/ReactCommon/react/nativemodule/samples/BUCK similarity index 97% rename from ReactCommon/turbomodule/samples/BUCK rename to ReactCommon/react/nativemodule/samples/BUCK index ae15f4706b606f..c760c2f06218ab 100644 --- a/ReactCommon/turbomodule/samples/BUCK +++ b/ReactCommon/react/nativemodule/samples/BUCK @@ -75,6 +75,6 @@ rn_xplat_cxx_library( ], exported_deps = [ "//xplat/jsi:jsi", - react_native_xplat_target("turbomodule/core:core"), + react_native_xplat_target("react/nativemodule/core:core"), ], ) diff --git a/ReactCommon/turbomodule/samples/NativeSampleTurboCxxModuleSpecJSI.cpp b/ReactCommon/react/nativemodule/samples/NativeSampleTurboCxxModuleSpecJSI.cpp similarity index 100% rename from ReactCommon/turbomodule/samples/NativeSampleTurboCxxModuleSpecJSI.cpp rename to ReactCommon/react/nativemodule/samples/NativeSampleTurboCxxModuleSpecJSI.cpp diff --git a/ReactCommon/turbomodule/samples/NativeSampleTurboCxxModuleSpecJSI.h b/ReactCommon/react/nativemodule/samples/NativeSampleTurboCxxModuleSpecJSI.h similarity index 100% rename from ReactCommon/turbomodule/samples/NativeSampleTurboCxxModuleSpecJSI.h rename to ReactCommon/react/nativemodule/samples/NativeSampleTurboCxxModuleSpecJSI.h diff --git a/ReactCommon/turbomodule/samples/SampleTurboCxxModule.cpp b/ReactCommon/react/nativemodule/samples/SampleTurboCxxModule.cpp similarity index 100% rename from ReactCommon/turbomodule/samples/SampleTurboCxxModule.cpp rename to ReactCommon/react/nativemodule/samples/SampleTurboCxxModule.cpp diff --git a/ReactCommon/turbomodule/samples/SampleTurboCxxModule.h b/ReactCommon/react/nativemodule/samples/SampleTurboCxxModule.h similarity index 100% rename from ReactCommon/turbomodule/samples/SampleTurboCxxModule.h rename to ReactCommon/react/nativemodule/samples/SampleTurboCxxModule.h diff --git a/ReactCommon/turbomodule/samples/platform/ios/RCTNativeSampleTurboModuleSpec.h b/ReactCommon/react/nativemodule/samples/platform/ios/RCTNativeSampleTurboModuleSpec.h similarity index 100% rename from ReactCommon/turbomodule/samples/platform/ios/RCTNativeSampleTurboModuleSpec.h rename to ReactCommon/react/nativemodule/samples/platform/ios/RCTNativeSampleTurboModuleSpec.h diff --git a/ReactCommon/turbomodule/samples/platform/ios/RCTNativeSampleTurboModuleSpec.mm b/ReactCommon/react/nativemodule/samples/platform/ios/RCTNativeSampleTurboModuleSpec.mm similarity index 100% rename from ReactCommon/turbomodule/samples/platform/ios/RCTNativeSampleTurboModuleSpec.mm rename to ReactCommon/react/nativemodule/samples/platform/ios/RCTNativeSampleTurboModuleSpec.mm diff --git a/ReactCommon/turbomodule/samples/platform/ios/RCTSampleTurboCxxModule.h b/ReactCommon/react/nativemodule/samples/platform/ios/RCTSampleTurboCxxModule.h similarity index 100% rename from ReactCommon/turbomodule/samples/platform/ios/RCTSampleTurboCxxModule.h rename to ReactCommon/react/nativemodule/samples/platform/ios/RCTSampleTurboCxxModule.h diff --git a/ReactCommon/turbomodule/samples/platform/ios/RCTSampleTurboCxxModule.mm b/ReactCommon/react/nativemodule/samples/platform/ios/RCTSampleTurboCxxModule.mm similarity index 100% rename from ReactCommon/turbomodule/samples/platform/ios/RCTSampleTurboCxxModule.mm rename to ReactCommon/react/nativemodule/samples/platform/ios/RCTSampleTurboCxxModule.mm diff --git a/ReactCommon/turbomodule/samples/platform/ios/RCTSampleTurboModule.h b/ReactCommon/react/nativemodule/samples/platform/ios/RCTSampleTurboModule.h similarity index 100% rename from ReactCommon/turbomodule/samples/platform/ios/RCTSampleTurboModule.h rename to ReactCommon/react/nativemodule/samples/platform/ios/RCTSampleTurboModule.h diff --git a/ReactCommon/turbomodule/samples/platform/ios/RCTSampleTurboModule.mm b/ReactCommon/react/nativemodule/samples/platform/ios/RCTSampleTurboModule.mm similarity index 100% rename from ReactCommon/turbomodule/samples/platform/ios/RCTSampleTurboModule.mm rename to ReactCommon/react/nativemodule/samples/platform/ios/RCTSampleTurboModule.mm diff --git a/ReactCommon/turbomodule/samples/platform/ios/SampleTurboCxxModuleLegacyImpl.cpp b/ReactCommon/react/nativemodule/samples/platform/ios/SampleTurboCxxModuleLegacyImpl.cpp similarity index 100% rename from ReactCommon/turbomodule/samples/platform/ios/SampleTurboCxxModuleLegacyImpl.cpp rename to ReactCommon/react/nativemodule/samples/platform/ios/SampleTurboCxxModuleLegacyImpl.cpp diff --git a/ReactCommon/turbomodule/samples/platform/ios/SampleTurboCxxModuleLegacyImpl.h b/ReactCommon/react/nativemodule/samples/platform/ios/SampleTurboCxxModuleLegacyImpl.h similarity index 100% rename from ReactCommon/turbomodule/samples/platform/ios/SampleTurboCxxModuleLegacyImpl.h rename to ReactCommon/react/nativemodule/samples/platform/ios/SampleTurboCxxModuleLegacyImpl.h diff --git a/ReactCommon/react/renderer/animations/LayoutAnimationDriver.cpp b/ReactCommon/react/renderer/animations/LayoutAnimationDriver.cpp index 583176fefac4dd..d6b46c68a01207 100644 --- a/ReactCommon/react/renderer/animations/LayoutAnimationDriver.cpp +++ b/ReactCommon/react/renderer/animations/LayoutAnimationDriver.cpp @@ -191,8 +191,10 @@ void LayoutAnimationDriver::animationMutationsForFrame( // Queue up "final" mutations for all keyframes in the completed animation for (auto const &keyframe : animation.keyFrames) { - if (!keyframe.invalidated && - keyframe.finalMutationForKeyFrame.hasValue()) { + if (keyframe.invalidated) { + continue; + } + if (keyframe.finalMutationForKeyFrame.hasValue()) { auto const &finalMutationForKeyFrame = *keyframe.finalMutationForKeyFrame; PrintMutationInstruction( @@ -201,12 +203,28 @@ void LayoutAnimationDriver::animationMutationsForFrame( // Copy so that if something else mutates the inflight animations, it // won't change this mutation after this point. + ShadowView oldShadowView{}; + if (finalMutationForKeyFrame.type != + ShadowViewMutation::Type::Update) { + oldShadowView = finalMutationForKeyFrame.oldChildShadowView; + } mutationsList.push_back( ShadowViewMutation{finalMutationForKeyFrame.type, finalMutationForKeyFrame.parentShadowView, - finalMutationForKeyFrame.oldChildShadowView, + oldShadowView, finalMutationForKeyFrame.newChildShadowView, finalMutationForKeyFrame.index}); + } else { + // Issue a final UPDATE so that the final props object sent to the + // mounting layer is the same as the one on the ShadowTree. This is + // mostly to make the MountingCoordinator StubViewTree assertions + // pass. + mutationsList.push_back( + ShadowViewMutation{ShadowViewMutation::Type::Update, + keyframe.parentView, + {}, + keyframe.viewEnd, + -1}); } } diff --git a/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp b/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp index 66bcab91d50292..4264be5d3c613b 100644 --- a/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp +++ b/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -30,7 +31,8 @@ namespace facebook { namespace react { #ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING -std::string GetMutationInstructionString(ShadowViewMutation const &mutation) { +static std::string GetMutationInstructionString( + ShadowViewMutation const &mutation) { bool mutationIsRemove = mutation.type == ShadowViewMutation::Type::Remove; bool mutationIsInsert = mutation.type == ShadowViewMutation::Type::Insert; bool mutationIsDelete = mutation.type == ShadowViewMutation::Type::Delete; @@ -395,9 +397,12 @@ void LayoutAnimationKeyFrameManager:: adjustImmediateMutationIndicesForDelayedMutations( SurfaceId surfaceId, ShadowViewMutation &mutation, - ShadowViewMutationList *auxiliaryMutations) const { + ConsecutiveAdjustmentMetadata &consecutiveAdjustmentMetadata, + bool skipLastAnimation, + bool lastAnimationOnly) const { bool isRemoveMutation = mutation.type == ShadowViewMutation::Type::Remove; - assert(isRemoveMutation || mutation.type == ShadowViewMutation::Type::Insert); + bool isInsertMutation = mutation.type == ShadowViewMutation::Type::Insert; + assert(isRemoveMutation || isInsertMutation); // TODO: turn all of this into a lambda and share code? if (mutatedViewIsVirtual(mutation)) { @@ -411,36 +416,35 @@ void LayoutAnimationKeyFrameManager:: "[IndexAdjustment] Calling adjustImmediateMutationIndicesForDelayedMutations for:", mutation); + // When adjusting INSERTs, we want to batch adjacent inserts so they're all + // adjacent to each other in the resulting view. For instance, if we're + // processing INSERTS into positions 2,3,4,5,6, etc, it is possible that + // delayed mutations would otherwise cause the inserts to be adjusted to + // positions 2, 4, 6, 8... etc, creating a striping effect. We want to prevent + // that. + if (isInsertMutation && + mutation.parentShadowView.tag == + consecutiveAdjustmentMetadata.lastAdjustedParent && + mutation.index == (consecutiveAdjustmentMetadata.lastIndexOriginal + 1)) { + PrintMutationInstruction( + std::string( + "[IndexAdjustment] adjustImmediateMutationIndicesForDelayedMutations: Adjusting consecutive INSERT mutation by ") + + std::to_string(consecutiveAdjustmentMetadata.lastAdjustedDelta), + mutation); + consecutiveAdjustmentMetadata.lastIndexOriginal = mutation.index; + mutation.index += consecutiveAdjustmentMetadata.lastAdjustedDelta; + return; + } + // First, collect all final mutations that could impact this immediate // mutation. std::vector candidateMutations{}; - if (auxiliaryMutations != nullptr) { - for (auto &auxMutation : *auxiliaryMutations) { - if (auxMutation.parentShadowView.tag != mutation.parentShadowView.tag) { - continue; - } - if (auxMutation.type != ShadowViewMutation::Type::Remove) { - continue; - } - if (mutatedViewIsVirtual(auxMutation)) { - continue; - } - if (auxMutation.oldChildShadowView.tag == - (isRemoveMutation ? mutation.oldChildShadowView.tag - : mutation.newChildShadowView.tag)) { - continue; - } - - PrintMutationInstructionRelative( - "[IndexAdjustment] adjustImmediateMutationIndicesForDelayedMutations auxiliary CANDIDATE for:", - mutation, - auxMutation); - candidateMutations.push_back(&auxMutation); - } - } - - for (auto &inflightAnimation : inflightAnimations_) { + for (auto inflightAnimationIt = + inflightAnimations_.rbegin() + (skipLastAnimation ? 1 : 0); + inflightAnimationIt != inflightAnimations_.rend(); + inflightAnimationIt++) { + auto &inflightAnimation = *inflightAnimationIt; if (inflightAnimation.surfaceId != surfaceId) { continue; } @@ -463,9 +467,6 @@ void LayoutAnimationKeyFrameManager:: continue; } - if (animatedKeyFrame.type != AnimationConfigurationType::Noop) { - continue; - } if (!animatedKeyFrame.finalMutationForKeyFrame.has_value()) { continue; } @@ -490,21 +491,33 @@ void LayoutAnimationKeyFrameManager:: delayedMutation); candidateMutations.push_back(&delayedMutation); } + + if (lastAnimationOnly) { + break; + } } // While the mutation keeps being affected, keep checking. We use the vector // so we only perform one adjustment per delayed mutation. See comments at // bottom of adjustDelayedMutationIndicesForMutation for further explanation. bool changed = true; + int adjustedDelta = 0; + if (isInsertMutation) { + consecutiveAdjustmentMetadata.lastAdjustedParent = + mutation.parentShadowView.tag; + consecutiveAdjustmentMetadata.lastIndexOriginal = mutation.index; + } while (changed) { changed = false; candidateMutations.erase( std::remove_if( candidateMutations.begin(), candidateMutations.end(), - [&mutation, &changed](ShadowViewMutation *candidateMutation) { - if (candidateMutation->index <= mutation.index) { + [&](ShadowViewMutation *candidateMutation) { + bool indexConflicts = candidateMutation->index <= mutation.index; + if (indexConflicts) { mutation.index++; + adjustedDelta++; changed = true; PrintMutationInstructionRelative( "[IndexAdjustment] adjustImmediateMutationIndicesForDelayedMutations: Adjusting mutation UPWARD", @@ -516,19 +529,17 @@ void LayoutAnimationKeyFrameManager:: }), candidateMutations.end()); } -} - -void LayoutAnimationKeyFrameManager:: - adjustLastAnimationDelayedMutationIndicesForMutation( - SurfaceId surfaceId, - ShadowViewMutation const &mutation) const { - adjustDelayedMutationIndicesForMutation(surfaceId, mutation, true); + if (isInsertMutation) { + consecutiveAdjustmentMetadata.lastAdjustedDelta = adjustedDelta; + } else { + consecutiveAdjustmentMetadata.lastAdjustedParent = -1; + } } void LayoutAnimationKeyFrameManager::adjustDelayedMutationIndicesForMutation( SurfaceId surfaceId, ShadowViewMutation const &mutation, - bool lastAnimationOnly) const { + bool skipLastAnimation) const { bool isRemoveMutation = mutation.type == ShadowViewMutation::Type::Remove; bool isInsertMutation = mutation.type == ShadowViewMutation::Type::Insert; assert(isRemoveMutation || isInsertMutation); @@ -544,7 +555,8 @@ void LayoutAnimationKeyFrameManager::adjustDelayedMutationIndicesForMutation( // mutation. std::vector candidateMutations{}; - for (auto inflightAnimationIt = inflightAnimations_.rbegin(); + for (auto inflightAnimationIt = + inflightAnimations_.rbegin() + (skipLastAnimation ? 1 : 0); inflightAnimationIt != inflightAnimations_.rend(); inflightAnimationIt++) { auto &inflightAnimation = *inflightAnimationIt; @@ -571,9 +583,6 @@ void LayoutAnimationKeyFrameManager::adjustDelayedMutationIndicesForMutation( continue; } - if (animatedKeyFrame.type != AnimationConfigurationType::Noop) { - continue; - } if (!animatedKeyFrame.finalMutationForKeyFrame.has_value()) { continue; } @@ -586,19 +595,19 @@ void LayoutAnimationKeyFrameManager::adjustDelayedMutationIndicesForMutation( continue; } - if (!mutatedViewIsVirtual(*animatedKeyFrame.finalMutationForKeyFrame) && - finalAnimationMutation.type == ShadowViewMutation::Type::Remove) { - PrintMutationInstructionRelative( - "[IndexAdjustment] adjustDelayedMutationIndicesForMutation: CANDIDATE:", - mutation, - *animatedKeyFrame.finalMutationForKeyFrame); - candidateMutations.push_back( - animatedKeyFrame.finalMutationForKeyFrame.get_pointer()); + if (finalAnimationMutation.type != ShadowViewMutation::Type::Remove) { + continue; + } + if (mutatedViewIsVirtual(*animatedKeyFrame.finalMutationForKeyFrame)) { + continue; } - } - if (lastAnimationOnly) { - break; + PrintMutationInstructionRelative( + "[IndexAdjustment] adjustDelayedMutationIndicesForMutation: CANDIDATE:", + mutation, + *animatedKeyFrame.finalMutationForKeyFrame); + candidateMutations.push_back( + animatedKeyFrame.finalMutationForKeyFrame.get_pointer()); } } @@ -684,18 +693,6 @@ LayoutAnimationKeyFrameManager::getAndEraseConflictingAnimations( continue; } - // bool hasFinalMutation = - // animatedKeyFrame.finalMutationForKeyFrame.hasValue(); - // int finalMutationTag = hasFinalMutation - // ? (((*animatedKeyFrame.finalMutationForKeyFrame).type == - // ShadowViewMutation::Create || - // (*animatedKeyFrame.finalMutationForKeyFrame).type == - // ShadowViewMutation::Insert) - // ? (*animatedKeyFrame.finalMutationForKeyFrame) - // .newChildShadowView.tag - // : (*animatedKeyFrame.finalMutationForKeyFrame) - // .oldChildShadowView.tag) - // : -1; bool conflicting = animatedKeyFrame.tag == baselineShadowView.tag || ((mutation.type == ShadowViewMutation::Type::Delete || mutation.type == ShadowViewMutation::Type::Create) && @@ -703,20 +700,6 @@ LayoutAnimationKeyFrameManager::getAndEraseConflictingAnimations( finalMutationTag == baselineShadowView.tag*/ ; - // In some bizarre situations, there can be an ongoing Delete - // animation, and then a conflicting mutation to create and/or delete - // the same tag. In actuality this "bizarre" situation is just the - // animation of repeatedly flattening and unflattening a view; but - // it's not clear how to gracefully recover from this, so we just - // ensure that the Deletion is never executed in those cases. In these - // cases, the ongoing animation will stop; the view still exists; and - // then either a "Create" or "delete" animation will be recreated and - // executed for that tag. - bool shouldExecuteFinalMutation = - !(animatedKeyFrame.finalMutationForKeyFrame.hasValue() && - (*animatedKeyFrame.finalMutationForKeyFrame).type == - ShadowViewMutation::Delete); - // Conflicting animation detected: if we're mutating a tag under // animation, or deleting the parent of a tag under animation, or // reparenting. @@ -734,7 +717,9 @@ LayoutAnimationKeyFrameManager::getAndEraseConflictingAnimations( animatedKeyFrame.invalidated = true; - if (shouldExecuteFinalMutation) { + if (animatedKeyFrame.finalMutationForKeyFrame.has_value() && + !mutatedViewIsVirtual( + *animatedKeyFrame.finalMutationForKeyFrame)) { conflictingAnimations.push_back(std::make_tuple( animatedKeyFrame, *mutationConfig, &inflightAnimation)); } @@ -747,7 +732,7 @@ LayoutAnimationKeyFrameManager::getAndEraseConflictingAnimations( *animatedKeyFrame.finalMutationForKeyFrame); } else { PrintMutationInstruction( - "Found mutation that conflicts with existing in-flight animation:", + "Found mutation that conflicts with existing in-flight animation (no final mutation):", mutation); } #endif @@ -755,13 +740,6 @@ LayoutAnimationKeyFrameManager::getAndEraseConflictingAnimations( // Delete from existing animation it = inflightAnimation.keyFrames.erase(it); } else { - //#ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING - // if (hasFinalMutation) { - // PrintMutationInstructionRelative("getAndEraseConflictingAnimations, - // NOT erasing non-conflicting mutation of ", mutation, - // *animatedKeyFrame.finalMutationForKeyFrame); - // } - //#endif it++; } } @@ -843,7 +821,8 @@ LayoutAnimationKeyFrameManager::pullTransaction( if (keyframe.invalidated) { continue; } - if (keyframe.finalMutationForKeyFrame) { + if (keyframe.finalMutationForKeyFrame && + !mutatedViewIsVirtual(*keyframe.finalMutationForKeyFrame)) { std::string msg = "Animation " + std::to_string(i) + " keyframe " + std::to_string(j) + ": Final Animation"; PrintMutationInstruction(msg, *keyframe.finalMutationForKeyFrame); @@ -898,15 +877,23 @@ LayoutAnimationKeyFrameManager::pullTransaction( // being moves on the Differ level, since we know that there? We could use // TinyMap here, but it's not exposed by Differentiator (yet). std::vector insertedTags; - std::vector createdTags; + std::vector deletedTags; + std::vector reparentedTags; // tags that are deleted and recreated std::unordered_map movedTags; - std::vector reparentedTags; for (const auto &mutation : mutations) { if (mutation.type == ShadowViewMutation::Type::Insert) { insertedTags.push_back(mutation.newChildShadowView.tag); } + if (mutation.type == ShadowViewMutation::Type::Delete) { + deletedTags.push_back(mutation.oldChildShadowView.tag); + } if (mutation.type == ShadowViewMutation::Type::Create) { - createdTags.push_back(mutation.newChildShadowView.tag); + if (std::find( + deletedTags.begin(), + deletedTags.end(), + mutation.newChildShadowView.tag) != deletedTags.end()) { + reparentedTags.push_back(mutation.newChildShadowView.tag); + } } } @@ -953,26 +940,15 @@ LayoutAnimationKeyFrameManager::pullTransaction( // This should eventually be optimized out of the diffing algorithm, but // for now we detect reparenting and prevent the corresponding // Delete/Create instructions from being animated. - bool isReparented = - (mutation.type == ShadowViewMutation::Delete && - std::find( - createdTags.begin(), - createdTags.end(), - mutation.oldChildShadowView.tag) != createdTags.end()) || - (mutation.type == ShadowViewMutation::Create && - std::find( - reparentedTags.begin(), - reparentedTags.end(), - mutation.newChildShadowView.tag) != reparentedTags.end()); + bool isReparented = std::find( + reparentedTags.begin(), + reparentedTags.end(), + baselineShadowView.tag) != reparentedTags.end(); if (isRemoveReinserted) { movedTags.insert({mutation.oldChildShadowView.tag, mutation}); } - if (isReparented && mutation.type == ShadowViewMutation::Delete) { - reparentedTags.push_back(mutation.oldChildShadowView.tag); - } - // Inserts that follow a "remove" of the same tag should be treated as // an update (move) animation. bool wasInsertedTagRemoved = false; @@ -1204,30 +1180,31 @@ LayoutAnimationKeyFrameManager::pullTransaction( } #ifdef RN_SHADOW_TREE_INTROSPECTION +#ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING { - std::stringstream ss(getDebugDescription(immediateMutations, {})); - std::string to; - while (std::getline(ss, to, '\n')) { - LOG(ERROR) - << "LayoutAnimationKeyFrameManager.cpp: got IMMEDIATE list: Line: " - << to; + int idx = 0; + for (auto &mutation : immediateMutations) { + PrintMutationInstruction( + std::string("IMMEDIATE list: ") + std::to_string(idx) + "/" + + std::to_string(immediateMutations.size()), + mutation); + idx++; } } { + int idx = 0; for (const auto &keyframe : keyFramesToAnimate) { - if (keyframe.finalMutationForKeyFrame) { - std::stringstream ss( - getDebugDescription(*keyframe.finalMutationForKeyFrame, {})); - std::string to; - while (std::getline(ss, to, '\n')) { - LOG(ERROR) - << "LayoutAnimationKeyFrameManager.cpp: got FINAL list: Line: " - << to; - } + if (keyframe.finalMutationForKeyFrame.has_value()) { + PrintMutationInstruction( + std::string("FINAL list: ") + std::to_string(idx) + "/" + + std::to_string(keyFramesToAnimate.size()), + *keyframe.finalMutationForKeyFrame); } + idx++; } } +#endif #endif auto finalConflictingMutations = ShadowViewMutationList{}; @@ -1235,7 +1212,16 @@ LayoutAnimationKeyFrameManager::pullTransaction( auto &keyFrame = std::get<0>(conflictingKeyframeTuple); if (keyFrame.finalMutationForKeyFrame.hasValue()) { auto &mutation = *keyFrame.finalMutationForKeyFrame; - finalConflictingMutations.push_back(mutation); + if (mutation.type == ShadowViewMutation::Type::Update) { + finalConflictingMutations.push_back( + ShadowViewMutation::UpdateMutation( + mutation.parentShadowView, + {}, + mutation.newChildShadowView, + mutation.index)); + } else { + finalConflictingMutations.push_back(mutation); + } } } @@ -1246,56 +1232,121 @@ LayoutAnimationKeyFrameManager::pullTransaction( finalConflictingMutations.end(), &shouldFirstComeBeforeSecondMutation); - // Use "final conflicting mutations" to adjust delayed mutations *before* - // we adjust immediate mutations based on delayed mutations + std::stable_sort( + immediateMutations.begin(), + immediateMutations.end(), + &shouldFirstComeBeforeSecondRemovesOnly); + + animation.keyFrames = keyFramesToAnimate; + inflightAnimations_.push_back(std::move(animation)); + + // At this point, we have the following information and knowledge graph: + // Knowledge Graph: + // [ImmediateMutations] -> assumes [FinalConflicting], [FrameDelayed], + // [Delayed] already executed [FrameDelayed] -> assumes + // [FinalConflicting], [Delayed] already executed [FinalConflicting] -> is + // adjusted based on [Delayed], no dependency on [FinalConflicting], + // [FrameDelayed] [Delayed] -> assumes [FinalConflicting], + // [ImmediateMutations] not executed yet + ConsecutiveAdjustmentMetadata consecutiveAdjustmentMetadata{}; + + // Adjust [Delayed] based on [FinalConflicting] + // Knowledge Graph: + // [ImmediateMutations] -> assumes [FinalConflicting], [FrameDelayed], + // [Delayed] already executed [FrameDelayed] -> assumes + // [FinalConflicting], [Delayed] already executed [FinalConflicting] -> is + // adjusted based on [Delayed], no dependency on [FinalConflicting], + // [FrameDelayed] [Delayed] -> adjusted for [FinalConflicting]; assumes + // [ImmediateMutations] not executed yet #ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING - LOG(ERROR) - << "Adjust delayed mutations based on finalConflictingMutations"; + LOG(ERROR) << "Adjust [Delayed] based on [FinalConflicting]"; #endif for (auto &mutation : finalConflictingMutations) { - adjustDelayedMutationIndicesForMutation(surfaceId, mutation); + if (mutation.type == ShadowViewMutation::Type::Insert || + mutation.type == ShadowViewMutation::Type::Remove) { + adjustDelayedMutationIndicesForMutation(surfaceId, mutation, true); + } } - // Adjust keyframes based on already-delayed, existing animations, before - // queueing. We adjust them as if finalConflictingMutations have already - // been executed - in all cases, finalConflictingMutations will be - // executed before any of these delayed mutations are. + // Adjust [FrameDelayed] based on [Delayed] + // Knowledge Graph: + // [ImmediateExecutions] -> assumes [FinalConflicting], [Delayed], + // [FrameDelayed] already executed [FrameDelayed] -> adjusted for + // [Delayed]; assumes [FinalConflicting] already executed + // [FinalConflicting] -> is adjusted based on [Delayed], no dependency on + // [FinalConflicting], [FrameDelayed] [Delayed] -> adjusted for + // [FinalConflicting]; assumes [ImmediateExecutions] not executed yet #ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING - LOG(ERROR) - << "Adjust immediate keyFramesToAnimate based on delayed mutations and finalConflictingMutations"; + LOG(ERROR) << "Adjust [FrameDelayed] based on [Delayed]"; #endif - for (auto &keyframe : keyFramesToAnimate) { + for (auto &keyframe : inflightAnimations_.back().keyFrames) { if (keyframe.finalMutationForKeyFrame.has_value()) { - auto &delayedMutation = *keyframe.finalMutationForKeyFrame; - if (delayedMutation.type == ShadowViewMutation::Type::Remove) { + auto &mutation = *keyframe.finalMutationForKeyFrame; + if (mutation.type == ShadowViewMutation::Type::Insert || + mutation.type == ShadowViewMutation::Type::Remove) { + // When adjusting, skip adjusting against last animation - because + // all `mutation`s here come from the last animation, so we can't + // adjust a batch against itself. adjustImmediateMutationIndicesForDelayedMutations( - surfaceId, delayedMutation /*, &finalConflictingMutations*/); + surfaceId, mutation, consecutiveAdjustmentMetadata, true); } } } - // REMOVE mutations from this animation batch *cannot* be impacted by - // other REMOVEs from this batch, since they're already taken into - // account. INSERTs can impact delayed REMOVEs; see below. + // Adjust [ImmediateExecutions] based on [Delayed] + // Knowledge Graph: + // [ImmediateExecutions] -> adjusted for [FrameDelayed], [Delayed]; + // assumes [FinalConflicting] already executed [FrameDelayed] -> adjusted + // for [Delayed]; assumes [FinalConflicting] already executed + // [FinalConflicting] -> is adjusted based on [Delayed], no dependency on + // [FinalConflicting], [FrameDelayed] [Delayed] -> adjusted for + // [FinalConflicting]; assumes [ImmediateExecutions] not executed yet #ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING - LOG(ERROR) - << "Adjust immediateMutations REMOVEs only, based on previously delayed mutations, without most-recent animation"; + LOG(ERROR) << "Adjust [ImmediateExecutions] based on [Delayed]"; #endif - std::stable_sort( - immediateMutations.begin(), - immediateMutations.end(), - &shouldFirstComeBeforeSecondRemovesOnly); + consecutiveAdjustmentMetadata = ConsecutiveAdjustmentMetadata{}; for (auto &mutation : immediateMutations) { - if (mutation.type == ShadowViewMutation::Type::Remove) { + // Note: when adjusting [ImmediateExecutions] based on [FrameDelayed], + // we need only adjust Inserts. Since inserts are executed + // highest-index-first, lower indices being delayed does not impact the + // higher-index removals; and conversely, higher indices being delayed + // cannot impact lower index removal, regardless of order. + if (mutation.type == ShadowViewMutation::Type::Insert || + mutation.type == ShadowViewMutation::Type::Remove) { adjustImmediateMutationIndicesForDelayedMutations( - surfaceId, mutation); - adjustDelayedMutationIndicesForMutation(surfaceId, mutation); + surfaceId, + mutation, + consecutiveAdjustmentMetadata, + mutation.type == ShadowViewMutation::Type::Remove); } } - animation.keyFrames = keyFramesToAnimate; - inflightAnimations_.push_back(std::move(animation)); + // Adjust [Delayed] based on [ImmediateExecutions] and [FinalConflicting] + // Knowledge Graph: + // [ImmediateExecutions] -> adjusted for [FrameDelayed], [Delayed]; + // assumes [FinalConflicting] already executed [FrameDelayed] -> adjusted + // for [Delayed]; assumes [FinalConflicting] already executed + // [FinalConflicting] -> is adjusted based on [Delayed], no dependency on + // [FinalConflicting], [FrameDelayed] [Delayed] -> adjusted for + // [FinalConflicting], [ImmediateExecutions] +#ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING + LOG(ERROR) + << "Adjust [Delayed] based on [ImmediateExecutions] and [FinalConflicting]"; +#endif + for (auto &mutation : immediateMutations) { + if (mutation.type == ShadowViewMutation::Type::Insert || + mutation.type == ShadowViewMutation::Type::Remove) { + // Here we need to adjust both Delayed and FrameDelayed mutations. + // Delayed Removes can be impacted by non-delayed Inserts from the + // same frame. + adjustDelayedMutationIndicesForMutation(surfaceId, mutation); + } + } + // If the knowledge graph progression above is correct, it is now safe to + // execute finalConflictingMutations and immediateMutations in that order, + // and to queue the delayed animations from this frame. + // // Execute the conflicting, delayed operations immediately. Any UPDATE // operations that smoothly transition into another animation will be // overridden by generated UPDATE operations at the end of the list, and @@ -1303,72 +1354,75 @@ LayoutAnimationKeyFrameManager::pullTransaction( // Additionally, this should allow us to avoid performing index adjustment // between this list of conflicting animations and the batch we're about // to execute. - mutations = ShadowViewMutationList{}; - for (auto &mutation : finalConflictingMutations) { - mutations.push_back(mutation); + finalConflictingMutations.insert( + finalConflictingMutations.end(), + immediateMutations.begin(), + immediateMutations.end()); + mutations = finalConflictingMutations; + } /* if (currentAnimation) */ else { + // If there's no "next" animation, make sure we queue up "final" + // operations from all ongoing, conflicting animations. +#ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING + LOG(ERROR) << "No Animation: Queue up final conflicting animations"; +#endif + ShadowViewMutationList finalMutationsForConflictingAnimations{}; + for (auto &conflictingKeyframeTuple : conflictingAnimations) { + auto &keyFrame = std::get<0>(conflictingKeyframeTuple); + if (keyFrame.finalMutationForKeyFrame.hasValue()) { + PrintMutationInstruction( + "No Animation: Queueing final mutation instruction", + *keyFrame.finalMutationForKeyFrame); + finalMutationsForConflictingAnimations.push_back( + *keyFrame.finalMutationForKeyFrame); + } } - // Before computing mutations based on animations / final mutations for - // this frame, we want to update any pending final mutations since they - // will execute *after* this batch of immediate mutations. Important case - // to consider (as an example, there are other interesting cases): there's - // a delayed "Remove", then an immediate "insert" is scheduled for an - // earlier index with the same parent. The remove needs to be adjusted - // upward here. Conversely, Inserts at later indices will assume the - // remove has already been executed, which may not be the case. + // Make sure that all operations execute in the proper order. + // REMOVE operations with highest indices must operate first. + std::stable_sort( + finalMutationsForConflictingAnimations.begin(), + finalMutationsForConflictingAnimations.end(), + &shouldFirstComeBeforeSecondMutation); + #ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING LOG(ERROR) - << "Adjust immediateMutations and delayed mutations, including just-queued animations, based on each one"; + << "No Animation: Adjust delayed mutations based on all finalMutationsForConflictingAnimations"; #endif - for (auto &mutation : immediateMutations) { - if (mutation.type == ShadowViewMutation::Type::Remove) { - adjustLastAnimationDelayedMutationIndicesForMutation( - surfaceId, mutation); - } else if (mutation.type == ShadowViewMutation::Type::Insert) { - adjustImmediateMutationIndicesForDelayedMutations( - surfaceId, mutation); + for (auto &mutation : finalMutationsForConflictingAnimations) { + if (mutation.type == ShadowViewMutation::Type::Remove || + mutation.type == ShadowViewMutation::Type::Insert) { adjustDelayedMutationIndicesForMutation(surfaceId, mutation); } } - // These will be executed immediately. These should already be sorted - // properly. - mutations.insert( - mutations.end(), - immediateMutations.begin(), - immediateMutations.end()); - } /* if (currentAnimation) */ else { // The ShadowTree layer doesn't realize that certain operations have been // delayed, so we must adjust all Remove and Insert operations based on // what else has been deferred, whether we are executing this immediately // or later. +#ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING + LOG(ERROR) + << "No Animation: Adjust mutations based on remaining delayed mutations"; +#endif + ConsecutiveAdjustmentMetadata consecutiveAdjustmentMetadata{}; for (auto &mutation : mutations) { if (mutation.type == ShadowViewMutation::Type::Remove || mutation.type == ShadowViewMutation::Type::Insert) { adjustImmediateMutationIndicesForDelayedMutations( - surfaceId, mutation); - adjustDelayedMutationIndicesForMutation(surfaceId, mutation); + surfaceId, mutation, consecutiveAdjustmentMetadata); } } - // If there's no "next" animation, make sure we queue up "final" - // operations from all ongoing, conflicting animations. - ShadowViewMutationList finalMutationsForConflictingAnimations{}; - for (auto &conflictingKeyframeTuple : conflictingAnimations) { - auto &keyFrame = std::get<0>(conflictingKeyframeTuple); - if (keyFrame.finalMutationForKeyFrame.hasValue()) { - finalMutationsForConflictingAnimations.push_back( - *keyFrame.finalMutationForKeyFrame); +#ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING + LOG(ERROR) + << "No Animation: Adjust delayed mutations based on all immediate mutations"; +#endif + for (auto &mutation : mutations) { + if (mutation.type == ShadowViewMutation::Type::Remove || + mutation.type == ShadowViewMutation::Type::Insert) { + adjustDelayedMutationIndicesForMutation(surfaceId, mutation); } } - // Make sure that all operations execute in the proper order. - // REMOVE operations with highest indices must operate first. - std::stable_sort( - finalMutationsForConflictingAnimations.begin(), - finalMutationsForConflictingAnimations.end(), - &shouldFirstComeBeforeSecondMutation); - // Append mutations to this list and swap - so that the final // conflicting mutations happen before any other mutations finalMutationsForConflictingAnimations.insert( @@ -1388,13 +1442,6 @@ LayoutAnimationKeyFrameManager::pullTransaction( ShadowViewMutationList mutationsForAnimation{}; animationMutationsForFrame(surfaceId, mutationsForAnimation, now); - // Erase any remaining animations that conflict with these mutations - // In some marginal cases, a DELETE animation can be queued up and a final - // DELETE mutation be executed by the animation driver. These cases deserve - // further scrutiny, but for now to prevent crashes, just make sure the queued - // DELETE operations are removed. - getAndEraseConflictingAnimations(surfaceId, mutationsForAnimation, true); - // If any delayed removes were executed, update remaining delayed keyframes #ifdef LAYOUT_ANIMATION_VERBOSE_LOGGING LOG(ERROR) @@ -1427,7 +1474,8 @@ LayoutAnimationKeyFrameManager::pullTransaction( if (keyframe.invalidated) { continue; } - if (keyframe.finalMutationForKeyFrame) { + if (keyframe.finalMutationForKeyFrame && + !mutatedViewIsVirtual(*keyframe.finalMutationForKeyFrame)) { std::string msg = "Animation " + std::to_string(i) + " keyframe " + std::to_string(j) + ": Final Animation"; PrintMutationInstruction(msg, *keyframe.finalMutationForKeyFrame); diff --git a/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h b/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h index 686fcaa9da9e21..e6aba6e3c07bcd 100644 --- a/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h +++ b/ReactCommon/react/renderer/animations/LayoutAnimationKeyFrameManager.h @@ -110,6 +110,12 @@ struct AnimationKeyFrame { bool invalidated{false}; }; +struct ConsecutiveAdjustmentMetadata { + Tag lastAdjustedParent{-1}; + int lastAdjustedDelta{0}; + int lastIndexOriginal{0}; +}; + class LayoutAnimationCallbackWrapper { public: LayoutAnimationCallbackWrapper(jsi::Function &&callback) @@ -212,16 +218,14 @@ class LayoutAnimationKeyFrameManager : public UIManagerAnimationDelegate, void adjustImmediateMutationIndicesForDelayedMutations( SurfaceId surfaceId, ShadowViewMutation &mutation, - ShadowViewMutationList *auxiliaryMutations = nullptr) const; + ConsecutiveAdjustmentMetadata &consecutiveAdjustmentMetadata, + bool skipLastAnimation = false, + bool lastAnimationOnly = false) const; void adjustDelayedMutationIndicesForMutation( SurfaceId surfaceId, ShadowViewMutation const &mutation, - bool lastAnimationOnly = false) const; - - void adjustLastAnimationDelayedMutationIndicesForMutation( - SurfaceId surfaceId, - ShadowViewMutation const &mutation) const; + bool skipLastAnimation = false) const; std::vector> getAndEraseConflictingAnimations( diff --git a/ReactCommon/react/renderer/attributedstring/conversions.h b/ReactCommon/react/renderer/attributedstring/conversions.h index 26c8b5f37f0799..a121a09028e2ea 100644 --- a/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/ReactCommon/react/renderer/attributedstring/conversions.h @@ -768,6 +768,10 @@ inline folly::dynamic toDynamic(const TextAttributes &textAttributes) { _textAttributes( "layoutDirection", toString(*textAttributes.layoutDirection)); } + if (textAttributes.accessibilityRole.has_value()) { + _textAttributes( + "accessibilityRole", toString(*textAttributes.accessibilityRole)); + } return _textAttributes; } diff --git a/ReactCommon/react/renderer/components/image/BUCK b/ReactCommon/react/renderer/components/image/BUCK index 682ea9ddb22312..205771229aea14 100644 --- a/ReactCommon/react/renderer/components/image/BUCK +++ b/ReactCommon/react/renderer/components/image/BUCK @@ -47,10 +47,7 @@ rn_xplat_cxx_library( tests = [":tests"], visibility = ["PUBLIC"], deps = [ - "//third-party/glog:glog", - "//xplat/fbsystrace:fbsystrace", "//xplat/folly:headers_only", - "//xplat/folly:memory", "//xplat/folly:molly", YOGA_CXX_TARGET, react_native_xplat_target("react/renderer/debug:debug"), diff --git a/ReactCommon/react/renderer/components/image/ImageProps.cpp b/ReactCommon/react/renderer/components/image/ImageProps.cpp index c716b786f7d390..4a496bba12d011 100644 --- a/ReactCommon/react/renderer/components/image/ImageProps.cpp +++ b/ReactCommon/react/renderer/components/image/ImageProps.cpp @@ -30,7 +30,12 @@ ImageProps::ImageProps(const ImageProps &sourceProps, const RawProps &rawProps) capInsets( convertRawProp(rawProps, "capInsets", sourceProps.capInsets, {})), tintColor( - convertRawProp(rawProps, "tintColor", sourceProps.tintColor, {})) {} + convertRawProp(rawProps, "tintColor", sourceProps.tintColor, {})), + internal_analyticTag(convertRawProp( + rawProps, + "internal_analyticTag", + sourceProps.internal_analyticTag, + {})) {} } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/components/image/ImageProps.h b/ReactCommon/react/renderer/components/image/ImageProps.h index 7ed6ac4e1800e1..7ecfc27b8f7143 100644 --- a/ReactCommon/react/renderer/components/image/ImageProps.h +++ b/ReactCommon/react/renderer/components/image/ImageProps.h @@ -26,6 +26,7 @@ class ImageProps final : public ViewProps { const Float blurRadius{}; const EdgeInsets capInsets{}; const SharedColor tintColor{}; + const std::string internal_analyticTag{}; }; } // namespace react diff --git a/ReactCommon/react/renderer/components/image/ImageShadowNode.h b/ReactCommon/react/renderer/components/image/ImageShadowNode.h index 0e60f78deef2dd..12490840b611bd 100644 --- a/ReactCommon/react/renderer/components/image/ImageShadowNode.h +++ b/ReactCommon/react/renderer/components/image/ImageShadowNode.h @@ -46,7 +46,7 @@ class ImageShadowNode final : public ConcreteViewShadowNode< ShadowNodeFamilyFragment const &familyFragment, ComponentDescriptor const &componentDescriptor) { auto imageSource = ImageSource{ImageSource::Type::Invalid}; - return {imageSource, {imageSource, nullptr}, 0}; + return {imageSource, {imageSource, nullptr, nullptr}, 0}; } #pragma mark - LayoutableShadowNode diff --git a/ReactCommon/react/renderer/components/slider/BUCK b/ReactCommon/react/renderer/components/slider/BUCK index 7030c52be1751f..00f0df782327af 100644 --- a/ReactCommon/react/renderer/components/slider/BUCK +++ b/ReactCommon/react/renderer/components/slider/BUCK @@ -80,10 +80,7 @@ rn_xplat_cxx_library( ], visibility = ["PUBLIC"], deps = [ - "//third-party/glog:glog", - "//xplat/fbsystrace:fbsystrace", "//xplat/folly:headers_only", - "//xplat/folly:memory", "//xplat/folly:molly", YOGA_CXX_TARGET, react_native_xplat_target("react/renderer/debug:debug"), diff --git a/ReactCommon/react/renderer/components/slider/SliderShadowNode.h b/ReactCommon/react/renderer/components/slider/SliderShadowNode.h index 82ab4d0b1dc6c2..c012ff45a517fa 100644 --- a/ReactCommon/react/renderer/components/slider/SliderShadowNode.h +++ b/ReactCommon/react/renderer/components/slider/SliderShadowNode.h @@ -44,13 +44,13 @@ class SliderShadowNode final : public ConcreteViewShadowNode< ComponentDescriptor const &componentDescriptor) { auto imageSource = ImageSource{ImageSource::Type::Invalid}; return {imageSource, - {imageSource, nullptr}, + {imageSource, nullptr, nullptr}, imageSource, - {imageSource, nullptr}, + {imageSource, nullptr, nullptr}, imageSource, - {imageSource, nullptr}, + {imageSource, nullptr, nullptr}, imageSource, - {imageSource, nullptr}}; + {imageSource, nullptr, nullptr}}; } #pragma mark - LayoutableShadowNode diff --git a/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.cpp b/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.cpp index 1295108286f14b..ee16f5d02655c9 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.cpp +++ b/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.cpp @@ -38,12 +38,17 @@ static jsi::Value linesMeasurementsPayload( void ParagraphEventEmitter::onTextLayout( LinesMeasurements const &linesMeasurements) const { - dispatchEvent( - "textLayout", - [linesMeasurements](jsi::Runtime &runtime) { - return linesMeasurementsPayload(runtime, linesMeasurements); - }, - EventPriority::AsynchronousBatched); + { + std::lock_guard guard(linesMeasurementsMutex_); + if (linesMeasurementsMetrics_ == linesMeasurements) { + return; + } + linesMeasurementsMetrics_ = linesMeasurements; + } + + dispatchEvent("textLayout", [linesMeasurements](jsi::Runtime &runtime) { + return linesMeasurementsPayload(runtime, linesMeasurements); + }); } } // namespace react diff --git a/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.h b/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.h index b73eb967d5afb2..597ef6baa2ddcb 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.h +++ b/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.h @@ -18,6 +18,10 @@ class ParagraphEventEmitter : public ViewEventEmitter { using ViewEventEmitter::ViewEventEmitter; void onTextLayout(LinesMeasurements const &linesMeasurements) const; + + private: + mutable std::mutex linesMeasurementsMutex_; + mutable LinesMeasurements linesMeasurementsMetrics_; }; } // namespace react diff --git a/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp b/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp index 993cc65b8834ee..87745126463ca9 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -169,7 +169,6 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { content.paragraphAttributes, layoutConstraints); -#ifndef ANDROID if (getConcreteProps().onTextLayout) { auto linesMeasurements = textLayoutManager_->measureLines( content.attributedString, @@ -177,10 +176,9 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { measurement.size); getConcreteEventEmitter().onTextLayout(linesMeasurements); } -#endif if (content.attachments.empty()) { - // No attachments, nothing to layout. + // No attachments to layout. return; } diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp index 42b47276bf024c..86420fe470da86 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp @@ -131,6 +131,11 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { return; } + // If props event counter is less than what we already have in state, skip it + if (getConcreteProps().mostRecentEventCount < state.mostRecentEventCount) { + return; + } + // Store default TextAttributes in state. // In the case where the TextInput is completely empty (no value, no // defaultValue, no placeholder, no children) there are therefore no fragments diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.cpp b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.cpp index 6cb7b115612114..70f89eb40e7561 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.cpp +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.cpp @@ -13,15 +13,82 @@ namespace facebook { namespace react { +AndroidTextInputState::AndroidTextInputState( + int64_t mostRecentEventCount, + AttributedString const &attributedString, + AttributedString const &reactTreeAttributedString, + ParagraphAttributes const ¶graphAttributes, + TextAttributes const &defaultTextAttributes, + ShadowView const &defaultParentShadowView, + SharedTextLayoutManager const &layoutManager, + float defaultThemePaddingStart, + float defaultThemePaddingEnd, + float defaultThemePaddingTop, + float defaultThemePaddingBottom) + : mostRecentEventCount(mostRecentEventCount), + cachedAttributedStringId(0), + attributedString(attributedString), + reactTreeAttributedString(reactTreeAttributedString), + paragraphAttributes(paragraphAttributes), + defaultTextAttributes(defaultTextAttributes), + defaultParentShadowView(defaultParentShadowView), + layoutManager(layoutManager), + defaultThemePaddingStart(defaultThemePaddingStart), + defaultThemePaddingEnd(defaultThemePaddingEnd), + defaultThemePaddingTop(defaultThemePaddingTop), + defaultThemePaddingBottom(defaultThemePaddingBottom) {} + +AndroidTextInputState::AndroidTextInputState( + AndroidTextInputState const &previousState, + folly::dynamic const &data) + : mostRecentEventCount(data.getDefault( + "mostRecentEventCount", + previousState.mostRecentEventCount) + .getInt()), + cachedAttributedStringId(data.getDefault( + "opaqueCacheId", + previousState.cachedAttributedStringId) + .getInt()), + attributedString(previousState.attributedString), + reactTreeAttributedString(previousState.reactTreeAttributedString), + paragraphAttributes(previousState.paragraphAttributes), + defaultTextAttributes(previousState.defaultTextAttributes), + defaultParentShadowView(previousState.defaultParentShadowView), + layoutManager(previousState.layoutManager), + defaultThemePaddingStart(data.getDefault( + "themePaddingStart", + previousState.defaultThemePaddingStart) + .getDouble()), + defaultThemePaddingEnd(data.getDefault( + "themePaddingEnd", + previousState.defaultThemePaddingEnd) + .getDouble()), + defaultThemePaddingTop(data.getDefault( + "themePaddingTop", + previousState.defaultThemePaddingTop) + .getDouble()), + defaultThemePaddingBottom(data.getDefault( + "themePaddingBottom", + previousState.defaultThemePaddingBottom) + .getDouble()){}; + #ifdef ANDROID folly::dynamic AndroidTextInputState::getDynamic() const { - // Java doesn't need all fields, so we don't pass them along. + // Java doesn't need all fields, so we don't pass them all along. folly::dynamic newState = folly::dynamic::object(); - newState["mostRecentEventCount"] = mostRecentEventCount; - newState["attributedString"] = toDynamic(attributedString); - newState["hash"] = newState["attributedString"]["hash"]; - newState["paragraphAttributes"] = - toDynamic(paragraphAttributes); // TODO: can we memoize this in Java? + + // If we have a `cachedAttributedStringId` we know that we're (1) not trying + // to set a new string, so we don't need to pass it along; (2) setState was + // called from Java to trigger a relayout with a `cachedAttributedStringId`, + // so Java has all up-to-date information and we should pass an empty map + // through. + if (cachedAttributedStringId == 0) { + newState["mostRecentEventCount"] = mostRecentEventCount; + newState["attributedString"] = toDynamic(attributedString); + newState["hash"] = newState["attributedString"]["hash"]; + newState["paragraphAttributes"] = + toDynamic(paragraphAttributes); // TODO: can we memoize this in Java? + } return newState; } #endif diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h index 2c039ab8a3f92c..c97ade9565c5ec 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h @@ -29,7 +29,7 @@ class AndroidTextInputState final { * Stores an opaque cache ID used on the Java side to refer to a specific * AttributedString for measurement purposes only. */ - int cachedAttributedStringId{0}; + int64_t cachedAttributedStringId{0}; /* * All content of component represented as an `AttributedString`. @@ -93,53 +93,12 @@ class AndroidTextInputState final { float defaultThemePaddingStart, float defaultThemePaddingEnd, float defaultThemePaddingTop, - float defaultThemePaddingBottom) - : mostRecentEventCount(mostRecentEventCount), - cachedAttributedStringId(0), - attributedString(attributedString), - reactTreeAttributedString(reactTreeAttributedString), - paragraphAttributes(paragraphAttributes), - defaultTextAttributes(defaultTextAttributes), - defaultParentShadowView(defaultParentShadowView), - layoutManager(layoutManager), - defaultThemePaddingStart(defaultThemePaddingStart), - defaultThemePaddingEnd(defaultThemePaddingEnd), - defaultThemePaddingTop(defaultThemePaddingTop), - defaultThemePaddingBottom(defaultThemePaddingBottom) {} + float defaultThemePaddingBottom); + AndroidTextInputState() = default; AndroidTextInputState( AndroidTextInputState const &previousState, - folly::dynamic const &data) - : mostRecentEventCount(data.getDefault( - "mostRecentEventCount", - previousState.mostRecentEventCount) - .getInt()), - cachedAttributedStringId( - data.getDefault("cacheId", previousState.cachedAttributedStringId) - .getInt()), - attributedString(previousState.attributedString), - reactTreeAttributedString(previousState.reactTreeAttributedString), - paragraphAttributes(previousState.paragraphAttributes), - defaultTextAttributes(previousState.defaultTextAttributes), - defaultParentShadowView(previousState.defaultParentShadowView), - layoutManager(previousState.layoutManager), - defaultThemePaddingStart(data.getDefault( - "themePaddingStart", - previousState.defaultThemePaddingStart) - .getDouble()), - defaultThemePaddingEnd(data.getDefault( - "themePaddingEnd", - previousState.defaultThemePaddingEnd) - .getDouble()), - defaultThemePaddingTop(data.getDefault( - "themePaddingTop", - previousState.defaultThemePaddingTop) - .getDouble()), - defaultThemePaddingBottom( - data.getDefault( - "themePaddingBottom", - previousState.defaultThemePaddingBottom) - .getDouble()){}; + folly::dynamic const &data); folly::dynamic getDynamic() const; }; diff --git a/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h b/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h index e7a65c05b3b82c..0fe2def64c9413 100644 --- a/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h +++ b/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h @@ -105,13 +105,18 @@ class ConcreteViewShadowNode : public ConcreteShadowNode< void initialize() noexcept { auto &props = BaseShadowNode::getConcreteProps(); - BaseShadowNode::orderIndex_ = props.zIndex.value_or(0); - if (props.yogaStyle.display() == YGDisplayNone) { BaseShadowNode::traits_.set(ShadowNodeTraits::Trait::Hidden); } else { BaseShadowNode::traits_.unset(ShadowNodeTraits::Trait::Hidden); } + + // `zIndex` is only defined for non-`static` positioned views. + if (props.yogaStyle.positionType() != YGPositionTypeStatic) { + BaseShadowNode::orderIndex_ = props.zIndex.value_or(0); + } else { + BaseShadowNode::orderIndex_ = 0; + } } }; diff --git a/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp b/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp index 4b582c6ba47bb3..738dec8d6467c7 100644 --- a/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp +++ b/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp @@ -68,7 +68,9 @@ void TouchEventEmitter::onTouchStart(TouchEvent const &event) const { } void TouchEventEmitter::onTouchMove(TouchEvent const &event) const { - dispatchTouchEvent("touchMove", event, EventPriority::AsynchronousBatched); + dispatchUniqueEvent("touchMove", [event](jsi::Runtime &runtime) { + return touchEventPayload(runtime, event); + }); } void TouchEventEmitter::onTouchEnd(TouchEvent const &event) const { diff --git a/ReactCommon/react/renderer/components/view/ViewEventEmitter.cpp b/ReactCommon/react/renderer/components/view/ViewEventEmitter.cpp index fe2a83f5f43d8d..e26f7c143a6dfc 100644 --- a/ReactCommon/react/renderer/components/view/ViewEventEmitter.cpp +++ b/ReactCommon/react/renderer/components/view/ViewEventEmitter.cpp @@ -46,16 +46,31 @@ void ViewEventEmitter::onLayout(const LayoutMetrics &layoutMetrics) const { lastLayoutMetrics_ = layoutMetrics; } - dispatchEvent("layout", [frame = layoutMetrics.frame](jsi::Runtime &runtime) { - auto layout = jsi::Object(runtime); - layout.setProperty(runtime, "x", frame.origin.x); - layout.setProperty(runtime, "y", frame.origin.y); - layout.setProperty(runtime, "width", frame.size.width); - layout.setProperty(runtime, "height", frame.size.height); - auto payload = jsi::Object(runtime); - payload.setProperty(runtime, "layout", std::move(layout)); - return payload; - }); + std::atomic_uint_fast8_t *eventCounter = &eventCounter_; + uint_fast8_t expectedEventCount = ++*eventCounter; + + // dispatchUniqueEvent only drops consecutive onLayout events to the same + // node. We want to drop *any* unprocessed onLayout events when there's a + // newer one. + dispatchEvent( + "layout", + [frame = layoutMetrics.frame, expectedEventCount, eventCounter]( + jsi::Runtime &runtime) { + uint_fast8_t actualEventCount = eventCounter->load(); + if (expectedEventCount != actualEventCount) { + // Drop stale events + return jsi::Value::null(); + } + + auto layout = jsi::Object(runtime); + layout.setProperty(runtime, "x", frame.origin.x); + layout.setProperty(runtime, "y", frame.origin.y); + layout.setProperty(runtime, "width", frame.size.width); + layout.setProperty(runtime, "height", frame.size.height); + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "layout", std::move(layout)); + return jsi::Value(std::move(payload)); + }); } } // namespace react diff --git a/ReactCommon/react/renderer/components/view/ViewEventEmitter.h b/ReactCommon/react/renderer/components/view/ViewEventEmitter.h index 0aa1f5e588eba2..237c9a53b1038b 100644 --- a/ReactCommon/react/renderer/components/view/ViewEventEmitter.h +++ b/ReactCommon/react/renderer/components/view/ViewEventEmitter.h @@ -40,6 +40,7 @@ class ViewEventEmitter : public TouchEventEmitter { private: mutable std::mutex layoutMetricsMutex_; mutable LayoutMetrics lastLayoutMetrics_; + mutable std::atomic_uint_fast8_t eventCounter_{0}; }; } // namespace react diff --git a/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index f4d504f126889a..124babc4c195a5 100644 --- a/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -45,7 +45,7 @@ void ViewShadowNode::initialize() noexcept { viewProps.opacity != 1.0 || viewProps.transform != Transform{} || viewProps.elevation != 0 || (viewProps.zIndex.has_value() && - viewProps.yogaStyle.positionType() == YGPositionTypeAbsolute) || + viewProps.yogaStyle.positionType() != YGPositionTypeStatic) || viewProps.yogaStyle.display() == YGDisplayNone || viewProps.getClipsContentToBounds() || isColorMeaningful(viewProps.shadowColor) || diff --git a/ReactCommon/react/renderer/core/ConcreteState.h b/ReactCommon/react/renderer/core/ConcreteState.h index c57a6518bc61e0..33fa5dac788398 100644 --- a/ReactCommon/react/renderer/core/ConcreteState.h +++ b/ReactCommon/react/renderer/core/ConcreteState.h @@ -97,7 +97,37 @@ class ConcreteState : public State { return std::make_shared( callback(*std::static_pointer_cast(oldData))); }, - failureCallback}; + failureCallback, + false}; + + family->dispatchRawState(std::move(stateUpdate), priority); + } + + /* + * An experimental version of `updateState` function that re-commit the state + * update over and over again until it succeeded. To cancel the state update + * operation, the state update lambda needs to return `nullptr`. + */ + void updateStateWithAutorepeat( + std::function callback, + EventPriority priority = EventPriority::AsynchronousBatched) const { + auto family = family_.lock(); + + if (!family) { + // No more nodes of this family exist anymore, + // updating state is impossible. + return; + } + + auto stateUpdate = StateUpdate{ + family, + [=](StateData::Shared const &oldData) -> StateData::Shared { + assert(oldData); + return callback(*std::static_pointer_cast(oldData)); + }, + nullptr, + true, + }; family->dispatchRawState(std::move(stateUpdate), priority); } diff --git a/ReactCommon/react/renderer/core/EventTarget.cpp b/ReactCommon/react/renderer/core/EventTarget.cpp index c9f418c2d88fa6..01a5a77bd50f48 100644 --- a/ReactCommon/react/renderer/core/EventTarget.cpp +++ b/ReactCommon/react/renderer/core/EventTarget.cpp @@ -14,7 +14,7 @@ using Tag = EventTarget::Tag; EventTarget::EventTarget( jsi::Runtime &runtime, - const jsi::Value &instanceHandle, + jsi::Value const &instanceHandle, Tag tag) : weakInstanceHandle_( jsi::WeakObject(runtime, instanceHandle.asObject(runtime))), diff --git a/ReactCommon/react/renderer/core/EventTarget.h b/ReactCommon/react/renderer/core/EventTarget.h index 8bd7a03a1995ac..92f382a96c4965 100644 --- a/ReactCommon/react/renderer/core/EventTarget.h +++ b/ReactCommon/react/renderer/core/EventTarget.h @@ -35,7 +35,7 @@ class EventTarget { /* * Constructs an EventTarget from a weak instance handler and a tag. */ - EventTarget(jsi::Runtime &runtime, const jsi::Value &instanceHandle, Tag tag); + EventTarget(jsi::Runtime &runtime, jsi::Value const &instanceHandle, Tag tag); /* * Sets the `enabled` flag that allows creating a strong instance handle from diff --git a/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp b/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp index 51b29867787525..3809740c83e72d 100644 --- a/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp +++ b/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp @@ -238,7 +238,17 @@ ShadowNode::Shared LayoutableShadowNode::findNodeAtPoint( auto newPoint = point - transformedFrame.origin - layoutableShadowNode->getContentOriginOffset(); - for (const auto &childShadowNode : node->getChildren()) { + + auto sortedChildren = node->getChildren(); + std::stable_sort( + sortedChildren.begin(), + sortedChildren.end(), + [](auto const &lhs, auto const &rhs) -> bool { + return lhs->getOrderIndex() < rhs->getOrderIndex(); + }); + + for (auto it = sortedChildren.rbegin(); it != sortedChildren.rend(); it++) { + auto const &childShadowNode = *it; auto hitView = findNodeAtPoint(childShadowNode, newPoint); if (hitView) { return hitView; diff --git a/ReactCommon/react/renderer/core/ShadowNode.cpp b/ReactCommon/react/renderer/core/ShadowNode.cpp index 8d93a0f9ec0049..a5839fc820d80b 100644 --- a/ReactCommon/react/renderer/core/ShadowNode.cpp +++ b/ReactCommon/react/renderer/core/ShadowNode.cpp @@ -28,21 +28,6 @@ bool ShadowNode::sameFamily(const ShadowNode &first, const ShadowNode &second) { return first.family_ == second.family_; } -static int computeStateRevision( - State::Shared const &state, - SharedShadowNodeSharedList const &children) { - int fragmentStateRevision = state ? state->getRevision() : 0; - int childrenSum = 0; - - if (children) { - for (auto const &child : *children) { - childrenSum += child->getStateRevision(); - } - } - - return fragmentStateRevision + childrenSum; -} - #pragma mark - Constructors ShadowNode::ShadowNode( @@ -59,7 +44,6 @@ ShadowNode::ShadowNode( : emptySharedShadowNodeSharedList()), state_(fragment.state), orderIndex_(0), - stateRevision_(computeStateRevision(state_, children_)), family_(family), traits_(traits) { assert(props_); @@ -89,7 +73,6 @@ ShadowNode::ShadowNode( fragment.state ? fragment.state : sourceShadowNode.getMostRecentState()), orderIndex_(sourceShadowNode.orderIndex_), - stateRevision_(computeStateRevision(state_, children_)), family_(sourceShadowNode.family_), traits_(sourceShadowNode.traits_) { @@ -184,8 +167,6 @@ void ShadowNode::appendChild(const ShadowNode::Shared &child) { nonConstChildren->push_back(child); child->family_->setParent(family_); - - stateRevision_ += child->getStateRevision(); } void ShadowNode::replaceChild( @@ -194,8 +175,6 @@ void ShadowNode::replaceChild( int suggestedIndex) { ensureUnsealed(); - stateRevision_ += newChild->getStateRevision() - oldChild.getStateRevision(); - cloneChildrenIfShared(); newChild->family_->setParent(family_); @@ -244,10 +223,6 @@ ShadowNodeFamily const &ShadowNode::getFamily() const { return *family_; } -int ShadowNode::getStateRevision() const { - return stateRevision_; -} - ShadowNode::Unshared ShadowNode::cloneTree( ShadowNodeFamily const &shadowNodeFamily, std::function @@ -295,7 +270,6 @@ std::string ShadowNode::getDebugName() const { std::string ShadowNode::getDebugValue() const { return "r" + folly::to(revision_) + "/sr" + - folly::to(stateRevision_) + "/s" + folly::to(state_ ? state_->getRevision() : 0) + (getSealed() ? "/sealed" : ""); } diff --git a/ReactCommon/react/renderer/core/ShadowNode.h b/ReactCommon/react/renderer/core/ShadowNode.h index ce83ccf1e5bdbf..30f5774b7d5f2f 100644 --- a/ReactCommon/react/renderer/core/ShadowNode.h +++ b/ReactCommon/react/renderer/core/ShadowNode.h @@ -203,13 +203,6 @@ class ShadowNode : public Sealable, public DebugStringConvertible { private: friend ShadowNodeFamily; - /** - * This number is deterministically, statelessly recomputable . It tells us - * the version of the state of the entire subtree, including this component - * and all descendants. - */ - int stateRevision_; - /* * Clones the list of children (and creates a new `shared_ptr` to it) if * `childrenAreShared_` flag is `true`. diff --git a/ReactCommon/react/renderer/core/StateUpdate.h b/ReactCommon/react/renderer/core/StateUpdate.h index a77abb90dc6d5d..7931f5e080066f 100644 --- a/ReactCommon/react/renderer/core/StateUpdate.h +++ b/ReactCommon/react/renderer/core/StateUpdate.h @@ -26,6 +26,7 @@ class StateUpdate { SharedShadowNodeFamily family; Callback callback; FailureCallback failureCallback; + bool autorepeat; }; } // namespace react diff --git a/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp b/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp index 79787b4bf9bfef..37f1c4106db603 100644 --- a/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp +++ b/ReactCommon/react/renderer/core/tests/FindNodeAtPointTest.cpp @@ -6,134 +6,224 @@ */ #include +#include +#include + #include "TestComponent.h" using namespace facebook::react; -/* - *┌─────────────────────────┐ - *│nodeA_ │ - *│ │ - *│ │ - *│ │ - *│ │ - *│ │ - *│ │ - *│ ┌────────────────┐ │ - *│ │nodeAA_ │ │ - *│ │ │ │ - *│ │ ┌───────┐ │ │ - *│ │ │nodeAA_│ │ │ - *│ │ │ │ │ │ - *│ │ └───────┘ │ │ - *│ └────────────────┘ │ - *└─────────────────────────┘ - */ -class FindNodeAtPointTest : public ::testing::Test { - protected: - FindNodeAtPointTest() - : eventDispatcher_(std::shared_ptr()), - componentDescriptor_(TestComponentDescriptor({eventDispatcher_})) { - auto traits = TestShadowNode::BaseTraits(); - - auto familyA = std::make_shared( - ShadowNodeFamilyFragment{ - /* .tag = */ 9, - /* .surfaceId = */ 1, - /* .eventEmitter = */ nullptr, - }, - eventDispatcher_, - componentDescriptor_); - - nodeA_ = std::make_shared( - ShadowNodeFragment{ - /* .props = */ std::make_shared(), - /* .children = */ ShadowNode::emptySharedShadowNodeSharedList(), - }, - familyA, - traits); - - auto familyAA = std::make_shared( - ShadowNodeFamilyFragment{ - /* .tag = */ 10, - /* .surfaceId = */ 1, - /* .eventEmitter = */ nullptr, - }, - eventDispatcher_, - componentDescriptor_); - - nodeAA_ = std::make_shared( - ShadowNodeFragment{ - /* .props = */ std::make_shared(), - /* .children = */ ShadowNode::emptySharedShadowNodeSharedList(), - }, - familyAA, - traits); - - auto familyAAA = std::make_shared( - ShadowNodeFamilyFragment{ - /* .tag = */ 11, - /* .surfaceId = */ 1, - /* .eventEmitter = */ nullptr, - }, - eventDispatcher_, - componentDescriptor_); - - nodeAAA_ = std::make_shared( - ShadowNodeFragment{ - /* .props = */ std::make_shared(), - /* .children = */ ShadowNode::emptySharedShadowNodeSharedList(), - }, - familyAAA, - traits); - - nodeA_->appendChild(nodeAA_); - nodeAA_->appendChild(nodeAAA_); - - auto layoutMetrics = EmptyLayoutMetrics; - - layoutMetrics.frame = facebook::react::Rect{ - facebook::react::Point{0, 0}, facebook::react::Size{1000, 1000}}; - nodeA_->setLayoutMetrics(layoutMetrics); - - layoutMetrics.frame = facebook::react::Rect{ - facebook::react::Point{100, 100}, facebook::react::Size{100, 100}}; - nodeAA_->setLayoutMetrics(layoutMetrics); - - layoutMetrics.frame = facebook::react::Rect{facebook::react::Point{10, 10}, - facebook::react::Size{10, 10}}; - nodeAAA_->setLayoutMetrics(layoutMetrics); - } - - std::shared_ptr eventDispatcher_; - std::shared_ptr nodeA_; - std::shared_ptr nodeAA_; - std::shared_ptr nodeAAA_; - TestComponentDescriptor componentDescriptor_; -}; - -TEST_F(FindNodeAtPointTest, withoutTransform) { +TEST(FindNodeAtPointTest, withoutTransform) { + auto builder = simpleComponentBuilder(); + + // clang-format off + auto element = + Element() + .tag(1) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {1000, 1000}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {100, 100}; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(3) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {10, 10}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }) + }); + + auto parentShadowNode = builder.build(element); + EXPECT_EQ( - LayoutableShadowNode::findNodeAtPoint(nodeA_, {115, 115}), nodeAAA_); - EXPECT_EQ(LayoutableShadowNode::findNodeAtPoint(nodeA_, {105, 105}), nodeAA_); - EXPECT_EQ(LayoutableShadowNode::findNodeAtPoint(nodeA_, {900, 900}), nodeA_); + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {115, 115})->getTag(), 3); + EXPECT_EQ(LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {105, 105})->getTag(), 2); + EXPECT_EQ(LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {900, 900})->getTag(), 1); EXPECT_EQ( - LayoutableShadowNode::findNodeAtPoint(nodeA_, {1001, 1001}), nullptr); + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {1001, 1001}), nullptr); } -TEST_F(FindNodeAtPointTest, viewIsTranslated) { - nodeA_->_contentOriginOffset = {-100, -100}; +TEST(FindNodeAtPointTest, viewIsTranslated) { + auto builder = simpleComponentBuilder(); + + // clang-format off + auto element = + Element() + .tag(1) + .finalize([](ScrollViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {1000, 1000}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .stateData([](ScrollViewState &data) { + data.contentOffset = {100, 100}; + }) + .children({ + Element() + .tag(2) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {100, 100}; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(3) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {10, 10}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }) + }); + + auto parentShadowNode = builder.build(element); EXPECT_EQ( - LayoutableShadowNode::findNodeAtPoint(nodeA_, {15, 15})->getTag(), - nodeAAA_->getTag()); - EXPECT_EQ(LayoutableShadowNode::findNodeAtPoint(nodeA_, {5, 5}), nodeAA_); + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {15, 15})->getTag(), + 3); + EXPECT_EQ(LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {5, 5})->getTag(), 2); } -TEST_F(FindNodeAtPointTest, viewIsScaled) { - nodeAAA_->_transform = Transform::Identity() * Transform::Scale(0.5, 0.5, 0); +TEST(FindNodeAtPointTest, viewIsScaled) { + auto builder = simpleComponentBuilder(); + + // clang-format off + auto element = + Element() + .tag(1) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {1000, 1000}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {100, 100}; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(3) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->transform = Transform::Scale(0.5, 0.5, 0); + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {10, 10}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }) + }); + + auto parentShadowNode = builder.build(element); + + EXPECT_EQ( + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {119, 119})->getTag(), + 2); +} +TEST(FindNodeAtPointTest, overlappingViews) { + auto builder = simpleComponentBuilder(); + + // clang-format off + auto element = + Element() + .tag(1) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {25, 25}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }), + Element() + .tag(3) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {50, 50}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }); + + auto parentShadowNode = builder.build(element); + EXPECT_EQ( - LayoutableShadowNode::findNodeAtPoint(nodeA_, {119, 119})->getTag(), - nodeAA_->getTag()); + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {50, 50})->getTag(), 3); } + +TEST(FindNodeAtPointTest, overlappingViewsWithZIndex) { + auto builder = simpleComponentBuilder(); + + // clang-format off + auto element = + Element() + .tag(1) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->zIndex = 1; + auto &yogaStyle = sharedProps->yogaStyle; + yogaStyle.positionType() = YGPositionTypeAbsolute; + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {25, 25}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }), + Element() + .tag(3) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {50, 50}; + layoutMetrics.frame.size = {50, 50}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }); + + auto parentShadowNode = builder.build(element); + + EXPECT_EQ( + LayoutableShadowNode::findNodeAtPoint(parentShadowNode, {50, 50})->getTag(), 2); +} + + diff --git a/ReactCommon/react/renderer/core/tests/TestComponent.h b/ReactCommon/react/renderer/core/tests/TestComponent.h index 936fe0167cbe0a..9943d78a031b49 100644 --- a/ReactCommon/react/renderer/core/tests/TestComponent.h +++ b/ReactCommon/react/renderer/core/tests/TestComponent.h @@ -33,7 +33,7 @@ static const char TestComponentName[] = "Test"; class TestProps : public ViewProps { public: - using ViewProps::ViewProps; + TestProps() = default; TestProps(const TestProps &sourceProps, const RawProps &rawProps) : ViewProps(sourceProps, rawProps) {} diff --git a/ReactCommon/react/renderer/element/Element.h b/ReactCommon/react/renderer/element/Element.h index 44ff785ccdb917..b6e36a789effd4 100644 --- a/ReactCommon/react/renderer/element/Element.h +++ b/ReactCommon/react/renderer/element/Element.h @@ -93,7 +93,8 @@ class Element final { * Sets `state` using callback. */ Element &stateData(std::function callback) { - fragment_.stateCallback = [&]() -> StateData::Shared { + fragment_.stateCallback = [callback = + std::move(callback)]() -> StateData::Shared { auto stateData = ConcreteStateData(); callback(stateData); return std::make_shared(stateData); diff --git a/ReactCommon/react/renderer/graphics/BUCK b/ReactCommon/react/renderer/graphics/BUCK index 9b7fd05c2d0b46..87064ce89c0436 100644 --- a/ReactCommon/react/renderer/graphics/BUCK +++ b/ReactCommon/react/renderer/graphics/BUCK @@ -7,6 +7,7 @@ load( "fb_xplat_cxx_test", "get_apple_compiler_flags", "get_apple_inspector_flags", + "react_native_xplat_target", "rn_xplat_cxx_library", "subdir_glob", ) @@ -95,6 +96,7 @@ rn_xplat_cxx_library( tests = [":tests"], visibility = ["PUBLIC"], deps = [ + react_native_xplat_target("better:better"), "//third-party/glog:glog", "//xplat/fbsystrace:fbsystrace", "//xplat/folly:headers_only", diff --git a/ReactCommon/react/renderer/graphics/Rect.h b/ReactCommon/react/renderer/graphics/Rect.h index 20efdfddf9ae1b..78b29c4a3b76e3 100644 --- a/ReactCommon/react/renderer/graphics/Rect.h +++ b/ReactCommon/react/renderer/graphics/Rect.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include diff --git a/ReactCommon/react/renderer/graphics/platform/ios/Color.cpp b/ReactCommon/react/renderer/graphics/platform/ios/Color.cpp index 8cbdeee7fdc60b..c1023384cac7bd 100644 --- a/ReactCommon/react/renderer/graphics/platform/ios/Color.cpp +++ b/ReactCommon/react/renderer/graphics/platform/ios/Color.cpp @@ -12,28 +12,27 @@ namespace facebook { namespace react { SharedColor colorFromComponents(ColorComponents components) { - const CGFloat componentsArray[] = { - components.red, components.green, components.blue, components.alpha}; - - auto color = CGColorCreate(CGColorSpaceCreateDeviceRGB(), componentsArray); - - return SharedColor(color, CGColorRelease); + float ratio = 255.9999; + return SharedColor( + ((int)(components.alpha * ratio) & 0xff) << 24 | + ((int)(components.red * ratio) & 0xff) << 16 | + ((int)(components.green * ratio) & 0xff) << 8 | + ((int)(components.blue * ratio) & 0xff)); } -ColorComponents colorComponentsFromColor(SharedColor color) { - if (!color) { +ColorComponents colorComponentsFromColor(SharedColor sharedColor) { + if (!sharedColor) { // Empty color object can be considered as `clear` (black, fully // transparent) color. return ColorComponents{0, 0, 0, 0}; } - auto numberOfComponents __unused = CGColorGetNumberOfComponents(color.get()); - assert(numberOfComponents == 4); - const CGFloat *components = CGColorGetComponents(color.get()); - return ColorComponents{(float)components[0], - (float)components[1], - (float)components[2], - (float)components[3]}; + float ratio = 256; + Color color = *sharedColor; + return ColorComponents{(float)((color >> 16) & 0xff) / ratio, + (float)((color >> 8) & 0xff) / ratio, + (float)((color >> 0) & 0xff) / ratio, + (float)((color >> 24) & 0xff) / ratio}; } SharedColor clearColor() { diff --git a/ReactCommon/react/renderer/graphics/platform/ios/Color.h b/ReactCommon/react/renderer/graphics/platform/ios/Color.h index abaf98f4245e56..0b8ab406b40f50 100644 --- a/ReactCommon/react/renderer/graphics/platform/ios/Color.h +++ b/ReactCommon/react/renderer/graphics/platform/ios/Color.h @@ -7,17 +7,17 @@ #pragma once -#include +#include -#include #include #include namespace facebook { namespace react { -using Color = CGColor; -using SharedColor = std::shared_ptr; +using Color = int32_t; + +using SharedColor = better::optional; SharedColor colorFromComponents(ColorComponents components); ColorComponents colorComponentsFromColor(SharedColor color); diff --git a/ReactCommon/react/renderer/imagemanager/BUCK b/ReactCommon/react/renderer/imagemanager/BUCK index 9ea987914490db..a9797d2884b068 100644 --- a/ReactCommon/react/renderer/imagemanager/BUCK +++ b/ReactCommon/react/renderer/imagemanager/BUCK @@ -103,10 +103,7 @@ rn_xplat_cxx_library( tests = [":tests"], visibility = ["PUBLIC"], deps = [ - "//third-party/glog:glog", - "//xplat/fbsystrace:fbsystrace", "//xplat/folly:headers_only", - "//xplat/folly:memory", "//xplat/folly:molly", YOGA_CXX_TARGET, react_native_xplat_target("react/renderer/core:core"), diff --git a/ReactCommon/react/renderer/imagemanager/ImageRequest.h b/ReactCommon/react/renderer/imagemanager/ImageRequest.h index 8c5dc8d5fe814c..a77c1f76c794dd 100644 --- a/ReactCommon/react/renderer/imagemanager/ImageRequest.h +++ b/ReactCommon/react/renderer/imagemanager/ImageRequest.h @@ -11,6 +11,7 @@ #include #include #include +#include #include namespace facebook { @@ -30,6 +31,7 @@ class ImageRequest final { */ ImageRequest( const ImageSource &imageSource, + std::shared_ptr telemetry, std::shared_ptr instrumentation); /* @@ -49,6 +51,13 @@ class ImageRequest final { */ void setCancelationFunction(std::function cancelationFunction); + /* + * Returns the Image Source associated with the request. + */ + const ImageSource getImageSource() const { + return imageSource_; + } + /* * Returns stored observer coordinator as a shared pointer. * Retain this *or* `ImageRequest` to ensure a correct lifetime of the object. @@ -63,6 +72,12 @@ class ImageRequest final { */ const ImageResponseObserverCoordinator &getObserverCoordinator() const; + /* + * Returns stored image telemetry object as a shared pointer. + * Retain this *or* `ImageRequest` to ensure a correct lifetime of the object. + */ + const std::shared_ptr &getSharedTelemetry() const; + /* * Returns stored image instrumentation object as a shared pointer. * Retain this *or* `ImageRequest` to ensure a correct lifetime of the object. @@ -83,6 +98,11 @@ class ImageRequest final { */ ImageSource imageSource_; + /* + * Image telemetry associated with the request. + */ + std::shared_ptr telemetry_{}; + /* * Event coordinator associated with the reqest. */ diff --git a/ReactCommon/react/renderer/imagemanager/ImageResponse.cpp b/ReactCommon/react/renderer/imagemanager/ImageResponse.cpp index ab874617d7d492..361e5fe7f88541 100644 --- a/ReactCommon/react/renderer/imagemanager/ImageResponse.cpp +++ b/ReactCommon/react/renderer/imagemanager/ImageResponse.cpp @@ -10,12 +10,18 @@ namespace facebook { namespace react { -ImageResponse::ImageResponse(const std::shared_ptr &image) - : image_(image) {} +ImageResponse::ImageResponse( + const std::shared_ptr &image, + const std::shared_ptr &metadata) + : image_(image), metadata_(metadata) {} std::shared_ptr ImageResponse::getImage() const { return image_; } +std::shared_ptr ImageResponse::getMetadata() const { + return metadata_; +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/imagemanager/ImageResponse.h b/ReactCommon/react/renderer/imagemanager/ImageResponse.h index 52efffd9760369..23cc49f65bf680 100644 --- a/ReactCommon/react/renderer/imagemanager/ImageResponse.h +++ b/ReactCommon/react/renderer/imagemanager/ImageResponse.h @@ -23,12 +23,18 @@ class ImageResponse final { Failed, }; - ImageResponse(const std::shared_ptr &image); + ImageResponse( + const std::shared_ptr &image, + const std::shared_ptr &metadata); std::shared_ptr getImage() const; + std::shared_ptr getMetadata() const; + private: std::shared_ptr image_{}; + + std::shared_ptr metadata_{}; }; } // namespace react diff --git a/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.cpp b/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.cpp index d50469818fcdeb..3f3fd6a5ffbbb4 100644 --- a/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.cpp +++ b/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.cpp @@ -24,8 +24,9 @@ void ImageResponseObserverCoordinator::addObserver( } case ImageResponse::Status::Completed: { auto imageData = imageData_; + auto imageMetadata = imageMetadata_; mutex_.unlock(); - observer.didReceiveImage(ImageResponse{imageData}); + observer.didReceiveImage(ImageResponse{imageData, imageMetadata}); break; } case ImageResponse::Status::Failed: { @@ -63,6 +64,7 @@ void ImageResponseObserverCoordinator::nativeImageResponseComplete( ImageResponse const &imageResponse) const { mutex_.lock(); imageData_ = imageResponse.getImage(); + imageMetadata_ = imageResponse.getMetadata(); assert(status_ == ImageResponse::Status::Loading); status_ = ImageResponse::Status::Completed; auto observers = observers_; diff --git a/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.h b/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.h index f28e7e5805c37c..6b1f8301588197 100644 --- a/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.h +++ b/ReactCommon/react/renderer/imagemanager/ImageResponseObserverCoordinator.h @@ -73,6 +73,12 @@ class ImageResponseObserverCoordinator { */ mutable std::shared_ptr imageData_; + /* + * Cache image metadata. + * Mutable: protected by mutex_. + */ + mutable std::shared_ptr imageMetadata_; + /* * Observer and data mutex. */ diff --git a/ReactCommon/react/renderer/imagemanager/ImageTelemetry.cpp b/ReactCommon/react/renderer/imagemanager/ImageTelemetry.cpp new file mode 100644 index 00000000000000..7e754307feca7c --- /dev/null +++ b/ReactCommon/react/renderer/imagemanager/ImageTelemetry.cpp @@ -0,0 +1,36 @@ +/* + * 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. + */ + +#include "ImageTelemetry.h" + +namespace facebook { +namespace react { + +void ImageTelemetry::willRequestUrl() { + assert(willRequestUrlTime_ == kTelemetryUndefinedTimePoint); + willRequestUrlTime_ = telemetryTimePointNow(); +} + +SurfaceId ImageTelemetry::getSurfaceId() const { + return surfaceId_; +} + +std::string ImageTelemetry::getLoaderModuleName() const { + return loaderModuleName_; +} + +void ImageTelemetry::setLoaderModuleName(std::string const &loaderModuleName) { + loaderModuleName_ = loaderModuleName; +} + +TelemetryTimePoint ImageTelemetry::getWillRequestUrlTime() const { + assert(willRequestUrlTime_ != kTelemetryUndefinedTimePoint); + return willRequestUrlTime_; +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/imagemanager/ImageTelemetry.h b/ReactCommon/react/renderer/imagemanager/ImageTelemetry.h new file mode 100644 index 00000000000000..312d19b19e9862 --- /dev/null +++ b/ReactCommon/react/renderer/imagemanager/ImageTelemetry.h @@ -0,0 +1,45 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +/* + * Represents telemetry data associated with a image request + */ +class ImageTelemetry final { + public: + ImageTelemetry(SurfaceId const surfaceId) : surfaceId_(surfaceId) {} + + /* + * Signaling + */ + void willRequestUrl(); + + /* + * Reading + */ + TelemetryTimePoint getWillRequestUrlTime() const; + + SurfaceId getSurfaceId() const; + std::string getLoaderModuleName() const; + void setLoaderModuleName(std::string const &loaderModuleName); + + private: + TelemetryTimePoint willRequestUrlTime_{kTelemetryUndefinedTimePoint}; + + const SurfaceId surfaceId_; + std::string loaderModuleName_{""}; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/imagemanager/platform/cxx/react/renderer/imagemanager/ImageManager.cpp b/ReactCommon/react/renderer/imagemanager/platform/cxx/react/renderer/imagemanager/ImageManager.cpp index 245c3b5e7a783b..c1d4d1ac63e7b4 100644 --- a/ReactCommon/react/renderer/imagemanager/platform/cxx/react/renderer/imagemanager/ImageManager.cpp +++ b/ReactCommon/react/renderer/imagemanager/platform/cxx/react/renderer/imagemanager/ImageManager.cpp @@ -24,7 +24,7 @@ ImageRequest ImageManager::requestImage( const ImageSource &imageSource, SurfaceId surfaceId) const { // Not implemented. - return ImageRequest(imageSource, nullptr); + return ImageRequest(imageSource, nullptr, nullptr); } } // namespace react diff --git a/ReactCommon/react/renderer/imagemanager/platform/cxx/react/renderer/imagemanager/ImageRequest.cpp b/ReactCommon/react/renderer/imagemanager/platform/cxx/react/renderer/imagemanager/ImageRequest.cpp index 35475d1083149a..9cdbc7fdd0dda0 100644 --- a/ReactCommon/react/renderer/imagemanager/platform/cxx/react/renderer/imagemanager/ImageRequest.cpp +++ b/ReactCommon/react/renderer/imagemanager/platform/cxx/react/renderer/imagemanager/ImageRequest.cpp @@ -12,14 +12,19 @@ namespace react { ImageRequest::ImageRequest( const ImageSource &imageSource, + std::shared_ptr telemetry, std::shared_ptr instrumentation) - : imageSource_(imageSource), instrumentation_(instrumentation) { + : imageSource_(imageSource), + telemetry_(telemetry), + instrumentation_(instrumentation) { // Not implemented. } ImageRequest::ImageRequest(ImageRequest &&other) noexcept : imageSource_(std::move(other.imageSource_)), - coordinator_(std::move(other.coordinator_)) { + telemetry_(std::move(other.telemetry_)), + coordinator_(std::move(other.coordinator_)), + instrumentation_(std::move(other.instrumentation_)) { // Not implemented. } diff --git a/ReactCommon/react/renderer/imagemanager/platform/ios/ImageRequest.cpp b/ReactCommon/react/renderer/imagemanager/platform/ios/ImageRequest.cpp index 96911824679a85..216e1ffdfd986d 100644 --- a/ReactCommon/react/renderer/imagemanager/platform/ios/ImageRequest.cpp +++ b/ReactCommon/react/renderer/imagemanager/platform/ios/ImageRequest.cpp @@ -12,18 +12,23 @@ namespace react { ImageRequest::ImageRequest( const ImageSource &imageSource, + std::shared_ptr telemetry, std::shared_ptr instrumentation) - : imageSource_(imageSource), instrumentation_(instrumentation) { + : imageSource_(imageSource), + telemetry_(telemetry), + instrumentation_(instrumentation) { coordinator_ = std::make_shared(); } ImageRequest::ImageRequest(ImageRequest &&other) noexcept : imageSource_(std::move(other.imageSource_)), + telemetry_(std::move(other.telemetry_)), coordinator_(std::move(other.coordinator_)), instrumentation_(std::move(other.instrumentation_)) { other.moved_ = true; other.coordinator_ = nullptr; other.cancelRequest_ = nullptr; + other.telemetry_ = nullptr; other.instrumentation_ = nullptr; } @@ -38,6 +43,11 @@ void ImageRequest::setCancelationFunction( cancelRequest_ = cancelationFunction; } +const std::shared_ptr &ImageRequest::getSharedTelemetry() + const { + return telemetry_; +} + const ImageResponseObserverCoordinator &ImageRequest::getObserverCoordinator() const { return *coordinator_; diff --git a/ReactCommon/react/renderer/imagemanager/platform/ios/RCTImageManager.mm b/ReactCommon/react/renderer/imagemanager/platform/ios/RCTImageManager.mm index 9ad64c68fd28e2..b719be1158fbb8 100644 --- a/ReactCommon/react/renderer/imagemanager/platform/ios/RCTImageManager.mm +++ b/ReactCommon/react/renderer/imagemanager/platform/ios/RCTImageManager.mm @@ -42,7 +42,9 @@ - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfa SystraceSection s("RCTImageManager::requestImage"); auto imageInstrumentation = std::make_shared(_imageLoader); - auto imageRequest = ImageRequest(imageSource, imageInstrumentation); + auto telemetry = std::make_shared(surfaceId); + telemetry->willRequestUrl(); + auto imageRequest = ImageRequest(imageSource, telemetry, imageInstrumentation); auto weakObserverCoordinator = (std::weak_ptr)imageRequest.getSharedObserverCoordinator(); @@ -62,14 +64,21 @@ - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfa dispatch_async(_backgroundSerialQueue, ^{ NSURLRequest *request = NSURLRequestFromImageSource(imageSource); - auto completionBlock = ^(NSError *error, UIImage *image) { + BOOL hasModuleName = [self->_imageLoader respondsToSelector:@selector(loaderModuleNameForRequestUrl:)]; + NSString *moduleName = hasModuleName ? [self->_imageLoader loaderModuleNameForRequestUrl:request.URL] : nil; + std::string moduleCString = + std::string([moduleName UTF8String], [moduleName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); + telemetry->setLoaderModuleName(moduleCString); + + auto completionBlock = ^(NSError *error, UIImage *image, id metadata) { auto observerCoordinator = weakObserverCoordinator.lock(); if (!observerCoordinator) { return; } if (image && !error) { - observerCoordinator->nativeImageResponseComplete(ImageResponse(wrapManagedObject(image))); + auto wrappedMetadata = metadata ? wrapManagedObject(metadata) : nullptr; + observerCoordinator->nativeImageResponseComplete(ImageResponse(wrapManagedObject(image), wrappedMetadata)); } else { observerCoordinator->nativeImageResponseFailed(); } diff --git a/ReactCommon/react/renderer/imagemanager/platform/ios/RCTSyncImageManager.mm b/ReactCommon/react/renderer/imagemanager/platform/ios/RCTSyncImageManager.mm index 6f8c54d492c72b..df89da51cecf31 100644 --- a/ReactCommon/react/renderer/imagemanager/platform/ios/RCTSyncImageManager.mm +++ b/ReactCommon/react/renderer/imagemanager/platform/ios/RCTSyncImageManager.mm @@ -36,7 +36,8 @@ - (instancetype)initWithImageLoader:(id)i - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfaceId { - auto imageRequest = ImageRequest(imageSource, nullptr); + auto telemetry = std::make_shared(surfaceId); + auto imageRequest = ImageRequest(imageSource, telemetry, nullptr); auto weakObserverCoordinator = (std::weak_ptr)imageRequest.getSharedObserverCoordinator(); @@ -49,14 +50,15 @@ - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfa NSURLRequest *request = NSURLRequestFromImageSource(imageSource); - auto completionBlock = ^(NSError *error, UIImage *image) { + auto completionBlock = ^(NSError *error, UIImage *image, id metadata) { auto observerCoordinator = weakObserverCoordinator.lock(); if (!observerCoordinator) { return; } if (image && !error) { - observerCoordinator->nativeImageResponseComplete(ImageResponse(wrapManagedObject(image))); + auto wrappedMetadata = metadata ? wrapManagedObject(metadata) : nullptr; + observerCoordinator->nativeImageResponseComplete(ImageResponse(wrapManagedObject(image), wrappedMetadata)); } else { observerCoordinator->nativeImageResponseFailed(); } diff --git a/ReactCommon/react/renderer/mounting/Differentiator.cpp b/ReactCommon/react/renderer/mounting/Differentiator.cpp index 300c38059f8a64..d7fee08924385b 100644 --- a/ReactCommon/react/renderer/mounting/Differentiator.cpp +++ b/ReactCommon/react/renderer/mounting/Differentiator.cpp @@ -1076,11 +1076,7 @@ static void calculateShadowViewMutationsV2( for (auto &oldFlattenedNode : oldFlattenedNodes) { auto unvisitedOldChildPairIt = unvisitedOldChildPairs.find( oldFlattenedNode.shadowView.tag); - if (unvisitedOldChildPairIt != unvisitedOldChildPairs.end()) { - // Node unvisited - delete it entirely - deleteMutations.push_back(ShadowViewMutation::DeleteMutation( - oldFlattenedNode.shadowView)); - } else { + if (unvisitedOldChildPairIt == unvisitedOldChildPairs.end()) { // Node was visited - make sure to remove it from // "newRemainingPairs" map auto newRemainingIt = @@ -1202,11 +1198,7 @@ static void calculateShadowViewMutationsV2( for (auto &oldFlattenedNode : oldFlattenedNodes) { auto unvisitedOldChildPairIt = unvisitedOldChildPairs.find( oldFlattenedNode.shadowView.tag); - if (unvisitedOldChildPairIt != unvisitedOldChildPairs.end()) { - // Node unvisited - delete it entirely - deleteMutations.push_back(ShadowViewMutation::DeleteMutation( - oldFlattenedNode.shadowView)); - } else { + if (unvisitedOldChildPairIt == unvisitedOldChildPairs.end()) { // Node was visited - make sure to remove it from // "newRemainingPairs" map auto newRemainingIt = diff --git a/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp b/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp index 7ad2b08e5e5700..2c9af008a80447 100644 --- a/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp +++ b/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp @@ -23,13 +23,13 @@ MountingCoordinator::MountingCoordinator( ShadowTreeRevision baseRevision, std::weak_ptr delegate, bool enableReparentingDetection) - : surfaceId_(baseRevision.getRootShadowNode().getSurfaceId()), + : surfaceId_(baseRevision.rootShadowNode->getSurfaceId()), baseRevision_(baseRevision), mountingOverrideDelegate_(delegate), telemetryController_(*this), enableReparentingDetection_(enableReparentingDetection) { #ifdef RN_SHADOW_TREE_INTROSPECTION - stubViewTree_ = stubViewTreeFromShadowNode(baseRevision_.getRootShadowNode()); + stubViewTree_ = stubViewTreeFromShadowNode(*baseRevision_.rootShadowNode); #endif } @@ -37,17 +37,15 @@ SurfaceId MountingCoordinator::getSurfaceId() const { return surfaceId_; } -void MountingCoordinator::push(ShadowTreeRevision &&revision) const { +void MountingCoordinator::push(ShadowTreeRevision const &revision) const { { std::lock_guard lock(mutex_); assert( - !lastRevision_.has_value() || - revision.getNumber() != lastRevision_->getNumber()); + !lastRevision_.has_value() || revision.number != lastRevision_->number); - if (!lastRevision_.has_value() || - lastRevision_->getNumber() < revision.getNumber()) { - lastRevision_ = std::move(revision); + if (!lastRevision_.has_value() || lastRevision_->number < revision.number) { + lastRevision_ = revision; } } @@ -60,7 +58,7 @@ void MountingCoordinator::revoke() const { // 1. We need to stop retaining `ShadowNode`s to not prolong their lifetime // to prevent them from overliving `ComponentDescriptor`s. // 2. A possible call to `pullTransaction()` should return empty optional. - baseRevision_.rootShadowNode_.reset(); + baseRevision_.rootShadowNode.reset(); lastRevision_.reset(); } @@ -90,18 +88,17 @@ better::optional MountingCoordinator::pullTransaction() if (lastRevision_.has_value()) { number_++; - auto telemetry = lastRevision_->getTelemetry(); + auto telemetry = lastRevision_->telemetry; telemetry.willDiff(); auto mutations = calculateShadowViewMutations( - baseRevision_.getRootShadowNode(), lastRevision_->getRootShadowNode()); + *baseRevision_.rootShadowNode, + *lastRevision_->rootShadowNode, + enableReparentingDetection_); telemetry.didDiff(); - baseRevision_ = std::move(*lastRevision_); - lastRevision_.reset(); - transaction = MountingTransaction{ surfaceId_, number_, std::move(mutations), telemetry}; } @@ -119,10 +116,13 @@ better::optional MountingCoordinator::pullTransaction() mutations = transaction->getMutations(); telemetry = transaction->getTelemetry(); } else { + number_++; telemetry.willLayout(); telemetry.didLayout(); telemetry.willCommit(); telemetry.didCommit(); + telemetry.willDiff(); + telemetry.didDiff(); } transaction = mountingOverrideDelegate->pullTransaction( @@ -141,15 +141,17 @@ better::optional MountingCoordinator::pullTransaction() // If the transaction was overridden, we don't have a model of the shadow // tree therefore we cannot validate the validity of the mutation // instructions. - if (!shouldOverridePullTransaction) { - auto line = std::string{}; - + if (!shouldOverridePullTransaction && lastRevision_.has_value()) { auto stubViewTree = - stubViewTreeFromShadowNode(baseRevision_.getRootShadowNode()); + stubViewTreeFromShadowNode(*lastRevision_->rootShadowNode); + + bool treesEqual = stubViewTree_ == stubViewTree; - if (stubViewTree_ != stubViewTree) { + if (!treesEqual) { + // Display debug info + auto line = std::string{}; std::stringstream ssOldTree( - baseRevision_.getRootShadowNode().getDebugDescription()); + baseRevision_.rootShadowNode->getDebugDescription()); while (std::getline(ssOldTree, line, '\n')) { LOG(ERROR) << "Old tree:" << line; } @@ -160,19 +162,21 @@ better::optional MountingCoordinator::pullTransaction() } std::stringstream ssNewTree( - lastRevision_->getRootShadowNode().getDebugDescription()); + lastRevision_->rootShadowNode->getDebugDescription()); while (std::getline(ssNewTree, line, '\n')) { LOG(ERROR) << "New tree:" << line; } } - assert( - (stubViewTree_ == stubViewTree) && - "Incorrect set of mutations detected."); + assert((treesEqual) && "Incorrect set of mutations detected."); } } #endif + if (lastRevision_.has_value()) { + baseRevision_ = std::move(*lastRevision_); + lastRevision_.reset(); + } return transaction; } diff --git a/ReactCommon/react/renderer/mounting/MountingCoordinator.h b/ReactCommon/react/renderer/mounting/MountingCoordinator.h index 54bb0a3a63d4a7..4563dc7bd0aa5c 100644 --- a/ReactCommon/react/renderer/mounting/MountingCoordinator.h +++ b/ReactCommon/react/renderer/mounting/MountingCoordinator.h @@ -91,7 +91,7 @@ class MountingCoordinator final { private: friend class ShadowTree; - void push(ShadowTreeRevision &&revision) const; + void push(ShadowTreeRevision const &revision) const; /* * Revokes the last pushed `ShadowTreeRevision`. diff --git a/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/ReactCommon/react/renderer/mounting/ShadowTree.cpp index 6e02e4aeeff81b..d7d3a8461542bd 100644 --- a/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -17,11 +17,12 @@ #include #include "ShadowTreeDelegate.h" -#include "TreeStateReconciliation.h" namespace facebook { namespace react { +using CommitStatus = ShadowTree::CommitStatus; + /* * Generates (possibly) a new tree where all nodes with non-obsolete `State` * objects. If all `State` objects in the tree are not obsolete for the moment @@ -236,17 +237,19 @@ ShadowTree::ShadowTree( auto family = rootComponentDescriptor.createFamily( ShadowNodeFamilyFragment{surfaceId, surfaceId, noopEventEmitter}, nullptr); - rootShadowNode_ = std::static_pointer_cast( + + auto rootShadowNode = std::static_pointer_cast( rootComponentDescriptor.createShadowNode( ShadowNodeFragment{ /* .props = */ props, }, family)); + currentRevision_ = ShadowTreeRevision{ + rootShadowNode, ShadowTreeRevision::Number{0}, TransactionTelemetry{}}; + mountingCoordinator_ = std::make_shared( - ShadowTreeRevision{rootShadowNode_, 0, {}}, - mountingOverrideDelegate, - enableReparentingDetection); + currentRevision_, mountingOverrideDelegate, enableReparentingDetection); } ShadowTree::~ShadowTree() { @@ -261,7 +264,7 @@ MountingCoordinator::Shared ShadowTree::getMountingCoordinator() const { return mountingCoordinator_; } -void ShadowTree::commit( +CommitStatus ShadowTree::commit( ShadowTreeCommitTransaction transaction, bool enableStateReconciliation) const { SystraceSection s("ShadowTree::commit"); @@ -270,8 +273,10 @@ void ShadowTree::commit( while (true) { attempts++; - if (tryCommit(transaction, enableStateReconciliation)) { - return; + + auto status = tryCommit(transaction, enableStateReconciliation); + if (status != CommitStatus::Failed) { + return status; } // After multiple attempts, we failed to commit the transaction. @@ -280,7 +285,7 @@ void ShadowTree::commit( } } -bool ShadowTree::tryCommit( +CommitStatus ShadowTree::tryCommit( ShadowTreeCommitTransaction transaction, bool enableStateReconciliation) const { SystraceSection s("ShadowTree::tryCommit"); @@ -288,41 +293,31 @@ bool ShadowTree::tryCommit( auto telemetry = TransactionTelemetry{}; telemetry.willCommit(); - RootShadowNode::Shared oldRootShadowNode; + auto oldRevision = ShadowTreeRevision{}; + auto newRevision = ShadowTreeRevision{}; { - // Reading `rootShadowNode_` in shared manner. + // Reading `currentRevision_` in shared manner. std::shared_lock lock(commitMutex_); - oldRootShadowNode = rootShadowNode_; + oldRevision = currentRevision_; } - RootShadowNode::Unshared newRootShadowNode = transaction(oldRootShadowNode); + auto newRootShadowNode = transaction(*oldRevision.rootShadowNode); if (!newRootShadowNode) { - return false; + return CommitStatus::Cancelled; } if (enableStateReconciliation) { - if (enableNewStateReconciliation_) { - auto updatedNewRootShadowNode = - progressState(*newRootShadowNode, *oldRootShadowNode); - if (updatedNewRootShadowNode) { - newRootShadowNode = - std::static_pointer_cast(updatedNewRootShadowNode); - } - } else { - // Compare state revisions of old and new root - // Children of the root node may be mutated in-place - UnsharedShadowNode reconciledNode = - reconcileStateWithTree(newRootShadowNode.get(), oldRootShadowNode); - if (reconciledNode != nullptr) { - newRootShadowNode = std::make_shared( - *reconciledNode, ShadowNodeFragment{}); - } + auto updatedNewRootShadowNode = + progressState(*newRootShadowNode, *oldRevision.rootShadowNode); + if (updatedNewRootShadowNode) { + newRootShadowNode = + std::static_pointer_cast(updatedNewRootShadowNode); } } - // Layout nodes + // Layout nodes. std::vector affectedLayoutableNodes{}; affectedLayoutableNodes.reserve(1024); @@ -335,48 +330,52 @@ bool ShadowTree::tryCommit( // Seal the shadow node so it can no longer be mutated newRootShadowNode->sealRecursive(); - auto revisionNumber = ShadowTreeRevision::Number{}; - { - // Updating `rootShadowNode_` in unique manner if it hasn't changed. + // Updating `currentRevision_` in unique manner if it hasn't changed. std::unique_lock lock(commitMutex_); - if (rootShadowNode_ != oldRootShadowNode) { - return false; + if (currentRevision_.number != oldRevision.number) { + return CommitStatus::Failed; } - rootShadowNode_ = newRootShadowNode; + auto newRevisionNumber = oldRevision.number + 1; { std::lock_guard dispatchLock(EventEmitter::DispatchMutex()); updateMountedFlag( - oldRootShadowNode->getChildren(), newRootShadowNode->getChildren()); + currentRevision_.rootShadowNode->getChildren(), + newRootShadowNode->getChildren()); } - revisionNumber_++; - revisionNumber = revisionNumber_; + telemetry.didCommit(); + telemetry.setRevisionNumber(newRevisionNumber); + + newRevision = + ShadowTreeRevision{newRootShadowNode, newRevisionNumber, telemetry}; + + currentRevision_ = newRevision; } emitLayoutEvents(affectedLayoutableNodes); - telemetry.didCommit(); - telemetry.setRevisionNumber(revisionNumber); - - mountingCoordinator_->push( - ShadowTreeRevision{newRootShadowNode, revisionNumber, telemetry}); + mountingCoordinator_->push(newRevision); notifyDelegatesOfUpdates(); - return true; + return CommitStatus::Succeeded; +} + +ShadowTreeRevision ShadowTree::getCurrentRevision() const { + std::shared_lock lock(commitMutex_); + return currentRevision_; } void ShadowTree::commitEmptyTree() const { commit( - [](RootShadowNode::Shared const &oldRootShadowNode) - -> RootShadowNode::Unshared { + [](RootShadowNode const &oldRootShadowNode) -> RootShadowNode::Unshared { return std::make_shared( - *oldRootShadowNode, + oldRootShadowNode, ShadowNodeFragment{ /* .props = */ ShadowNodeFragment::propsPlaceholder(), /* .children = */ ShadowNode::emptySharedShadowNodeSharedList(), diff --git a/ReactCommon/react/renderer/mounting/ShadowTree.h b/ReactCommon/react/renderer/mounting/ShadowTree.h index 70155de275f11d..1a2851d57345f4 100644 --- a/ReactCommon/react/renderer/mounting/ShadowTree.h +++ b/ReactCommon/react/renderer/mounting/ShadowTree.h @@ -24,13 +24,19 @@ namespace facebook { namespace react { using ShadowTreeCommitTransaction = std::function; + RootShadowNode const &oldRootShadowNode)>; /* * Represents the shadow tree and its lifecycle. */ class ShadowTree final { public: + enum class CommitStatus { + Succeeded, + Failed, + Cancelled, + }; + /* * Creates a new shadow tree instance. */ @@ -53,20 +59,25 @@ class ShadowTree final { /* * Performs commit calling `transaction` function with a `oldRootShadowNode` * and expecting a `newRootShadowNode` as a return value. - * The `transaction` function can abort commit returning `nullptr`. - * Returns `true` if the operation finished successfully. + * The `transaction` function can cancel commit returning `nullptr`. */ - bool tryCommit( + CommitStatus tryCommit( ShadowTreeCommitTransaction transaction, bool enableStateReconciliation = false) const; /* * Calls `tryCommit` in a loop until it finishes successfully. */ - void commit( + CommitStatus commit( ShadowTreeCommitTransaction transaction, bool enableStateReconciliation = false) const; + /* + * Returns a `ShadowTreeRevision` representing the momentary state of + * the `ShadowTree`. + */ + ShadowTreeRevision getCurrentRevision() const; + /* * Commit an empty tree (a new `RootShadowNode` with no children). */ @@ -85,31 +96,19 @@ class ShadowTree final { * Temporary. * Do not use. */ - void setEnableNewStateReconciliation(bool value) { - enableNewStateReconciliation_ = value; - } void setEnableReparentingDetection(bool value) { enableReparentingDetection_ = value; } private: - RootShadowNode::Unshared cloneRootShadowNode( - RootShadowNode::Shared const &oldRootShadowNode, - LayoutConstraints const &layoutConstraints, - LayoutContext const &layoutContext) const; - void emitLayoutEvents( std::vector &affectedLayoutableNodes) const; SurfaceId const surfaceId_; ShadowTreeDelegate const &delegate_; mutable better::shared_mutex commitMutex_; - mutable RootShadowNode::Shared - rootShadowNode_; // Protected by `commitMutex_`. - mutable ShadowTreeRevision::Number revisionNumber_{ - 0}; // Protected by `commitMutex_`. + mutable ShadowTreeRevision currentRevision_; // Protected by `commitMutex_`. MountingCoordinator::Shared mountingCoordinator_; - bool enableNewStateReconciliation_{false}; bool enableReparentingDetection_{false}; }; diff --git a/ReactCommon/react/renderer/mounting/ShadowTreeRegistry.cpp b/ReactCommon/react/renderer/mounting/ShadowTreeRegistry.cpp index 6966870619bd73..38540ecfd738d5 100644 --- a/ReactCommon/react/renderer/mounting/ShadowTreeRegistry.cpp +++ b/ReactCommon/react/renderer/mounting/ShadowTreeRegistry.cpp @@ -21,13 +21,18 @@ void ShadowTreeRegistry::add(std::unique_ptr &&shadowTree) const { registry_.emplace(shadowTree->getSurfaceId(), std::move(shadowTree)); } -void ShadowTreeRegistry::remove(SurfaceId surfaceId) const { +std::unique_ptr ShadowTreeRegistry::remove( + SurfaceId surfaceId) const { std::unique_lock lock(mutex_); auto iterator = registry_.find(surfaceId); - if (iterator != registry_.end()) { - registry_.erase(iterator); + if (iterator == registry_.end()) { + return {}; } + + auto shadowTree = std::unique_ptr(iterator->second.release()); + registry_.erase(iterator); + return shadowTree; } bool ShadowTreeRegistry::visit( diff --git a/ReactCommon/react/renderer/mounting/ShadowTreeRegistry.h b/ReactCommon/react/renderer/mounting/ShadowTreeRegistry.h index 21bcc0fca0729e..e2cee3d8c65b79 100644 --- a/ReactCommon/react/renderer/mounting/ShadowTreeRegistry.h +++ b/ReactCommon/react/renderer/mounting/ShadowTreeRegistry.h @@ -34,9 +34,11 @@ class ShadowTreeRegistry final { /* * Removes a `ShadowTree` instance with given `surfaceId` from the registry * and returns it as a result. + * The ownership of the instance is also transferred to the caller. + * Returns `nullptr` if a `ShadowTree` with given `surfaceId` was not found. * Can be called from any thread. */ - void remove(SurfaceId surfaceId) const; + std::unique_ptr remove(SurfaceId surfaceId) const; /* * Finds a `ShadowTree` instance with a given `surfaceId` in the registry and diff --git a/ReactCommon/react/renderer/mounting/ShadowTreeRevision.cpp b/ReactCommon/react/renderer/mounting/ShadowTreeRevision.cpp index 36108534f397b2..f56d03ec2023e0 100644 --- a/ReactCommon/react/renderer/mounting/ShadowTreeRevision.cpp +++ b/ReactCommon/react/renderer/mounting/ShadowTreeRevision.cpp @@ -6,33 +6,3 @@ */ #include "ShadowTreeRevision.h" - -namespace facebook { -namespace react { - -using Number = ShadowTreeRevision::Number; - -ShadowTreeRevision::ShadowTreeRevision( - ShadowNode::Shared const &rootShadowNode, - Number number, - TransactionTelemetry telemetry) - : rootShadowNode_(rootShadowNode), number_(number), telemetry_(telemetry) {} - -TransactionTelemetry const &ShadowTreeRevision::getTelemetry() const { - return telemetry_; -} - -ShadowNode::Shared ShadowTreeRevision::getSharedRootShadowNode() { - return rootShadowNode_; -} - -ShadowNode const &ShadowTreeRevision::getRootShadowNode() { - return *rootShadowNode_; -} - -Number ShadowTreeRevision::getNumber() const { - return number_; -} - -} // namespace react -} // namespace facebook diff --git a/ReactCommon/react/renderer/mounting/ShadowTreeRevision.h b/ReactCommon/react/renderer/mounting/ShadowTreeRevision.h index 3aa40d1657d98b..d58dbd643288d7 100644 --- a/ReactCommon/react/renderer/mounting/ShadowTreeRevision.h +++ b/ReactCommon/react/renderer/mounting/ShadowTreeRevision.h @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -29,41 +30,12 @@ class ShadowTreeRevision final { */ using Number = int64_t; - /* - * Creates the object with given root shadow node, revision number and - * telemetry. - */ - ShadowTreeRevision( - ShadowNode::Shared const &rootShadowNode, - Number number, - TransactionTelemetry telemetry); - - /* - * Returns telemetry associated with this revision. - */ - TransactionTelemetry const &getTelemetry() const; - - /* - * Methods from this section are meant to be used by - * `MountingOverrideDelegate` only. - */ - public: - ShadowNode const &getRootShadowNode(); - ShadowNode::Shared getSharedRootShadowNode(); - - /* - * Methods from this section are meant to be used by `MountingCoordinator` - * only. - */ - private: + friend class ShadowTree; friend class MountingCoordinator; - Number getNumber() const; - - private: - ShadowNode::Shared rootShadowNode_; - Number number_; - TransactionTelemetry telemetry_; + RootShadowNode::Shared rootShadowNode; + Number number; + TransactionTelemetry telemetry; }; } // namespace react diff --git a/ReactCommon/react/renderer/mounting/StubViewTree.cpp b/ReactCommon/react/renderer/mounting/StubViewTree.cpp index fc68777f349fda..698af4374f2522 100644 --- a/ReactCommon/react/renderer/mounting/StubViewTree.cpp +++ b/ReactCommon/react/renderer/mounting/StubViewTree.cpp @@ -83,13 +83,15 @@ void StubViewTree::mutate( STUB_VIEW_ASSERT(registry.find(parentTag) != registry.end()); auto parentStubView = registry[parentTag]; auto childTag = mutation.newChildShadowView.tag; - STUB_VIEW_LOG({ - LOG(ERROR) << "StubView: Insert: " << childTag << " into " - << parentTag << " at " << mutation.index; - }); STUB_VIEW_ASSERT(registry.find(childTag) != registry.end()); auto childStubView = registry[childTag]; childStubView->update(mutation.newChildShadowView); + STUB_VIEW_LOG({ + LOG(ERROR) << "StubView: Insert: " << childTag << " into " + << parentTag << " at " << mutation.index << "(" + << parentStubView->children.size() << " children)"; + }); + STUB_VIEW_ASSERT(parentStubView->children.size() >= mutation.index); parentStubView->children.insert( parentStubView->children.begin() + mutation.index, childStubView); break; @@ -106,6 +108,7 @@ void StubViewTree::mutate( << parentTag << " at index " << mutation.index << " with " << parentStubView->children.size() << " children"; }); + STUB_VIEW_ASSERT(parentStubView->children.size() > mutation.index); STUB_VIEW_ASSERT(registry.find(childTag) != registry.end()); auto childStubView = registry[childTag]; bool childIsCorrect = @@ -130,8 +133,15 @@ void StubViewTree::mutate( STUB_VIEW_LOG({ LOG(ERROR) << "StubView: Update: " << mutation.newChildShadowView.tag; }); + + // We don't have a strict requirement that oldChildShadowView has any + // data. In particular, LayoutAnimations can produce UPDATEs with only a + // new node. STUB_VIEW_ASSERT( - mutation.newChildShadowView.tag == mutation.oldChildShadowView.tag); + mutation.newChildShadowView.tag == + mutation.oldChildShadowView.tag || + mutation.oldChildShadowView.tag == 0); + STUB_VIEW_ASSERT( registry.find(mutation.newChildShadowView.tag) != registry.end()); auto stubView = registry[mutation.newChildShadowView.tag]; diff --git a/ReactCommon/react/renderer/mounting/TransactionTelemetry.cpp b/ReactCommon/react/renderer/mounting/TransactionTelemetry.cpp index f1ac3f659feaa7..d81ed7c6bf44b1 100644 --- a/ReactCommon/react/renderer/mounting/TransactionTelemetry.cpp +++ b/ReactCommon/react/renderer/mounting/TransactionTelemetry.cpp @@ -12,18 +12,18 @@ namespace facebook { namespace react { -using ThreadLocalTransactionTelemetry = ThreadStorage; +thread_local TransactionTelemetry *threadLocalTransactionTelemetry = nullptr; TransactionTelemetry *TransactionTelemetry::threadLocalTelemetry() { - return ThreadLocalTransactionTelemetry::getInstance().get().value_or(nullptr); + return threadLocalTransactionTelemetry; } void TransactionTelemetry::setAsThreadLocal() { - ThreadLocalTransactionTelemetry::getInstance().set(this); + threadLocalTransactionTelemetry = this; } void TransactionTelemetry::unsetAsThreadLocal() { - ThreadLocalTransactionTelemetry::getInstance().set(nullptr); + threadLocalTransactionTelemetry = nullptr; } void TransactionTelemetry::willCommit() { diff --git a/ReactCommon/react/renderer/mounting/TransactionTelemetry.h b/ReactCommon/react/renderer/mounting/TransactionTelemetry.h index b71e10bae33e78..88ef2a23b0e3c4 100644 --- a/ReactCommon/react/renderer/mounting/TransactionTelemetry.h +++ b/ReactCommon/react/renderer/mounting/TransactionTelemetry.h @@ -11,7 +11,6 @@ #include #include -#include namespace facebook { namespace react { diff --git a/ReactCommon/react/renderer/mounting/TreeStateReconciliation.cpp b/ReactCommon/react/renderer/mounting/TreeStateReconciliation.cpp deleted file mode 100644 index 58017f16a3c6c8..00000000000000 --- a/ReactCommon/react/renderer/mounting/TreeStateReconciliation.cpp +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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. - */ - -#include "TreeStateReconciliation.h" - -namespace facebook { -namespace react { - -using ChangedShadowNodePairs = - std::vector>; - -/** - * Clones any children in the subtree that need to be cloned, and adds those to - * the `changedPairs` vector argument. - */ -static ChangedShadowNodePairs reconcileStateWithChildren( - SharedShadowNodeList const &newChildren, - SharedShadowNodeList const &oldChildren) { - ChangedShadowNodePairs changedPairs; - // Find children that are the same family in both trees. - // We only want to find nodes that existing in the new tree - if they - // don't exist in the new tree, they're being deleted; if they don't exist - // in the old tree, they're new. We don't need to deal with either of those - // cases here. - // Currently we use a naive double loop - this could be improved, but we need - // to be able to handle cases where nodes are entirely reordered, for - // instance. - for (auto const &child : newChildren) { - auto const oldChild = std::find_if( - oldChildren.begin(), oldChildren.end(), [&](const auto &el) { - return ShadowNode::sameFamily(*child, *el); - }); - - if (oldChild != oldChildren.end()) { - UnsharedShadowNode newChild = - reconcileStateWithTree(child.get(), *oldChild); - if (newChild != nullptr) { - changedPairs.push_back(std::make_pair(child, newChild)); - } - } - }; - - return changedPairs; -} - -UnsharedShadowNode reconcileStateWithTree( - ShadowNode const *newNode, - SharedShadowNode committedNode) { - // If the revisions on the node are the same, we can finish here. - // Subtrees are guaranteed to be identical at this point, too. - if (committedNode->getStateRevision() <= newNode->getStateRevision()) { - return nullptr; - } - - // If we got this fair, we're guaranteed that the state of 1) this node, - // and/or 2) some descendant node is out-of-date and must be reconciled. - // This requires traversing all children, and we must at *least* clone - // this node, whether or not we clone and update any children. - auto const &newChildren = newNode->getChildren(); - auto const &oldChildren = committedNode->getChildren(); - auto const changedPairs = - reconcileStateWithChildren(newChildren, oldChildren); - - ShadowNode::SharedListOfShared clonedChildren = - ShadowNodeFragment::childrenPlaceholder(); - - // If any children were cloned, we need to recreate the child list. - // This won't cause any children to be cloned that weren't already cloned - - // it just collects all children, cloned or uncloned, into a new list. - if (!changedPairs.empty()) { - ShadowNode::UnsharedListOfShared newList = - std::make_shared(); - for (std::size_t i = 0, j = 0; i < newChildren.size(); ++i) { - if (j < changedPairs.size() && changedPairs[j].first == newChildren[i]) { - newList->push_back(changedPairs[j].second); - ++j; - } else { - newList->push_back(newChildren[i]); - } - } - clonedChildren = newList; - } - - return newNode->clone({/* .props = */ ShadowNodeFragment::propsPlaceholder(), - /* .children = */ clonedChildren, - /* .state = */ newNode->getMostRecentState()}); -} - -} // namespace react -} // namespace facebook diff --git a/ReactCommon/react/renderer/mounting/TreeStateReconciliation.h b/ReactCommon/react/renderer/mounting/TreeStateReconciliation.h deleted file mode 100644 index ff95b7e763d2c8..00000000000000 --- a/ReactCommon/react/renderer/mounting/TreeStateReconciliation.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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. - */ - -#include -#include - -namespace facebook { -namespace react { - -/** - * Problem Description: because of C++ State, the React Native C++ ShadowTree - * can diverge from the ReactJS ShadowTree; ReactJS communicates all tree - * changes to C++, but C++ state commits are not propagated to ReactJS (ReactJS - * may or may not clone nodes with state changes, but it has no way of knowing - * if it /should/ clone those nodes; so those clones may never happen). This - * causes a number of problems. This function resolves the problem by taking a - * candidate tree being committed, and sees if any State changes need to be - * applied to it. If any changes need to be made, a new ShadowNode is returned; - * otherwise, nullptr is returned if the node is already consistent with the - * latest tree, including all state changes. - * - * This should be called during the commit phase, pre-layout and pre-diff. - */ -UnsharedShadowNode reconcileStateWithTree( - ShadowNode const *newNode, - SharedShadowNode committedNode); - -} // namespace react -} // namespace facebook diff --git a/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp b/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp index 851231da950841..1ecfd21a69b670 100644 --- a/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp +++ b/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp @@ -5,12 +5,15 @@ * LICENSE file in the root directory of this source tree. */ +#include + #include #include #include #include #include +#include #include #include "Entropy.h" @@ -102,6 +105,29 @@ static void testShadowNodeTreeLifeCycle( auto mutations = calculateShadowViewMutations( *currentRootNode, *nextRootNode, useFlattener); + // If using flattener: make sure that in a single frame, a DELETE for a + // view is not followed by a CREATE for the same view. + if (useFlattener) { + std::vector deletedTags{}; + for (auto const &mutation : mutations) { + if (mutation.type == ShadowViewMutation::Type::Delete) { + deletedTags.push_back(mutation.oldChildShadowView.tag); + } + } + for (auto const &mutation : mutations) { + if (mutation.type == ShadowViewMutation::Type::Create) { + if (std::find( + deletedTags.begin(), + deletedTags.end(), + mutation.newChildShadowView.tag) != deletedTags.end()) { + LOG(ERROR) << "Deleted tag was recreated in mutations list: [" + << mutation.newChildShadowView.tag << "]"; + FAIL(); + } + } + } + } + // Mutating the view tree. viewTree.mutate(mutations); diff --git a/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp b/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp index d32e1d69a5c42c..99b86ca357b1b7 100644 --- a/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp +++ b/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp @@ -32,7 +32,7 @@ class DummyShadowTreeDelegate : public ShadowTreeDelegate { inline ShadowNode const *findDescendantNode( ShadowNode const &shadowNode, ShadowNodeFamily const &family) { - auto result = (ShadowNode const *){nullptr}; + ShadowNode const *result = nullptr; shadowNode.cloneTree(family, [&](ShadowNode const &oldShadowNode) { result = &oldShadowNode; return oldShadowNode.clone({}); @@ -43,16 +43,8 @@ inline ShadowNode const *findDescendantNode( inline ShadowNode const *findDescendantNode( ShadowTree const &shadowTree, ShadowNodeFamily const &family) { - auto result = (ShadowNode const *){nullptr}; - - shadowTree.tryCommit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { - result = findDescendantNode(*oldRootShadowNode, family); - return nullptr; - }, - false); - - return result; + return findDescendantNode( + *shadowTree.getCurrentRevision().rootShadowNode, family); } TEST(StateReconciliationTest, testStateReconciliation) { @@ -107,7 +99,7 @@ TEST(StateReconciliationTest, testStateReconciliation) { {}}; shadowTree.commit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { + [&](RootShadowNode const &oldRootShadowNode) { return std::static_pointer_cast(rootShadowNodeState1); }, true); @@ -131,7 +123,7 @@ TEST(StateReconciliationTest, testStateReconciliation) { findDescendantNode(*rootShadowNodeState2, family)->getState(), state2); shadowTree.commit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { + [&](RootShadowNode const &oldRootShadowNode) { return std::static_pointer_cast(rootShadowNodeState2); }, true); @@ -153,7 +145,7 @@ TEST(StateReconciliationTest, testStateReconciliation) { findDescendantNode(*rootShadowNodeState3, family)->getState(), state3); shadowTree.commit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { + [&](RootShadowNode const &oldRootShadowNode) { return std::static_pointer_cast(rootShadowNodeState3); }, true); @@ -168,7 +160,7 @@ TEST(StateReconciliationTest, testStateReconciliation) { // Here we commit the old tree but we expect that the state associated with // the node will stay the same (newer that the old tree has). shadowTree.commit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { + [&](RootShadowNode const &oldRootShadowNode) { return std::static_pointer_cast(rootShadowNodeState2); }, true); diff --git a/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 4cefd596a70911..1f0c617ef8d58a 100644 --- a/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -108,17 +108,19 @@ Scheduler::Scheduler( #ifdef ANDROID enableReparentingDetection_ = reactNativeConfig_->getBool( "react_fabric:enable_reparenting_detection_android"); - enableNewStateReconciliation_ = reactNativeConfig_->getBool( - "react_fabric:enable_new_state_reconciliation_android"); removeOutstandingSurfacesOnDestruction_ = reactNativeConfig_->getBool( "react_fabric:remove_outstanding_surfaces_on_destruction_android"); + uiManager_->experimentEnableStateUpdateWithAutorepeat = + reactNativeConfig_->getBool( + "react_fabric:enable_state_update_with_autorepeat_android"); #else enableReparentingDetection_ = reactNativeConfig_->getBool( "react_fabric:enable_reparenting_detection_ios"); - enableNewStateReconciliation_ = reactNativeConfig_->getBool( - "react_fabric:enable_new_state_reconciliation_ios"); removeOutstandingSurfacesOnDestruction_ = reactNativeConfig_->getBool( "react_fabric:remove_outstanding_surfaces_on_destruction_ios"); + uiManager_->experimentEnableStateUpdateWithAutorepeat = + reactNativeConfig_->getBool( + "react_fabric:enable_state_update_with_autorepeat_ios"); #endif } @@ -192,8 +194,6 @@ void Scheduler::startSurface( mountingOverrideDelegate, enableReparentingDetection_); - shadowTree->setEnableNewStateReconciliation(enableNewStateReconciliation_); - auto uiManager = uiManager_; uiManager->getShadowTreeRegistry().add(std::move(shadowTree)); @@ -226,9 +226,9 @@ void Scheduler::renderTemplateToSurface( uiManager_->getShadowTreeRegistry().visit( surfaceId, [=](const ShadowTree &shadowTree) { return shadowTree.tryCommit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { + [&](RootShadowNode const &oldRootShadowNode) { return std::make_shared( - *oldRootShadowNode, + oldRootShadowNode, ShadowNodeFragment{ /* .props = */ ShadowNodeFragment::propsPlaceholder(), /* .children = */ @@ -249,19 +249,16 @@ void Scheduler::stopSurface(SurfaceId surfaceId) const { // Stop any ongoing animations. uiManager_->stopSurfaceForAnimationDelegate(surfaceId); - // Note, we have to do in inside `visit` function while the Shadow Tree - // is still being registered. - uiManager_->getShadowTreeRegistry().visit( - surfaceId, [](ShadowTree const &shadowTree) { - // As part of stopping a Surface, we need to properly destroy all - // mounted views, so we need to commit an empty tree to trigger all - // side-effects that will perform that. - shadowTree.commitEmptyTree(); - }); - // Waiting for all concurrent commits to be finished and unregistering the // `ShadowTree`. - uiManager_->getShadowTreeRegistry().remove(surfaceId); + auto shadowTree = uiManager_->getShadowTreeRegistry().remove(surfaceId); + + // As part of stopping a Surface, we need to properly destroy all + // mounted views, so we need to commit an empty tree to trigger all + // side-effects (including destroying and removing mounted views). + if (shadowTree) { + shadowTree->commitEmptyTree(); + } // We execute JavaScript/React part of the process at the very end to minimize // any visible side-effects of stopping the Surface. Any possible commits from @@ -281,19 +278,16 @@ Size Scheduler::measureSurface( const LayoutContext &layoutContext) const { SystraceSection s("Scheduler::measureSurface"); - Size size; + auto currentRootShadowNode = RootShadowNode::Shared{}; uiManager_->getShadowTreeRegistry().visit( surfaceId, [&](const ShadowTree &shadowTree) { - shadowTree.tryCommit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { - auto rootShadowNode = - oldRootShadowNode->clone(layoutConstraints, layoutContext); - rootShadowNode->layoutIfNeeded(); - size = rootShadowNode->getLayoutMetrics().frame.size; - return nullptr; - }); + currentRootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; }); - return size; + + auto rootShadowNode = + currentRootShadowNode->clone(layoutConstraints, layoutContext); + rootShadowNode->layoutIfNeeded(); + return rootShadowNode->getLayoutMetrics().frame.size; } MountingCoordinator::Shared Scheduler::findMountingCoordinator( @@ -314,8 +308,8 @@ void Scheduler::constraintSurfaceLayout( uiManager_->getShadowTreeRegistry().visit( surfaceId, [&](ShadowTree const &shadowTree) { - shadowTree.commit([&](RootShadowNode::Shared const &oldRootShadowNode) { - return oldRootShadowNode->clone(layoutConstraints, layoutContext); + shadowTree.commit([&](RootShadowNode const &oldRootShadowNode) { + return oldRootShadowNode.clone(layoutConstraints, layoutContext); }); }); } diff --git a/ReactCommon/react/renderer/scheduler/Scheduler.h b/ReactCommon/react/renderer/scheduler/Scheduler.h index c025b39a3b98dd..3b5693cb724093 100644 --- a/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -138,7 +138,6 @@ class Scheduler final : public UIManagerDelegate { * Temporary flags. */ bool enableReparentingDetection_{false}; - bool enableNewStateReconciliation_{false}; bool removeOutstandingSurfacesOnDestruction_{false}; }; diff --git a/ReactCommon/react/renderer/scheduler/SynchronousEventBeat.cpp b/ReactCommon/react/renderer/scheduler/SynchronousEventBeat.cpp index a30552eb51c29e..245e07b463bcdf 100644 --- a/ReactCommon/react/renderer/scheduler/SynchronousEventBeat.cpp +++ b/ReactCommon/react/renderer/scheduler/SynchronousEventBeat.cpp @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import "SynchronousEventBeat.h" +#include "SynchronousEventBeat.h" namespace facebook { namespace react { diff --git a/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.cpp b/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.cpp index 70904ed8b64de8..d3d9845f93c518 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.cpp +++ b/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.cpp @@ -8,5 +8,58 @@ #include "TextMeasureCache.h" namespace facebook { -namespace react {} // namespace react +namespace react { + +static Rect rectFromDynamic(folly::dynamic const &data) { + Point origin; + origin.x = data.getDefault("x", 0).getDouble(); + origin.y = data.getDefault("y", 0).getDouble(); + Size size; + size.width = data.getDefault("width", 0).getDouble(); + size.height = data.getDefault("height", 0).getDouble(); + Rect frame; + frame.origin = origin; + frame.size = size; + return frame; +} + +LineMeasurement::LineMeasurement( + std::string text, + Rect frame, + Float descender, + Float capHeight, + Float ascender, + Float xHeight) + : text(text), + frame(frame), + descender(descender), + capHeight(capHeight), + ascender(ascender) {} + +LineMeasurement::LineMeasurement(folly::dynamic const &data) + : text(data.getDefault("text", "").getString()), + frame(rectFromDynamic(data)), + descender(data.getDefault("descender", 0).getDouble()), + capHeight(data.getDefault("capHeight", 0).getDouble()), + ascender(data.getDefault("ascender", 0).getDouble()), + xHeight(data.getDefault("xHeight", 0).getDouble()) {} + +bool LineMeasurement::operator==(LineMeasurement const &rhs) const { + return std::tie( + this->text, + this->frame, + this->descender, + this->capHeight, + this->ascender, + this->xHeight) == + std::tie( + rhs.text, + rhs.frame, + rhs.descender, + rhs.capHeight, + rhs.ascender, + rhs.xHeight); +} + +} // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h b/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h index 4a15c17925684b..d0e71872810d75 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h +++ b/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h @@ -23,6 +23,18 @@ struct LineMeasurement { Float capHeight; Float ascender; Float xHeight; + + LineMeasurement( + std::string text, + Rect frame, + Float descender, + Float capHeight, + Float ascender, + Float xHeight); + + LineMeasurement(folly::dynamic const &data); + + bool operator==(LineMeasurement const &rhs) const; }; using LinesMeasurements = std::vector; diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index 86b26366874f35..be0201dec74d43 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -37,7 +37,7 @@ TextMeasurement TextLayoutManager::measure( } TextMeasurement TextLayoutManager::measureCachedSpannableById( - int cacheId, + int64_t cacheId, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const { const jni::global_ref &fabricUIManager = @@ -64,7 +64,7 @@ TextMeasurement TextLayoutManager::measureCachedSpannableById( auto maximumSize = layoutConstraints.maximumSize; local_ref componentName = make_jstring("RCTText"); - folly::dynamic cacheIdMap; + folly::dynamic cacheIdMap = folly::dynamic::object; cacheIdMap["cacheId"] = cacheId; local_ref attributedStringRNM = ReadableNativeMap::newObjectCxxArgs(cacheIdMap); @@ -88,12 +88,60 @@ TextMeasurement TextLayoutManager::measureCachedSpannableById( maximumSize.height, attachmentPositions)); + // Clean up allocated ref - it still takes up space in the JNI ref table even + // though it's 0 length + env->DeleteLocalRef(attachmentPositions); + // TODO: currently we do not support attachments for cached IDs - should we? auto attachments = TextMeasurement::Attachments{}; return TextMeasurement{size, attachments}; } +LinesMeasurements TextLayoutManager::measureLines( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + Size size) const { + const jni::global_ref &fabricUIManager = + contextContainer_->at>("FabricUIManager"); + static auto measureLines = + jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") + ->getMethod("measureLines"); + + auto serializedAttributedString = toDynamic(attributedString); + + local_ref attributedStringRNM = + ReadableNativeMap::newObjectCxxArgs(serializedAttributedString); + local_ref paragraphAttributesRNM = + ReadableNativeMap::newObjectCxxArgs(toDynamic(paragraphAttributes)); + + local_ref attributedStringRM = make_local( + reinterpret_cast(attributedStringRNM.get())); + local_ref paragraphAttributesRM = make_local( + reinterpret_cast(paragraphAttributesRNM.get())); + + auto array = measureLines( + fabricUIManager, + attributedStringRM.get(), + paragraphAttributesRM.get(), + size.width, + size.height); + + auto dynamicArray = cthis(array)->consume(); + LinesMeasurements lineMeasurements; + lineMeasurements.reserve(dynamicArray.size()); + + for (auto const &data : dynamicArray) { + lineMeasurements.push_back(LineMeasurement(data)); + } + + return lineMeasurements; +} + TextMeasurement TextLayoutManager::doMeasure( AttributedString attributedString, ParagraphAttributes paragraphAttributes, @@ -172,8 +220,10 @@ TextMeasurement TextLayoutManager::doMeasure( } } } - // DELETE REF + + // Clean up allocated ref env->DeleteLocalRef(attachmentPositions); + return TextMeasurement{size, attachments}; } diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index 926a9498f289b6..681c54e22fb793 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -42,10 +42,19 @@ class TextLayoutManager { * opaque cache ID. */ TextMeasurement measureCachedSpannableById( - int cacheId, + int64_t cacheId, ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const; + /* + * Measures lines of `attributedString` using native text rendering + * infrastructure. + */ + LinesMeasurements measureLines( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + Size size) const; + /* * Returns an opaque pointer to platform-specific TextLayoutManager. * Is used on a native views layer to delegate text rendering to the manager. diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextPrimitivesConversions.h b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextPrimitivesConversions.h index fdfb6e2ce61c68..c78daec7b7a3d5 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextPrimitivesConversions.h +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTTextPrimitivesConversions.h @@ -96,7 +96,24 @@ inline static NSUnderlineStyle RCTNSUnderlineStyleFromStyleAndPattern( return style; } -inline static UIColor *RCTUIColorFromSharedColor(const SharedColor &color) +inline static UIColor *RCTUIColorFromSharedColor(const SharedColor &sharedColor) { - return color ? [UIColor colorWithCGColor:color.get()] : nil; + if (!sharedColor) { + return nil; + } + + if (*facebook::react::clearColor() == *sharedColor) { + return [UIColor clearColor]; + } + + if (*facebook::react::blackColor() == *sharedColor) { + return [UIColor blackColor]; + } + + if (*facebook::react::whiteColor() == *sharedColor) { + return [UIColor whiteColor]; + } + + auto components = facebook::react::colorComponentsFromColor(sharedColor); + return [UIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha]; } diff --git a/ReactCommon/react/renderer/uimanager/UIManager.cpp b/ReactCommon/react/renderer/uimanager/UIManager.cpp index 701e9e83ee435a..a8b4c6964808fc 100644 --- a/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -97,9 +97,9 @@ void UIManager::completeSurface( shadowTreeRegistry_.visit(surfaceId, [&](ShadowTree const &shadowTree) { shadowTree.commit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { + [&](RootShadowNode const &oldRootShadowNode) { return std::make_shared( - *oldRootShadowNode, + oldRootShadowNode, ShadowNodeFragment{ /* .props = */ ShadowNodeFragment::propsPlaceholder(), /* .children = */ rootChildren, @@ -139,12 +139,7 @@ ShadowNode::Shared UIManager::getNewestCloneOfShadowNode( auto ancestorShadowNode = ShadowNode::Shared{}; shadowTreeRegistry_.visit( shadowNode.getSurfaceId(), [&](ShadowTree const &shadowTree) { - shadowTree.tryCommit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { - ancestorShadowNode = oldRootShadowNode; - return nullptr; - }, - true); + ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode; }); if (!ancestorShadowNode) { @@ -178,9 +173,9 @@ void UIManager::setNativeProps( shadowTreeRegistry_.visit( shadowNode.getSurfaceId(), [&](ShadowTree const &shadowTree) { shadowTree.tryCommit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { + [&](RootShadowNode const &oldRootShadowNode) { return std::static_pointer_cast( - oldRootShadowNode->cloneTree( + oldRootShadowNode.cloneTree( shadowNode.getFamily(), [&](ShadowNode const &oldShadowNode) { return oldShadowNode.clone({ @@ -205,13 +200,9 @@ LayoutMetrics UIManager::getRelativeLayoutMetrics( if (!ancestorShadowNode) { shadowTreeRegistry_.visit( shadowNode.getSurfaceId(), [&](ShadowTree const &shadowTree) { - shadowTree.tryCommit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { - owningAncestorShadowNode = oldRootShadowNode; - ancestorShadowNode = oldRootShadowNode.get(); - return nullptr; - }, - true); + owningAncestorShadowNode = + shadowTree.getCurrentRevision().rootShadowNode; + ancestorShadowNode = owningAncestorShadowNode.get(); }); } else { // It is possible for JavaScript (or other callers) to have a reference @@ -232,17 +223,61 @@ LayoutMetrics UIManager::getRelativeLayoutMetrics( shadowNode.getFamily(), *layoutableAncestorShadowNode, policy); } +void UIManager::updateStateWithAutorepeat( + StateUpdate const &stateUpdate) const { + auto &callback = stateUpdate.callback; + auto &family = stateUpdate.family; + auto &componentDescriptor = family->getComponentDescriptor(); + + shadowTreeRegistry_.visit( + family->getSurfaceId(), [&](ShadowTree const &shadowTree) { + shadowTree.commit([&](RootShadowNode const &oldRootShadowNode) { + auto isValid = true; + + auto rootNode = oldRootShadowNode.cloneTree( + *family, [&](ShadowNode const &oldShadowNode) { + auto newData = + callback(oldShadowNode.getState()->getDataPointer()); + + if (!newData) { + isValid = false; + // Just return something, we will discard it anyway. + return oldShadowNode.clone({}); + } + + auto newState = + componentDescriptor.createState(*family, newData); + + return oldShadowNode.clone({ + /* .props = */ ShadowNodeFragment::propsPlaceholder(), + /* .children = */ + ShadowNodeFragment::childrenPlaceholder(), + /* .state = */ newState, + }); + }); + + return isValid ? std::static_pointer_cast(rootNode) + : nullptr; + }); + }); +} + void UIManager::updateState(StateUpdate const &stateUpdate) const { + if (stateUpdate.autorepeat || experimentEnableStateUpdateWithAutorepeat) { + updateStateWithAutorepeat(stateUpdate); + return; + } + auto &callback = stateUpdate.callback; auto &family = stateUpdate.family; auto &componentDescriptor = family->getComponentDescriptor(); shadowTreeRegistry_.visit( family->getSurfaceId(), [&](ShadowTree const &shadowTree) { - bool updateSucceeded = shadowTree.tryCommit( - [&](RootShadowNode::Shared const &oldRootShadowNode) { - return std::static_pointer_cast< - RootShadowNode>(oldRootShadowNode->cloneTree( + auto status = shadowTree.tryCommit([&](RootShadowNode const + &oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.cloneTree( *family, [&](ShadowNode const &oldShadowNode) { auto newData = callback(oldShadowNode.getState()->getDataPointer()); @@ -256,8 +291,9 @@ void UIManager::updateState(StateUpdate const &stateUpdate) const { /* .state = */ newState, }); })); - }); - if (!updateSucceeded && stateUpdate.failureCallback) { + }); + if (status != ShadowTree::CommitStatus::Succeeded && + stateUpdate.failureCallback) { stateUpdate.failureCallback(); } }); @@ -275,8 +311,8 @@ void UIManager::dispatchCommand( void UIManager::configureNextLayoutAnimation( jsi::Runtime &runtime, RawValue const &config, - const jsi::Value &successCallback, - const jsi::Value &failureCallback) const { + jsi::Value const &successCallback, + jsi::Value const &failureCallback) const { if (animationDelegate_) { animationDelegate_->uiManagerDidConfigureNextLayoutAnimation( runtime, diff --git a/ReactCommon/react/renderer/uimanager/UIManager.h b/ReactCommon/react/renderer/uimanager/UIManager.h index 1b3f5d73aa50e8..1235cf3532af14 100644 --- a/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/ReactCommon/react/renderer/uimanager/UIManager.h @@ -73,6 +73,11 @@ class UIManager final : public ShadowTreeDelegate { ShadowTree const &shadowTree, MountingCoordinator::Shared const &mountingCoordinator) const override; + /* + * Temporary flags. + */ + bool experimentEnableStateUpdateWithAutorepeat{false}; + private: friend class UIManagerBinding; friend class Scheduler; @@ -128,6 +133,7 @@ class UIManager final : public ShadowTreeDelegate { * and performs a commit. */ void updateState(StateUpdate const &stateUpdate) const; + void updateStateWithAutorepeat(StateUpdate const &stateUpdate) const; void dispatchCommand( const ShadowNode::Shared &shadowNode, @@ -141,8 +147,8 @@ class UIManager final : public ShadowTreeDelegate { void configureNextLayoutAnimation( jsi::Runtime &runtime, RawValue const &config, - const jsi::Value &successCallback, - const jsi::Value &failureCallback) const; + jsi::Value const &successCallback, + jsi::Value const &failureCallback) const; ShadowTreeRegistry const &getShadowTreeRegistry() const; diff --git a/ReactCommon/react/renderer/uimanager/UIManagerAnimationDelegate.h b/ReactCommon/react/renderer/uimanager/UIManagerAnimationDelegate.h index cfbb0c00e558ea..9e55d1f3862e91 100644 --- a/ReactCommon/react/renderer/uimanager/UIManagerAnimationDelegate.h +++ b/ReactCommon/react/renderer/uimanager/UIManagerAnimationDelegate.h @@ -26,8 +26,8 @@ class UIManagerAnimationDelegate { virtual void uiManagerDidConfigureNextLayoutAnimation( jsi::Runtime &runtime, RawValue const &config, - const jsi::Value &successCallback, - const jsi::Value &failureCallback) const = 0; + jsi::Value const &successCallback, + jsi::Value const &failureCallback) const = 0; /** * Set ComponentDescriptor registry. diff --git a/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index c09bd31766f9cf..14eee6775f405b 100644 --- a/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -17,7 +17,7 @@ namespace react { static jsi::Object getModule( jsi::Runtime &runtime, - const std::string &moduleName) { + std::string const &moduleName) { auto batchedBridge = runtime.global().getPropertyAsObject(runtime, "__fbBatchedBridge"); auto getCallableModule = @@ -82,8 +82,8 @@ void UIManagerBinding::attach(std::shared_ptr const &uiManager) { void UIManagerBinding::startSurface( jsi::Runtime &runtime, SurfaceId surfaceId, - const std::string &moduleName, - const folly::dynamic &initalProps) const { + std::string const &moduleName, + folly::dynamic const &initalProps) const { folly::dynamic parameters = folly::dynamic::object(); parameters["rootTag"] = surfaceId; parameters["initialProps"] = initalProps; @@ -128,13 +128,18 @@ void UIManagerBinding::stopSurface(jsi::Runtime &runtime, SurfaceId surfaceId) void UIManagerBinding::dispatchEvent( jsi::Runtime &runtime, - const EventTarget *eventTarget, - const std::string &type, - const ValueFactory &payloadFactory) const { + EventTarget const *eventTarget, + std::string const &type, + ValueFactory const &payloadFactory) const { SystraceSection s("UIManagerBinding::dispatchEvent"); auto payload = payloadFactory(runtime); + // If a payload is null, the factory has decided to cancel the event + if (payload.isNull()) { + return; + } + auto instanceHandle = eventTarget ? [&]() { auto instanceHandle = eventTarget->getInstanceHandle(runtime); @@ -153,7 +158,7 @@ void UIManagerBinding::dispatchEvent( : jsi::Value::null(); auto &eventHandlerWrapper = - static_cast(*eventHandler_); + static_cast(*eventHandler_); eventHandlerWrapper.callback.call( runtime, @@ -168,7 +173,7 @@ void UIManagerBinding::invalidate() const { jsi::Value UIManagerBinding::get( jsi::Runtime &runtime, - const jsi::PropNameID &name) { + jsi::PropNameID const &name) { auto methodName = name.utf8(runtime); // Convert shared_ptr to a raw ptr @@ -207,10 +212,10 @@ jsi::Value UIManagerBinding::get( name, 5, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { return valueFromShadowNode( runtime, uiManager->createNode( @@ -229,10 +234,10 @@ jsi::Value UIManagerBinding::get( name, 1, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { return valueFromShadowNode( runtime, uiManager->cloneNode(shadowNodeFromValue(runtime, arguments[0]))); @@ -245,10 +250,10 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { uiManager->setJSResponder( shadowNodeFromValue(runtime, arguments[0]), arguments[1].getBool()); @@ -263,10 +268,10 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { auto node = shadowNodeFromValue(runtime, arguments[0]); auto locationX = (Float)arguments[1].getNumber(); auto locationY = (Float)arguments[2].getNumber(); @@ -293,10 +298,10 @@ jsi::Value UIManagerBinding::get( name, 0, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { uiManager->clearJSResponder(); return jsi::Value::undefined(); @@ -310,10 +315,10 @@ jsi::Value UIManagerBinding::get( name, 1, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { return valueFromShadowNode( runtime, uiManager->cloneNode( @@ -329,11 +334,11 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { - const auto &rawProps = RawProps(runtime, arguments[1]); + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { + auto const &rawProps = RawProps(runtime, arguments[1]); return valueFromShadowNode( runtime, uiManager->cloneNode( @@ -350,11 +355,11 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { - const auto &rawProps = RawProps(runtime, arguments[1]); + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { + auto const &rawProps = RawProps(runtime, arguments[1]); return valueFromShadowNode( runtime, uiManager->cloneNode( @@ -370,10 +375,10 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { uiManager->appendChild( shadowNodeFromValue(runtime, arguments[0]), shadowNodeFromValue(runtime, arguments[1])); @@ -386,10 +391,10 @@ jsi::Value UIManagerBinding::get( runtime, name, 1, - [](jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + [](jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { auto shadowNodeList = std::make_shared(SharedShadowNodeList({})); return valueFromShadowNodeList(runtime, shadowNodeList); @@ -401,10 +406,10 @@ jsi::Value UIManagerBinding::get( runtime, name, 2, - [](jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + [](jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { auto shadowNodeList = shadowNodeListFromValue(runtime, arguments[0]); auto shadowNode = shadowNodeFromValue(runtime, arguments[1]); shadowNodeList->push_back(shadowNode); @@ -420,11 +425,11 @@ jsi::Value UIManagerBinding::get( runtime, name, 2, - [uiManager, sharedUIManager = uiManager_]( - jsi::Runtime &runtime, + [ uiManager, sharedUIManager = uiManager_ ]( + jsi::Runtime & runtime, jsi::Value const &thisValue, jsi::Value const *arguments, - size_t count) -> jsi::Value { + size_t count) noexcept->jsi::Value { auto surfaceId = surfaceIdFromValue(runtime, arguments[0]); auto shadowNodeList = shadowNodeListFromValue(runtime, arguments[1]); @@ -448,10 +453,10 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, + jsi::Runtime & runtime, jsi::Value const &thisValue, jsi::Value const *arguments, - size_t count) -> jsi::Value { + size_t count) noexcept->jsi::Value { uiManager->completeSurface( surfaceIdFromValue(runtime, arguments[0]), shadowNodeListFromValue(runtime, arguments[1])); @@ -467,10 +472,10 @@ jsi::Value UIManagerBinding::get( name, 1, [this]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { auto eventHandler = arguments[0].getObject(runtime).getFunction(runtime); eventHandler_ = @@ -485,10 +490,10 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { auto layoutMetrics = uiManager->getRelativeLayoutMetrics( *shadowNodeFromValue(runtime, arguments[0]), shadowNodeFromValue(runtime, arguments[1]).get(), @@ -509,10 +514,10 @@ jsi::Value UIManagerBinding::get( name, 3, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { uiManager->dispatchCommand( shadowNodeFromValue(runtime, arguments[0]), stringFromValue(runtime, arguments[1]), @@ -529,10 +534,10 @@ jsi::Value UIManagerBinding::get( name, 4, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { auto layoutMetrics = uiManager->getRelativeLayoutMetrics( *shadowNodeFromValue(runtime, arguments[0]), shadowNodeFromValue(runtime, arguments[1]).get(), @@ -565,10 +570,10 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { auto layoutMetrics = uiManager->getRelativeLayoutMetrics( *shadowNodeFromValue(runtime, arguments[0]), nullptr, @@ -600,10 +605,10 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { auto layoutMetrics = uiManager->getRelativeLayoutMetrics( *shadowNodeFromValue(runtime, arguments[0]), nullptr, @@ -635,10 +640,10 @@ jsi::Value UIManagerBinding::get( name, 2, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { uiManager->setNativeProps( *shadowNodeFromValue(runtime, arguments[0]), RawProps(runtime, arguments[1])); @@ -653,10 +658,10 @@ jsi::Value UIManagerBinding::get( name, 3, [uiManager]( - jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { + jsi::Runtime & runtime, + jsi::Value const &thisValue, + jsi::Value const *arguments, + size_t count) noexcept->jsi::Value { uiManager->configureNextLayoutAnimation( runtime, // TODO: pass in JSI value instead of folly::dynamic to RawValue diff --git a/ReactCommon/react/renderer/uimanager/UIManagerBinding.h b/ReactCommon/react/renderer/uimanager/UIManagerBinding.h index d4e133f59d38d4..d5616310716a19 100644 --- a/ReactCommon/react/renderer/uimanager/UIManagerBinding.h +++ b/ReactCommon/react/renderer/uimanager/UIManagerBinding.h @@ -47,8 +47,8 @@ class UIManagerBinding : public jsi::HostObject { void startSurface( jsi::Runtime &runtime, SurfaceId surfaceId, - const std::string &moduleName, - const folly::dynamic &initalProps) const; + std::string const &moduleName, + folly::dynamic const &initalProps) const; /* * Stops React Native Surface with given id. @@ -62,9 +62,9 @@ class UIManagerBinding : public jsi::HostObject { */ void dispatchEvent( jsi::Runtime &runtime, - const EventTarget *eventTarget, - const std::string &type, - const ValueFactory &payloadFactory) const; + EventTarget const *eventTarget, + std::string const &type, + ValueFactory const &payloadFactory) const; /* * Invalidates the binding and underlying UIManager. @@ -78,11 +78,11 @@ class UIManagerBinding : public jsi::HostObject { /* * `jsi::HostObject` specific overloads. */ - jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override; + jsi::Value get(jsi::Runtime &runtime, jsi::PropNameID const &name) override; private: std::shared_ptr uiManager_; - std::unique_ptr eventHandler_; + std::unique_ptr eventHandler_; }; } // namespace react diff --git a/ReactCommon/react/renderer/uimanager/primitives.h b/ReactCommon/react/renderer/uimanager/primitives.h index cd9dde4a662c69..9fb2ea92786844 100644 --- a/ReactCommon/react/renderer/uimanager/primitives.h +++ b/ReactCommon/react/renderer/uimanager/primitives.h @@ -42,7 +42,7 @@ struct ShadowNodeListWrapper : public jsi::HostObject { inline static ShadowNode::Shared shadowNodeFromValue( jsi::Runtime &runtime, - const jsi::Value &value) { + jsi::Value const &value) { return value.getObject(runtime) .getHostObject(runtime) ->shadowNode; @@ -57,7 +57,7 @@ inline static jsi::Value valueFromShadowNode( inline static SharedShadowNodeUnsharedList shadowNodeListFromValue( jsi::Runtime &runtime, - const jsi::Value &value) { + jsi::Value const &value) { return value.getObject(runtime) .getHostObject(runtime) ->shadowNodeList; @@ -72,31 +72,31 @@ inline static jsi::Value valueFromShadowNodeList( inline static SharedEventTarget eventTargetFromValue( jsi::Runtime &runtime, - const jsi::Value &eventTargetValue, - const jsi::Value &tagValue) { + jsi::Value const &eventTargetValue, + jsi::Value const &tagValue) { return std::make_shared( runtime, eventTargetValue, tagValue.getNumber()); } -inline static Tag tagFromValue(jsi::Runtime &runtime, const jsi::Value &value) { +inline static Tag tagFromValue(jsi::Runtime &runtime, jsi::Value const &value) { return (Tag)value.getNumber(); } inline static SurfaceId surfaceIdFromValue( jsi::Runtime &runtime, - const jsi::Value &value) { + jsi::Value const &value) { return (SurfaceId)value.getNumber(); } inline static std::string stringFromValue( jsi::Runtime &runtime, - const jsi::Value &value) { + jsi::Value const &value) { return value.getString(runtime).utf8(runtime); } inline static folly::dynamic commandArgsFromValue( jsi::Runtime &runtime, - const jsi::Value &value) { + jsi::Value const &value) { return jsi::dynamicFromValue(runtime, value); } diff --git a/ReactCommon/reactperflogger/React-perflogger.podspec b/ReactCommon/reactperflogger/React-perflogger.podspec index 43d795d08e3ad0..b620cbf0e270bb 100644 --- a/ReactCommon/reactperflogger/React-perflogger.podspec +++ b/ReactCommon/reactperflogger/React-perflogger.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "**/*.{cpp,h}" s.header_dir = "reactperflogger" diff --git a/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec b/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec index e7fc8d2d2a3319..152c1f639b205c 100644 --- a/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec +++ b/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :tvos => "10.0" } + s.platforms = { :ios => "10.0" } s.source = source s.source_files = "**/*.{cpp,h}" s.header_dir = "ReactCommon" diff --git a/ReactCommon/yoga/Yoga.podspec b/ReactCommon/yoga/Yoga.podspec index b0a69cbb9b603a..500dfee8e47ac5 100644 --- a/ReactCommon/yoga/Yoga.podspec +++ b/ReactCommon/yoga/Yoga.podspec @@ -43,7 +43,7 @@ Pod::Spec.new do |spec| ] # Pinning to the same version as React.podspec. - spec.platforms = { :ios => "10.0", :tvos => "10.0" } + spec.platforms = { :ios => "10.0" } # Set this environment variable when *not* using the `:path` option to install the pod. # E.g. when publishing this spec to a spec repo. diff --git a/gradle.properties b/gradle.properties index 9f6c5bf6c1c1df..f0a3fdf5d79fed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ -org.gradle.configureondemand=true +# This is causing issue with dependencies task: https://github.com/gradle/gradle/issues/9645#issuecomment-530746758 +# org.gradle.configureondemand=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c9a2247756f21..bca17f36566b30 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/index.js b/index.js index 63c1d5f96fd29f..581c1a9522fb38 100644 --- a/index.js +++ b/index.js @@ -80,7 +80,6 @@ import typeof StyleSheet from './Libraries/StyleSheet/StyleSheet'; import typeof Systrace from './Libraries/Performance/Systrace'; import typeof ToastAndroid from './Libraries/Components/ToastAndroid/ToastAndroid'; import typeof * as TurboModuleRegistry from './Libraries/TurboModule/TurboModuleRegistry'; -import typeof TVEventHandler from './Libraries/Components/AppleTV/TVEventHandler'; import typeof UIManager from './Libraries/ReactNative/UIManager'; import typeof useColorScheme from './Libraries/Utilities/useColorScheme'; import typeof useWindowDimensions from './Libraries/Utilities/useWindowDimensions'; @@ -417,9 +416,6 @@ module.exports = { get TurboModuleRegistry(): TurboModuleRegistry { return require('./Libraries/TurboModule/TurboModuleRegistry'); }, - get TVEventHandler(): TVEventHandler { - return require('./Libraries/Components/AppleTV/TVEventHandler'); - }, get UIManager(): UIManager { return require('./Libraries/ReactNative/UIManager'); }, diff --git a/package.json b/package.json index 98757ed0375887..433b4c90a32c7b 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,7 @@ "bin": "./cli.js", "description": "A framework for building native apps using React", "license": "MIT", - "repository": { - "type": "git", - "url": "git@github.com:facebook/react-native.git" - }, + "repository": "github:facebook/react-native", "engines": { "node": ">=10" }, @@ -64,6 +61,7 @@ "format": "npm run prettier && npm run clang-format", "prettier": "prettier --write \"./**/*.{js,md,yml}\"", "format-check": "prettier --list-different \"./**/*.{js,md,yml}\"", + "update-lock": "npx yarn-deduplicate", "docker-setup-android": "docker pull reactnativecommunity/react-native-android", "docker-build-android": "docker build -t reactnativeci/android -f .circleci/Dockerfiles/Dockerfile.android .", "test-android-run-instrumentation": "docker run --cap-add=SYS_ADMIN -it reactnativeci/android bash .circleci/Dockerfiles/scripts/run-android-docker-instrumentation-tests.sh", @@ -96,7 +94,7 @@ "base64-js": "^1.1.2", "event-target-shim": "^5.0.1", "fbjs-scripts": "^1.1.0", - "hermes-engine": "~0.6.0", + "hermes-engine": "~0.7.0", "invariant": "^2.2.4", "jsc-android": "^245459.0.0", "metro-babel-register": "0.63.0", @@ -118,7 +116,7 @@ "ws": "^6.1.4" }, "devDependencies": { - "flow-bin": "^0.132.0", + "flow-bin": "^0.134.0", "react": "16.13.1" }, "detox": { @@ -129,7 +127,7 @@ "android.emu.release": { "binaryPath": "packages/rn-tester/android/app/build/outputs/apk/hermes/release/app-hermes-x86-release.apk", "testBinaryPath": "packages/rn-tester/android/app/build/outputs/apk/androidTest/hermes/release/app-hermes-release-androidTest.apk", - "build": "./gradlew :packages:rn-tester:android:app:assembleRelease RNTester:android:app:assembleAndroidTest -DtestBuildType=release", + "build": "./gradlew :packages:rn-tester:android:app:assembleRelease :packages:rn-tester:android:app:assembleAndroidTest -DtestBuildType=release", "type": "android.emulator", "device": { "avdName": "Nexus_6_API_29" @@ -138,7 +136,7 @@ "android.emu.debug": { "binaryPath": "packages/rn-tester/android/app/build/outputs/apk/hermes/debug/app-hermes-x86-debug.apk", "testBinaryPath": "packages/rn-tester/android/app/build/outputs/apk/androidTest/hermes/debug/app-hermes-debug-androidTest.apk", - "build": "./gradlew :packages:rn-tester:android:app:assembleDebug RNTester:android:app:assembleAndroidTest -DtestBuildType=debug", + "build": "./gradlew :packages:rn-tester:android:app:assembleDebug :packages:rn-tester:android:app:assembleAndroidTest -DtestBuildType=debug", "type": "android.emulator", "device": { "avdName": "Nexus_6_API_29" diff --git a/packages/assets/package.json b/packages/assets/package.json index 13064909c99bb9..885e7175338867 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -4,7 +4,8 @@ "description": "Asset support code for React Native.", "repository": { "type": "git", - "url": "git@github.com:facebook/react-native.git" + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/assets" }, "license": "MIT" } diff --git a/packages/babel-plugin-codegen/package.json b/packages/babel-plugin-codegen/package.json index ee2f27f894d54b..5360b6829c8a31 100644 --- a/packages/babel-plugin-codegen/package.json +++ b/packages/babel-plugin-codegen/package.json @@ -4,7 +4,8 @@ "description": "Babel plugin to generate native module and view manager code for React Native.", "repository": { "type": "git", - "url": "git@github.com:facebook/react-native.git" + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/babel-plugin-codegen" }, "dependencies": { "react-native-codegen": "*" diff --git a/packages/eslint-config-react-native-community/package.json b/packages/eslint-config-react-native-community/package.json index a05f990d388fcc..3b8bc5635a3812 100644 --- a/packages/eslint-config-react-native-community/package.json +++ b/packages/eslint-config-react-native-community/package.json @@ -6,7 +6,8 @@ "license": "MIT", "repository": { "type": "git", - "url": "git@github.com:facebook/react-native.git" + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/eslint-config-react-native-community" }, "homepage": "https://github.com/facebook/react-native/tree/master/packages/eslint-config-react-native-community#readme", "dependencies": { diff --git a/packages/eslint-plugin-codegen/BUCK b/packages/eslint-plugin-codegen/BUCK new file mode 100644 index 00000000000000..8fd38289431bdb --- /dev/null +++ b/packages/eslint-plugin-codegen/BUCK @@ -0,0 +1,23 @@ +load("@fbsource//tools/build_defs/third_party:yarn_defs.bzl", "yarn_workspace") + +yarn_workspace( + name = "yarn-workspace", + srcs = glob( + ["**/*.js"], + exclude = [ + "**/__fixtures__/**", + "**/__flowtests__/**", + "**/__mocks__/**", + "**/__server_snapshot_tests__/**", + "**/__tests__/**", + "**/node_modules/**", + "**/node_modules/.bin/**", + "**/.*", + "**/.*/**", + "**/.*/.*", + "**/*.xcodeproj/**", + "**/*.xcworkspace/**", + ], + ), + visibility = ["PUBLIC"], +) diff --git a/packages/eslint-plugin-codegen/__tests__/eslint-tester.js b/packages/eslint-plugin-codegen/__tests__/eslint-tester.js new file mode 100644 index 00000000000000..ea767a705b790f --- /dev/null +++ b/packages/eslint-plugin-codegen/__tests__/eslint-tester.js @@ -0,0 +1,22 @@ +/** + * 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. + * + * @format + */ + +'use strict'; + +const ESLintTester = require('eslint').RuleTester; + +ESLintTester.setDefaultConfig({ + parser: require.resolve('babel-eslint'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, +}); + +module.exports = ESLintTester; diff --git a/packages/eslint-plugin-codegen/__tests__/react-native-modules-test.js b/packages/eslint-plugin-codegen/__tests__/react-native-modules-test.js new file mode 100644 index 00000000000000..8d38ab0e1aff81 --- /dev/null +++ b/packages/eslint-plugin-codegen/__tests__/react-native-modules-test.js @@ -0,0 +1,397 @@ +/** + * 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. + * + * @emails react_native + * @format + */ + +'use strict'; + +const ESLintTester = require('./eslint-tester.js'); + +const rule = require('../react-native-modules'); + +const NATIVE_MODULES_DIR = __dirname; + +const eslintTester = new ESLintTester(); + +const VALID_SPECS = [ + // Standard specification will all supported param types. + { + code: ` +'use strict'; + +import {TurboModuleRegistry, type TurboModule} from 'react-native'; + +export interface Spec extends TurboModule { + func1(): void, + func2(a: number, b: string, c: boolean): void, + func3(a: Object, b: Array, c: () => void): void, + func4(a: ?string): void, + func5(a: ?Object, b: ?Array, c: ?() => void): void, + func6(a: string[], b: ?number[]): void, + func7(): string, + func8(): Object, + func9(): Promise, + func10(): number, + func11(): boolean, + func12(a: {|x: string|}): void, + func13(a: $ReadOnlyArray): void, + func14(): {| + x: number, + y: string, + |}, + a: number, + b: string, + c: {a: number, b: string}, +} + +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + }, + + // With convenience API wrapper + { + code: ` +'use strict'; + +import {TurboModuleRegistry, type TurboModule} from 'react-native'; + +export interface Spec extends TurboModule { + func1(a: Object): void, +} + +const NativeModule = TurboModuleRegistry.get('XYZ'); +const NativeXYZ = { + func1(a?: number): void { + NativeModule.func1(a || {}); + }, +}; +export default NativeXYZ; +`, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + }, + + // Non-spec JS file. + { + code: ` +'use strict'; + +import {Platform} from 'react-native'; + +export default Platform.OS; +`, + }, +]; + +const INVALID_SPECS = [ + // Haste module name doesn't start with "Native" + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(): void, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/XYZ.js`, + errors: [ + { + message: rule.errors.invalidHasteName('XYZ'), + }, + ], + }, + + // Invalid Spec interface name + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Foo extends TurboModule { + func1(): void, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.invalidNativeModuleInterfaceName('Foo'), + }, + { + message: rule.errors.specNotDeclaredInFile(), + }, + ], + output: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(): void, +} +export default TurboModuleRegistry.get('XYZ'); + `, + }, + + // Missing method in Spec + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.missingSpecInterfaceMethod(), + }, + ], + }, + + // Invalid Spec method return type + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(): XYZ, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.unsupportedMethodReturnType('XYZ'), + }, + ], + }, + + // Unsupported Spec property + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + id: Map, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.unsupportedType('Map'), + }, + ], + }, + + // Unsupported nested Spec property + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + a: { + b: number, + c: () => number, + }, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.unsupportedType('Function'), + }, + ], + }, + + // Unsupported Spec method arg type + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +type SomeType = {}; +export interface Spec extends TurboModule { + func1(a: SomeType): void, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.unsupportedType('SomeType'), + }, + ], + }, + + // Unsupported Spec method arg type: optional + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(a?: string): void, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.unsupportedType('optional string'), + }, + ], + }, + + // Unsupported Spec method arg type: unsupported nullable + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +type Foo = {}; +export interface Spec extends TurboModule { + func1(a: ?Foo): void, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.unsupportedType('nullable Foo'), + }, + ], + }, + + // Unsupported Spec method arg type: generic Function + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(a: Function): void, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.unsupportedType('Function'), + }, + ], + }, + + // Unsupported Spec method arg type: nullable generic Function + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(a: ?Function): void, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.unsupportedType('nullable Function'), + }, + ], + }, + + // Spec method object return type must be exact + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(a: string): {}, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.inexactObjectReturnType(), + }, + ], + }, + + // Untyped NativeModule require + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.untypedModuleRequire('get'), + }, + ], + }, + + // Incorrectly typed NativeModule require: 0 types + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export default TurboModuleRegistry.get<>('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.incorrectlyTypedModuleRequire('get'), + }, + ], + }, + + // Incorrectly typed NativeModule require: 1 type, but wrong + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(a: string): {||}, +} + +// According to Flow, this also conforms to TurboModule +type Spec1 = {| + getConstants: () => {...} +|}; + +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.incorrectlyTypedModuleRequire('get'), + }, + ], + }, + + // Incorrectly typed NativeModule require: > 1 type + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +export interface Spec extends TurboModule { + func1(a: string): {||}, +} +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.incorrectlyTypedModuleRequire('get'), + }, + ], + }, + + // NativeModule spec not declared in file + { + code: ` +import {TurboModuleRegistry, type TurboModule} from 'react-native'; +import type {Spec} from 'NativeFoo'; +export default TurboModuleRegistry.get('XYZ'); + `, + filename: `${NATIVE_MODULES_DIR}/NativeXYZ.js`, + errors: [ + { + message: rule.errors.specNotDeclaredInFile(), + }, + ], + }, +]; + +eslintTester.run('../react-native-modules', rule, { + valid: VALID_SPECS, + invalid: INVALID_SPECS, +}); diff --git a/packages/eslint-plugin-codegen/index.js b/packages/eslint-plugin-codegen/index.js new file mode 100644 index 00000000000000..60a5f864f7ac42 --- /dev/null +++ b/packages/eslint-plugin-codegen/index.js @@ -0,0 +1,19 @@ +/** + * 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. + * + * @emails react_native + * @format + */ + +'use strict'; + +const reactNativeModules = require('./react-native-modules'); + +module.exports = { + rules: { + 'react-native-modules': reactNativeModules, + }, +}; diff --git a/packages/eslint-plugin-codegen/package.json b/packages/eslint-plugin-codegen/package.json new file mode 100644 index 00000000000000..bc2b272dfe81af --- /dev/null +++ b/packages/eslint-plugin-codegen/package.json @@ -0,0 +1,12 @@ +{ + "name": "@react-native/eslint-plugin-codegen", + "version": "0.0.1", + "description": "ESLint rules to validate NativeModule and Component Specs", + "main": "index.js", + "repository": { + "type": "git", + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/eslint-plugin-codegen" + }, + "license": "MIT" +} diff --git a/packages/eslint-plugin-codegen/react-native-modules.js b/packages/eslint-plugin-codegen/react-native-modules.js new file mode 100644 index 00000000000000..fd55aca4db660e --- /dev/null +++ b/packages/eslint-plugin-codegen/react-native-modules.js @@ -0,0 +1,467 @@ +/** + * 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. + * + * @emails react_native + * @format + */ + +'use strict'; + +const path = require('path'); + +const supportedTypes = [ + 'ArrayTypeAnnotation', + 'BooleanTypeAnnotation', + 'NumberTypeAnnotation', + 'StringTypeAnnotation', +]; + +const supportedTypeAliases = { + BooleanTypeAnnotation: 'boolean', + FunctionTypeAnnotation: 'Function', + NumberTypeAnnotation: 'number', + StringTypeAnnotation: 'string', + ObjectTypeAnnotation: 'object', +}; + +const supportedGenericTypes = [ + 'Array', + 'Object', + 'RootTag', + '$ReadOnly', + '$ReadOnlyArray', +]; + +const supportedNullableTypes = [ + 'ArrayTypeAnnotation', + 'BooleanTypeAnnotation', + 'FunctionTypeAnnotation', + 'NumberTypeAnnotation', + 'ObjectTypeAnnotation', + 'StringTypeAnnotation', +]; + +const supportedMethodReturnTypes = [ + 'BooleanTypeAnnotation', + 'NumberTypeAnnotation', + 'ObjectTypeAnnotation', + 'StringTypeAnnotation', + 'VoidTypeAnnotation', +]; + +const errors = { + invalidNativeModuleInterfaceName(interfaceName) { + return ( + "NativeModule interfaces must be named 'Spec', " + + `got '${interfaceName}'.` + ); + }, + inexactObjectReturnType() { + return 'Spec interface method object return type must be exact.'; + }, + invalidHasteName(hasteName) { + return ( + 'Module name for a NativeModule JS wrapper must start with ' + + `'Native', got '${hasteName}' instead.` + ); + }, + missingSpecInterfaceMethod() { + return 'NativeModule Spec interface must define at least one method'; + }, + unsupportedMethodReturnType(typeName) { + return ( + `Spec interface method has unsupported return type '${typeName}'. ` + + 'See https://fburl.com/rn-nativemodules for more details.' + ); + }, + unsupportedType(typeName) { + return ( + `Unsupported type '${typeName}' for Spec interface. ` + + 'See https://fburl.com/rn-nativemodules for more details.' + ); + }, + untypedModuleRequire(requireMethodName) { + return ( + 'NativeModule require not type-safe. Please require with the NativeModule interface ' + + `'Spec': TurboModuleRegistry.${requireMethodName}` + ); + }, + incorrectlyTypedModuleRequire(requireMethodName) { + return ( + 'NativeModule require incorrectly typed. Please require with the NativeModule interface identifier ' + + `'Spec', and nothing else: TurboModuleRegistry.${requireMethodName}` + ); + }, + specNotDeclaredInFile() { + return "The NativeModule interface 'Spec' wasn't declared in this NativeModule spec file."; + }, +}; + +function interfaceExtendsFrom(node, superInterfaceName) { + return ( + node.type === 'InterfaceDeclaration' && + node.extends[0] && + node.extends[0].id.name === superInterfaceName + ); +} + +function isSupportedFunctionParam(node) { + if (node.type !== 'FunctionTypeParam') { + return false; + } + + if (node.optional) { + return false; + } + + return findUnsupportedType(node.typeAnnotation, true) == null; +} + +function findUnsupportedType(typeAnnotation, supportCallbacks) { + if (supportedTypes.includes(typeAnnotation.type)) { + return null; + } + + if (typeAnnotation.type === 'NullableTypeAnnotation') { + if (supportedNullableTypes.includes(typeAnnotation.typeAnnotation.type)) { + return null; + } + typeAnnotation = typeAnnotation.typeAnnotation; + } + + if (typeAnnotation.type === 'FunctionTypeAnnotation' && supportCallbacks) { + return null; + } + + if (typeAnnotation.type === 'GenericTypeAnnotation') { + if (!supportedGenericTypes.includes(typeAnnotation.id.name)) { + return typeAnnotation; + } + if ( + !isGenericArrayTypeAnnotation(typeAnnotation) && + typeAnnotation.typeParameters + ) { + for (const param of typeAnnotation.typeParameters.params) { + const unsupported = findUnsupportedType(param, supportCallbacks); + if (unsupported != null) { + return unsupported; + } + } + } + return null; + } + + if (typeAnnotation.type === 'ObjectTypeAnnotation') { + for (const prop of typeAnnotation.properties) { + const unsupported = findUnsupportedType(prop.value, supportCallbacks); + if (unsupported != null) { + return unsupported; + } + } + return null; + } + + return typeAnnotation; +} + +function functionParamTypeName(node) { + if (node.type !== 'FunctionTypeParam') { + return null; + } + + const parts = []; + if (node.optional) { + parts.push('optional'); + } + parts.push(functionParamTypeAnnotationName(node.typeAnnotation)); + return parts.join(' '); +} + +function functionParamTypeAnnotationName(typeAnnotation) { + const {id, type} = typeAnnotation; + if (type === 'GenericTypeAnnotation') { + return id.name; + } + + const parts = []; + if (type === 'NullableTypeAnnotation') { + parts.push('nullable'); + parts.push(functionParamTypeAnnotationName(typeAnnotation.typeAnnotation)); + } else { + parts.push(supportedTypeAliases[type] || type); + } + return parts.join(' '); +} + +function getTypeName(typeAnnotation) { + const {id, type} = typeAnnotation; + if (type === 'GenericTypeAnnotation') { + return id.name; + } + return supportedTypeAliases[type]; +} + +function checkSupportedSpecProperty(context, node) { + if (node.type !== 'FunctionTypeAnnotation') { + const unsupportedNode = findUnsupportedType(node, false); + if (unsupportedNode != null) { + context.report({ + node: unsupportedNode, + message: errors.unsupportedType( + functionParamTypeAnnotationName(unsupportedNode), + ), + }); + return false; + } + return true; + } + + if (!isSupportedMethodReturnTypeAnnotation(node.returnType)) { + context.report({ + node: node.returnType, + message: errors.unsupportedMethodReturnType(getTypeName(node.returnType)), + }); + return false; + } + + // Check for exact object return type. + if ( + node.returnType.type === 'ObjectTypeAnnotation' && + !node.returnType.exact + ) { + context.report({ + node: node.returnType, + message: errors.inexactObjectReturnType(), + }); + } + + for (const param of node.params) { + if (!isSupportedFunctionParam(param)) { + context.report({ + node: param.typeAnnotation, + message: errors.unsupportedType(functionParamTypeName(param)), + }); + return false; + } + } + + return true; +} + +function isPromiseTypeAnnotation(typeAnnotation) { + return ( + typeAnnotation.type === 'GenericTypeAnnotation' && + typeAnnotation.id && + typeAnnotation.id.name === 'Promise' + ); +} + +function isGenericArrayTypeAnnotation(typeAnnotation) { + return ( + typeAnnotation.type === 'GenericTypeAnnotation' && + typeAnnotation.id && + typeAnnotation.id.name === 'Array' + ); +} + +function isGenericObjectTypeAnnotation(typeAnnotation) { + return ( + typeAnnotation.type === 'GenericTypeAnnotation' && + typeAnnotation.id && + typeAnnotation.id.name === 'Object' + ); +} + +function isSupportedMethodReturnTypeAnnotation(typeAnnotation) { + const resolvedType = + typeAnnotation.type === 'NullableTypeAnnotation' + ? typeAnnotation.typeAnnotation + : typeAnnotation; + return ( + supportedMethodReturnTypes.includes(resolvedType.type) || + isGenericArrayTypeAnnotation(resolvedType) || + isGenericObjectTypeAnnotation(resolvedType) || + isPromiseTypeAnnotation(resolvedType) + ); +} + +const VALID_SPEC_NAMES = /^Native\S+$/; + +function isModuleRequire(node) { + if (node.type !== 'CallExpression') { + return false; + } + + const callExpression = node; + + if (callExpression.callee.type !== 'MemberExpression') { + return false; + } + + const memberExpression = callExpression.callee; + if ( + !( + memberExpression.object.type === 'Identifier' && + memberExpression.object.name === 'TurboModuleRegistry' + ) + ) { + return false; + } + + if ( + !( + memberExpression.property.type === 'Identifier' && + (memberExpression.property.name === 'get' || + memberExpression.property.name === 'getEnforcing') + ) + ) { + return false; + } + return true; +} + +function isGeneratedFile(context) { + return ( + context + .getSourceCode() + .getText() + .indexOf('@' + 'generated SignedSource<<') !== -1 + ); +} + +/** + * A lint rule to guide best practices in writing type safe React NativeModules. + */ +function rule(context) { + const filename = context.getFilename(); + + if (isGeneratedFile(context)) { + return {}; + } + + const sourceCode = context.getSourceCode().getText(); + if (!sourceCode.includes('TurboModuleRegistry')) { + return {}; + } + + const specIdentifierUsages = []; + const declaredModuleInterfaces = []; + + return { + 'Program:exit': function() { + if ( + specIdentifierUsages.length > 0 && + declaredModuleInterfaces.length === 0 + ) { + specIdentifierUsages.forEach(specNode => { + context.report({ + node: specNode, + message: errors.specNotDeclaredInFile(), + }); + }); + } + }, + CallExpression(node) { + if (!isModuleRequire(node)) { + return; + } + + /** + * Validate that NativeModule requires are typed + */ + + const {typeArguments} = node; + + if (typeArguments == null) { + const methodName = node.callee.property.name; + context.report({ + node, + message: errors.untypedModuleRequire(methodName), + }); + return; + } + + if (typeArguments.type !== 'TypeParameterInstantiation') { + return; + } + + const [param] = typeArguments.params; + + /** + * Validate that NativeModule requires are correctly typed + */ + + if ( + typeArguments.params.length !== 1 || + param.type !== 'GenericTypeAnnotation' || + param.id.name !== 'Spec' + ) { + const methodName = node.callee.property.name; + context.report({ + node, + message: errors.incorrectlyTypedModuleRequire(methodName), + }); + return; + } + + specIdentifierUsages.push(param); + return true; + }, + InterfaceDeclaration(node) { + if ( + !interfaceExtendsFrom(node, 'DEPRECATED_RCTExport') && + !interfaceExtendsFrom(node, 'TurboModule') + ) { + return; + } + + const basename = path.basename(filename, '.js'); + if ( + basename && + basename !== 'RCTExport' && + !VALID_SPEC_NAMES.test(basename) + ) { + context.report({ + loc: {start: {line: 0, column: 0}}, + message: errors.invalidHasteName(basename), + }); + } + + if (node.id.name !== 'Spec') { + context.report({ + node, + message: errors.invalidNativeModuleInterfaceName(node.id.name), + fix: fixer => fixer.replaceText(node.id, 'Spec'), + }); + return; + } + + declaredModuleInterfaces.push(node); + + if (!node.body.properties.length) { + context.report({ + node: node.body, + message: errors.missingSpecInterfaceMethod(), + }); + return; + } + + let hasUnsupportedProp = false; + node.body.properties.forEach(prop => { + if (hasUnsupportedProp) { + return; + } + if (!checkSupportedSpecProperty(context, prop.value)) { + hasUnsupportedProp = true; + } + }); + }, + }; +} + +rule.errors = errors; + +module.exports = rule; diff --git a/packages/eslint-plugin-react-native-community/package.json b/packages/eslint-plugin-react-native-community/package.json index dad503b7c0e447..947d62498461b2 100644 --- a/packages/eslint-plugin-react-native-community/package.json +++ b/packages/eslint-plugin-react-native-community/package.json @@ -5,7 +5,8 @@ "main": "index.js", "repository": { "type": "git", - "url": "git@github.com:facebook/react-native.git" + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/eslint-plugin-react-native-community" }, "license": "MIT" } diff --git a/packages/normalize-color/package.json b/packages/normalize-color/package.json index a9fd8fcef1b23d..9ff2e8a0733d9f 100644 --- a/packages/normalize-color/package.json +++ b/packages/normalize-color/package.json @@ -4,7 +4,8 @@ "description": "Color normalization code for React Native.", "repository": { "type": "git", - "url": "git@github.com:facebook/react-native.git" + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/normalize-color" }, "license": "MIT" } diff --git a/packages/polyfills/package.json b/packages/polyfills/package.json index ed96b9f671dea5..6f00c64c7fca72 100644 --- a/packages/polyfills/package.json +++ b/packages/polyfills/package.json @@ -4,7 +4,8 @@ "description": "Polyfills for React Native.", "repository": { "type": "git", - "url": "git@github.com:facebook/react-native.git" + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/polyfills" }, "license": "MIT" } diff --git a/packages/react-native-codegen/DEFS.bzl b/packages/react-native-codegen/DEFS.bzl index da49dac7fbdb32..33abd24eeaffe5 100644 --- a/packages/react-native-codegen/DEFS.bzl +++ b/packages/react-native-codegen/DEFS.bzl @@ -383,7 +383,7 @@ def rn_codegen_cxx_modules( ], visibility = ["PUBLIC"], exported_deps = [ - react_native_xplat_target("turbomodule/core:core"), + react_native_xplat_target("react/nativemodule/core:core"), ], ) diff --git a/packages/react-native-codegen/android/.gitattributes b/packages/react-native-codegen/android/.gitattributes deleted file mode 100644 index 00a51aff5e5a83..00000000000000 --- a/packages/react-native-codegen/android/.gitattributes +++ /dev/null @@ -1,6 +0,0 @@ -# -# https://help.github.com/articles/dealing-with-line-endings/ -# -# These are explicitly windows files and should use crlf -*.bat text eol=crlf - diff --git a/packages/react-native-codegen/android/.gitignore b/packages/react-native-codegen/android/.gitignore deleted file mode 100644 index 1b6985c0094c8e..00000000000000 --- a/packages/react-native-codegen/android/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Ignore Gradle project-specific cache directory -.gradle - -# Ignore Gradle build output directory -build diff --git a/packages/react-native-codegen/android/generator/build.gradle b/packages/react-native-codegen/android/generator/build.gradle deleted file mode 100644 index 1ba58144a415db..00000000000000 --- a/packages/react-native-codegen/android/generator/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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. - */ - -plugins { - id 'java' - id 'application' -} - -dependencies { - implementation 'com.squareup:javapoet:1.13.0' -} - -application { - mainClassName = 'com.facebook.react.codegen.JavaGeneratorMain' -} diff --git a/packages/react-native-codegen/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-codegen/android/gradle/wrapper/gradle-wrapper.properties index 6c9a2247756f21..bca17f36566b30 100644 --- a/packages/react-native-codegen/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/react-native-codegen/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/generator/JavaGenerator.java b/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/generator/JavaGenerator.java index ee2a968bd23011..c0abf23bbb7f24 100644 --- a/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/generator/JavaGenerator.java +++ b/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/generator/JavaGenerator.java @@ -84,7 +84,7 @@ private final void writeTypeSpecToFile(final TypeSpec typeSpec) // Instead of using JavaFile.writeTo() API, manage the output files ourselves because // JavaFile.addFileComment() does not support "block comment" style. // See https://github.com/square/javapoet/issues/682#issuecomment-512238075. - Path outputDirPath = mOutputDir.toPath(); + Path outputDirPath = new File(mOutputDir, "java").toPath(); if (Files.exists(outputDirPath) && !Files.isDirectory(outputDirPath)) { throw new CodegenException( diff --git a/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/plugin/CodegenPlugin.java b/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/plugin/CodegenPlugin.java index 4ea0a13e09f6f3..1d58a2e561259d 100644 --- a/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/plugin/CodegenPlugin.java +++ b/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/plugin/CodegenPlugin.java @@ -25,6 +25,12 @@ public class CodegenPlugin implements Plugin { public void apply(final Project project) { + // This flag should have been defined in CodegenPluginExtension, but the extension values + // resolution is pending project full evaluation. Given that no codegen actual task should + // be defined if the flag is not enabled, read directly from env var here. + final String useCodegenVar = System.getenv("USE_CODEGEN"); + final boolean enableCodegen = + useCodegenVar != null && (Boolean.parseBoolean(useCodegenVar) || useCodegenVar.equals("1")); final CodegenPluginExtension extension = project.getExtensions().create("react", CodegenPluginExtension.class, project); @@ -39,7 +45,8 @@ public void apply(final Project project) { "generateCodegenSchemaFromJavaScript", Exec.class, task -> { - if (!extension.enableCodegen) { + if (!enableCodegen) { + task.commandLine("echo", "Skipping: not using react-native-codegen."); return; } @@ -79,53 +86,45 @@ public void apply(final Project project) { "generateCodegenArtifactsFromSchema", Exec.class, task -> { - if (!extension.enableCodegen) { + if (!enableCodegen) { + task.commandLine("echo", "Skipping: not using react-native-codegen."); return; } task.dependsOn("generateCodegenSchemaFromJavaScript"); - // TODO: The codegen tool should produce this outputDir structure based on - // the provided Java package name. - File outputDir = - new File( - generatedSrcDir, - "java/" + extension.codegenJavaPackageName.replace(".", "/")); - task.getInputs() .files(project.fileTree(ImmutableMap.of("dir", extension.codegenDir()))); task.getInputs().files(generatedSchemaFile); - task.getOutputs().dir(outputDir); + task.getOutputs().dir(generatedSrcDir); if (extension.useJavaGenerator) { task.doLast( s -> { generateJavaFromSchemaWithJavaGenerator( - generatedSchemaFile, - extension.codegenJavaPackageName, - new File(generatedSrcDir, "java")); + generatedSchemaFile, extension.codegenJavaPackageName, generatedSrcDir); }); - // TODO: generate JNI C++ files. - task.commandLine("echo"); - } else { - ImmutableList execCommands = - new ImmutableList.Builder() - .add("yarn") - .addAll(ImmutableList.copyOf(extension.nodeExecutableAndArgs)) - .add(extension.codegenGenerateNativeModuleSpecsCLI().getAbsolutePath()) - .add("android") - .add(generatedSchemaFile.getAbsolutePath()) - .add(outputDir.getAbsolutePath()) - .build(); - task.commandLine(execCommands); } + + ImmutableList execCommands = + new ImmutableList.Builder() + .add("yarn") + .addAll(ImmutableList.copyOf(extension.nodeExecutableAndArgs)) + .add(extension.codegenGenerateNativeModuleSpecsCLI().getAbsolutePath()) + .add("android") + .add(generatedSchemaFile.getAbsolutePath()) + .add(generatedSrcDir.getAbsolutePath()) + .add(extension.libraryName) + .add(extension.codegenJavaPackageName) + .build(); + task.commandLine(execCommands); }); // 4. Add dependencies & generated sources to the project. // Note: This last step needs to happen after the project has been evaluated. project.afterEvaluate( s -> { - if (!extension.enableCodegen) { + if (!enableCodegen) { return; } @@ -151,7 +150,6 @@ public void apply(final Project project) { .getByName("main") .getJava() .srcDir(new File(generatedSrcDir, "java")); - // TODO: Add JNI sources. }); } diff --git a/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/plugin/CodegenPluginExtension.java b/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/plugin/CodegenPluginExtension.java index 658230bd459337..fe540dd353dc92 100644 --- a/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/plugin/CodegenPluginExtension.java +++ b/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/src/main/java/com/facebook/react/codegen/plugin/CodegenPluginExtension.java @@ -7,20 +7,23 @@ package com.facebook.react.codegen.plugin; +import com.google.common.base.CaseFormat; import java.io.File; +import java.util.StringTokenizer; import org.gradle.api.Project; public class CodegenPluginExtension { // TODO: Remove beta. public String codegenJavaPackageName = "com.facebook.fbreact.specs.beta"; - public boolean enableCodegen = false; public File jsRootDir; + public String libraryName; public String[] nodeExecutableAndArgs = {"node"}; public File reactNativeRootDir; public boolean useJavaGenerator = false; public CodegenPluginExtension(final Project project) { this.reactNativeRootDir = new File(project.getRootDir(), "node_modules/react-native"); + this.libraryName = projectPathToLibraryName(project.getPath()); } public File codegenDir() { @@ -34,4 +37,16 @@ public File codegenGenerateSchemaCLI() { public File codegenGenerateNativeModuleSpecsCLI() { return new File(this.reactNativeRootDir, "scripts/generate-native-modules-specs-cli.js"); } + + private String projectPathToLibraryName(final String projectPath) { + final StringTokenizer tokenizer = new StringTokenizer(projectPath, ":-_."); + final StringBuilder nameBuilder = new StringBuilder(); + + while (tokenizer.hasMoreTokens()) { + nameBuilder.append(CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, tokenizer.nextToken())); + } + nameBuilder.append("Spec"); + + return nameBuilder.toString(); + } } diff --git a/packages/react-native-codegen/package.json b/packages/react-native-codegen/package.json index a68d170a740d1c..6f6cac7bb3599f 100644 --- a/packages/react-native-codegen/package.json +++ b/packages/react-native-codegen/package.json @@ -5,7 +5,8 @@ "homepage": "https://github.com/facebook/react-native/tree/master/packages/react-native-codegen", "repository": { "type": "git", - "url": "git@github.com:facebook/react-native.git" + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/react-native-codegen" }, "scripts": { "build": "yarn clean && node scripts/build.js --verbose", diff --git a/packages/react-native-codegen/src/generators/RNCodegen.js b/packages/react-native-codegen/src/generators/RNCodegen.js index ce6221db60e3ee..576d984abd8d71 100644 --- a/packages/react-native-codegen/src/generators/RNCodegen.js +++ b/packages/react-native-codegen/src/generators/RNCodegen.js @@ -27,6 +27,8 @@ const generateModuleH = require('./modules/GenerateModuleH.js'); const generateModuleCpp = require('./modules/GenerateModuleCpp.js'); const generateModuleHObjCpp = require('./modules/GenerateModuleHObjCpp.js'); const generateModuleJavaSpec = require('./modules/GenerateModuleJavaSpec.js'); +const GenerateModuleJniCpp = require('./modules/GenerateModuleJniCpp.js'); +const GenerateModuleJniH = require('./modules/GenerateModuleJniH.js'); const generateModuleMm = require('./modules/GenerateModuleMm.js'); const generatePropsJavaInterface = require('./components/GeneratePropsJavaInterface.js'); const generatePropsJavaDelegate = require('./components/GeneratePropsJavaDelegate.js'); @@ -74,7 +76,11 @@ const GENERATORS = { generateModuleH.generate, generateModuleHObjCpp.generate, generateModuleMm.generate, - // TODO: Java output and the C++ output need to be separated. + ], + // TODO: Refactor this to consolidate various C++ output variation instead of forking Android. + modulesAndroid: [ + GenerateModuleJniCpp.generate, + GenerateModuleJniH.generate, generateModuleJavaSpec.generate, ], tests: [generateTests.generate], diff --git a/packages/react-native-codegen/src/generators/components/GenerateTests.js b/packages/react-native-codegen/src/generators/components/GenerateTests.js index 06c953d49eb8f2..5d7a5af846a56a 100644 --- a/packages/react-native-codegen/src/generators/components/GenerateTests.js +++ b/packages/react-native-codegen/src/generators/components/GenerateTests.js @@ -75,6 +75,7 @@ function getTestCasesForProp(propName, typeAnnotation) { propName: propName, propValue: typeAnnotation.default != null ? typeAnnotation.default : true, }); + // $FlowFixMe[incompatible-type] } else if (typeAnnotation.type === 'IntegerTypeAnnotation') { cases.push({ propName, diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js new file mode 100644 index 00000000000000..aac88138b51f45 --- /dev/null +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -0,0 +1,349 @@ +/** + * 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. + * + * @flow strict + * @format + */ + +'use strict'; + +import type { + FunctionTypeAnnotationParam, + FunctionTypeAnnotationReturn, + NativeModuleShape, + ObjectTypeAliasTypeShape, + SchemaType, +} from '../../CodegenSchema'; + +const {getTypeAliasTypeAnnotation} = require('./Utils'); + +type FilesOutput = Map; + +const propertyHeaderTemplate = + 'static facebook::jsi::Value __hostFunction_Native::_MODULE_NAME_::SpecJSI_::_PROPERTY_NAME_::(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {'; + +const propertyCastTemplate = + 'static_cast(turboModule).invokeJavaMethod(rt, ::_KIND_::, "::_PROPERTY_NAME_::", "::_SIGNATURE_::", args, count);'; + +const propertyTemplate = ` +${propertyHeaderTemplate} + return ${propertyCastTemplate} +}`; + +const propertyDefTemplate = + ' methodMap_["::_PROPERTY_NAME_::"] = MethodMetadata {::_ARGS_COUNT_::, __hostFunction_Native::_MODULE_NAME_::SpecJSI_::_PROPERTY_NAME_::};'; + +const moduleTemplate = ` +::_TURBOMODULE_METHOD_INVOKERS_:: + +Native::_MODULE_NAME_::SpecJSI::Native::_MODULE_NAME_::SpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { +::_PROPERTIES_MAP_:: +}`.trim(); + +const oneModuleLookupTemplate = ` if (moduleName == "::_MODULE_NAME_::") { + return std::make_shared(params); + }`; + +const template = ` +/** + * 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. + * + * ${'@'}generated by codegen project: GenerateModuleJniCpp.js + */ + +#include ::_INCLUDE_:: + +namespace facebook { +namespace react { + +::_MODULES_:: + +std::shared_ptr ::_LIBRARY_NAME_::_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { +::_MODULE_LOOKUP_:: + return nullptr; +} + +} // namespace react +} // namespace facebook +`; + +function translateReturnTypeToKind( + typeAnnotation: FunctionTypeAnnotationReturn, +): string { + switch (typeAnnotation.type) { + case 'ReservedFunctionValueTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return 'NumberKind'; + default: + (typeAnnotation.name: empty); + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${typeAnnotation.name}`, + ); + } + case 'VoidTypeAnnotation': + return 'VoidKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + case 'BooleanTypeAnnotation': + return 'BooleanKind'; + case 'NumberTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'Int32TypeAnnotation': + return 'NumberKind'; + case 'GenericPromiseTypeAnnotation': + return 'PromiseKind'; + case 'GenericObjectTypeAnnotation': + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'ArrayTypeAnnotation': + return 'ArrayKind'; + default: + // TODO (T65847278): Figure out why this does not work. + // (typeAnnotation.type: empty); + throw new Error( + `Unknown prop type for returning value, found: ${typeAnnotation.type}"`, + ); + } +} + +function translateParamTypeToJniType( + param: FunctionTypeAnnotationParam, + aliases: $ReadOnly<{[aliasName: string]: ObjectTypeAliasTypeShape, ...}>, +): string { + const {nullable, typeAnnotation} = param; + + const realTypeAnnotation = + typeAnnotation.type === 'TypeAliasTypeAnnotation' + ? getTypeAliasTypeAnnotation(typeAnnotation.name, aliases) + : typeAnnotation; + + switch (realTypeAnnotation.type) { + case 'ReservedFunctionValueTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return 'D'; + default: + (realTypeAnnotation.name: empty); + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${realTypeAnnotation.name}`, + ); + } + case 'VoidTypeAnnotation': + return 'V'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + case 'BooleanTypeAnnotation': + return nullable ? 'Ljava/lang/Boolean' : 'Z'; + case 'NumberTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'Int32TypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'GenericPromiseTypeAnnotation': + return 'Lcom/facebook/react/bridge/Promise;'; + case 'GenericObjectTypeAnnotation': + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableMap;'; + case 'ArrayTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableArray;'; + case 'FunctionTypeAnnotation': + return 'Lcom/facebook/react/bridge/Callback;'; + default: + throw new Error( + `Unknown prop type for method arg, found: ${realTypeAnnotation.type}"`, + ); + } +} + +function translateReturnTypeToJniType( + typeAnnotation: FunctionTypeAnnotationReturn, +): string { + const {nullable} = typeAnnotation; + + switch (typeAnnotation.type) { + case 'ReservedFunctionValueTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return 'D'; + default: + (typeAnnotation.name: empty); + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${typeAnnotation.name}`, + ); + } + case 'VoidTypeAnnotation': + return 'V'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + case 'BooleanTypeAnnotation': + return nullable ? 'Ljava/lang/Boolean' : 'Z'; + case 'NumberTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'Int32TypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'GenericPromiseTypeAnnotation': + return 'Lcom/facebook/react/bridge/Promise;'; + case 'GenericObjectTypeAnnotation': + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableMap;'; + case 'ArrayTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableArray;'; + default: + throw new Error( + `Unknown prop type for method return type, found: ${typeAnnotation.type}"`, + ); + } +} + +function translateMethodTypeToJniSignature( + property, + aliases: $ReadOnly<{[aliasName: string]: ObjectTypeAliasTypeShape, ...}>, +): string { + const {name, typeAnnotation} = property; + const {returnTypeAnnotation} = typeAnnotation; + + const params = [...typeAnnotation.params]; + let processedReturnTypeAnnotation = returnTypeAnnotation; + const isPromiseReturn = + returnTypeAnnotation.type === 'GenericPromiseTypeAnnotation'; + if (isPromiseReturn) { + processedReturnTypeAnnotation = { + nullable: false, + type: 'VoidTypeAnnotation', + }; + } + + const argsSignatureParts = params.map(t => { + return translateParamTypeToJniType(t, aliases); + }); + if (isPromiseReturn) { + // Additional promise arg for this case. + argsSignatureParts.push(translateReturnTypeToJniType(returnTypeAnnotation)); + } + const argsSignature = argsSignatureParts.join(''); + const returnSignature = + name === 'getConstants' + ? 'Ljava/util/Map;' + : translateReturnTypeToJniType(processedReturnTypeAnnotation); + + return `(${argsSignature})${returnSignature}`; +} + +function translateMethodForImplementation( + property, + aliases: $ReadOnly<{[aliasName: string]: ObjectTypeAliasTypeShape, ...}>, +): string { + const {returnTypeAnnotation} = property.typeAnnotation; + + const numberOfParams = + property.typeAnnotation.params.length + + (returnTypeAnnotation.type === 'GenericPromiseTypeAnnotation' ? 1 : 0); + const translatedArguments = property.typeAnnotation.params + .map(param => param.name) + .concat( + returnTypeAnnotation.type === 'GenericPromiseTypeAnnotation' + ? ['promise'] + : [], + ) + .slice(1) + .join(':') + .concat(':'); + if ( + property.name === 'getConstants' && + returnTypeAnnotation.type === 'ObjectTypeAnnotation' && + returnTypeAnnotation.properties && + returnTypeAnnotation.properties.length === 0 + ) { + return ''; + } + return propertyTemplate + .replace(/::_KIND_::/g, translateReturnTypeToKind(returnTypeAnnotation)) + .replace(/::_PROPERTY_NAME_::/g, property.name) + .replace( + /::_ARGS_::/g, + numberOfParams === 0 + ? '' + : (numberOfParams === 1 ? '' : ':') + translatedArguments, + ) + .replace( + /::_SIGNATURE_::/g, + translateMethodTypeToJniSignature(property, aliases), + ); +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + moduleSpecName: string, + ): FilesOutput { + const nativeModules: {[name: string]: NativeModuleShape, ...} = Object.keys( + schema.modules, + ) + .map(moduleName => { + const modules = schema.modules[moduleName].nativeModules; + if (modules == null) { + return null; + } + + return modules; + }) + .filter(Boolean) + .reduce((acc, modules) => Object.assign(acc, modules), {}); + + const modules = Object.keys(nativeModules) + .map(name => { + const {aliases, properties} = nativeModules[name]; + const translatedMethods = properties + .map(property => translateMethodForImplementation(property, aliases)) + .join('\n'); + return moduleTemplate + .replace(/::_TURBOMODULE_METHOD_INVOKERS_::/g, translatedMethods) + .replace( + '::_PROPERTIES_MAP_::', + properties + .map( + ({ + name: propertyName, + typeAnnotation: {params, returnTypeAnnotation}, + }) => + propertyName === 'getConstants' && + returnTypeAnnotation.type === 'ObjectTypeAnnotation' && + returnTypeAnnotation.properties && + returnTypeAnnotation.properties.length === 0 + ? '' + : propertyDefTemplate + .replace(/::_PROPERTY_NAME_::/g, propertyName) + .replace(/::_ARGS_COUNT_::/g, params.length.toString()), + ) + .join('\n'), + ) + .replace(/::_MODULE_NAME_::/g, name); + }) + .join('\n'); + + const moduleLookup = Object.keys(nativeModules) + .map(name => { + return oneModuleLookupTemplate.replace(/::_MODULE_NAME_::/g, name); + }) + .join('\n'); + + const fileName = `${moduleSpecName}-generated.cpp`; + const replacedTemplate = template + .replace(/::_MODULES_::/g, modules) + .replace(/::_LIBRARY_NAME_::/g, libraryName) + .replace(/::_MODULE_LOOKUP_::/g, moduleLookup) + .replace(/::_INCLUDE_::/g, `"${moduleSpecName}.h"`); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniH.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniH.js new file mode 100644 index 00000000000000..4d9a5aac0cf688 --- /dev/null +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniH.js @@ -0,0 +1,119 @@ +/** + * 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. + * + * @flow strict + * @format + */ + +'use strict'; + +import type {SchemaType} from '../../CodegenSchema'; + +type FilesOutput = Map; + +const moduleTemplate = `/** + * JNI C++ class for module '::_MODULE_NAME_::' + */ +class JSI_EXPORT Native::_MODULE_NAME_::SpecJSI : public JavaTurboModule { +public: + Native::_MODULE_NAME_::SpecJSI(const JavaTurboModule::InitParams ¶ms); +}; +`; + +const template = `/** + * 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. + * + * ${'@'}generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +::_MODULES_:: + +std::shared_ptr ::_LIBRARY_NAME_::_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook +`; + +const androidMkTemplate = `# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := ::_LIBRARY_NAME_:: + +LOCAL_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SHARED_LIBRARIES := libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES := libjsi + +LOCAL_CFLAGS := \\ + -DLOG_TAG=\\"ReactNative\\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) +`; + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + moduleSpecName: string, + ): FilesOutput { + const nativeModules = Object.keys(schema.modules) + .sort() + .map(moduleName => { + const modules = schema.modules[moduleName].nativeModules; + if (modules == null) { + return null; + } + + return modules; + }) + .filter(Boolean) + .reduce((acc, components) => Object.assign(acc, components), {}); + + const modules = Object.keys(nativeModules) + .map(name => moduleTemplate.replace(/::_MODULE_NAME_::/g, name)) + .join('\n'); + + const fileName = `${moduleSpecName}.h`; + const replacedTemplate = template + .replace(/::_MODULES_::/g, modules) + .replace(/::_LIBRARY_NAME_::/g, libraryName); + return new Map([ + [fileName, replacedTemplate], + [ + 'Android.mk', + androidMkTemplate.replace( + /::_LIBRARY_NAME_::/g, + `react_codegen_${libraryName.toLowerCase()}`, + ), + ], + ]); + }, +}; diff --git a/packages/react-native-codegen/src/generators/modules/Utils.js b/packages/react-native-codegen/src/generators/modules/Utils.js index ee20099e68d909..8ce5c95416f610 100644 --- a/packages/react-native-codegen/src/generators/modules/Utils.js +++ b/packages/react-native-codegen/src/generators/modules/Utils.js @@ -29,6 +29,7 @@ function getTypeAliasTypeAnnotation( `Unsupported type for "${name}". Please provide properties.`, ); } + // $FlowFixMe[incompatible-type] if (typeAnnotation.type === 'TypeAliasTypeAnnotation') { return getTypeAliasTypeAnnotation(typeAnnotation.name, aliases); } diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js new file mode 100644 index 00000000000000..e6030bca0fc2d1 --- /dev/null +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js @@ -0,0 +1,29 @@ +/** + * 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. + * + * @emails oncall+react_native + * @flow strict-local + * @format + */ + +'use strict'; + +const fixtures = require('../__test_fixtures__/fixtures.js'); +const generator = require('../GenerateModuleJniCpp.js'); + +describe('GenerateModuleJniCpp', () => { + Object.keys(fixtures) + .sort() + .forEach(fixtureName => { + const fixture = fixtures[fixtureName]; + + it(`can generate fixture ${fixtureName}`, () => { + expect( + generator.generate(fixtureName, fixture, 'SampleSpec'), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniH-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniH-test.js new file mode 100644 index 00000000000000..dcbd78a42c604d --- /dev/null +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniH-test.js @@ -0,0 +1,29 @@ +/** + * 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. + * + * @emails oncall+react_native + * @flow strict-local + * @format + */ + +'use strict'; + +const fixtures = require('../__test_fixtures__/fixtures.js'); +const generator = require('../GenerateModuleJniH.js'); + +describe('GenerateModuleJniH', () => { + Object.keys(fixtures) + .sort() + .forEach(fixtureName => { + const fixture = fixtures[fixtureName]; + + it(`can generate fixture ${fixtureName}`, () => { + expect( + generator.generate(fixtureName, fixture, 'SampleSpec'), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap new file mode 100644 index 00000000000000..47f3a3168ef989 --- /dev/null +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap @@ -0,0 +1,427 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenerateModuleJniCpp can generate fixture COMPLEX_OBJECTS 1`] = ` +Map { + "SampleSpec-generated.cpp" => " +/** + * 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. + * + * @generated by codegen project: GenerateModuleJniCpp.js + */ + +#include \\"SampleSpec.h\\" + +namespace facebook { +namespace react { + + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_difficult(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, ObjectKind, \\"difficult\\", \\"(Lcom/facebook/react/bridge/ReadableMap;)Lcom/facebook/react/bridge/WritableMap;\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_optionals(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"optionals\\", \\"(Lcom/facebook/react/bridge/ReadableMap;)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_optionalMethod(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"optionalMethod\\", \\"(Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Callback;Lcom/facebook/react/bridge/ReadableArray;)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getArrays(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"getArrays\\", \\"(Lcom/facebook/react/bridge/ReadableMap;)V\\", args, count); +} + +NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + methodMap_[\\"difficult\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_difficult}; + methodMap_[\\"optionals\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_optionals}; + methodMap_[\\"optionalMethod\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleSpecJSI_optionalMethod}; + methodMap_[\\"getArrays\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getArrays}; +} + +std::shared_ptr COMPLEX_OBJECTS_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { + if (moduleName == \\"SampleTurboModule\\") { + return std::make_shared(params); + } + return nullptr; +} + +} // namespace react +} // namespace facebook +", +} +`; + +exports[`GenerateModuleJniCpp can generate fixture EMPTY_NATIVE_MODULES 1`] = ` +Map { + "SampleSpec-generated.cpp" => " +/** + * 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. + * + * @generated by codegen project: GenerateModuleJniCpp.js + */ + +#include \\"SampleSpec.h\\" + +namespace facebook { +namespace react { + + + +NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + +} + +std::shared_ptr EMPTY_NATIVE_MODULES_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { + if (moduleName == \\"SampleTurboModule\\") { + return std::make_shared(params); + } + return nullptr; +} + +} // namespace react +} // namespace facebook +", +} +`; + +exports[`GenerateModuleJniCpp can generate fixture NATIVE_MODULES_WITH_TYPE_ALIASES 1`] = ` +Map { + "SampleSpec-generated.cpp" => " +/** + * 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. + * + * @generated by codegen project: GenerateModuleJniCpp.js + */ + +#include \\"SampleSpec.h\\" + +namespace facebook { +namespace react { + + + +static facebook::jsi::Value __hostFunction_NativeAliasTurboModuleSpecJSI_cropImage(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"cropImage\\", \\"(Lcom/facebook/react/bridge/ReadableMap;)V\\", args, count); +} + +NativeAliasTurboModuleSpecJSI::NativeAliasTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + + methodMap_[\\"cropImage\\"] = MethodMetadata {1, __hostFunction_NativeAliasTurboModuleSpecJSI_cropImage}; +} + +std::shared_ptr NATIVE_MODULES_WITH_TYPE_ALIASES_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { + if (moduleName == \\"AliasTurboModule\\") { + return std::make_shared(params); + } + return nullptr; +} + +} // namespace react +} // namespace facebook +", +} +`; + +exports[`GenerateModuleJniCpp can generate fixture REAL_MODULE_EXAMPLE 1`] = ` +Map { + "SampleSpec-generated.cpp" => " +/** + * 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. + * + * @generated by codegen project: GenerateModuleJniCpp.js + */ + +#include \\"SampleSpec.h\\" + +namespace facebook { +namespace react { + + + +static facebook::jsi::Value __hostFunction_NativeCameraRollManagerSpecJSI_getPhotos(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"getPhotos\\", \\"(Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Promise;)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeCameraRollManagerSpecJSI_saveToCameraRoll(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"saveToCameraRoll\\", \\"(Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeCameraRollManagerSpecJSI_deletePhotos(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"deletePhotos\\", \\"(Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/Promise;)V\\", args, count); +} + +NativeCameraRollManagerSpecJSI::NativeCameraRollManagerSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + + methodMap_[\\"getPhotos\\"] = MethodMetadata {1, __hostFunction_NativeCameraRollManagerSpecJSI_getPhotos}; + methodMap_[\\"saveToCameraRoll\\"] = MethodMetadata {2, __hostFunction_NativeCameraRollManagerSpecJSI_saveToCameraRoll}; + methodMap_[\\"deletePhotos\\"] = MethodMetadata {1, __hostFunction_NativeCameraRollManagerSpecJSI_deletePhotos}; +} + +static facebook::jsi::Value __hostFunction_NativeImagePickerIOSSpecJSI_openCameraDialog(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"openCameraDialog\\", \\"(Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Callback;Lcom/facebook/react/bridge/Callback;)V\\", args, count); +} + +NativeImagePickerIOSSpecJSI::NativeImagePickerIOSSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + methodMap_[\\"openCameraDialog\\"] = MethodMetadata {3, __hostFunction_NativeImagePickerIOSSpecJSI_openCameraDialog}; +} + +static facebook::jsi::Value __hostFunction_NativeExceptionsManagerSpecJSI_reportFatalException(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"reportFatalException\\", \\"(Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;D)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeExceptionsManagerSpecJSI_reportSoftException(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"reportSoftException\\", \\"(Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;D)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeExceptionsManagerSpecJSI_reportException(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"reportException\\", \\"(Lcom/facebook/react/bridge/ReadableMap;)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeExceptionsManagerSpecJSI_updateExceptionMessage(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"updateExceptionMessage\\", \\"(Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;D)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeExceptionsManagerSpecJSI_dismissRedbox(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"dismissRedbox\\", \\"()V\\", args, count); +} + +NativeExceptionsManagerSpecJSI::NativeExceptionsManagerSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + methodMap_[\\"reportFatalException\\"] = MethodMetadata {3, __hostFunction_NativeExceptionsManagerSpecJSI_reportFatalException}; + methodMap_[\\"reportSoftException\\"] = MethodMetadata {3, __hostFunction_NativeExceptionsManagerSpecJSI_reportSoftException}; + methodMap_[\\"reportException\\"] = MethodMetadata {1, __hostFunction_NativeExceptionsManagerSpecJSI_reportException}; + methodMap_[\\"updateExceptionMessage\\"] = MethodMetadata {3, __hostFunction_NativeExceptionsManagerSpecJSI_updateExceptionMessage}; + methodMap_[\\"dismissRedbox\\"] = MethodMetadata {0, __hostFunction_NativeExceptionsManagerSpecJSI_dismissRedbox}; +} + +std::shared_ptr REAL_MODULE_EXAMPLE_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { + if (moduleName == \\"CameraRollManager\\") { + return std::make_shared(params); + } + if (moduleName == \\"ImagePickerIOS\\") { + return std::make_shared(params); + } + if (moduleName == \\"ExceptionsManager\\") { + return std::make_shared(params); + } + return nullptr; +} + +} // namespace react +} // namespace facebook +", +} +`; + +exports[`GenerateModuleJniCpp can generate fixture SIMPLE_NATIVE_MODULES 1`] = ` +Map { + "SampleSpec-generated.cpp" => " +/** + * 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. + * + * @generated by codegen project: GenerateModuleJniCpp.js + */ + +#include \\"SampleSpec.h\\" + +namespace facebook { +namespace react { + + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getConstants(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, ObjectKind, \\"getConstants\\", \\"()Ljava/util/Map;\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidFunc(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidFunc\\", \\"()V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getBool(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, BooleanKind, \\"getBool\\", \\"(Z)Z\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getNumber(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, NumberKind, \\"getNumber\\", \\"(D)D\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getString(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, StringKind, \\"getString\\", \\"(Ljava/lang/String;)Ljava/lang/String;\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getArray(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, ArrayKind, \\"getArray\\", \\"(Lcom/facebook/react/bridge/ReadableArray;)Lcom/facebook/react/bridge/WritableArray;\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getObject(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, ObjectKind, \\"getObject\\", \\"(Lcom/facebook/react/bridge/ReadableMap;)Lcom/facebook/react/bridge/WritableMap;\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getRootTag(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, NumberKind, \\"getRootTag\\", \\"(D)D\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getValue(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, ObjectKind, \\"getValue\\", \\"(DLjava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)Lcom/facebook/react/bridge/WritableMap;\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getValueWithCallback(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"getValueWithCallback\\", \\"(Lcom/facebook/react/bridge/Callback;)V\\", args, count); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getValueWithPromise(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"getValueWithPromise\\", \\"(ZLcom/facebook/react/bridge/Promise;)V\\", args, count); +} + +NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getObject}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_getValueWithPromise}; +} + +std::shared_ptr SIMPLE_NATIVE_MODULES_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { + if (moduleName == \\"SampleTurboModule\\") { + return std::make_shared(params); + } + return nullptr; +} + +} // namespace react +} // namespace facebook +", +} +`; + +exports[`GenerateModuleJniCpp can generate fixture TWO_MODULES_DIFFERENT_FILES 1`] = ` +Map { + "SampleSpec-generated.cpp" => " +/** + * 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. + * + * @generated by codegen project: GenerateModuleJniCpp.js + */ + +#include \\"SampleSpec.h\\" + +namespace facebook { +namespace react { + + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidFunc(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidFunc\\", \\"()V\\", args, count); +} + +NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_voidFunc}; +} + + +static facebook::jsi::Value __hostFunction_NativeSample2TurboModuleSpecJSI_voidFunc(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidFunc\\", \\"()V\\", args, count); +} + +NativeSample2TurboModuleSpecJSI::NativeSample2TurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSample2TurboModuleSpecJSI_voidFunc}; +} + +std::shared_ptr TWO_MODULES_DIFFERENT_FILES_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { + if (moduleName == \\"SampleTurboModule\\") { + return std::make_shared(params); + } + if (moduleName == \\"Sample2TurboModule\\") { + return std::make_shared(params); + } + return nullptr; +} + +} // namespace react +} // namespace facebook +", +} +`; + +exports[`GenerateModuleJniCpp can generate fixture TWO_MODULES_SAME_FILE 1`] = ` +Map { + "SampleSpec-generated.cpp" => " +/** + * 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. + * + * @generated by codegen project: GenerateModuleJniCpp.js + */ + +#include \\"SampleSpec.h\\" + +namespace facebook { +namespace react { + + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidFunc(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidFunc\\", \\"()V\\", args, count); +} + +NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_voidFunc}; +} + +static facebook::jsi::Value __hostFunction_NativeSample2TurboModuleSpecJSI_voidFunc(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidFunc\\", \\"()V\\", args, count); +} + +NativeSample2TurboModuleSpecJSI::NativeSample2TurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSample2TurboModuleSpecJSI_voidFunc}; +} + +std::shared_ptr TWO_MODULES_SAME_FILE_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { + if (moduleName == \\"SampleTurboModule\\") { + return std::make_shared(params); + } + if (moduleName == \\"Sample2TurboModule\\") { + return std::make_shared(params); + } + return nullptr; +} + +} // namespace react +} // namespace facebook +", +} +`; diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap new file mode 100644 index 00000000000000..efe08283828084 --- /dev/null +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap @@ -0,0 +1,488 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenerateModuleJniH can generate fixture COMPLEX_OBJECTS 1`] = ` +Map { + "SampleSpec.h" => "/** + * 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. + * + * @generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * JNI C++ class for module 'SampleTurboModule' + */ +class JSI_EXPORT NativeSampleTurboModuleSpecJSI : public JavaTurboModule { +public: + NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + + +std::shared_ptr COMPLEX_OBJECTS_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook +", + "Android.mk" => "# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := react_codegen_complex_objects + +LOCAL_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SHARED_LIBRARIES := libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES := libjsi + +LOCAL_CFLAGS := \\\\ + -DLOG_TAG=\\\\\\"ReactNative\\\\\\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) +", +} +`; + +exports[`GenerateModuleJniH can generate fixture EMPTY_NATIVE_MODULES 1`] = ` +Map { + "SampleSpec.h" => "/** + * 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. + * + * @generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * JNI C++ class for module 'SampleTurboModule' + */ +class JSI_EXPORT NativeSampleTurboModuleSpecJSI : public JavaTurboModule { +public: + NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + + +std::shared_ptr EMPTY_NATIVE_MODULES_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook +", + "Android.mk" => "# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := react_codegen_empty_native_modules + +LOCAL_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SHARED_LIBRARIES := libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES := libjsi + +LOCAL_CFLAGS := \\\\ + -DLOG_TAG=\\\\\\"ReactNative\\\\\\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) +", +} +`; + +exports[`GenerateModuleJniH can generate fixture NATIVE_MODULES_WITH_TYPE_ALIASES 1`] = ` +Map { + "SampleSpec.h" => "/** + * 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. + * + * @generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * JNI C++ class for module 'AliasTurboModule' + */ +class JSI_EXPORT NativeAliasTurboModuleSpecJSI : public JavaTurboModule { +public: + NativeAliasTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + + +std::shared_ptr NATIVE_MODULES_WITH_TYPE_ALIASES_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook +", + "Android.mk" => "# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := react_codegen_native_modules_with_type_aliases + +LOCAL_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SHARED_LIBRARIES := libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES := libjsi + +LOCAL_CFLAGS := \\\\ + -DLOG_TAG=\\\\\\"ReactNative\\\\\\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) +", +} +`; + +exports[`GenerateModuleJniH can generate fixture REAL_MODULE_EXAMPLE 1`] = ` +Map { + "SampleSpec.h" => "/** + * 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. + * + * @generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * JNI C++ class for module 'CameraRollManager' + */ +class JSI_EXPORT NativeCameraRollManagerSpecJSI : public JavaTurboModule { +public: + NativeCameraRollManagerSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + +/** + * JNI C++ class for module 'ExceptionsManager' + */ +class JSI_EXPORT NativeExceptionsManagerSpecJSI : public JavaTurboModule { +public: + NativeExceptionsManagerSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + +/** + * JNI C++ class for module 'ImagePickerIOS' + */ +class JSI_EXPORT NativeImagePickerIOSSpecJSI : public JavaTurboModule { +public: + NativeImagePickerIOSSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + + +std::shared_ptr REAL_MODULE_EXAMPLE_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook +", + "Android.mk" => "# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := react_codegen_real_module_example + +LOCAL_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SHARED_LIBRARIES := libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES := libjsi + +LOCAL_CFLAGS := \\\\ + -DLOG_TAG=\\\\\\"ReactNative\\\\\\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) +", +} +`; + +exports[`GenerateModuleJniH can generate fixture SIMPLE_NATIVE_MODULES 1`] = ` +Map { + "SampleSpec.h" => "/** + * 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. + * + * @generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * JNI C++ class for module 'SampleTurboModule' + */ +class JSI_EXPORT NativeSampleTurboModuleSpecJSI : public JavaTurboModule { +public: + NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + + +std::shared_ptr SIMPLE_NATIVE_MODULES_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook +", + "Android.mk" => "# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := react_codegen_simple_native_modules + +LOCAL_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SHARED_LIBRARIES := libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES := libjsi + +LOCAL_CFLAGS := \\\\ + -DLOG_TAG=\\\\\\"ReactNative\\\\\\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) +", +} +`; + +exports[`GenerateModuleJniH can generate fixture TWO_MODULES_DIFFERENT_FILES 1`] = ` +Map { + "SampleSpec.h" => "/** + * 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. + * + * @generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * JNI C++ class for module 'SampleTurboModule' + */ +class JSI_EXPORT NativeSampleTurboModuleSpecJSI : public JavaTurboModule { +public: + NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + +/** + * JNI C++ class for module 'Sample2TurboModule' + */ +class JSI_EXPORT NativeSample2TurboModuleSpecJSI : public JavaTurboModule { +public: + NativeSample2TurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + + +std::shared_ptr TWO_MODULES_DIFFERENT_FILES_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook +", + "Android.mk" => "# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := react_codegen_two_modules_different_files + +LOCAL_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SHARED_LIBRARIES := libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES := libjsi + +LOCAL_CFLAGS := \\\\ + -DLOG_TAG=\\\\\\"ReactNative\\\\\\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) +", +} +`; + +exports[`GenerateModuleJniH can generate fixture TWO_MODULES_SAME_FILE 1`] = ` +Map { + "SampleSpec.h" => "/** + * 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. + * + * @generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * JNI C++ class for module 'SampleTurboModule' + */ +class JSI_EXPORT NativeSampleTurboModuleSpecJSI : public JavaTurboModule { +public: + NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + +/** + * JNI C++ class for module 'Sample2TurboModule' + */ +class JSI_EXPORT NativeSample2TurboModuleSpecJSI : public JavaTurboModule { +public: + NativeSample2TurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + + +std::shared_ptr TWO_MODULES_SAME_FILE_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook +", + "Android.mk" => "# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := react_codegen_two_modules_same_file + +LOCAL_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) + +LOCAL_SHARED_LIBRARIES := libreact_nativemodule_core + +LOCAL_STATIC_LIBRARIES := libjsi + +LOCAL_CFLAGS := \\\\ + -DLOG_TAG=\\\\\\"ReactNative\\\\\\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) +", +} +`; diff --git a/packages/rn-tester/Gemfile b/packages/rn-tester/Gemfile index 998d2ba5f851de..b3234140e54d42 100644 --- a/packages/rn-tester/Gemfile +++ b/packages/rn-tester/Gemfile @@ -1,4 +1,4 @@ # Gemfile source 'https://rubygems.org' -gem 'cocoapods', '= 1.8.4' +gem 'cocoapods', '= 1.9.3' diff --git a/packages/rn-tester/Podfile b/packages/rn-tester/Podfile index 574cbb4d0d0ccc..9331c0b35c2b31 100644 --- a/packages/rn-tester/Podfile +++ b/packages/rn-tester/Podfile @@ -50,7 +50,7 @@ def frameworks_pre_install(installer) installer.pod_targets.each do |pod| if static_frameworks.include?(pod.name) def pod.build_type - Pod::Target::BuildType.static_library + Pod::BuildType.static_library end end end diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index 4aed3559d8229f..0ea79d0fad55c3 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -11,7 +11,7 @@ PODS: - React-Core (= 1000.0.0) - React-jsi (= 1000.0.0) - ReactCommon/turbomodule/core (= 1000.0.0) - - Flipper (0.41.5): + - Flipper (0.54.0): - Flipper-Folly (~> 2.2) - Flipper-RSocket (~> 1.1) - Flipper-DoubleConversion (1.1.7) @@ -25,36 +25,36 @@ PODS: - Flipper-PeerTalk (0.0.4) - Flipper-RSocket (1.1.0): - Flipper-Folly (~> 2.2) - - FlipperKit (0.41.5): - - FlipperKit/Core (= 0.41.5) - - FlipperKit/Core (0.41.5): - - Flipper (~> 0.41.5) + - FlipperKit (0.54.0): + - FlipperKit/Core (= 0.54.0) + - FlipperKit/Core (0.54.0): + - Flipper (~> 0.54.0) - FlipperKit/CppBridge - FlipperKit/FBCxxFollyDynamicConvert - FlipperKit/FBDefines - FlipperKit/FKPortForwarding - - FlipperKit/CppBridge (0.41.5): - - Flipper (~> 0.41.5) - - FlipperKit/FBCxxFollyDynamicConvert (0.41.5): + - FlipperKit/CppBridge (0.54.0): + - Flipper (~> 0.54.0) + - FlipperKit/FBCxxFollyDynamicConvert (0.54.0): - Flipper-Folly (~> 2.2) - - FlipperKit/FBDefines (0.41.5) - - FlipperKit/FKPortForwarding (0.41.5): + - FlipperKit/FBDefines (0.54.0) + - FlipperKit/FKPortForwarding (0.54.0): - CocoaAsyncSocket (~> 7.6) - Flipper-PeerTalk (~> 0.0.4) - - FlipperKit/FlipperKitHighlightOverlay (0.41.5) - - FlipperKit/FlipperKitLayoutPlugin (0.41.5): + - FlipperKit/FlipperKitHighlightOverlay (0.54.0) + - FlipperKit/FlipperKitLayoutPlugin (0.54.0): - FlipperKit/Core - FlipperKit/FlipperKitHighlightOverlay - FlipperKit/FlipperKitLayoutTextSearchable - YogaKit (~> 1.18) - - FlipperKit/FlipperKitLayoutTextSearchable (0.41.5) - - FlipperKit/FlipperKitNetworkPlugin (0.41.5): + - FlipperKit/FlipperKitLayoutTextSearchable (0.54.0) + - FlipperKit/FlipperKitNetworkPlugin (0.54.0): - FlipperKit/Core - - FlipperKit/FlipperKitReactPlugin (0.41.5): + - FlipperKit/FlipperKitReactPlugin (0.54.0): - FlipperKit/Core - - FlipperKit/FlipperKitUserDefaultsPlugin (0.41.5): + - FlipperKit/FlipperKitUserDefaultsPlugin (0.54.0): - FlipperKit/Core - - FlipperKit/SKIOSNetworkPlugin (0.41.5): + - FlipperKit/SKIOSNetworkPlugin (0.54.0): - FlipperKit/Core - FlipperKit/FlipperKitNetworkPlugin - glog (0.3.5) @@ -357,25 +357,25 @@ DEPENDENCIES: - DoubleConversion (from `../../third-party-podspecs/DoubleConversion.podspec`) - FBLazyVector (from `../../Libraries/FBLazyVector`) - FBReactNativeSpec (from `../../Libraries/FBReactNativeSpec`) - - Flipper (~> 0.41.1) + - Flipper (~> 0.54.0) - Flipper-DoubleConversion (= 1.1.7) - Flipper-Folly (~> 2.2) - Flipper-Glog (= 0.3.6) - Flipper-PeerTalk (~> 0.0.4) - Flipper-RSocket (~> 1.1) - - FlipperKit (~> 0.41.1) - - FlipperKit/Core (~> 0.41.1) - - FlipperKit/CppBridge (~> 0.41.1) - - FlipperKit/FBCxxFollyDynamicConvert (~> 0.41.1) - - FlipperKit/FBDefines (~> 0.41.1) - - FlipperKit/FKPortForwarding (~> 0.41.1) - - FlipperKit/FlipperKitHighlightOverlay (~> 0.41.1) - - FlipperKit/FlipperKitLayoutPlugin (~> 0.41.1) - - FlipperKit/FlipperKitLayoutTextSearchable (~> 0.41.1) - - FlipperKit/FlipperKitNetworkPlugin (~> 0.41.1) - - FlipperKit/FlipperKitReactPlugin (~> 0.41.1) - - FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.41.1) - - FlipperKit/SKIOSNetworkPlugin (~> 0.41.1) + - FlipperKit (~> 0.54.0) + - FlipperKit/Core (~> 0.54.0) + - FlipperKit/CppBridge (~> 0.54.0) + - FlipperKit/FBCxxFollyDynamicConvert (~> 0.54.0) + - FlipperKit/FBDefines (~> 0.54.0) + - FlipperKit/FKPortForwarding (~> 0.54.0) + - FlipperKit/FlipperKitHighlightOverlay (~> 0.54.0) + - FlipperKit/FlipperKitLayoutPlugin (~> 0.54.0) + - FlipperKit/FlipperKitLayoutTextSearchable (~> 0.54.0) + - FlipperKit/FlipperKitNetworkPlugin (~> 0.54.0) + - FlipperKit/FlipperKitReactPlugin (~> 0.54.0) + - FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.54.0) + - FlipperKit/SKIOSNetworkPlugin (~> 0.54.0) - glog (from `../../third-party-podspecs/glog.podspec`) - RCT-Folly (from `../../third-party-podspecs/RCT-Folly.podspec`) - RCTRequired (from `../../Libraries/RCTRequired`) @@ -489,45 +489,45 @@ SPEC CHECKSUMS: CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845 CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f DoubleConversion: cde416483dac037923206447da6e1454df403714 - FBLazyVector: 8ea0285646adaf7fa725c20ed737c49ee5ea680a - FBReactNativeSpec: 29b1b8a11346e71351f3a2ba126439810edee362 - Flipper: 33585e2d9810fe5528346be33bcf71b37bb7ae13 + FBLazyVector: fe973c09b2299b5e8154186ecf1f6554b4f70987 + FBReactNativeSpec: 20a9345af9157362b51ab0258d842cb7bb347d19 + Flipper: be611d4b742d8c87fbae2ca5f44603a02539e365 Flipper-DoubleConversion: 38631e41ef4f9b12861c67d17cb5518d06badc41 Flipper-Folly: c12092ea368353b58e992843a990a3225d4533c3 Flipper-Glog: 1dfd6abf1e922806c52ceb8701a3599a79a200a6 Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9 Flipper-RSocket: 64e7431a55835eb953b0bf984ef3b90ae9fdddd7 - FlipperKit: bc68102cd4952a258a23c9c1b316c7bec1fecf83 + FlipperKit: ab353d41aea8aae2ea6daaf813e67496642f3d7d glog: 40a13f7840415b9a77023fbcae0f1e6f43192af3 OpenSSL-Universal: 8b48cc0d10c1b2923617dfe5c178aa9ed2689355 RCT-Folly: b39288cedafe50da43317ec7d91bcc8cc0abbf33 - RCTRequired: 34582e9b3ebe69f244e091f37218d318406d5c4a - RCTTypeSafety: 84db212a990ce622a28f1bcb1ac68c658e722373 - React: cafb3c2321f7df55ce90dbf29d513799a79e4418 - React-callinvoker: 0dada022d38b73e6e15b33e2a96476153f79bbf6 - React-Core: d377a770bb13aa5120a6ce553f75f0e1cbc1aafe - React-CoreModules: f38b671f8df4c1c744ed69f00264539a7c4024b4 + RCTRequired: d3d4ce60e1e2282864d7560340690a3c8c646de1 + RCTTypeSafety: 4da4f9f218727257c50fd3bf2683a06cdb4fede3 + React: 87b3271d925336a94620915db5845c67c5dbbd77 + React-callinvoker: e9524d75cf0b7ae108868f8d34c0b8d7dc08ec03 + React-Core: f4eeb7ca3d6a7c2879d1d5b093800f23da9be617 + React-CoreModules: 87f011fa87190ffe979e443ce578ec93ec6ff4d4 React-cxxreact: de6de17eac6bbaa4f9fad46b66e7f0c4aaaf863d - React-jsi: 652ad7cb7ff8c87e0e9fb11e9ebcbbc70cdfe865 - React-jsiexecutor: f20d2b5986dbe3d0e94ccbf2d8da24e0d9c42cc9 - React-jsinspector: 7fbf9b42b58b02943a0d89b0ba9fff0070f2de98 - React-perflogger: 1f668f3e4d1adef1fafb3b95e7d6cf922113fe31 - React-RCTActionSheet: 51c43beeb74ef41189e87fe9823e53ebf6210359 - React-RCTAnimation: 62f271148b71d0200773b4959e99a80f624182d4 - React-RCTBlob: b81d69c4dc7f7891a6a3ec969bba9b0a9801b062 - React-RCTImage: 37646ebc761e9f68781867e9e802afdadd18a3dc - React-RCTLinking: b5c261eb3befe7d5c62a4706ae904943e73f9c82 - React-RCTNetwork: 2f6c4ba283c9c2ea768fecc6c681d3ab9448b5f5 - React-RCTPushNotification: a4f86ef3d7c0cb3ee570108a62e39fcd296a5030 - React-RCTSettings: 5a76683d9cf7408b5f8c3306cda3eaf4db191fce + React-jsi: 790da16b69a61adc36829eed43c44187c1488d10 + React-jsiexecutor: 17a3e26806bc19d8be7b6c83792bffc46df796be + React-jsinspector: 01db8cd098c7ab72bd09abdda522a08c9acd3af9 + React-perflogger: 37913fce32026582ad0244b585d1e52652fd01c0 + React-RCTActionSheet: e6562ea4df7099af4023d1bd0e9716e43b45a5c9 + React-RCTAnimation: fc2f655a64f0791879ab03843cca90c53737d1cb + React-RCTBlob: 5f82467e5d3bef65d05cdd900df6e12b0849744a + React-RCTImage: f3a98834281555ce1bbbe1af0306aaf40ac70fc7 + React-RCTLinking: 801d05ad5e6d1636e977f4dfeab21f87358a02a5 + React-RCTNetwork: b5e2f27a098ca52d98426328640314a499da6d00 + React-RCTPushNotification: ce60993f816f917a6495227e16978b5fd550d73b + React-RCTSettings: 3cb638230af06ba769edc0bc4ed4123040b1b4e2 React-RCTTest: 090e9816044220c39462be109dab6d473d94b1c9 - React-RCTText: 6c01963d3e562109f5548262b09b1b2bc260dd60 - React-RCTVibration: 0997fcaf753c7ac0a341177db120eebc438484cf - React-runtimeexecutor: 60dd6204a13f68a1aa1118870edcc604a791df2b - ReactCommon: 1f231b4fbed866032aa084ca23973063bbb51927 - Yoga: f7fa200d8c49f97b54c9421079e781fb900b5cae + React-RCTText: 51a41bf9d18a91b2437b833ed4246754baf830d0 + React-RCTVibration: a1cce36dd452eb88296d99d80d66f2c5bd50aad4 + React-runtimeexecutor: 53867815d0a01e53a2c901cb7f01076216c5c799 + ReactCommon: dc40b68c9a72e99e07f831b0e87a859f4660208b + Yoga: 69ef0b2bba5387523f793957a9f80dbd61e89631 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 3045d3456f0f2268f15db9a527839e6a19e6071c +PODFILE CHECKSUM: c38c19657f5aaa2d604f5f4c607f030b60452997 -COCOAPODS: 1.8.4 +COCOAPODS: 1.9.3 diff --git a/packages/rn-tester/README.md b/packages/rn-tester/README.md index e286de8cb51702..e1d52db22883a7 100644 --- a/packages/rn-tester/README.md +++ b/packages/rn-tester/README.md @@ -13,7 +13,7 @@ Before running the app, make sure you ran: ### Running on iOS Both macOS and Xcode are required. - +- `cd packages/rn-tester` - Install [Bundler](https://bundler.io/): `gem install bundler`. We use bundler to install the right version of [CocoaPods](https://cocoapods.org/) locally. - Install Bundler and CocoaPods dependencies: `bundle install && bundle exec pod install` - Open the generated `RNTesterPods.xcworkspace`. This is not checked in, as it is generated by CocoaPods. Do not open `RNTesterPods.xcodeproj` directly. @@ -25,7 +25,7 @@ You'll need to have all the [prerequisites](https://github.com/facebook/react-na Start an Android emulator. cd react-native - ./gradlew :RNTester:android:app:installJscDebug + ./gradlew :packages:rn-tester:android:app:installJscDebug ./scripts/packager.sh _Note: Building for the first time can take a while._ diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 04b79666cd32d1..e877dd0c278422 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -164,7 +164,7 @@ E7DB215F22B2F3EC005AC45F /* RCTUIManagerScenarioTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManagerScenarioTests.m; sourceTree = ""; }; E7DB216022B2F3EC005AC45F /* RNTesterSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTesterSnapshotTests.m; sourceTree = ""; }; E7DB216122B2F3EC005AC45F /* RCTRootViewIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootViewIntegrationTests.m; sourceTree = ""; }; - E7DB218B22B41FCD005AC45F /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + E7DB218B22B41FCD005AC45F /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = XCTest.framework; sourceTree = DEVELOPER_DIR; }; E9618482EC8608D4872A6E28 /* Pods-RNTesterUnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTesterUnitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -454,7 +454,7 @@ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1140; + LastUpgradeCheck = 1160; ORGANIZATIONNAME = Facebook; TargetAttributes = { E7DB209E22B2BA84005AC45F = { diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/xcshareddata/xcschemes/RNTester.xcscheme b/packages/rn-tester/RNTesterPods.xcodeproj/xcshareddata/xcschemes/RNTester.xcscheme index 938a25682dbd5c..7342976848dd0d 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/xcshareddata/xcschemes/RNTester.xcscheme +++ b/packages/rn-tester/RNTesterPods.xcodeproj/xcshareddata/xcschemes/RNTester.xcscheme @@ -1,6 +1,6 @@ v.name.contains("hermes") }, jsRootDir: "$rootDir/RNTester", + enableCodegen: (System.getenv('USE_CODEGEN') ?: '0').toBoolean(), enableFabric: (System.getenv('USE_FABRIC') ?: '0').toBoolean(), ] @@ -106,6 +107,11 @@ def enableSeparateBuildPerCPUArchitecture = true */ def enableProguardInReleaseBuilds = true +/** + * Build and enable codegen-related output in RN Tester app. + */ +def enableCodegen = project.ext.react.enableCodegen + /** * Build and enable Fabric in RN Tester app. */ @@ -148,6 +154,7 @@ android { testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' buildConfigField("boolean", "ENABLE_FABRIC", "$enableFabric") + buildConfigField("boolean", "ENABLE_TURBOMODULE", "$enableCodegen") // If using codegen, assume using TurboModule } signingConfigs { release { @@ -179,6 +186,33 @@ android { } } +if (enableCodegen) { + def reactAndroidProjectDir = project(':ReactAndroid').projectDir; + android { + defaultConfig { + externalNativeBuild { + ndkBuild { + abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a" + arguments "APP_PLATFORM=android-16", + "APP_STL=c++_shared", + "NDK_TOOLCHAIN_VERSION=clang", + // The following paths assume building React Native from source. + "GENERATED_SRC_DIR=$buildDir/generated/source", + "REACT_ANDROID_DIR=$reactAndroidProjectDir" + cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1" + cppFlags "-std=c++1y" + targets "rntester_appmodules" + } + } + } + externalNativeBuild { + ndkBuild { + path "$projectDir/src/main/jni/Android.mk" + } + } + } +} + configurations { hermesDebugImplementation {} hermesReleaseImplementation {} @@ -220,7 +254,6 @@ dependencies { } react { - enableCodegen = System.getenv("USE_CODEGEN") ?: false jsRootDir = file("$rootDir/packages/rn-tester") reactNativeRootDir = file("$rootDir") useJavaGenerator = System.getenv("USE_CODEGEN_JAVAPOET") ?: false diff --git a/packages/rn-tester/android/app/gradle.properties b/packages/rn-tester/android/app/gradle.properties index 4ff9751aab7d8b..5c93b3bca832dc 100644 --- a/packages/rn-tester/android/app/gradle.properties +++ b/packages/rn-tester/android/app/gradle.properties @@ -1,5 +1,6 @@ org.gradle.parallel=true -org.gradle.configureondemand=true +# This is causing issue with dependencies task: https://github.com/gradle/gradle/issues/9645#issuecomment-530746758 +# org.gradle.configureondemand=true MYAPP_RELEASE_STORE_FILE=my-release-key.keystore MYAPP_RELEASE_KEY_ALIAS=androiddebugkey MYAPP_RELEASE_STORE_PASSWORD=android @@ -9,4 +10,4 @@ android.useAndroidX=true android.enableJetifier=true # Version of flipper SDK to use with React Native -FLIPPER_VERSION=0.37.0 +FLIPPER_VERSION=0.54.0 diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java index d7a315ca26dae4..4cbb384cded703 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java @@ -7,8 +7,6 @@ package com.facebook.react.uiapp; -import static com.facebook.react.uiapp.RNTesterApplication.IS_FABRIC_ENABLED; - import android.content.res.Configuration; import android.os.Bundle; import androidx.annotation.Nullable; @@ -31,7 +29,7 @@ public RNTesterActivityDelegate(ReactActivity activity, String mainComponentName @Override protected ReactRootView createRootView() { ReactRootView reactRootView = new ReactRootView(getContext()); - reactRootView.setIsFabric(IS_FABRIC_ENABLED); + reactRootView.setIsFabric(BuildConfig.ENABLE_FABRIC); return reactRootView; } diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java index e9a946ee224f75..c191ecaec94d9d 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java @@ -7,16 +7,14 @@ package com.facebook.react.uiapp; -import static com.facebook.react.uiapp.BuildConfig.ENABLE_FABRIC; - import android.app.Application; import android.content.Context; import androidx.annotation.Nullable; -import com.facebook.react.BuildConfig; import com.facebook.react.ReactApplication; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JSIModule; import com.facebook.react.bridge.JSIModulePackage; import com.facebook.react.bridge.JSIModuleProvider; import com.facebook.react.bridge.JSIModuleSpec; @@ -24,11 +22,13 @@ import com.facebook.react.bridge.JavaScriptContextHolder; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.UIManager; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.fabric.ComponentFactory; import com.facebook.react.fabric.CoreComponentsRegistry; import com.facebook.react.fabric.FabricJSIModuleProvider; import com.facebook.react.fabric.ReactNativeConfig; import com.facebook.react.shell.MainReactPackage; +import com.facebook.react.turbomodule.core.TurboModuleManager; import com.facebook.react.views.text.ReactFontManager; import com.facebook.soloader.SoLoader; import java.lang.reflect.InvocationTargetException; @@ -38,8 +38,6 @@ public class RNTesterApplication extends Application implements ReactApplication { - static final boolean IS_FABRIC_ENABLED = ENABLE_FABRIC; - private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override @@ -65,7 +63,7 @@ public List getPackages() { @Nullable @Override protected JSIModulePackage getJSIModulePackage() { - if (!IS_FABRIC_ENABLED) { + if (!BuildConfig.ENABLE_FABRIC && !ReactFeatureFlags.useTurboModules) { return null; } @@ -75,44 +73,81 @@ public List getJSIModules( final ReactApplicationContext reactApplicationContext, final JavaScriptContextHolder jsContext) { List specs = new ArrayList<>(); - specs.add( - new JSIModuleSpec() { - @Override - public JSIModuleType getJSIModuleType() { - return JSIModuleType.UIManager; - } - - @Override - public JSIModuleProvider getJSIModuleProvider() { - ComponentFactory ComponentFactory = new ComponentFactory(); - CoreComponentsRegistry.register(ComponentFactory); - return new FabricJSIModuleProvider( - reactApplicationContext, - ComponentFactory, - // TODO: T71362667 add ReactNativeConfig's support in RNTester - new ReactNativeConfig() { - @Override - public boolean getBool(String s) { - return true; - } - - @Override - public int getInt64(String s) { - return 0; - } - - @Override - public String getString(String s) { - return ""; - } - - @Override - public double getDouble(String s) { - return 0; - } - }); - } - }); + + // Install the new native module system. + if (ReactFeatureFlags.useTurboModules) { + specs.add( + new JSIModuleSpec() { + @Override + public JSIModuleType getJSIModuleType() { + return JSIModuleType.TurboModuleManager; + } + + @Override + public JSIModuleProvider getJSIModuleProvider() { + return new JSIModuleProvider() { + @Override + public JSIModule get() { + ReactInstanceManager reactInstanceManager = getReactInstanceManager(); + List packages = reactInstanceManager.getPackages(); + + return new TurboModuleManager( + jsContext, + new RNTesterTurboModuleManagerDelegate( + reactApplicationContext, packages), + reactApplicationContext + .getCatalystInstance() + .getJSCallInvokerHolder(), + reactApplicationContext + .getCatalystInstance() + .getNativeCallInvokerHolder()); + } + }; + } + }); + } + + // Install the new renderer. + if (BuildConfig.ENABLE_FABRIC) { + specs.add( + new JSIModuleSpec() { + @Override + public JSIModuleType getJSIModuleType() { + return JSIModuleType.UIManager; + } + + @Override + public JSIModuleProvider getJSIModuleProvider() { + ComponentFactory ComponentFactory = new ComponentFactory(); + CoreComponentsRegistry.register(ComponentFactory); + return new FabricJSIModuleProvider( + reactApplicationContext, + ComponentFactory, + // TODO: T71362667 add ReactNativeConfig's support in RNTester + new ReactNativeConfig() { + @Override + public boolean getBool(String s) { + return false; + } + + @Override + public int getInt64(String s) { + return 0; + } + + @Override + public String getString(String s) { + return ""; + } + + @Override + public double getDouble(String s) { + return 0; + } + }); + } + }); + } return specs; } @@ -122,6 +157,8 @@ public double getDouble(String s) { @Override public void onCreate() { + // Set `USE_CODEGEN` env var when building RNTester to enable TurboModule. + ReactFeatureFlags.useTurboModules = BuildConfig.ENABLE_TURBOMODULE; ReactFontManager.getInstance().addCustomFont(this, "Rubik", R.font.rubik); super.onCreate(); SoLoader.init(this, /* native exopackage */ false); diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterTurboModuleManagerDelegate.java b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterTurboModuleManagerDelegate.java new file mode 100644 index 00000000000000..0baf04bc1d7da5 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterTurboModuleManagerDelegate.java @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package com.facebook.react.uiapp; + +import androidx.annotation.VisibleForTesting; +import com.facebook.jni.HybridData; +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.turbomodule.core.ReactPackageTurboModuleManagerDelegate; +import com.facebook.soloader.SoLoader; +import java.util.List; + +/** This class is responsible for creating all the TurboModules for the RNTester app. */ +public class RNTesterTurboModuleManagerDelegate extends ReactPackageTurboModuleManagerDelegate { + static { + SoLoader.loadLibrary("rntester_appmodules"); + } + + protected native HybridData initHybrid(); + + @VisibleForTesting + native boolean canCreateTurboModule(String moduleName); + + public RNTesterTurboModuleManagerDelegate( + ReactApplicationContext context, List packages) { + super(context, packages); + } +} diff --git a/packages/rn-tester/android/app/src/main/jni/Android.mk b/packages/rn-tester/android/app/src/main/jni/Android.mk new file mode 100644 index 00000000000000..2029b87348dd24 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/jni/Android.mk @@ -0,0 +1,22 @@ +# 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. + +THIS_DIR := $(call my-dir) + +include $(REACT_ANDROID_DIR)/Android-prebuilt.mk + +LOCAL_PATH := $(THIS_DIR) + +include $(CLEAR_VARS) +LOCAL_MODULE := rntester_appmodules +# Note: Build the react-native-codegen output along with other app-specific C++ files. +LOCAL_C_INCLUDES := $(LOCAL_PATH) $(GENERATED_SRC_DIR)/codegen/jni +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) $(wildcard $(GENERATED_SRC_DIR)/codegen/jni/*.cpp) +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) $(GENERATED_SRC_DIR)/codegen/jni +LOCAL_SHARED_LIBRARIES := libfbjni libreact_nativemodule_core libturbomodulejsijni libreact_codegen_reactandroidspec +LOCAL_CFLAGS := \ + -DLOG_TAG=\"ReactNative\" +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall +include $(BUILD_SHARED_LIBRARY) diff --git a/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp b/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp new file mode 100644 index 00000000000000..f4ed589bcbd37d --- /dev/null +++ b/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp @@ -0,0 +1,17 @@ +/* + * 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. + */ + +#include + +#include "RNTesterTurboModuleManagerDelegate.h" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + return facebook::jni::initialize(vm, [] { + // TODO: dvacca ramanpreet unify this with the way "ComponentDescriptorFactory" is defined in Fabric + facebook::react::RNTesterTurboModuleManagerDelegate::registerNatives(); + }); +} diff --git a/packages/rn-tester/android/app/src/main/jni/RNTesterAppModuleProvider.cpp b/packages/rn-tester/android/app/src/main/jni/RNTesterAppModuleProvider.cpp new file mode 100644 index 00000000000000..493d8267e3cf03 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/jni/RNTesterAppModuleProvider.cpp @@ -0,0 +1,73 @@ +/* + * 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. + */ + +#include "RNTesterAppModuleProvider.h" + +#include +#include + +namespace facebook { +namespace react { + +std::shared_ptr RNTesterAppModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { + auto module = PackagesRnTesterAndroidAppSpec_ModuleProvider(moduleName, params); + if (module != nullptr) { + return module; + } + + // TODO: fix up the ReactAndroidSpec_ModuleProvider() to avoid the Android prefix. + if (moduleName == "DatePicker") { + return std::make_shared(params); + } + if (moduleName == "DialogManager") { + return std::make_shared(params); + } + if (moduleName == "ImageLoader") { + return std::make_shared(params); + } + if (moduleName == "Networking") { + return std::make_shared(params); + } + if (moduleName == "Permissions") { + return std::make_shared(params); + } + if (moduleName == "PlatformConstants") { + return std::make_shared(params); + } + if (moduleName == "StatusBarManager") { + return std::make_shared(params); + } + if (moduleName == "Toast") { + return std::make_shared(params); + } + + // TODO: handle some special case naming. + if (moduleName == "IntentAndroid") { + return std::make_shared(params); + } + + // TODO: Animated module has special cases. + if ("NativeAnimatedModule" == moduleName) { + return std::make_shared(params); + } + if ("NativeAnimatedTurboModule" == moduleName) { + return std::make_shared(params); + } + + // TODO: handle multiple names for one spec. + if ("AsyncLocalStorage" == moduleName) { + return std::make_shared(params); + } + if ("AsyncSQLiteDBStorage" == moduleName) { + return std::make_shared(params); + } + + return ReactAndroidSpec_ModuleProvider(moduleName, params); +} + +} // namespace react +} // namespace facebook diff --git a/packages/rn-tester/android/app/src/main/jni/RNTesterAppModuleProvider.h b/packages/rn-tester/android/app/src/main/jni/RNTesterAppModuleProvider.h new file mode 100644 index 00000000000000..1bdc8477ed5ac9 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/jni/RNTesterAppModuleProvider.h @@ -0,0 +1,21 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +#include + +namespace facebook { +namespace react { + +std::shared_ptr RNTesterAppModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook diff --git a/packages/rn-tester/android/app/src/main/jni/RNTesterTurboModuleManagerDelegate.cpp b/packages/rn-tester/android/app/src/main/jni/RNTesterTurboModuleManagerDelegate.cpp new file mode 100644 index 00000000000000..e6ca3d0ecd0ac2 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/jni/RNTesterTurboModuleManagerDelegate.cpp @@ -0,0 +1,40 @@ +/* + * 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. + */ + +#include "RNTesterTurboModuleManagerDelegate.h" + +#include "RNTesterAppModuleProvider.h" + +namespace facebook { +namespace react { + +jni::local_ref RNTesterTurboModuleManagerDelegate::initHybrid(jni::alias_ref) { + return makeCxxInstance(); +} + +void RNTesterTurboModuleManagerDelegate::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", RNTesterTurboModuleManagerDelegate::initHybrid), + makeNativeMethod("canCreateTurboModule", RNTesterTurboModuleManagerDelegate::canCreateTurboModule), + }); +} + +std::shared_ptr RNTesterTurboModuleManagerDelegate::getTurboModule(const std::string name, const std::shared_ptr jsInvoker) { + // Not implemented yet: provide pure-C++ NativeModules here. + return nullptr; +} + +std::shared_ptr RNTesterTurboModuleManagerDelegate::getTurboModule(const std::string name, const JavaTurboModule::InitParams ¶ms) { + return RNTesterAppModuleProvider(name, params); +} + +bool RNTesterTurboModuleManagerDelegate::canCreateTurboModule(std::string name) { + return getTurboModule(name, nullptr) != nullptr || getTurboModule(name, {.moduleName = name}) != nullptr; +} + +} // namespace react +} // namespace facebook diff --git a/packages/rn-tester/android/app/src/main/jni/RNTesterTurboModuleManagerDelegate.h b/packages/rn-tester/android/app/src/main/jni/RNTesterTurboModuleManagerDelegate.h new file mode 100644 index 00000000000000..8091b73569c1e1 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/jni/RNTesterTurboModuleManagerDelegate.h @@ -0,0 +1,42 @@ +/* + * 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. + */ + +#include +#include + +#include +#include + +namespace facebook { +namespace react { + +class RNTesterTurboModuleManagerDelegate : public jni::HybridClass { +public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/uiapp/RNTesterTurboModuleManagerDelegate;"; + + static jni::local_ref initHybrid(jni::alias_ref); + + static void registerNatives(); + + std::shared_ptr getTurboModule(const std::string name, const std::shared_ptr jsInvoker) override; + std::shared_ptr getTurboModule(const std::string name, const JavaTurboModule::InitParams ¶ms) override; + + /** + * Test-only method. Allows user to verify whether a TurboModule can be created + * by instances of this class. + */ + bool canCreateTurboModule(std::string name); + +private: + friend HybridBase; + using HybridBase::HybridBase; + +}; + +} // namespace react +} // namespace facebook diff --git a/packages/rn-tester/e2e/__tests__/Alert-test.js b/packages/rn-tester/e2e/__tests__/Alert-test.js new file mode 100644 index 00000000000000..2e9f57906b06a5 --- /dev/null +++ b/packages/rn-tester/e2e/__tests__/Alert-test.js @@ -0,0 +1,51 @@ +/** + * 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. + * + * @format + */ + +/* global device, element, by, expect, waitFor */ +const {openExampleWithTitle} = require('../e2e-helpers'); + +describe('Alert', () => { + beforeAll(async () => { + await element(by.id('apis-tab')).tap(); + await element(by.id('explorer_search')).replaceText('Alert'); + await element(by.label('Alerts')).tap(); + }); + + afterAll(async () => { + await element(by.label('Back')).tap(); + }); + + it('AlertWithDefaultButton: should show alert dialog with message and default button', async () => { + const alertMessage = 'An external USB drive has been detected!'; + + await openExampleWithTitle('Alert with default Button'); + await element(by.id('alert-with-default-button')).tap(); + await expect(element(by.text(alertMessage))).toBeVisible(); + await element(by.text('OK')).tap(); + }); + + it('AlertWithThreeButtons: should show alert dialog with three buttons', async () => { + const alertMessage = 'Do you want to save your changes?'; + + await openExampleWithTitle('Alert with three Buttons'); + await element(by.id('alert-with-three-buttons')).tap(); + await expect(element(by.text(alertMessage))).toBeVisible(); + await expect(element(by.text('Cancel'))).toBeVisible(); + await expect(element(by.text('No'))).toBeVisible(); + await expect(element(by.text('Yes'))).toBeVisible(); + await element(by.text('Yes')).tap(); + }); + + it('AlertWithThreeButtons: should successfully call the callback on button press', async () => { + await openExampleWithTitle('Alert with three Buttons'); + await element(by.id('alert-with-three-buttons')).tap(); + await element(by.text('Cancel')).tap(); + await expect(element(by.text('Log: Cancel Pressed!'))).toBeVisible(); + }); +}); diff --git a/packages/rn-tester/e2e/__tests__/Button-test.js b/packages/rn-tester/e2e/__tests__/Button-test.js index 53588b168a525a..1b67f2a5871209 100644 --- a/packages/rn-tester/e2e/__tests__/Button-test.js +++ b/packages/rn-tester/e2e/__tests__/Button-test.js @@ -23,36 +23,75 @@ describe('Button', () => { ); }); - it('Simple button should be tappable', async () => { - await openExampleWithTitle('Simple Button'); - await element(by.id('simple_button')).tap(); - await expect(element(by.text('Simple has been pressed!'))).toBeVisible(); + it('Default Styling button should be tappable', async () => { + await openExampleWithTitle('Default Styling'); + await element(by.id('button_default_styling')).tap(); + await expect( + element(by.text('Your application has been submitted!')), + ).toBeVisible(); await element(by.text('OK')).tap(); }); - it('Adjusted color button should be tappable', async () => { - await openExampleWithTitle('Adjusted color'); - await element(by.id('purple_button')).tap(); - await expect(element(by.text('Purple has been pressed!'))).toBeVisible(); + it('Red color button should be tappable', async () => { + await openExampleWithTitle('Color'); + await element(by.id('cancel_button')).tap(); + await expect( + element(by.text('Your application has been cancelled!')), + ).toBeVisible(); await element(by.text('OK')).tap(); }); it("Two buttons with JustifyContent:'space-between' should be tappable", async () => { - await openExampleWithTitle('Fit to text layout'); - await element(by.id('left_button')).tap(); - await expect(element(by.text('Left has been pressed!'))).toBeVisible(); + await openExampleWithTitle('Two Buttons'); + await element(by.id('two_cancel_button')).tap(); + await expect( + element(by.text('Your application has been cancelled!')), + ).toBeVisible(); + await element(by.text('OK')).tap(); + + await element(by.id('two_submit_button')).tap(); + await expect( + element(by.text('Your application has been submitted!')), + ).toBeVisible(); + await element(by.text('OK')).tap(); + }); + + it("Three buttons with JustifyContent:'space-between' should be tappable", async () => { + await openExampleWithTitle('Three Buttons'); + await element(by.id('three_cancel_button')).tap(); + await expect( + element(by.text('Your application has been cancelled!')), + ).toBeVisible(); await element(by.text('OK')).tap(); - await element(by.id('right_button')).tap(); - await expect(element(by.text('Right has been pressed!'))).toBeVisible(); + await openExampleWithTitle('Three Buttons'); + await element(by.id('three_save_button')).tap(); + await expect( + element(by.text('Your application has been saved!')), + ).toBeVisible(); + await element(by.text('OK')).tap(); + + await element(by.id('three_submit_button')).tap(); + await expect( + element(by.text('Your application has been submitted!')), + ).toBeVisible(); await element(by.text('OK')).tap(); }); it('Disabled button should not interact', async () => { - await openExampleWithTitle('Disabled Button'); + await openExampleWithTitle('Disabled'); await element(by.id('disabled_button')).tap(); await expect( - element(by.text('Disabled has been pressed!')), + element(by.text('Your application has been submitted!')), ).toBeNotVisible(); }); + + it('AccessibilityLabel button should be tappable', async () => { + await openExampleWithTitle('AccessibilityLabel'); + await element(by.id('accessibilityLabel_button')).tap(); + await expect( + element(by.text('Your application has been submitted!')), + ).toBeVisible(); + await element(by.text('OK')).tap(); + }); }); diff --git a/packages/rn-tester/e2e/__tests__/DatePickerIOS-test.js b/packages/rn-tester/e2e/__tests__/DatePickerIOS-test.js deleted file mode 100644 index 0656f30345421c..00000000000000 --- a/packages/rn-tester/e2e/__tests__/DatePickerIOS-test.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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. - * - * @emails oncall+react_native - * @format - */ - -/* global element, by, expect, device */ - -const { - openComponentWithLabel, - openExampleWithTitle, -} = require('../e2e-helpers'); - -describe('DatePickerIOS', () => { - beforeAll(async () => { - await device.reloadReactNative(); - await openComponentWithLabel( - 'DatePickerIOS', - 'DatePickerIOS Select dates and times using the native UIDatePicker.', - ); - }); - - it('Should change indicator with datetime picker', async () => { - await openExampleWithTitle('Date and time picker'); - const testID = 'date-and-time'; - - const testElement = await element( - by.type('UIPickerView').withAncestor(by.id(testID)), - ); - const dateIndicator = await element(by.id('date-indicator')); - const timeIndicator = await element(by.id('time-indicator')); - - await expect(testElement).toBeVisible(); - await expect(dateIndicator).toBeVisible(); - await expect(timeIndicator).toBeVisible(); - - await testElement.setColumnToValue(0, 'Dec 4'); - await testElement.setColumnToValue(1, '4'); - await testElement.setColumnToValue(2, '10'); - await testElement.setColumnToValue(3, 'AM'); - - await expect(dateIndicator).toHaveText('12/4/2006'); - await expect(timeIndicator).toHaveText('04:10 AM'); - }); - - it('Should change indicator with date-only picker', async () => { - await openExampleWithTitle('Date only picker'); - const testID = 'date-only'; - - const testElement = await element( - by.type('UIPickerView').withAncestor(by.id(testID)), - ); - const indicator = await element(by.id('date-indicator')); - - await expect(testElement).toBeVisible(); - await expect(indicator).toBeVisible(); - - await testElement.setColumnToValue(0, 'November'); - await testElement.setColumnToValue(1, '3'); - await testElement.setColumnToValue(2, '2006'); - - await expect(indicator).toHaveText('11/3/2006'); - }); -}); diff --git a/packages/rn-tester/e2e/__tests__/Picker-test.js b/packages/rn-tester/e2e/__tests__/Picker-test.js deleted file mode 100644 index 822818ad9eb3a0..00000000000000 --- a/packages/rn-tester/e2e/__tests__/Picker-test.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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. - * - * @emails oncall+react_native - * @format - */ - -/* global device, element, by, expect */ -const { - openComponentWithLabel, - openExampleWithTitle, -} = require('../e2e-helpers'); - -describe('Picker', () => { - beforeAll(async () => { - await device.reloadReactNative(); - await openComponentWithLabel( - 'Picker', - 'Picker Provides multiple options to choose from, using either a dropdown menu or a dialog.', - ); - }); - - it('should be selectable by ID', async () => { - await openExampleWithTitle('Basic picker'); - await expect(element(by.id('basic-picker'))).toBeVisible(); - }); -}); diff --git a/packages/rn-tester/js/RNTesterApp.android.js b/packages/rn-tester/js/RNTesterApp.android.js index abf6121bc1c308..ba0aa1846d0dde 100644 --- a/packages/rn-tester/js/RNTesterApp.android.js +++ b/packages/rn-tester/js/RNTesterApp.android.js @@ -10,331 +10,9 @@ 'use strict'; -const RNTesterActions = require('./utils/RNTesterActions'); -const RNTesterExampleContainer = require('./components/RNTesterExampleContainer'); -const RNTesterExampleList = require('./components/RNTesterExampleList'); -const RNTesterList = require('./utils/RNTesterList'); -const RNTesterNavigationReducer = require('./utils/RNTesterNavigationReducer'); -const React = require('react'); -const RNTesterNavBar = require('./components/RNTesterNavbar'); +import {AppRegistry} from 'react-native'; -const { - AppRegistry, - AsyncStorage, - BackHandler, - StyleSheet, - Text, - UIManager, - useColorScheme, - View, - LogBox, -} = require('react-native'); - -import type {RNTesterExample} from './types/RNTesterTypes'; -import type {RNTesterNavigationState} from './utils/RNTesterNavigationReducer'; -import {RNTesterThemeContext, themes} from './components/RNTesterTheme'; -import RNTesterDocumentationURL from './components/RNTesterDocumentationURL'; -import { - RNTesterBookmarkContext, - bookmarks, -} from './components/RNTesterBookmark'; -import type {RNTesterBookmark} from './components/RNTesterBookmark'; - -import { - initializeAsyncStore, - addApi, - addComponent, - removeApi, - removeComponent, - checkBookmarks, - updateRecentlyViewedList, -} from './utils/RNTesterAsyncStorageAbstraction'; - -UIManager.setLayoutAnimationEnabledExperimental && - UIManager.setLayoutAnimationEnabledExperimental(true); - -type Props = {exampleFromAppetizeParams?: ?string, ...}; - -const APP_STATE_KEY = 'RNTesterAppState.v2'; - -const Header = ({ - title, - documentationURL, -}: { - title: string, - documentationURL?: string, - ... -}) => ( - - {theme => { - return ( - - - - {title} - - {documentationURL && ( - - )} - - - ); - }} - -); - -const RNTesterExampleContainerViaHook = ({ - title, - module, - exampleRef, -}: { - title: string, - module: RNTesterExample, - exampleRef: () => void, - ... -}) => { - const colorScheme = useColorScheme(); - const theme = colorScheme === 'dark' ? themes.dark : themes.light; - return ( - - -
- - - - ); -}; - -const RNTesterExampleListViaHook = ({ - title, - onNavigate, - UpdateRecentlyViewedList, - recentComponents, - recentApis, - bookmark, - list, - screen, -}: { - title: string, - screen: string, - onNavigate?: () => mixed, - UpdateRecentlyViewedList?: (item: RNTesterExample, key: string) => mixed, - recentComponents: Array, - recentApis: Array, - list: { - ComponentExamples: Array, - APIExamples: Array, - ... - }, - bookmark: RNTesterBookmark, - ... -}) => { - const colorScheme = useColorScheme(); - const theme = colorScheme === 'dark' ? themes.dark : themes.light; - const exampleTitle = - screen === 'component' - ? 'Component Store' - : screen === 'api' - ? 'API Store' - : 'Bookmarks'; - return ( - - - -
- - - - - ); -}; - -class RNTesterApp extends React.Component { - constructor() { - super(); - - // RNTester App currently uses Async Storage from react-native for storing navigation state - // and bookmark items. - // TODO: Add Native Async Storage Module in RNTester - LogBox.ignoreLogs([new RegExp('has been extracted from react-native')]); - - this.state = { - openExample: null, - Components: bookmarks.Components, - Api: bookmarks.Api, - recentComponents: [], - recentApis: [], - screen: 'component', - AddApi: (apiName, api) => addApi(apiName, api, this), - AddComponent: (componentName, component) => - addComponent(componentName, component, this), - RemoveApi: apiName => removeApi(apiName, this), - RemoveComponent: componentName => removeComponent(componentName, this), - checkBookmark: (title, key) => checkBookmarks(title, key, this), - updateRecentlyViewedList: (item, key) => - updateRecentlyViewedList(item, key, this), - }; - } - UNSAFE_componentWillMount() { - BackHandler.addEventListener('hardwareBackPress', () => - this._handleBackButtonPress(this.state.screen), - ); - } - - componentDidMount() { - initializeAsyncStore(this); - } - - render(): React.Node { - if (!this.state) { - return null; - } - return ( - - {this._renderApp({ - Components: this.state.Components, - Api: this.state.Api, - AddApi: this.state.AddApi, - AddComponent: this.state.AddComponent, - RemoveApi: this.state.RemoveApi, - RemoveComponent: this.state.RemoveComponent, - checkBookmark: this.state.checkBookmark, - })} - - - - - ); - } - - _renderApp(bookmark) { - const {openExample, screen} = this.state; - - if (openExample) { - const ExampleModule = RNTesterList.Modules[openExample]; - if (ExampleModule.external) { - return ( - { - this._handleAction(RNTesterActions.Back(screen)); - }} - ref={example => { - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue - * was found when making Flow check .android.js files. */ - this._exampleRef = example; - }} - /> - ); - } else if (ExampleModule) { - return ( - <> - =0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - title={ExampleModule.title} - module={ExampleModule} - exampleRef={example => { - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue - * was found when making Flow check .android.js files. */ - this._exampleRef = example; - }} - /> - - ); - } - } - - return ( - =0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - onNavigate={this._handleAction} - UpdateRecentlyViewedList={this.state.updateRecentlyViewedList} - recentComponents={this.state.recentComponents} - recentApis={this.state.recentApis} - bookmark={bookmark} - list={RNTesterList} - screen={screen} - /> - ); - } - - _handleAction = (action: Object): boolean => { - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - const newState = RNTesterNavigationReducer(this.state, action); - if (this.state !== newState) { - this.setState(newState, () => - AsyncStorage.setItem(APP_STATE_KEY, JSON.stringify(this.state)), - ); - return true; - } - return false; - }; - - _handleBackButtonPress = screen => { - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - if ( - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - this._exampleRef && - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - this._exampleRef.handleBackAction && - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - this._exampleRef.handleBackAction() - ) { - return true; - } - return this._handleAction(RNTesterActions.Back(screen)); - }; -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - toolbar: { - height: 56, - }, - toolbarLeft: { - marginTop: 2, - }, - toolbarCenter: { - flex: 1, - position: 'absolute', - top: 12, - left: 0, - right: 0, - alignItems: 'center', - }, - title: { - fontSize: 19, - fontWeight: '600', - textAlign: 'center', - }, - bottomNavbar: { - bottom: 0, - width: '100%', - display: 'flex', - flexDirection: 'column', - position: 'absolute', - }, -}); +import RNTesterApp from './RNTesterAppShared'; AppRegistry.registerComponent('RNTesterApp', () => RNTesterApp); diff --git a/packages/rn-tester/js/RNTesterApp.ios.js b/packages/rn-tester/js/RNTesterApp.ios.js index 96769429ce6eab..6e48c6ead2bef0 100644 --- a/packages/rn-tester/js/RNTesterApp.ios.js +++ b/packages/rn-tester/js/RNTesterApp.ios.js @@ -10,328 +10,14 @@ 'use strict'; -const RNTesterActions = require('./utils/RNTesterActions'); -const RNTesterExampleContainer = require('./components/RNTesterExampleContainer'); -const RNTesterExampleList = require('./components/RNTesterExampleList'); -const RNTesterList = require('./utils/RNTesterList.ios'); -const RNTesterNavigationReducer = require('./utils/RNTesterNavigationReducer'); -const React = require('react'); -const SnapshotViewIOS = require('./examples/Snapshot/SnapshotViewIOS.ios'); -const RNTesterNavbar = require('./components/RNTesterNavbar'); - -const { - AppRegistry, - AsyncStorage, - BackHandler, - Button, - Platform, - SafeAreaView, - StyleSheet, - Text, - useColorScheme, - View, - LogBox, -} = require('react-native'); +import {AppRegistry} from 'react-native'; +import React from 'react'; +import SnapshotViewIOS from './examples/Snapshot/SnapshotViewIOS.ios'; +import RNTesterExampleContainer from './components/RNTesterExampleContainer'; +import RNTesterList from './utils/RNTesterList'; +import RNTesterApp from './RNTesterAppShared'; import type {RNTesterExample} from './types/RNTesterTypes'; -import type { - RNTesterAction, - RNTesterExampleAction, -} from './utils/RNTesterActions'; -import type {RNTesterNavigationState} from './utils/RNTesterNavigationReducer'; -import {RNTesterThemeContext, themes} from './components/RNTesterTheme'; -import RNTesterDocumentationURL from './components/RNTesterDocumentationURL'; -import type {ColorSchemeName} from '../../../Libraries/Utilities/NativeAppearance'; -import { - RNTesterBookmarkContext, - bookmarks, -} from './components/RNTesterBookmark'; -import type {RNTesterBookmark} from './components/RNTesterBookmark'; - -type Props = {exampleFromAppetizeParams?: ?string, ...}; - -import { - initializeAsyncStore, - addApi, - addComponent, - removeApi, - removeComponent, - checkBookmarks, - updateRecentlyViewedList, -} from './utils/RNTesterAsyncStorageAbstraction'; - -const APP_STATE_KEY = 'RNTesterAppState.v2'; - -const Header = ({ - onBack, - title, - documentationURL, -}: { - onBack?: () => mixed, - title: string, - documentationURL?: string, - ... -}) => ( - - {theme => { - return ( - - - - - {title} - - {documentationURL && ( - - )} - - {onBack && ( - -