diff --git a/.appveyor/config.yml b/.appveyor/config.yml deleted file mode 100644 index ed8162cf135899..00000000000000 --- a/.appveyor/config.yml +++ /dev/null @@ -1,47 +0,0 @@ -environment: - ANDROID_HOME: "C:\\android-sdk-windows" - ANDROID_NDK: "C:\\android-sdk-windows\\android-ndk-r19c" - ANDROID_BUILD_VERSION: 28 - ANDROID_TOOLS_VERSION: 28.0.3 - - GRADLE_OPTS: -Dorg.gradle.daemon=false - - SDK_TOOLS_URL: https://dl.google.com/android/repository/sdk-tools-windows-3859397.zip - NDK_TOOLS_URL: https://dl.google.com/android/repository/android-ndk-r19c-windows-x86_64.zip - - matrix: - - nodejs_version: 8 - - nodejs_version: 10 - -install: - # Install Android SDK Tools - - mkdir "%ANDROID_HOME%" - - appveyor DownloadFile "%SDK_TOOLS_URL%" -FileName "%TMP%/sdk-tools.zip" - - 7z x "%TMP%/sdk-tools.zip" -o"%ANDROID_HOME%" > nul - - set PATH=%PATH%;"%ANDROID_HOME%\tools\bin" - - - yes 2> nul | sdkmanager --licenses > nul - - yes 2> nul | sdkmanager "system-images;android-19;google_apis;armeabi-v7a" - - yes 2> nul | sdkmanager "platforms;android-%ANDROID_BUILD_VERSION%" - - yes 2> nul | sdkmanager "build-tools;%ANDROID_TOOLS_VERSION%" - - yes 2> nul | sdkmanager "add-ons;addon-google_apis-google-23" - - yes 2> nul | sdkmanager "extras;android;m2repository" - - - appveyor DownloadFile "%NDK_TOOLS_URL%" -FileName "%TMP%/ndk.zip" - - 7z x "%TMP%/ndk.zip" -o"%ANDROID_HOME%" > nul - - - ps: Install-Product node $env:nodejs_version x64 - - npx envinfo@latest - - appveyor-retry yarn install - -build_script: - - yarn run flow-check-android - - yarn run flow-check-ios - - yarn run test - # - gradlew.bat RNTester:android:app:assembleRelease - -cache: - - node_modules - - "%LOCALAPPDATA%/Yarn" - - "%USERPROFILE%/.gradle/caches" - - "%USERPROFILE%/.gradle/wrapper" diff --git a/.buckconfig b/.buckconfig index 7f56a4943a410d..07ec813646f170 100644 --- a/.buckconfig +++ b/.buckconfig @@ -1,6 +1,6 @@ [android] - target = android-28 + target = android-30 [download] max_number_of_retries = 3 @@ -10,4 +10,4 @@ google = https://maven.google.com/ [alias] - rntester = //RNTester/android/app:app + rntester = //packages/rn-tester/android/app:app diff --git a/.circleci/Dockerfiles/Dockerfile.android b/.circleci/Dockerfiles/Dockerfile.android index 65e6c07d9d4a39..0500df637c360e 100644 --- a/.circleci/Dockerfiles/Dockerfile.android +++ b/.circleci/Dockerfiles/Dockerfile.android @@ -14,7 +14,7 @@ # and build a Android application that can be used to run the # tests specified in the scripts/ directory. # -FROM reactnativecommunity/react-native-android:2019-9-4 +FROM reactnativecommunity/react-native-android:4.0 LABEL Description="React Native Android Test Image" LABEL maintainer="Héctor Ramos " @@ -25,11 +25,13 @@ ENV JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF8" ADD .buckconfig /app/.buckconfig ADD .buckjavaargs /app/.buckjavaargs -ADD tools /app/tools +ADD Libraries /app/Libraries ADD ReactAndroid /app/ReactAndroid ADD ReactCommon /app/ReactCommon ADD React /app/React ADD keystores /app/keystores +ADD packages/react-native-codegen /app/packages/react-native-codegen +ADD tools /app/tools WORKDIR /app @@ -49,4 +51,3 @@ RUN yarn RUN ./gradlew :ReactAndroid:downloadBoost :ReactAndroid:downloadDoubleConversion :ReactAndroid:downloadFolly :ReactAndroid:downloadGlog RUN ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=1 - diff --git a/.circleci/Dockerfiles/scripts/run-android-docker-instrumentation-tests.sh b/.circleci/Dockerfiles/scripts/run-android-docker-instrumentation-tests.sh index d51860f1d6f037..b6a2427324e7d2 100644 --- a/.circleci/Dockerfiles/scripts/run-android-docker-instrumentation-tests.sh +++ b/.circleci/Dockerfiles/scripts/run-android-docker-instrumentation-tests.sh @@ -10,7 +10,7 @@ mount -o remount,exec /dev/shm AVD_UUID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1) # create virtual device -echo no | android create avd -n "$AVD_UUID" -f -t android-19 --abi default/armeabi-v7a +echo no | android create avd -n "$AVD_UUID" -f -t android-21 --abi default/armeabi-v7a # emulator setup emulator64-arm -avd $AVD_UUID -no-skin -no-audio -no-window -no-boot-anim & diff --git a/.circleci/Dockerfiles/scripts/run-ci-e2e-tests.sh b/.circleci/Dockerfiles/scripts/run-ci-e2e-tests.sh index e22867fd04163c..3176638424e7e0 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 @@ -114,7 +109,7 @@ function e2e_suite() { # create virtual device if ! android list avd | grep "$AVD_UUID" > /dev/null; then - echo no | android create avd -n "$AVD_UUID" -f -t android-19 --abi default/armeabi-v7a + echo no | android create avd -n "$AVD_UUID" -f -t android-21 --abi default/armeabi-v7a fi # newline at end of adb devices call and first line is headers @@ -199,7 +194,7 @@ function e2e_suite() { return 1 fi - echo "Starting packager server" + echo "Starting Metro" npm start >> /dev/null & SERVER_PID=$! sleep 15 diff --git a/.circleci/config.yml b/.circleci/config.yml index fa3fbf102e9b17..48759d49d51473 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,12 @@ version: 2.1 +# ------------------------- +# ORBS +# ------------------------- + +orbs: + win: circleci/windows@2.4.0 + # ------------------------- # DEFAULTS # ------------------------- @@ -7,23 +14,29 @@ defaults: &defaults working_directory: ~/react-native environment: - GIT_COMMIT_DESC: git log --format=oneline -n 1 $CIRCLE_SHA1 + # The public github tokens are publicly visible by design + - PUBLIC_PULLBOT_GITHUB_TOKEN_A: &github_pullbot_token_a "a6edf8e8d40ce4e8b11a" + - PUBLIC_PULLBOT_GITHUB_TOKEN_B: &github_pullbot_token_b "150e1341f4dd9c944d2a" + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A: &github_analysisbot_token_a "312d354b5c36f082cfe9" + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B: &github_analysisbot_token_b "07973d757026bdd9f196" # ------------------------- # EXECUTORS # ------------------------- executors: - node8: + nodelts: <<: *defaults docker: - - image: circleci/node:8 - nodelts: + # Note: Version set separately for Windows builds, see below. + - image: circleci/node:14 + nodeprevlts: <<: *defaults docker: - - image: circleci/node:lts + - image: circleci/node:12 reactnativeandroid: <<: *defaults docker: - - image: reactnativecommunity/react-native-android:2019-9-4 + - image: reactnativecommunity/react-native-android:4.0 resource_class: "large" environment: - TERM: "dumb" @@ -31,10 +44,15 @@ executors: - _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" - GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-XX:+HeapDumpOnOutOfMemoryError"' - BUILD_THREADS: 2 + # Repeated here, as the environment key in this executor will overwrite the one in defaults + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A: *github_analysisbot_token_a + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B: *github_analysisbot_token_b + - PUBLIC_PULLBOT_GITHUB_TOKEN_A: *github_pullbot_token_a + - PUBLIC_PULLBOT_GITHUB_TOKEN_B: *github_pullbot_token_b reactnativeios: <<: *defaults macos: - xcode: "10.3.0" + xcode: &_XCODE_VERSION "12.5.0" # ------------------------- # COMMANDS @@ -60,9 +78,8 @@ commands: - restore_cache: keys: - v4-yarn-cache-{{ arch }}-{{ checksum "yarn.lock" }} - - v4-yarn-cache-{{ arch }} - run: - name: Run Yarn + name: "Yarn: Install Dependencies" command: | # Skip yarn install on metro bump commits as the package is not yet # available on npm @@ -79,7 +96,6 @@ commands: - restore_cache: keys: - v3-buck-v2019.01.10.01-{{ checksum "scripts/circleci/buck_fetch.sh" }}} - - v3-buck-v2019.01.10.01- - run: name: Install BUCK command: | @@ -94,13 +110,21 @@ commands: - ~/okbuck key: v3-buck-v2019.01.10.01-{{ checksum "scripts/circleci/buck_fetch.sh" }} + install_github_bot_deps: + steps: + - run: + name: "Yarn: Install dependencies (GitHub bots)" + command: cd bots && yarn install --non-interactive --cache-folder ~/.cache/yarn + brew_install: parameters: package: description: Homebrew package to install type: string steps: - - run: HOMEBREW_NO_AUTO_UPDATE=1 brew install << parameters.package >> >/dev/null + - run: + name: "Brew: Install << parameters.package >>" + command: HOMEBREW_NO_AUTO_UPDATE=1 brew install << parameters.package >> >/dev/null with_brew_cache_span: parameters: @@ -109,39 +133,39 @@ commands: steps: - restore_cache: keys: - - v2-brew + - v4-brew - steps: << parameters.steps >> - save_cache: paths: - /usr/local/Homebrew - ~/Library/Caches/Homebrew - key: v2-brew + key: v4-brew - with_pods_cache_span: + with_rntester_pods_cache_span: parameters: steps: type: steps steps: - run: name: Setup CocoaPods cache - # Copy RNTester/Podfile.lock since it can be changed by pod install - command: cp RNTester/Podfile.lock RNTester/Podfile.lock.bak + # Copy packages/rn-tester/Podfile.lock since it can be changed by pod install + command: cp packages/rn-tester/Podfile.lock packages/rn-tester/Podfile.lock.bak - restore_cache: keys: - - v1-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "RNTester/Podfile.lock.bak" }} - - v1-pods-{{ .Environment.CIRCLE_JOB }}- + # The committed lockfile is generated using USE_FRAMEWORKS=0 and USE_HERMES=0 so it could load an outdated cache if a change + # only affects the frameworks or hermes config. To help prevent this also cache based on the content of Podfile. + - v3-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock.bak" }}-{{ checksum "packages/rn-tester/Podfile" }} - steps: << parameters.steps >> - save_cache: paths: - - RNTester/Pods - key: v1-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "RNTester/Podfile.lock.bak" }} + - packages/rn-tester/Pods + key: v3-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock.bak" }}-{{ checksum "packages/rn-tester/Podfile" }} download_gradle_dependencies: steps: - restore_cache: keys: - v1-gradle-{{ checksum "ReactAndroid/build.gradle" }}-{{ checksum "scripts/circleci/gradle_download_deps.sh" }} - - v1-gradle- - run: name: Download Dependencies Using Gradle command: ./scripts/circleci/gradle_download_deps.sh @@ -158,24 +182,33 @@ commands: name: Download Dependencies Using Buck command: ./scripts/circleci/buck_fetch.sh - # ------------------------- - # COMMANDS: Disabled Tests - # ------------------------- - run_podspec_tests: - steps: - - run: - name: Test CocoaPods - command: ./scripts/process-podspecs.sh - run_e2e_tests: + run_e2e: + parameters: + platform: + description: Target platform + type: enum + enum: ["android", "ios", "js"] + default: "js" + retries: + description: How many times the job should try to run these tests + type: integer + default: 3 steps: - run: - name: Full End-to-End Test Suite - command: node ./scripts/run-ci-e2e-tests.js --android --ios --js --retries 3; - run_android_e2e_tests: + name: "Run Tests: << parameters.platform >> End-to-End Tests" + command: node ./scripts/run-ci-e2e-tests.js --<< parameters.platform >> --retries << parameters.retries >> + + report_bundle_size: + parameters: + platform: + description: Target platform + type: enum + enum: ["android", "ios"] steps: + - install_github_bot_deps - run: - name: Android End-to-End Test Suite - command: node ./scripts/run-ci-e2e-tests.js --android --retries 3; + name: Report size of RNTester.app (analysis-bot) + command: GITHUB_TOKEN="$PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A""$PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B" scripts/circleci/report-bundle-size.sh << parameters.platform >> # ------------------------- # JOBS @@ -183,12 +216,12 @@ commands: jobs: setup: parameters: - executor: - type: executor - default: node8 - checkout_type: - type: string - default: node + executor: + type: executor + default: nodelts + checkout_type: + type: string + default: node executor: << parameters.executor >> steps: - checkout @@ -204,24 +237,20 @@ jobs: # Issues will be posted to the PR itself via GitHub bots. # This workflow should only fail if the bots fail to run. analyze_pr: - executor: nodelts - # The public github tokens are publicly visible by design - environment: - - PUBLIC_PULLBOT_GITHUB_TOKEN_A: "a6edf8e8d40ce4e8b11a" - - PUBLIC_PULLBOT_GITHUB_TOKEN_B: "150e1341f4dd9c944d2a" - - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A: "78a72af35445ca3f8180" - - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B: "b1a98e0bbd56ff1ccba1" - + executor: reactnativeandroid steps: - restore_cache_checkout: - checkout_type: node + checkout_type: android - run_yarn + - install_github_bot_deps + + # Note: The yarn gpg key needs to be refreshed to work around https://github.com/yarnpkg/yarn/issues/7866 - run: - name: Install dependencies + name: Install additional GitHub bot dependencies command: | - sudo apt update && sudo apt install -y shellcheck jq - cd bots && yarn install --non-interactive --cache-folder ~/.cache/yarn + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - + apt update && apt install -y shellcheck jq - run: name: Run linters against modified files (analysis-bot) @@ -235,15 +264,14 @@ jobs: DANGER_GITHUB_API_TOKEN="$PUBLIC_PULLBOT_GITHUB_TOKEN_A""$PUBLIC_PULLBOT_GITHUB_TOKEN_B" yarn danger ci --use-github-checks when: always - # ------------------------- # JOBS: Analyze Code # ------------------------- analyze_code: - executor: node8 + executor: reactnativeandroid steps: - restore_cache_checkout: - checkout_type: node + checkout_type: android - setup_artifacts - run_yarn @@ -252,6 +280,11 @@ jobs: command: scripts/circleci/exec_swallow_error.sh yarn lint --format junit -o ./reports/junit/eslint/results.xml when: always + - run: + name: Lint Java + command: scripts/circleci/exec_swallow_error.sh yarn lint-java --check + when: always + - run: name: Check for errors in code using Flow (iOS) command: yarn flow-check-ios @@ -280,35 +313,66 @@ jobs: # ------------------------- # JOBS: Test JavaScript # ------------------------- - # Runs JavaScript tests test_js: parameters: executor: type: executor - default: node8 + default: nodelts + run_disabled_tests: + type: boolean + default: false executor: << parameters.executor >> steps: - restore_cache_checkout: checkout_type: node - setup_artifacts - run_yarn + - run: + name: Install rsync + command: sudo apt-get install rsync + # ------------------------- + # Run JavaScript tests - run: - name: JavaScript Test Suite + name: "Run Tests: JavaScript Tests" command: node ./scripts/run-ci-javascript-tests.js --maxWorkers 2 + - run_e2e: + platform: js + + # Optionally, run disabled tests + - when: + condition: << parameters.run_disabled_tests >> + steps: + - run: echo "Failing tests may be moved here temporarily." + # ------------------------- - store_test_results: path: ./reports/junit + # ------------------------- # JOBS: Test iOS # ------------------------- - # Runs unit tests on iOS devices test_ios: executor: reactnativeios parameters: use_frameworks: type: boolean default: false + use_hermes: + type: boolean + default: false + run_unit_tests: + description: Specifies whether unit tests should run. + type: boolean + default: false + run_detox_tests: + description: Specifies whether Detox e2e tests should run. + type: boolean + default: false + run_disabled_tests: + description: Specifies whether disabled tests should run. Set this to true to debug failing tests. + type: boolean + default: false environment: - REPORTS_DIR: "./reports/junit" steps: @@ -318,128 +382,108 @@ jobs: - run_yarn - run: | - cd RNTester + cd packages/rn-tester bundle check || bundle install - run: name: Boot iPhone Simulator command: source scripts/.tests.env && xcrun simctl boot "$IOS_DEVICE" || true - - run: - name: Fetch CocoaPods Specs - command: | - curl https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash -s cf - - - when: - condition: << parameters.use_frameworks >> - steps: - - run: - name: Set USE_FRAMEWORKS=1 - command: echo "export USE_FRAMEWORKS=1" >> $BASH_ENV - - - with_pods_cache_span: - steps: - - run: - name: Generate RNTesterPods Workspace - command: cd RNTester && pod install --verbose - - - with_brew_cache_span: - steps: - - brew_install: - package: watchman - - run: touch .watchmanconfig - - - run: yarn test-ios - - store_test_results: - path: ./reports/junit - - # Runs iOS end-to-end tests - test_ios_e2e: - executor: reactnativeios - steps: - - restore_cache_checkout: - checkout_type: ios - - setup_artifacts - - run_yarn - - - run: - name: Boot iPhone Simulator - command: source scripts/.tests.env && xcrun simctl boot "$IOS_DEVICE" || true - - run: name: Configure Environment Variables command: | - echo 'export PATH=/usr/local/opt/node@8/bin:$PATH' >> $BASH_ENV + echo 'export PATH=/usr/local/opt/node@12/bin:$PATH' >> $BASH_ENV source $BASH_ENV - # Brew - with_brew_cache_span: steps: - brew_install: - package: node@8 - - run: HOMEBREW_NO_AUTO_UPDATE=1 brew tap wix/brew >/dev/null + package: watchman - brew_install: - package: applesimutils + package: node@12 + - run: + name: "Brew: Tap wix/brew" + command: HOMEBREW_NO_AUTO_UPDATE=1 brew tap wix/brew >/dev/null - brew_install: - package: watchman - # Configure Watchman - - run: touch .watchmanconfig - - - restore_cache: - keys: - - v1-cocoapods-{{ checksum "template/ios/Podfile" }} - - v1-cocoapods- - - - run: pod setup + package: applesimutils - run: - name: Generate RNTesterPods Workspace - command: pushd RNTester && pod install --verbose && popd + name: Configure Watchman + command: touch .watchmanconfig - - run: - name: Run Detox iOS End-to-End Tests - command: yarn run build-ios-e2e && yarn run test-ios-e2e - when: always + - when: + condition: << parameters.use_frameworks >> + steps: + - run: + name: Set USE_FRAMEWORKS=1 + command: echo "export USE_FRAMEWORKS=1" >> $BASH_ENV - - run: - name: Run iOS End-to-End Tests - command: | - # free up port 8081 for the packager before running tests - set +eo pipefail - lsof -i tcp:8081 | awk 'NR!=1 {print $2}' | xargs kill - set -eo pipefail - node ./scripts/run-ci-e2e-tests.js --ios --retries 3; - when: always + - when: + condition: << parameters.use_hermes >> + steps: + - run: + name: Set USE_HERMES=1 + command: echo "export USE_HERMES=1" >> $BASH_ENV - - save_cache: - paths: - - ~/.cocoapods/repos - key: v1-cocoapods-{{ checksum "template/ios/Podfile" }} + - run: + name: Setup the CocoaPods environment + command: pod setup - - store_test_results: - path: ./reports/junit + - with_rntester_pods_cache_span: + steps: + - run: + name: Generate RNTesterPods Workspace + command: cd packages/rn-tester && USE_FABRIC=1 bundle exec pod install --verbose - test_js_e2e: - executor: node8 - steps: - - restore_cache_checkout: - checkout_type: node - - setup_artifacts - - run_yarn - - run: sudo apt-get install rsync + # ------------------------- + # Runs iOS unit tests + - when: + condition: << parameters.run_unit_tests >> + steps: + - run: + name: "Run Tests: iOS Unit and Integration Tests" + command: yarn test-ios + # Runs iOS Detox e2e tests + - when: + condition: << parameters.run_detox_tests >> + steps: + - run: + name: "Run Tests: Detox iOS End-to-End Tests" + command: yarn run build-ios-e2e && yarn run test-ios-e2e - - run: - name: Run JavaScript End-to-End Tests - command: node ./scripts/run-ci-e2e-tests.js --js --retries 3 + # Optionally, run disabled tests + - when: + condition: << parameters.run_disabled_tests >> + steps: + - run: echo "Failing tests may be moved here temporarily." + - run: + name: "Run Tests: CocoaPods" + command: ./scripts/process-podspecs.sh + - run: + name: Free up port 8081 for iOS End-to-End Tests + command: | + # free up port 8081 for the packager before running tests + set +eo pipefail + lsof -i tcp:8081 | awk 'NR!=1 {print $2}' | xargs kill + set -eo pipefail + - run_e2e: + platform: ios + # ------------------------- + # Collect Results + - report_bundle_size: + platform: ios - store_test_results: path: ./reports/junit # ------------------------- # JOBS: Test Android # ------------------------- - # Run Android tests test_android: executor: reactnativeandroid + parameters: + run_disabled_tests: + type: boolean + default: false steps: - restore_cache_checkout: checkout_type: android @@ -460,8 +504,6 @@ jobs: command: source scripts/android-setup.sh && launchAVD background: true - # Keep configuring Android dependencies while AVD boots up - # Install Buck - install_buck_tooling @@ -494,24 +536,36 @@ jobs: name: Wait for Android Virtual Device command: source scripts/android-setup.sh && waitForAVD - # Test Suite - run: - name: Run Unit Tests - command: buck test ReactAndroid/src/test/... --config build.threads=$BUILD_THREADS --xml ./reports/buck/all-results-raw.xml + name: Assemble RNTester App + command: ./gradlew packages:rn-tester:android:app:assembleRelease + # ------------------------- + # Run Android tests + - run: + name: "Run Tests: Android Unit Tests" + command: buck test ReactAndroid/src/test/... --config build.threads=$BUILD_THREADS --xml ./reports/buck/all-results-raw.xml - run: - name: Run Instrumentation Tests + name: "Build Tests: Android Instrumentation Tests" + # Here, just build the instrumentation tests. There is a known issue with installing the APK to android-21+ emulator. command: | if [[ ! -e ReactAndroid/src/androidTest/assets/AndroidTestBundle.js ]]; then echo "JavaScript bundle missing, cannot run instrumentation tests. Verify Build JavaScript Bundle step completed successfully."; exit 1; fi - source scripts/android-setup.sh && NO_BUCKD=1 retry3 timeout 300 buck install ReactAndroid/src/androidTest/buck-runner:instrumentation-tests --config build.threads=$BUILD_THREADS + source scripts/android-setup.sh && NO_BUCKD=1 retry3 timeout 300 buck build ReactAndroid/src/androidTest/buck-runner:instrumentation-tests --config build.threads=$BUILD_THREADS - - run: - name: Build Android RNTester App - command: ./gradlew RNTester:android:app:assembleRelease + # Optionally, run disabled tests + - when: + condition: << parameters.run_disabled_tests >> + steps: + - run: echo "Failing tests may be moved here temporarily." + - run_e2e: + platform: android + # ------------------------- # Collect Results + - report_bundle_size: + platform: android - run: name: Collect Test Results command: | @@ -519,10 +573,10 @@ jobs: find . -type f -regex ".*/outputs/androidTest-results/connected/.*xml" -exec cp {} ./reports/outputs/ \; find . -type f -regex ".*/buck-out/gen/ReactAndroid/src/test/.*/.*xml" -exec cp {} ./reports/buck/ \; if [ -f ~/react-native/reports/buck/all-results-raw.xml ]; then + cd ~/okbuck ./tooling/junit/buck_to_junit.sh ~/react-native/reports/buck/all-results-raw.xml ~/react-native/reports/junit/results.xml fi when: always - - store_test_results: path: ./reports/junit @@ -539,16 +593,101 @@ jobs: source ~/.bashrc nvm i node npm i -g yarn - npx envinfo@latest + echo y | npx envinfo@latest yarn run docker-setup-android yarn run docker-build-android + # ------------------------- + # JOBS: Windows + # ------------------------- + test_windows: + executor: + name: win/default + parameters: + run_disabled_tests: + type: boolean + default: false + environment: + - ANDROID_HOME: "C:\\Android\\android-sdk" + - ANDROID_NDK: "C:\\Android\\android-sdk\\ndk\\20.1.5948944" + - ANDROID_BUILD_VERSION: 30 + - ANDROID_TOOLS_VERSION: 30.0.2 + - GRADLE_OPTS: -Dorg.gradle.daemon=false + - NDK_VERSION: 21.4.7075529 + steps: + - checkout + + - run: + name: Install Node + # Note: Version set separately for non-Windows builds, see above. + command: | + nvm install 14.17.0 + nvm use 14.17.0 + + # Setup Dependencies + - run: + name: Install Yarn + command: choco install yarn + + - run: + name: Display Environment info + command: npx envinfo@latest + + - restore_cache: + keys: + - v1-win-yarn-cache-{{ arch }}-{{ checksum "yarn.lock" }} + - run: + name: "Yarn: Install Dependencies" + command: yarn install --frozen-lockfile --non-interactive + - save_cache: + key: v1-win-yarn-cache-{{ arch }}-{{ checksum "yarn.lock" }} + paths: + - C:\Users\circleci\AppData\Local\Yarn + + # Try to install the SDK up to 3 times, since network flakiness can cause install failures + # Using a timeout of 9 mins, as circle ci will timeout if there is no output for 10 mins + - run: + name: Install Android SDK Tools + command: choco install android-sdk --timeout 540; if (!$?) { choco install android-sdk --timeout 540 --force --forcedependencies}; if (!$?) { choco install android-sdk --force --forcedependencies} + + - run: + name: Setup Android SDKs + command: | + sdkmanager --licenses + sdkmanager "system-images;android-21;google_apis;armeabi-v7a" + sdkmanager "platforms;android-%ANDROID_BUILD_VERSION%" + sdkmanager "build-tools;%ANDROID_TOOLS_VERSION%" + sdkmanager "add-ons;addon-google_apis-google-23" + sdkmanager "extras;android;m2repository" + sdkmanager "ndk;%NDK_VERSION%" + + # ------------------------- + # Run Tests + - run: + name: "Flow: Check Android" + command: yarn flow-check-android + - run: + name: "Flow: Check iOS" + command: yarn flow-check-ios + - run: + name: "Run Tests: JavaScript Tests" + command: yarn test + + # Optionally, run disabled tests + - when: + condition: << parameters.run_disabled_tests >> + steps: + - run: echo "Failing tests may be moved here temporarily." + - run: + name: Android Build + command: ./gradlew.bat packages:rn-tester:android:app:assembleRelease + # ------------------------- # JOBS: Coverage # ------------------------- # Collect JavaScript test coverage js_coverage: - executor: node8 + executor: nodelts environment: - CI_BRANCH: $CIRCLE_BRANCH - CI_PULL_REQUEST: $CIRCLE_PULL_REQUEST @@ -572,11 +711,19 @@ jobs: # ------------------------- # JOBS: Releases # ------------------------- - # Publishes new version onto npm - # Only works on stable branches when a properly tagged commit is pushed + # Publishes a new version onto npm publish_npm_package: + parameters: + publish_npm_args: + type: string + default: --nonightly executor: reactnativeandroid steps: + - run: + name: Add github.com to SSH known hosts + command: | + mkdir -p ~/.ssh + echo '|1|If6MU203eXTaaWL678YEfWkVMrw=|kqLeIAyTy8pzpj8x8Ae4Fr8Mtlc= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' >> ~/.ssh/known_hosts - restore_cache_checkout: checkout_type: android - run_yarn @@ -588,12 +735,25 @@ jobs: git config --global user.email "react-native-bot@users.noreply.github.com" git config --global user.name "npm Deployment Script" echo "machine github.com login react-native-bot password $GITHUB_TOKEN" > ~/.netrc - - run: node ./scripts/publish-npm.js + - run: node ./scripts/publish-npm.js << parameters.publish_npm_args >> + + # ------------------------- + # JOBS: Nightly + # ------------------------- + nightly_job: + machine: true + steps: + - run: + name: Nightly + command: | + echo "Nightly build run" # ------------------------- # WORK FLOWS # ------------------------- workflows: + version: 2 + tests: jobs: - setup: @@ -616,36 +776,67 @@ workflows: branches: ignore: gh-pages - test_js: + run_disabled_tests: false requires: - setup_js - - test_js_e2e: - requires: - - setup_js - - test_js - test_android: + run_disabled_tests: false requires: - setup_android - test_ios: + name: test_ios_unit_jsc + run_unit_tests: true requires: - setup_ios + # DISABLED: USE_FRAMEWORKS=1 not supported by Flipper + # - test_ios: + # name: test_ios_unit_frameworks_jsc + # use_frameworks: true + # run_unit_tests: true + # requires: + # - setup_ios - test_ios: - name: test_ios_frameworks - use_frameworks: true - requires: - - setup_ios - - test_ios_e2e: + name: test_ios_unit_hermes + use_hermes: true + run_unit_tests: true requires: - setup_ios - - test_js + # DISABLED: USE_FRAMEWORKS=1 not supported by Flipper + # - test_ios: + # name: test_ios_unit_frameworks_hermes + # use_hermes: true + # use_frameworks: true + # run_unit_tests: true + # requires: + # - setup_ios + # DISABLED: Detox tests need to be fixed + # - test_ios: + # name: test_ios_detox + # run_detox_tests: true + # requires: + # - setup_ios + # DISABLED: USE_FRAMEWORKS=1 not supported by Flipper + # - test_ios: + # name: test_ios_detox_frameworks + # use_frameworks: true + # run_detox_tests: true + # requires: + # - setup_ios - test_js: - name: test_js_lts - executor: nodelts + name: test_js_prev_lts + executor: nodeprevlts requires: - setup_js - test_docker: filters: branches: ignore: gh-pages + - test_windows: + filters: + branches: + ignore: gh-pages + run_disabled_tests: false + releases: jobs: - setup: @@ -674,11 +865,18 @@ workflows: analysis: jobs: - - setup + - setup: + name: setup_js + + - setup: + name: setup_android + checkout_type: android + executor: reactnativeandroid + # Run lints on every commit other than those to the gh-pages branch - analyze_code: requires: - - setup + - setup_android filters: branches: ignore: gh-pages @@ -686,7 +884,7 @@ workflows: # Run code checks on PRs from forks - analyze_pr: requires: - - setup + - setup_android filters: branches: only: /^pull\/.*$/ @@ -694,4 +892,24 @@ workflows: # Gather coverage - js_coverage: requires: - - setup + - setup_js + nightly: + triggers: + - schedule: + cron: "0 0 * * *" + filters: + branches: + only: + - master + jobs: + - nightly_job + + - setup: + name: setup_android + checkout_type: android + executor: reactnativeandroid + + - publish_npm_package: + publish_npm_args: --nightly + requires: + - setup_android diff --git a/.editorconfig b/.editorconfig index 45dc2a9a3fecd4..355a800148a2d9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,5 +13,12 @@ indent_size = 2 [*.gradle] indent_size = 4 +[*.kts] +indent_size = 4 + [BUCK] indent_size = 4 + +# Windows files +[*.bat] +end_of_line = crlf diff --git a/.eslintignore b/.eslintignore index fffb265e6fe623..f1beb735a483c6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,11 +1,10 @@ -# node_modules ignored by default - -**/staticBundle.js **/main.js -Libraries/vendor/**/* +**/staticBundle.js +bots/node_modules +docs/generatedComponentApiDocs.js +flow/ Libraries/Renderer/* +Libraries/vendor/**/* +node_modules/ packages/*/node_modules -pr-inactivity-bookmarklet.js -question-bookmarklet.js -flow/ -bots/node_modules \ No newline at end of file +packages/react-native-codegen/lib diff --git a/.eslintrc b/.eslintrc index 9385a7481c3ce6..f990717831deb2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,14 +5,28 @@ "./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, + "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 + } + }, + { + "files": [ + "flow-typed/**/*.js", + ], + "rules": { + quotes: 0 } }, { @@ -21,7 +35,7 @@ "**/__mocks__/**/*.js", "**/__tests__/**/*.js", "jest/**/*.js", - "RNTester/**/*.js", + "packages/rn-tester/**/*.js", ], "globals": { // Expose some Jest globals for test helpers @@ -39,8 +53,8 @@ ], "env": { "jasmine": true, - "jest": true, - }, - }, - ], + "jest": true + } + } + ] } diff --git a/.flowconfig b/.flowconfig index 4fe110f9f71d27..8d83331dec2718 100644 --- a/.flowconfig +++ b/.flowconfig @@ -11,10 +11,6 @@ ; Ignore "BUCK" generated dirs /\.buckd/ -; These should not be required directly -; require from fbjs/lib instead: require('fbjs/lib/warning') -.*/node_modules/warning/.* - ; Flow doesn't support platforms .*/Libraries/Utilities/LoadingView.js @@ -33,10 +29,10 @@ flow/ [options] emoji=true -esproposal.optional_chaining=enable -esproposal.nullish_coalescing=enable - exact_by_default=true +indexed_access=false + +format.bracket_spacing=false module.file_ext=.js module.file_ext=.json @@ -54,14 +50,6 @@ suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState suppress_type=$FlowFixMeEmpty -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ -suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError - -experimental.well_formed_exports=true -experimental.types_first=true -experimental.abstract_locations=true - [lints] sketchy-null-number=warn sketchy-null-mixed=warn @@ -70,10 +58,8 @@ untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn -inexact-spread=warn unnecessary-invariant=warn signature-verification-failure=warn -deprecated-utility=error [strict] deprecated-type @@ -85,4 +71,4 @@ untyped-import untyped-type-import [version] -^0.114.0 +^0.155.0 diff --git a/.flowconfig.android b/.flowconfig.android index f93639fd8d0917..0c3ca200341d97 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -11,10 +11,6 @@ ; Ignore "BUCK" generated dirs /\.buckd/ -; These should not be required directly -; require from fbjs/lib instead: require('fbjs/lib/warning') -.*/node_modules/warning/.* - ; Flow doesn't support platforms .*/Libraries/Utilities/LoadingView.js @@ -33,10 +29,10 @@ flow/ [options] emoji=true -esproposal.optional_chaining=enable -esproposal.nullish_coalescing=enable - exact_by_default=true +indexed_access=false + +format.bracket_spacing=false module.file_ext=.js module.file_ext=.json @@ -54,14 +50,6 @@ suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState suppress_type=$FlowFixMeEmpty -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_android\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_android\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ -suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError - -experimental.well_formed_exports=true -experimental.types_first=true -experimental.abstract_locations=true - [lints] sketchy-null-number=warn sketchy-null-mixed=warn @@ -70,10 +58,8 @@ untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn -inexact-spread=warn unnecessary-invariant=warn signature-verification-failure=warn -deprecated-utility=error [strict] deprecated-type @@ -85,4 +71,4 @@ untyped-import untyped-type-import [version] -^0.114.0 +^0.155.0 diff --git a/.gitattributes b/.gitattributes index 8f9372e008985a..45a3dcb2a20316 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,3 @@ -# 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 +# Windows files should use crlf line endings +# https://help.github.com/articles/dealing-with-line-endings/ +*.bat text eol=crlf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d699a8260ea745..38dbd76f893b2b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -35,3 +35,6 @@ Libraries/Text/* @shergin # These should not be modified through a GitHub PR LICENSE* @hramos @cpojer @yungsters + +# The eslint-config-react-native-community package requires manual publishing after merging +/packages/eslint-config-react-native-community/* @matt-oakes diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3a342453133dc8..7b530841f371c3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,4 @@ -👉 Please follow one of these issue templates: -- https://github.com/facebook/react-native/issues/new/choose +✋ To keep the backlog clean and actionable, issues will be +🚫 closed if they do not follow one of the issue templates: +👉 https://github.com/facebook/react-native/issues/new/choose -Note: to keep the backlog clean and actionable, issues may be immediately closed if they do not follow one of the above issue templates. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0856e966039493..083ab18aa0965c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,39 +2,29 @@ name: "🐛 Bug Report" about: Report a reproducible bug or regression in React Native. title: '' -labels: 'Bug' +labels: 'Needs: Triage :mag:' --- - +Please provide all the information requested. Issues that do not follow this format are likely to stall. -React Native version: - +## Description +Please provide a clear and concise description of what the bug is. Include screenshots if needed. +Please test using the latest React Native release to make sure your issue has not already been fixed: https://reactnative.dev/docs/upgrading.html + +## React Native version: +Run `react-native info` in your terminal and copy the results here. ## Steps To Reproduce +Provide a detailed list of steps that reproduce the issue. 1. 2. - - -Describe what you expected to happen: - - -Snack, code example, screenshot, or link to a repository: - - +## Expected Results +Describe what you expected to happen. +## Snack, code example, screenshot, or link to a repository: +Please provide a Snack (https://snack.expo.io/), a link to a repository on GitHub, or provide a minimal code example that reproduces the problem. +You may provide a screenshot of the application if you think it is relevant to your bug report. +Here are some tips for providing a minimal example: https://stackoverflow.com/help/mcve diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000000..7821e038ec89ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: 📃 Documentation Issue + url: https://github.com/facebook/react-native-website/issues + about: Please report documentation issues in the React Native website repository. + - name: ⤴️ Upgrade Issue + url: https://github.com/react-native-community/upgrade-support + about: Need help upgrading to a newer React Native version? Visit the Upgrade Support repository. + - name: 🤔 Questions and Help + url: https://reactnative.dev/help + about: Looking for help with your app? Please refer to the React Native community's support resources. + - name: 🚀 Discussions and Proposals + url: https://github.com/react-native-community/discussions-and-proposals + about: Discuss the future of React Native in the React Native community's discussions and proposals repository. diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index a7e1a6b0342c59..00000000000000 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: "📃 Documentation Issue" -about: Documentation issues are handled in https://github.com/facebook/react-native-website. -title: 'Docs:' -labels: 'Type: Docs' - ---- - -🚨 Please do not open a documentation issue in the core React Native repository. 🚨 - -The React Native website is hosted on a separate repository. You may let the -team know about any issues with the documentation by opening an issue there: -- https://github.com/facebook/react-native-website -- https://github.com/facebook/react-native-website/issues diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 984c4c27c1dda6..00000000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: "🤔 Questions and Help" -about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/react-native. -title: 'Question: ' -labels: 'Type: Question' - ---- - -🚨 The issue tracker is not for questions. 🚨 - -As it happens, support requests that are created as issues are likely to be closed. We want to make sure you are able to find the help you seek. Please take a look at the following resources. - -## Coding Questions - -### https://stackoverflow.com/questions/tagged/react-native - -If you have a coding question related to React Native, it might be better suited for Stack Overflow. It's a great place to browse through frequent questions about using React Native, as well as ask for help with specific questions. - - -## Talk to other React Native developers - -### https://www.reactiflux.com/ - -Reactiflux is an active community of React and React Native developers. If you are looking for immediate assistance or have a general question about React Native, the #react-native channel is a good place to start. - -If you want to participate in casual discussions about the use of React Native, consider participating in one of the following forums: -- Discord Community: https://discord.gg/0ZcbPKXt5bZjGY5n -- Spectrum Chat: https://spectrum.chat/react-native -- Facebook Group: https://www.facebook.com/groups/react.native.community - -If you'd like to discuss topics related to the future of React Native, or would like to propose a new feature or change before sending a pull request, please check out the discussions and proposals repo: -- https://github.com/react-native-community/discussions-and-proposals - -> For a full list of community resources, check out React Native's Community page at https://facebook.github.io/react-native/help. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d34a154fd4e4e1..073b9af6d242da 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,7 +6,9 @@ ## Changelog - + [CATEGORY] [TYPE] - Message diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index d229644aa40f9d..e597d1e3334e5a 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -27,9 +27,9 @@ If you'd like to discuss topics related to the future of React Native, please ch If you want to participate in casual discussions about the use of React Native, consider participating in one of the following forums: -- [Reactiflux Discord Server](https://www.reactiflux) +- [Reactiflux Discord Server](https://www.reactiflux.com) - [Spectrum Chat](https://spectrum.chat/react-native) - [React Native Community Facebook Group](https://www.facebook.com/groups/react.native.community) -> For a full list of community resources, check out [React Native's Community page](https://facebook.github.io/react-native/help). +> For a full list of community resources, check out [React Native's Community page](https://reactnative.dev/help). diff --git a/.github/respond-to-issue-based-on-label.yml b/.github/respond-to-issue-based-on-label.yml new file mode 100644 index 00000000000000..65131ff0840db0 --- /dev/null +++ b/.github/respond-to-issue-based-on-label.yml @@ -0,0 +1,56 @@ +# Configuration for Respond To Issue Based on Label https://github.com/marketplace/actions/respond-to-issue-based-on-label + +"Type: Invalid": + close: true +"Type: Question": + comment: > + We are using GitHub issues exclusively to track bugs in React Native. GitHub may not be the ideal place to ask a question, but you can try asking over on [Stack Overflow](http://stackoverflow.com/questions/tagged/react-native), or on [Reactiflux](https://www.reactiflux.com/). + close: true +"Type: Docs": + comment: > + Please report documentation issues in the [`react-native-website`](https://github.com/facebook/react-native-website/issues) repository. + close: true +"Type: Upgrade Issue": + comment: > + Do you need help upgrading to a newer React Native version? Visit the [Upgrade Support repository](https://github.com/react-native-community/upgrade-support) or use the [upgrade helper](https://react-native-community.github.io/upgrade-helper/) to see the changes that need to be made to upgrade your app. + close: true +"Resolution: For Stack Overflow": + comment: > + We are using GitHub issues exclusively to track bugs in the core React Native library. Please try asking over on [Stack Overflow](http://stackoverflow.com/questions/tagged/react-native) as it is better suited for this type of question. + close: true +"Needs: Issue Template": + comment: > +
:warning: + Missing Required Fields +
:information_source: + It looks like your issue may be missing some necessary information. GitHub provides an example template whenever a new issue is created. Could you go back and make sure to fill out the template? You may edit this issue, or close it and open a new one. +
+ labels: + - "Needs: Author Feedback" +"Needs: Environment Info": + comment: > +
:warning: + Missing Environment Information +
:information_source: + Your issue may be missing information about your development environment. You can obtain the missing information by running react-native info in a console. +
+ labels: + - "Needs: Author Feedback" +"Needs: Verify on Latest Version": + comment: > +
:warning: + Using Old Version +
:information_source: + It looks like you are using an older version of React Native. Please upgrade to the latest version, and verify if the issue persists. If it does not, please let us know so we can close out this issue. This helps us ensure we are looking at issues that still exist in the current release. +
+ labels: + - "Needs: Author Feedback" +"Needs: Repro": + comment: > +
:warning: + Missing Reproducible Example +
:information_source: + It looks like your issue is missing a reproducible example. Please provide a Snack or a repository that demonstrates the issue you are reporting in a minimal, complete, and reproducible manner. +
+ labels: + - "Needs: Author Feedback" diff --git a/.github/workflows/needs-attention.yml b/.github/workflows/needs-attention.yml new file mode 100644 index 00000000000000..e5c1a979653171 --- /dev/null +++ b/.github/workflows/needs-attention.yml @@ -0,0 +1,21 @@ +name: Issue Needs Attention +# This workflow is triggered on issue comments. +on: + issue_comment: + types: created + +jobs: + applyNeedsAttentionLabel: + name: Apply Needs Attention Label + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Apply Needs Attention Label + uses: hramos/needs-attention@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + response-required-label: "Needs: Author Feedback" + needs-attention-label: "Needs: Attention" + id: needs-attention + - name: Result + run: echo '${{ steps.needs-attention.outputs.result }}' diff --git a/.github/workflows/on-issue-labeled.yml b/.github/workflows/on-issue-labeled.yml new file mode 100644 index 00000000000000..e68682b2ecb71b --- /dev/null +++ b/.github/workflows/on-issue-labeled.yml @@ -0,0 +1,16 @@ +name: On Issue Labeled +# This workflow is triggered when a label is added to an issue. +on: + issues: + types: labeled + +jobs: + respondToIssueBasedOnLabel: + name: Respond to Issue Based on Label + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Respond to Issue Based on Label + uses: hramos/respond-to-issue-based-on-label@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 76c724ac1be2d1..0f2d0c91d407a4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,18 +23,32 @@ project.xcworkspace # Gradle /build/ -/RNTester/android/app/build/ -/RNTester/android/app/gradle/ -/RNTester/android/app/gradlew -/RNTester/android/app/gradlew.bat +/packages/react-native-codegen/android/build/ +/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/build +/packages/react-native-gradle-plugin/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 +/packages/rn-tester/android/app/gradlew.bat /ReactAndroid/build/ +/ReactAndroid/gradle/ +/ReactAndroid/gradlew +/ReactAndroid/gradlew.bat # Buck .buckd buck-out +/.lsp.buckd +/.lsp-buck-out /ReactAndroid/src/main/jni/prebuilt/lib/ /ReactAndroid/src/main/gen +# Android Studio +.project +.settings +.classpath + # Watchman .watchmanconfig @@ -67,28 +81,33 @@ package-lock.json # ReactCommon subdir shouldn't have Xcode project /ReactCommon/**/*.xcodeproj -RNTester/build +/packages/rn-tester/build +/packages/rn-tester/android/app/build/* # Libs that shouldn't have Xcode project /Libraries/FBLazyVector/**/*.xcodeproj -/Libraries/FBReactNativeSpec/**/*.xcodeproj /Libraries/RCTRequired/**/*.xcodeproj /React/CoreModules/**/*.xcodeproj +/React/FBReactNativeSpec/**/*.xcodeproj /packages/react-native-codegen/**/*.xcodeproj # CocoaPods /template/ios/Pods/ /template/ios/Podfile.lock -/RNTester/Gemfile.lock +/packages/rn-tester/Gemfile.lock # Ignore RNTester specific Pods, but keep the __offline_mirrors__ here. -RNTester/Pods/* -!RNTester/Pods/__offline_mirrors +/packages/rn-tester/Pods/* +!/packages/rn-tester/Pods/__offline_mirrors__ # react-native-codegen -/ReactCommon/fabric/components/rncore/ -/schema-rncore.json +/React/FBReactNativeSpec/FBReactNativeSpec +/packages/react-native-codegen/lib +/ReactCommon/react/renderer/components/rncore/ # Visual studio .vscode .vs + +# Android memory profiler files +*.hprof diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index dba04c1e1786b2..00000000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -8.11.3 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 84f3bf082aa3fe..179e9de2e3e288 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,8 +30,8 @@ We use GitHub issues and pull requests to keep track of bug reports and contribu You can learn more about the contribution process in the following documents: -* [Issues](https://github.com/facebook/react-native/wiki/Issues) -* [Pull Requests](https://github.com/facebook/react-native/wiki/Pull-Requests) +* [Issues](https://github.com/facebook/react-native/wiki/Triaging-GitHub-Issues) +* [Pull Requests](https://github.com/facebook/react-native/wiki/Managing-Pull-Requests) We also have a thriving community of contributors who would be happy to help you get set up. You can reach out to us through [@ReactNative](http://twitter.com/reactnative) (the React Native team) and [@ReactNativeComm](http://twitter.com/reactnativecomm) (the React Native Community organization). @@ -58,7 +58,7 @@ Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe ## Helping with Documentation -The React Native documentation is hosted as part of the React Native website repository at https://github.com/facebook/react-native-website. The website itself is located at and it is built using [Docusaurus](https://docusaurus.io/). If there's anything you'd like to change in the docs, you can get started by clicking on the "Edit" button located on the upper right of most pages in the website. +The React Native documentation is hosted as part of the React Native website repository at https://github.com/facebook/react-native-website. The website itself is located at and it is built using [Docusaurus](https://docusaurus.io/). If there's anything you'd like to change in the docs, you can get started by clicking on the "Edit" button located on the upper right of most pages in the website. If you are adding new functionality or introducing a change in behavior, we will ask you to update the documentation to reflect your changes. @@ -72,18 +72,22 @@ 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). The process of proposing a change to React Native can be summarized as follows: +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. If you've added code that should be tested, add tests. -3. If you've changed APIs, update the documentation. -4. Ensure the test suite passes, either locally or on CI once you opened a pull request. -5. Make sure your code lints (for example via `yarn lint --fix`). -6. Push the changes to your fork. -7. Create a pull request to the React Native repository. -8. Review and address comments on your pull request. +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. +6. Make sure your code lints (for example via `yarn lint --fix`). +7. Push the changes to your fork. +8. Create a pull request to the React Native repository. +9. Review and address comments on your pull request. 1. A bot may comment with suggestions. Generally we ask you to resolve these first before a maintainer will review your code. -9. If you haven't already, please complete the [Contributor License Agreement](https://github.com/facebook/react-native/wiki/Contributor-License-Agreement) ("CLA"). **[Complete your CLA here.](https://code.facebook.com/cla)** + 2. If changes are requested and addressed, please [request review](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to notify reviewers to take another look. +10. If you haven't already, please complete the [Contributor License Agreement](https://github.com/facebook/react-native/wiki/Contributor-License-Agreement) ("CLA"). **[Complete your CLA here.](https://code.facebook.com/cla)** If all goes well, your pull request will be merged. If it is not merged, maintainers will do their best to explain the reason why. diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md index 2095cbad199a46..657cfb43a11366 100644 --- a/ECOSYSTEM.md +++ b/ECOSYSTEM.md @@ -10,14 +10,15 @@ There are three types of stakeholders: ## Partners -Partners are companies that are significantly invested in React Native and have been for years. Informed by their use of React Native, they push for improvements of the core and/or the ecosystem around it. Facebook's partners think of React Native as a product: they understand the trade offs that the project makes as well as future plans and goals. Together we shape the vision for React Native to make it the best way to build applications. +Partners are companies that are significantly invested in React Native and have been for years. Informed by their use of React Native, they push for improvements of the core and/or the ecosystem around it. Partners think of React Native as a product: they understand the trade offs that the project makes as well as future plans and goals. Together we shape the vision for React Native to make it the best way to build applications. -Our current set of partners include Callstack, Expo, Infinite Red, Microsoft and Software Mansion. Many engineers from these companies are core contributors, and their partner responsibilities also include: +React Native's current set of partners include Callstack, Expo, Facebook, Infinite Red, Microsoft and Software Mansion. Many engineers from these companies are core contributors, and their partner responsibilities also include: * **[Callstack](https://callstack.com/):** Manages releases, maintains the [React Native CLI](https://github.com/react-native-community/react-native-cli) and organizes [React Native EU](https://react-native.eu/) * **[Expo](https://expo.io/):** Builds [expo](https://github.com/expo/expo) on top of React Native to simplify app development -* **[Infinite Red](https://infinite.red/):** Maintains the [ignite cli/boilerplate](https://github.com/infinitered/ignite), organizes [Chain React Conf](https://infinite.red/ChainReactConf) -* **[Microsoft](https://www.microsoft.com/en-gb/):** Develops [React Native Windows](https://github.com/Microsoft/react-native-windows) for the Universal Windows Platform (UWP) +* **[Facebook](https://opensource.facebook.com):** Oversees the React Native product and maintains the [React Native core repo](https://reactnative.dev/) +* **[Infinite Red](https://infinite.red/):** Maintains the [ignite cli/boilerplate](https://github.com/infinitered/ignite), organizes [Chain React Conf](https://cr.infinite.red/) +* **[Microsoft](http://aka.ms/reactnative):** Develops [React Native Windows](https://github.com/Microsoft/react-native-windows) and [React Native macOS](https://github.com/microsoft/react-native-macos) for building apps that target Windows and macOS * **[Software Mansion](https://swmansion.com/):** Maintain core infrastructure including JSC, Animated, and other popular third-party plugins. In terms of open source work, pull requests from partners are commonly prioritized. When you are contributing to React Native, you'll most likely meet somebody who works at one of the partner companies and who is a core contributor: diff --git a/IntegrationTests/AccessibilityManagerTest.js b/IntegrationTests/AccessibilityManagerTest.js index dd155cec22c517..a6f4c0d93bb64c 100644 --- a/IntegrationTests/AccessibilityManagerTest.js +++ b/IntegrationTests/AccessibilityManagerTest.js @@ -8,15 +8,12 @@ * @flow strict-local */ -'use strict'; - -const React = require('react'); -const ReactNative = require('react-native'); -const {View} = ReactNative; -const RCTDeviceEventEmitter = require('react-native/Libraries/EventEmitter/RCTDeviceEventEmitter'); -const {TestModule} = ReactNative.NativeModules; -import NativeAccessibilityManager from 'react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager'; import invariant from 'invariant'; +import NativeAccessibilityManager from 'react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager'; +import {DeviceEventEmitter, NativeModules, View} from 'react-native'; +import * as React from 'react'; + +const {TestModule} = NativeModules; class AccessibilityManagerTest extends React.Component<{...}> { componentDidMount() { @@ -39,7 +36,7 @@ class AccessibilityManagerTest extends React.Component<{...}> { accessibilityExtraExtraLarge: 11.0, accessibilityExtraExtraExtraLarge: 12.0, }); - RCTDeviceEventEmitter.addListener('didUpdateDimensions', update => { + DeviceEventEmitter.addListener('didUpdateDimensions', update => { TestModule.markTestPassed(update.window.fontScale === 4.0); }); } diff --git a/IntegrationTests/AsyncStorageTest.js b/IntegrationTests/AsyncStorageTest.js index e33cedfdd82482..51ce374556e90b 100644 --- a/IntegrationTests/AsyncStorageTest.js +++ b/IntegrationTests/AsyncStorageTest.js @@ -16,6 +16,7 @@ const {AsyncStorage, Text, View, StyleSheet} = ReactNative; const {TestModule} = ReactNative.NativeModules; const deepDiffer = require('react-native/Libraries/Utilities/differ/deepDiffer'); +const nullthrows = require('nullthrows'); const DEBUG = false; @@ -43,15 +44,32 @@ function expectTrue(condition: boolean, message: string) { } } +// Type-safe wrapper around JSON.stringify +function stringify( + value: + | void + | null + | string + | number + | boolean + | {...} + | $ReadOnlyArray, +): string { + if (typeof value === 'undefined') { + return 'undefined'; + } + return JSON.stringify(value); +} + function expectEqual(lhs, rhs, testname: string) { expectTrue( !deepDiffer(lhs, rhs), 'Error in test ' + testname + ': expected\n' + - JSON.stringify(rhs) + + stringify(rhs) + '\ngot\n' + - JSON.stringify(lhs), + stringify(lhs), ); } @@ -61,7 +79,7 @@ function expectAsyncNoError(place, err) { } expectTrue( err === null, - 'Unexpected error in ' + place + ': ' + JSON.stringify(err), + 'Unexpected error in ' + place + ': ' + stringify(err), ); } @@ -71,7 +89,7 @@ function testSetAndGet() { AsyncStorage.getItem(KEY_1, (err2, result) => { expectAsyncNoError('testSetAndGet/getItem', err2); expectEqual(result, VAL_1, 'testSetAndGet setItem'); - updateMessage('get(key_1) correctly returned ' + result); + updateMessage('get(key_1) correctly returned ' + String(result)); runTestCase('should get null for missing key', testMissingGet); }); }); @@ -81,7 +99,7 @@ function testMissingGet() { AsyncStorage.getItem(KEY_2, (err, result) => { expectAsyncNoError('testMissingGet/setItem', err); expectEqual(result, null, 'testMissingGet'); - updateMessage('missing get(key_2) correctly returned ' + result); + updateMessage('missing get(key_2) correctly returned ' + String(result)); runTestCase('check set twice results in a single key', testSetTwice); }); } @@ -105,8 +123,9 @@ function testRemoveItem() { AsyncStorage.getAllKeys((err, result) => { expectAsyncNoError('testRemoveItem/getAllKeys', err); expectTrue( - result.indexOf(KEY_1) >= 0 && result.indexOf(KEY_2) >= 0, - 'Missing KEY_1 or KEY_2 in ' + '(' + result + ')', + nullthrows(result).indexOf(KEY_1) >= 0 && + nullthrows(result).indexOf(KEY_2) >= 0, + 'Missing KEY_1 or KEY_2 in ' + '(' + nullthrows(result).join() + ')', ); updateMessage('testRemoveItem - add two items'); AsyncStorage.removeItem(KEY_1, err2 => { @@ -123,8 +142,8 @@ function testRemoveItem() { AsyncStorage.getAllKeys((err4, result3) => { expectAsyncNoError('testRemoveItem/getAllKeys', err4); expectTrue( - result3.indexOf(KEY_1) === -1, - 'Unexpected: KEY_1 present in ' + result3, + nullthrows(result3).indexOf(KEY_1) === -1, + 'Unexpected: KEY_1 present in ' + nullthrows(result3).join(), ); updateMessage('proper length returned.'); runTestCase('should merge values', testMerge); @@ -137,13 +156,17 @@ function testRemoveItem() { } function testMerge() { - AsyncStorage.setItem(KEY_MERGE, JSON.stringify(VAL_MERGE_1), err1 => { + AsyncStorage.setItem(KEY_MERGE, stringify(VAL_MERGE_1), err1 => { expectAsyncNoError('testMerge/setItem', err1); - AsyncStorage.mergeItem(KEY_MERGE, JSON.stringify(VAL_MERGE_2), err2 => { + AsyncStorage.mergeItem(KEY_MERGE, stringify(VAL_MERGE_2), err2 => { expectAsyncNoError('testMerge/mergeItem', err2); AsyncStorage.getItem(KEY_MERGE, (err3, result) => { expectAsyncNoError('testMerge/setItem', err3); - expectEqual(JSON.parse(result), VAL_MERGE_EXPECT, 'testMerge'); + expectEqual( + JSON.parse(nullthrows(result)), + VAL_MERGE_EXPECT, + 'testMerge', + ); updateMessage('objects deeply merged\nDone!'); runTestCase('multi set and get', testOptimizedMultiGet); }); @@ -152,7 +175,10 @@ function testMerge() { } function testOptimizedMultiGet() { - let batch = [[KEY_1, VAL_1], [KEY_2, VAL_2]]; + let batch = [ + [KEY_1, VAL_1], + [KEY_2, VAL_2], + ]; let keys = batch.map(([key, value]) => key); AsyncStorage.multiSet(batch, err1 => { // yes, twice on purpose @@ -162,8 +188,7 @@ function testOptimizedMultiGet() { expectAsyncNoError(`${i} testOptimizedMultiGet/multiGet`, err2); expectEqual(result, batch, `${i} testOptimizedMultiGet multiGet`); updateMessage( - 'multiGet([key_1, key_2]) correctly returned ' + - JSON.stringify(result), + 'multiGet([key_1, key_2]) correctly returned ' + stringify(result), ); done(); }); @@ -193,9 +218,10 @@ class AsyncStorageTest extends React.Component<{...}, $FlowFixMeState> { return ( - {/* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This - * comment suppresses an error found when Flow v0.54 was deployed. - * To see the error delete this comment and run Flow. */ + {/* $FlowFixMe[incompatible-type] (>=0.54.0 site=react_native_fb,react_ + * native_oss) This comment suppresses an error found when Flow v0.54 + * was deployed. To see the error delete this comment and run Flow. + */ this.constructor.displayName + ': '} {this.state.done ? 'Done' : 'Testing...'} {'\n\n' + this.state.messages} diff --git a/IntegrationTests/GlobalEvalWithSourceUrlTest.js b/IntegrationTests/GlobalEvalWithSourceUrlTest.js index 52fbe0e2fa8aa7..a69ee3728c48b0 100644 --- a/IntegrationTests/GlobalEvalWithSourceUrlTest.js +++ b/IntegrationTests/GlobalEvalWithSourceUrlTest.js @@ -10,6 +10,8 @@ 'use strict'; +import type {ExtendedError} from 'react-native/Libraries/Core/ExtendedError'; + const React = require('react'); const ReactNative = require('react-native'); const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); @@ -31,7 +33,7 @@ class GlobalEvalWithSourceUrlTest extends React.Component<{...}> { 'Expected globalEvalWithSourceUrl(expression) to return a value', ); } - let syntaxError; + let syntaxError: ?ExtendedError; try { global.globalEvalWithSourceUrl('{'); } catch (e) { @@ -42,7 +44,12 @@ class GlobalEvalWithSourceUrlTest extends React.Component<{...}> { 'Expected globalEvalWithSourceUrl to throw on a syntax error', ); } - if (!(syntaxError instanceof SyntaxError)) { + // Hermes throws an Error instead of a SyntaxError + // https://github.com/facebook/hermes/issues/400 + if ( + syntaxError.jsEngine !== 'hermes' && + !(syntaxError instanceof SyntaxError) + ) { throw new Error( 'Expected globalEvalWithSourceUrl to throw SyntaxError on a syntax error', ); @@ -59,7 +66,7 @@ class GlobalEvalWithSourceUrlTest extends React.Component<{...}> { 'Expected globalEvalWithSourceUrl to throw an Error object', ); } - const parsedStack = parseErrorStack(error); + const parsedStack = parseErrorStack(error?.stack); if (parsedStack[0].file !== url) { throw new Error( `Expected first eval stack frame to be in ${url} but found ${String( diff --git a/IntegrationTests/IntegrationTestHarnessTest.js b/IntegrationTests/IntegrationTestHarnessTest.js index 148462286d6228..256bcd210e7538 100644 --- a/IntegrationTests/IntegrationTestHarnessTest.js +++ b/IntegrationTests/IntegrationTestHarnessTest.js @@ -13,7 +13,6 @@ const React = require('react'); const ReactNative = require('react-native'); -const requestAnimationFrame = require('fbjs/lib/requestAnimationFrame'); const {Text, View, StyleSheet} = ReactNative; const {TestModule} = ReactNative.NativeModules; @@ -57,9 +56,10 @@ class IntegrationTestHarnessTest extends React.Component { return ( - {/* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This - * comment suppresses an error found when Flow v0.54 was deployed. - * To see the error delete this comment and run Flow. */ + {/* $FlowFixMe[incompatible-type] (>=0.54.0 site=react_native_fb,react_ + * native_oss) This comment suppresses an error found when Flow v0.54 + * was deployed. To see the error delete this comment and run Flow. + */ this.constructor.displayName + ': '} {this.state.done ? 'Done' : 'Testing...'} diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index c52fe515221ede..af41da151c47de 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -40,9 +40,9 @@ const TESTS = [ ]; TESTS.forEach( - /* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error found when Flow v0.54 was deployed. To see the error - * delete this comment and run Flow. */ + /* $FlowFixMe[incompatible-call] (>=0.54.0 site=react_native_fb,react_native_ + * oss) This comment suppresses an error found when Flow v0.54 was deployed. + * To see the error delete this comment and run Flow. */ test => AppRegistry.registerComponent(test.displayName, () => test), ); @@ -60,9 +60,10 @@ class IntegrationTestsApp extends React.Component<{...}, $FlowFixMeState> { if (this.state.test) { return ( - {/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This - * comment suppresses an error when upgrading Flow's support for - * React. To see the error delete this comment and run Flow. */} + {/* $FlowFixMe[type-as-value] (>=0.53.0 site=react_native_fb,react_ + * native_oss) This comment suppresses an error when upgrading + * Flow's support for React. To see the error delete this comment + * and run Flow. */} ); @@ -79,6 +80,10 @@ class IntegrationTestsApp extends React.Component<{...}, $FlowFixMeState> { {TESTS.map(test => [ this.setState({test})} + /* $FlowFixMe[incompatible-type] (>=0.115.0 site=react_native_fb) + * This comment suppresses an error found when Flow v0.115 was + * deployed. To see the error, delete this comment and run Flow. + */ style={styles.row}> {test.displayName} , diff --git a/IntegrationTests/LayoutEventsTest.js b/IntegrationTests/LayoutEventsTest.js index c86592a600b2f1..8c24abf36622ed 100644 --- a/IntegrationTests/LayoutEventsTest.js +++ b/IntegrationTests/LayoutEventsTest.js @@ -197,4 +197,5 @@ const styles = StyleSheet.create({ }, }); +LayoutEventsTest.displayName = 'LayoutEventsTest'; module.exports = LayoutEventsTest; diff --git a/IntegrationTests/LoggingTestModule.js b/IntegrationTests/LoggingTestModule.js index 6eab2d7337fab7..dc1dfb8ac03360 100644 --- a/IntegrationTests/LoggingTestModule.js +++ b/IntegrationTests/LoggingTestModule.js @@ -11,7 +11,6 @@ const BatchedBridge = require('react-native/Libraries/BatchedBridge/BatchedBridge'); -const warning = require('fbjs/lib/warning'); const invariant = require('invariant'); const LoggingTestModule = { @@ -24,7 +23,7 @@ const LoggingTestModule = { }, timeout_ms); }, warning: function(str) { - warning(false, str); + console.warn(str); }, invariant: function(str) { invariant(false, str); diff --git a/IntegrationTests/ReactContentSizeUpdateTest.js b/IntegrationTests/ReactContentSizeUpdateTest.js index eefb6681b258e9..9c826d9f4f5328 100644 --- a/IntegrationTests/ReactContentSizeUpdateTest.js +++ b/IntegrationTests/ReactContentSizeUpdateTest.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - const RCTNativeAppEventEmitter = require('react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter'); const React = require('react'); const ReactNative = require('react-native'); @@ -17,7 +15,7 @@ const ReactNative = require('react-native'); const {View} = ReactNative; const {TestModule} = ReactNative.NativeModules; -import type EmitterSubscription from 'react-native/Libraries/vendor/emitter/EmitterSubscription'; +import {type EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; const reactViewWidth = 101; const reactViewHeight = 102; @@ -33,7 +31,7 @@ type State = {| class ReactContentSizeUpdateTest extends React.Component { _timeoutID: ?TimeoutID = null; - _subscription: ?EmitterSubscription = null; + _subscription: ?EventSubscription = null; state: State = { height: reactViewHeight, diff --git a/IntegrationTests/SimpleSnapshotTest.js b/IntegrationTests/SimpleSnapshotTest.js index db50c75e2fad69..e7d91aa4b7021d 100644 --- a/IntegrationTests/SimpleSnapshotTest.js +++ b/IntegrationTests/SimpleSnapshotTest.js @@ -13,8 +13,6 @@ const React = require('react'); const ReactNative = require('react-native'); -const requestAnimationFrame = require('fbjs/lib/requestAnimationFrame'); - const {StyleSheet, View} = ReactNative; const {TestModule} = ReactNative.NativeModules; diff --git a/IntegrationTests/SizeFlexibilityUpdateTest.js b/IntegrationTests/SizeFlexibilityUpdateTest.js index fe7a2944853845..3577a5363b5339 100644 --- a/IntegrationTests/SizeFlexibilityUpdateTest.js +++ b/IntegrationTests/SizeFlexibilityUpdateTest.js @@ -8,15 +8,13 @@ * @flow strict-local */ -'use strict'; - const RCTNativeAppEventEmitter = require('react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter'); const React = require('react'); const ReactNative = require('react-native'); const {View} = ReactNative; const {TestModule} = ReactNative.NativeModules; -import type EmitterSubscription from 'react-native/Libraries/vendor/emitter/EmitterSubscription'; +import {type EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; const reactViewWidth = 111; const reactViewHeight = 222; @@ -31,7 +29,7 @@ type Props = $ReadOnly<{| |}>; class SizeFlexibilityUpdateTest extends React.Component { - _subscription: ?EmitterSubscription = null; + _subscription: ?EventSubscription = null; UNSAFE_componentWillMount() { this._subscription = RCTNativeAppEventEmitter.addListener( diff --git a/IntegrationTests/TimersTest.js b/IntegrationTests/TimersTest.js index 8550c6f00eec6b..30473e3b8e752c 100644 --- a/IntegrationTests/TimersTest.js +++ b/IntegrationTests/TimersTest.js @@ -97,36 +97,46 @@ class TimersTest extends React.Component { } componentDidMount() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testSetTimeout0, 1000); } testSetTimeout0() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testSetTimeout1, 0); } testSetTimeout1() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testSetTimeout50, 1); } testSetTimeout50() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testRequestAnimationFrame, 50); } testRequestAnimationFrame() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.requestAnimationFrame(this.testSetInterval0); } testSetInterval0() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._nextTest = this.testSetInterval20; + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._interval = this.setInterval(this._incrementInterval, 0); } testSetInterval20() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._nextTest = this.testSetImmediate; + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._interval = this.setInterval(this._incrementInterval, 20); } testSetImmediate() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setImmediate(this.testClearTimeout0); } @@ -139,6 +149,7 @@ class TimersTest extends React.Component { testClearTimeout30() { const timeout = this.setTimeout(() => this._fail('testClearTimeout30'), 30); this.clearTimeout(timeout); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testClearMulti, 50); } @@ -156,6 +167,7 @@ class TimersTest extends React.Component { fails.forEach(timeout => this.clearTimeout(timeout)); this.setTimeout(() => this.clearTimeout(delayClear), 20); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testOrdering, 50); } @@ -191,6 +203,7 @@ class TimersTest extends React.Component { ), 25, ); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.done, 50); } @@ -249,6 +262,7 @@ class TimersTest extends React.Component { this.clearInterval(this._interval); this._interval = null; } + // $FlowFixMe[method-unbinding] this.setState({count: 0}, this._nextTest); return; } @@ -267,4 +281,5 @@ const styles = StyleSheet.create({ }, }); +TimersTest.displayName = 'TimersTest'; module.exports = TimersTest; diff --git a/Libraries/ART/ARTCGFloatArray.h b/Libraries/ART/ARTCGFloatArray.h deleted file mode 100644 index c71adf73371b6c..00000000000000 --- a/Libraries/ART/ARTCGFloatArray.h +++ /dev/null @@ -1,18 +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. - */ - -// A little helper to make sure we have the right memory allocation ready for use. -// We assume that we will only this in one place so no reference counting is necessary. -// Needs to be freed when deallocated. - -// This is fragile since this relies on these values not getting reused. Consider -// wrapping these in an Obj-C class or some ARC hackery to get refcounting. - -typedef struct { - size_t count; - CGFloat *array; -} ARTCGFloatArray; diff --git a/Libraries/ART/ARTContainer.h b/Libraries/ART/ARTContainer.h deleted file mode 100644 index 96c393fed9e930..00000000000000 --- a/Libraries/ART/ARTContainer.h +++ /dev/null @@ -1,16 +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 - -@protocol ARTContainer - -// This is used as a hook for child to mark it's parent as dirty. -// This bubbles up to the root which gets marked as dirty. -- (void)invalidate; - -@end diff --git a/Libraries/ART/ARTGroup.h b/Libraries/ART/ARTGroup.h deleted file mode 100644 index 16749792176065..00000000000000 --- a/Libraries/ART/ARTGroup.h +++ /dev/null @@ -1,17 +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 "ARTContainer.h" -#import "ARTNode.h" - -@interface ARTGroup : ARTNode - -@property (nonatomic, assign) CGRect clipping; - -@end diff --git a/Libraries/ART/ARTGroup.m b/Libraries/ART/ARTGroup.m deleted file mode 100644 index 692d03035bcfcb..00000000000000 --- a/Libraries/ART/ARTGroup.m +++ /dev/null @@ -1,24 +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 - -@implementation ARTGroup - -- (void)renderLayerTo:(CGContextRef)context -{ - - if (!CGRectIsEmpty(self.clipping)) { - CGContextClipToRect(context, self.clipping); - } - - for (ARTNode *node in self.subviews) { - [node renderTo:context]; - } -} - -@end diff --git a/Libraries/ART/ARTNode.h b/Libraries/ART/ARTNode.h deleted file mode 100644 index 0dd647fb0b3165..00000000000000 --- a/Libraries/ART/ARTNode.h +++ /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. - */ - -#import - -/** - * ART nodes are implemented as empty UIViews but this is just an implementation detail to fit - * into the existing view management. They should also be shadow views and painted on a background - * thread. - */ - -@interface ARTNode : UIView - -@property (nonatomic, assign) CGFloat opacity; - -- (void)invalidate; -- (void)renderTo:(CGContextRef)context; - -/** - * renderTo will take opacity into account and draw renderLayerTo off-screen if there is opacity - * specified, then composite that onto the context. renderLayerTo always draws at opacity=1. - * @abstract - */ -- (void)renderLayerTo:(CGContextRef)context; - -@end diff --git a/Libraries/ART/ARTNode.m b/Libraries/ART/ARTNode.m deleted file mode 100644 index 8cd3c551b9484b..00000000000000 --- a/Libraries/ART/ARTNode.m +++ /dev/null @@ -1,80 +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 - -@implementation ARTNode - -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex -{ - [super insertReactSubview:subview atIndex:atIndex]; - [self insertSubview:subview atIndex:atIndex]; - [self invalidate]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - [super removeReactSubview:subview]; - [self invalidate]; -} - -- (void)didUpdateReactSubviews -{ - // Do nothing, as subviews are inserted by insertReactSubview: -} - -- (void)setOpacity:(CGFloat)opacity -{ - [self invalidate]; - _opacity = opacity; -} - -- (void)setTransform:(CGAffineTransform)transform -{ - [self invalidate]; - super.transform = transform; -} - -- (void)invalidate -{ - id container = (id)self.superview; - [container invalidate]; -} - -- (void)renderTo:(CGContextRef)context -{ - if (self.opacity <= 0) { - // Nothing to paint - return; - } - if (self.opacity >= 1) { - // Just paint at full opacity - CGContextSaveGState(context); - CGContextConcatCTM(context, self.transform); - CGContextSetAlpha(context, 1); - [self renderLayerTo:context]; - CGContextRestoreGState(context); - return; - } - // This needs to be painted on a layer before being composited. - CGContextSaveGState(context); - CGContextConcatCTM(context, self.transform); - CGContextSetAlpha(context, self.opacity); - CGContextBeginTransparencyLayer(context, NULL); - [self renderLayerTo:context]; - CGContextEndTransparencyLayer(context); - CGContextRestoreGState(context); -} - -- (void)renderLayerTo:(CGContextRef)context -{ - // abstract -} - -@end diff --git a/Libraries/ART/ARTRenderable.h b/Libraries/ART/ARTRenderable.h deleted file mode 100644 index 0fb86bd9b8511a..00000000000000 --- a/Libraries/ART/ARTRenderable.h +++ /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. - */ - -#import - -#import "ARTBrush.h" -#import "ARTCGFloatArray.h" -#import "ARTNode.h" - -@interface ARTRenderable : ARTNode - -@property (nonatomic, strong) ARTBrush *fill; -@property (nonatomic, assign) CGColorRef stroke; -@property (nonatomic, assign) CGFloat strokeWidth; -@property (nonatomic, assign) CGLineCap strokeCap; -@property (nonatomic, assign) CGLineJoin strokeJoin; -@property (nonatomic, assign) ARTCGFloatArray strokeDash; - -@end diff --git a/Libraries/ART/ARTRenderable.m b/Libraries/ART/ARTRenderable.m deleted file mode 100644 index e99bda698c7241..00000000000000 --- a/Libraries/ART/ARTRenderable.m +++ /dev/null @@ -1,87 +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 - -@implementation ARTRenderable - -- (void)setFill:(ARTBrush *)fill -{ - [self invalidate]; - _fill = fill; -} - -- (void)setStroke:(CGColorRef)stroke -{ - if (stroke == _stroke) { - return; - } - [self invalidate]; - CGColorRelease(_stroke); - _stroke = CGColorRetain(stroke); -} - -- (void)setStrokeWidth:(CGFloat)strokeWidth -{ - [self invalidate]; - _strokeWidth = strokeWidth; -} - -- (void)setStrokeCap:(CGLineCap)strokeCap -{ - [self invalidate]; - _strokeCap = strokeCap; -} - -- (void)setStrokeJoin:(CGLineJoin)strokeJoin -{ - [self invalidate]; - _strokeJoin = strokeJoin; -} - -- (void)setStrokeDash:(ARTCGFloatArray)strokeDash -{ - if (strokeDash.array == _strokeDash.array) { - return; - } - if (_strokeDash.array) { - free(_strokeDash.array); - } - [self invalidate]; - _strokeDash = strokeDash; -} - -- (void)dealloc -{ - CGColorRelease(_stroke); - if (_strokeDash.array) { - free(_strokeDash.array); - } -} - -- (void)renderTo:(CGContextRef)context -{ - if (self.opacity <= 0 || self.opacity >= 1 || (self.fill && self.stroke)) { - // If we have both fill and stroke, we will need to paint this using normal compositing - [super renderTo: context]; - return; - } - // This is a terminal with only one painting. Therefore we don't need to paint this - // off-screen. We can just composite it straight onto the buffer. - CGContextSaveGState(context); - CGContextConcatCTM(context, self.transform); - CGContextSetAlpha(context, self.opacity); - [self renderLayerTo:context]; - CGContextRestoreGState(context); -} - -- (void)renderLayerTo:(CGContextRef)context -{ - // abstract -} - -@end diff --git a/Libraries/ART/ARTShape.h b/Libraries/ART/ARTShape.h deleted file mode 100644 index 356c2d240c6eba..00000000000000 --- a/Libraries/ART/ARTShape.h +++ /dev/null @@ -1,16 +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 "ARTRenderable.h" - -@interface ARTShape : ARTRenderable - -@property (nonatomic, assign) CGPathRef d; - -@end diff --git a/Libraries/ART/ARTShape.m b/Libraries/ART/ARTShape.m deleted file mode 100644 index 0a70e3bbf17786..00000000000000 --- a/Libraries/ART/ARTShape.m +++ /dev/null @@ -1,66 +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 - -@implementation ARTShape - -- (void)setD:(CGPathRef)d -{ - if (d == _d) { - return; - } - [self invalidate]; - CGPathRelease(_d); - _d = CGPathRetain(d); -} - -- (void)dealloc -{ - CGPathRelease(_d); -} - -- (void)renderLayerTo:(CGContextRef)context -{ - if ((!self.fill && !self.stroke) || !self.d) { - return; - } - - CGPathDrawingMode mode = kCGPathStroke; - if (self.fill) { - if ([self.fill applyFillColor:context]) { - mode = kCGPathFill; - } else { - CGContextSaveGState(context); - CGContextAddPath(context, self.d); - CGContextClip(context); - [self.fill paint:context]; - CGContextRestoreGState(context); - if (!self.stroke) { - return; - } - } - } - if (self.stroke) { - CGContextSetStrokeColorWithColor(context, self.stroke); - CGContextSetLineWidth(context, self.strokeWidth); - CGContextSetLineCap(context, self.strokeCap); - CGContextSetLineJoin(context, self.strokeJoin); - ARTCGFloatArray dash = self.strokeDash; - if (dash.count) { - CGContextSetLineDash(context, 0, dash.array, dash.count); - } - if (mode == kCGPathFill) { - mode = kCGPathFillStroke; - } - } - - CGContextAddPath(context, self.d); - CGContextDrawPath(context, mode); -} - -@end diff --git a/Libraries/ART/ARTSurfaceView.h b/Libraries/ART/ARTSurfaceView.h deleted file mode 100644 index 43e61988d5775c..00000000000000 --- a/Libraries/ART/ARTSurfaceView.h +++ /dev/null @@ -1,14 +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 "ARTContainer.h" - -@interface ARTSurfaceView : UIView - -@end diff --git a/Libraries/ART/ARTSurfaceView.m b/Libraries/ART/ARTSurfaceView.m deleted file mode 100644 index a715efec5b1756..00000000000000 --- a/Libraries/ART/ARTSurfaceView.m +++ /dev/null @@ -1,56 +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 - -@implementation ARTSurfaceView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - self.opaque = NO; - } - - return self; -} - -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex -{ - [super insertReactSubview:subview atIndex:atIndex]; - [self insertSubview:subview atIndex:atIndex]; - [self invalidate]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - [super removeReactSubview:subview]; - [self invalidate]; -} - -- (void)didUpdateReactSubviews -{ - // Do nothing, as subviews are inserted by insertReactSubview: -} - -- (void)invalidate -{ - [self setNeedsDisplay]; -} - -- (void)drawRect:(CGRect)rect -{ - CGContextRef context = UIGraphicsGetCurrentContext(); - for (ARTNode *node in self.subviews) { - [node renderTo:context]; - } -} - -@end diff --git a/Libraries/ART/ARTText.h b/Libraries/ART/ARTText.h deleted file mode 100644 index 539cbd7feb8e81..00000000000000 --- a/Libraries/ART/ARTText.h +++ /dev/null @@ -1,18 +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 "ARTRenderable.h" -#import "ARTTextFrame.h" - -@interface ARTText : ARTRenderable - -@property (nonatomic, assign) CTTextAlignment alignment; -@property (nonatomic, assign) ARTTextFrame textFrame; - -@end diff --git a/Libraries/ART/ARTText.m b/Libraries/ART/ARTText.m deleted file mode 100644 index 0a7709d169cac0..00000000000000 --- a/Libraries/ART/ARTText.m +++ /dev/null @@ -1,125 +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 - -@implementation ARTText - -- (void)setAlignment:(CTTextAlignment)alignment -{ - [self invalidate]; - _alignment = alignment; -} - -static void ARTFreeTextFrame(ARTTextFrame frame) -{ - if (frame.count) { - // We must release each line before freeing up this struct - for (int i = 0; i < frame.count; i++) { - CFRelease(frame.lines[i]); - } - free(frame.lines); - free(frame.widths); - } -} - -- (void)setTextFrame:(ARTTextFrame)frame -{ - if (frame.lines != _textFrame.lines) { - ARTFreeTextFrame(_textFrame); - } - [self invalidate]; - _textFrame = frame; -} - -- (void)dealloc -{ - ARTFreeTextFrame(_textFrame); -} - -- (void)renderLayerTo:(CGContextRef)context -{ - ARTTextFrame frame = self.textFrame; - - if ((!self.fill && !self.stroke) || !frame.count) { - return; - } - - // to-do: draw along a path - - CGTextDrawingMode mode = kCGTextStroke; - if (self.fill) { - if ([self.fill applyFillColor:context]) { - mode = kCGTextFill; - } else { - - for (int i = 0; i < frame.count; i++) { - CGContextSaveGState(context); - // Inverse the coordinate space since CoreText assumes a bottom-up coordinate space - CGContextScaleCTM(context, 1.0, -1.0); - CGContextSetTextDrawingMode(context, kCGTextClip); - [self renderLineTo:context atIndex:i]; - // Inverse the coordinate space back to the original before filling - CGContextScaleCTM(context, 1.0, -1.0); - [self.fill paint:context]; - // Restore the state so that the next line can be clipped separately - CGContextRestoreGState(context); - } - - if (!self.stroke) { - return; - } - } - } - if (self.stroke) { - CGContextSetStrokeColorWithColor(context, self.stroke); - CGContextSetLineWidth(context, self.strokeWidth); - CGContextSetLineCap(context, self.strokeCap); - CGContextSetLineJoin(context, self.strokeJoin); - ARTCGFloatArray dash = self.strokeDash; - if (dash.count) { - CGContextSetLineDash(context, 0, dash.array, dash.count); - } - if (mode == kCGTextFill) { - mode = kCGTextFillStroke; - } - } - - CGContextSetTextDrawingMode(context, mode); - - // Inverse the coordinate space since CoreText assumes a bottom-up coordinate space - CGContextScaleCTM(context, 1.0, -1.0); - for (int i = 0; i < frame.count; i++) { - [self renderLineTo:context atIndex:i]; - } -} - -- (void)renderLineTo:(CGContextRef)context atIndex:(int)index -{ - ARTTextFrame frame = self.textFrame; - CGFloat shift; - switch (self.alignment) { - case kCTTextAlignmentRight: - shift = frame.widths[index]; - break; - case kCTTextAlignmentCenter: - shift = (frame.widths[index] / 2); - break; - default: - shift = 0; - break; - } - // We should consider snapping this shift to device pixels to improve rendering quality - // when a line has subpixel width. - CGContextSetTextPosition(context, -shift, -frame.baseLine - frame.lineHeight * index); - CTLineRef line = frame.lines[index]; - CTLineDraw(line, context); -} - -@end diff --git a/Libraries/ART/ARTTextFrame.h b/Libraries/ART/ARTTextFrame.h deleted file mode 100644 index 4b77fcf33c9bcb..00000000000000 --- a/Libraries/ART/ARTTextFrame.h +++ /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. - */ - -#import - -// A little helper to make sure we have a set of lines including width ready for use. -// We assume that we will only this in one place so no reference counting is necessary. -// Needs to be freed when deallocated. - -// This is fragile since this relies on these values not getting reused. Consider -// wrapping these in an Obj-C class or some ARC hackery to get refcounting. - -typedef struct { - size_t count; - CGFloat baseLine; // Distance from the origin to the base line of the first line - CGFloat lineHeight; // Distance between lines - CTLineRef *lines; - CGFloat *widths; // Width of each line -} ARTTextFrame; diff --git a/Libraries/ART/Brushes/ARTBrush.h b/Libraries/ART/Brushes/ARTBrush.h deleted file mode 100644 index 018246a267da48..00000000000000 --- a/Libraries/ART/Brushes/ARTBrush.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. - */ - -#import -#import - -@interface ARTBrush : NSObject - -/* @abstract */ -- (instancetype)initWithArray:(NSArray *)data NS_DESIGNATED_INITIALIZER; - -/** - * For certain brushes we can fast path a combined fill and stroke. - * For those brushes we override applyFillColor which sets the fill - * color to be used by those batch paints. Those return YES. - * We can't batch gradient painting in CoreGraphics, so those will - * return NO and paint gets called instead. - * @abstract - */ -- (BOOL)applyFillColor:(CGContextRef)context; - -/** - * paint fills the context with a brush. The context is assumed to - * be clipped. - * @abstract - */ -- (void)paint:(CGContextRef)context; - -@end diff --git a/Libraries/ART/Brushes/ARTBrush.m b/Libraries/ART/Brushes/ARTBrush.m deleted file mode 100644 index 478e8a9526b0fd..00000000000000 --- a/Libraries/ART/Brushes/ARTBrush.m +++ /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 - -@implementation ARTBrush - -- (instancetype)initWithArray:(NSArray *)data -{ - return [super init]; -} - -RCT_NOT_IMPLEMENTED(- (instancetype)init) - -- (BOOL)applyFillColor:(CGContextRef)context -{ - return NO; -} - -- (void)paint:(CGContextRef)context -{ - // abstract -} - -@end diff --git a/Libraries/ART/Brushes/ARTLinearGradient.h b/Libraries/ART/Brushes/ARTLinearGradient.h deleted file mode 100644 index d177081c714fe1..00000000000000 --- a/Libraries/ART/Brushes/ARTLinearGradient.h +++ /dev/null @@ -1,12 +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 "ARTBrush.h" - -@interface ARTLinearGradient : ARTBrush - -@end diff --git a/Libraries/ART/Brushes/ARTLinearGradient.m b/Libraries/ART/Brushes/ARTLinearGradient.m deleted file mode 100644 index 365821fb9a9de9..00000000000000 --- a/Libraries/ART/Brushes/ARTLinearGradient.m +++ /dev/null @@ -1,48 +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 "RCTConvert+ART.h" - -@implementation ARTLinearGradient -{ - CGGradientRef _gradient; - CGPoint _startPoint; - CGPoint _endPoint; -} - -- (instancetype)initWithArray:(NSArray *)array -{ - if ((self = [super initWithArray:array])) { - if (array.count < 5) { - RCTLogError(@"-[%@ %@] expects 5 elements, received %@", - self.class, NSStringFromSelector(_cmd), array); - return nil; - } - _startPoint = [RCTConvert CGPoint:array offset:1]; - _endPoint = [RCTConvert CGPoint:array offset:3]; - _gradient = CGGradientRetain([RCTConvert CGGradient:array offset:5]); - } - return self; -} - -- (void)dealloc -{ - CGGradientRelease(_gradient); -} - -- (void)paint:(CGContextRef)context -{ - CGGradientDrawingOptions extendOptions = - kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; - CGContextDrawLinearGradient(context, _gradient, _startPoint, _endPoint, extendOptions); -} - -@end diff --git a/Libraries/ART/Brushes/ARTPattern.h b/Libraries/ART/Brushes/ARTPattern.h deleted file mode 100644 index 72270ab8aa2020..00000000000000 --- a/Libraries/ART/Brushes/ARTPattern.h +++ /dev/null @@ -1,12 +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 "ARTBrush.h" - -@interface ARTPattern : ARTBrush - -@end diff --git a/Libraries/ART/Brushes/ARTPattern.m b/Libraries/ART/Brushes/ARTPattern.m deleted file mode 100644 index 3f369b32d4f695..00000000000000 --- a/Libraries/ART/Brushes/ARTPattern.m +++ /dev/null @@ -1,49 +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 "RCTConvert+ART.h" - -@implementation ARTPattern -{ - CGImageRef _image; - CGRect _rect; -} - -- (instancetype)initWithArray:(NSArray *)array -{ - if ((self = [super initWithArray:array])) { - if (array.count < 6) { - RCTLogError(@"-[%@ %@] expects 6 elements, received %@", - self.class, NSStringFromSelector(_cmd), array); - return nil; - } - _image = CGImageRetain([RCTConvert CGImage:array[1]]); - _rect = [RCTConvert CGRect:array offset:2]; - } - return self; -} - -- (void)dealloc -{ - CGImageRelease(_image); -} - -// Note: This could use applyFillColor with a pattern. This could be more efficient but -// to do that, we need to calculate our own user space CTM. - -- (void)paint:(CGContextRef)context -{ - CGContextDrawTiledImage(context, _rect, _image); -} - - - -@end diff --git a/Libraries/ART/Brushes/ARTRadialGradient.h b/Libraries/ART/Brushes/ARTRadialGradient.h deleted file mode 100644 index 8759649bd43a88..00000000000000 --- a/Libraries/ART/Brushes/ARTRadialGradient.h +++ /dev/null @@ -1,12 +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 "ARTBrush.h" - -@interface ARTRadialGradient : ARTBrush - -@end diff --git a/Libraries/ART/Brushes/ARTRadialGradient.m b/Libraries/ART/Brushes/ARTRadialGradient.m deleted file mode 100644 index 6a93994a7b3237..00000000000000 --- a/Libraries/ART/Brushes/ARTRadialGradient.m +++ /dev/null @@ -1,55 +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 "RCTConvert+ART.h" - -@implementation ARTRadialGradient -{ - CGGradientRef _gradient; - CGPoint _focusPoint; - CGPoint _centerPoint; - CGFloat _radius; - CGFloat _radiusRatio; -} - -- (instancetype)initWithArray:(NSArray *)array -{ - if ((self = [super initWithArray:array])) { - if (array.count < 7) { - RCTLogError(@"-[%@ %@] expects 7 elements, received %@", - self.class, NSStringFromSelector(_cmd), array); - return nil; - } - _radius = [RCTConvert CGFloat:array[3]]; - _radiusRatio = [RCTConvert CGFloat:array[4]] / _radius; - _focusPoint.x = [RCTConvert CGFloat:array[1]]; - _focusPoint.y = [RCTConvert CGFloat:array[2]] / _radiusRatio; - _centerPoint.x = [RCTConvert CGFloat:array[5]]; - _centerPoint.y = [RCTConvert CGFloat:array[6]] / _radiusRatio; - _gradient = CGGradientRetain([RCTConvert CGGradient:array offset:7]); - } - return self; -} - -- (void)dealloc -{ - CGGradientRelease(_gradient); -} - -- (void)paint:(CGContextRef)context -{ - CGAffineTransform transform = CGAffineTransformMakeScale(1, _radiusRatio); - CGContextConcatCTM(context, transform); - CGGradientDrawingOptions extendOptions = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; - CGContextDrawRadialGradient(context, _gradient, _focusPoint, 0, _centerPoint, _radius, extendOptions); -} - -@end diff --git a/Libraries/ART/Brushes/ARTSolidColor.h b/Libraries/ART/Brushes/ARTSolidColor.h deleted file mode 100644 index 27765c6843ef6c..00000000000000 --- a/Libraries/ART/Brushes/ARTSolidColor.h +++ /dev/null @@ -1,12 +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 "ARTBrush.h" - -@interface ARTSolidColor : ARTBrush - -@end diff --git a/Libraries/ART/Brushes/ARTSolidColor.m b/Libraries/ART/Brushes/ARTSolidColor.m deleted file mode 100644 index c7e3991cf88c00..00000000000000 --- a/Libraries/ART/Brushes/ARTSolidColor.m +++ /dev/null @@ -1,38 +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 "RCTConvert+ART.h" - -@implementation ARTSolidColor -{ - CGColorRef _color; -} - -- (instancetype)initWithArray:(NSArray *)array -{ - if ((self = [super initWithArray:array])) { - _color = CGColorRetain([RCTConvert CGColor:array offset:1]); - } - return self; -} - -- (void)dealloc -{ - CGColorRelease(_color); -} - -- (BOOL)applyFillColor:(CGContextRef)context -{ - CGContextSetFillColorWithColor(context, _color); - return YES; -} - -@end diff --git a/Libraries/ART/RCTConvert+ART.h b/Libraries/ART/RCTConvert+ART.h deleted file mode 100644 index 2225e186c50d56..00000000000000 --- a/Libraries/ART/RCTConvert+ART.h +++ /dev/null @@ -1,29 +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 "ARTBrush.h" -#import "ARTCGFloatArray.h" -#import "ARTTextFrame.h" - -@interface RCTConvert (ART) - -+ (CGPathRef)CGPath:(id)json CF_RETURNS_NOT_RETAINED; -+ (CTTextAlignment)CTTextAlignment:(id)json; -+ (ARTTextFrame)ARTTextFrame:(id)json; -+ (ARTCGFloatArray)ARTCGFloatArray:(id)json; -+ (ARTBrush *)ARTBrush:(id)json; - -+ (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset; -+ (CGRect)CGRect:(id)json offset:(NSUInteger)offset; -+ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset CF_RETURNS_NOT_RETAINED; -+ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset CF_RETURNS_NOT_RETAINED; - -@end diff --git a/Libraries/ART/RCTConvert+ART.m b/Libraries/ART/RCTConvert+ART.m deleted file mode 100644 index a8299d947cc7e6..00000000000000 --- a/Libraries/ART/RCTConvert+ART.m +++ /dev/null @@ -1,222 +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 "RCTConvert+ART.h" - -#import -#import - -#import -#import -#import -#import - -@implementation RCTConvert (ART) - -+ (CGPathRef)CGPath:(id)json -{ - NSArray *arr = [self NSNumberArray:json]; - - NSUInteger count = [arr count]; - -#define NEXT_VALUE [self double:arr[i++]] - - CGMutablePathRef path = CGPathCreateMutable(); - CGPathMoveToPoint(path, NULL, 0, 0); - - @try { - NSUInteger i = 0; - while (i < count) { - NSUInteger type = [arr[i++] unsignedIntegerValue]; - switch (type) { - case 0: - CGPathMoveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE); - break; - case 1: - CGPathCloseSubpath(path); - break; - case 2: - CGPathAddLineToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE); - break; - case 3: - CGPathAddCurveToPoint(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE); - break; - case 4: - CGPathAddArc(path, NULL, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE, NEXT_VALUE == 0); - break; - default: - RCTLogError(@"Invalid CGPath type %llu at element %llu of %@", (unsigned long long)type, (unsigned long long)i, arr); - CGPathRelease(path); - return NULL; - } - } - } - @catch (NSException *exception) { - RCTLogError(@"Invalid CGPath format: %@", arr); - CGPathRelease(path); - return NULL; - } - - return (CGPathRef)CFAutorelease(path); -} - -RCT_ENUM_CONVERTER(CTTextAlignment, (@{ - @"auto": @(kCTTextAlignmentNatural), - @"left": @(kCTTextAlignmentLeft), - @"center": @(kCTTextAlignmentCenter), - @"right": @(kCTTextAlignmentRight), - @"justify": @(kCTTextAlignmentJustified), -}), kCTTextAlignmentNatural, integerValue) - -// This takes a tuple of text lines and a font to generate a CTLine for each text line. -// This prepares everything for rendering a frame of text in ARTText. -+ (ARTTextFrame)ARTTextFrame:(id)json -{ - NSDictionary *dict = [self NSDictionary:json]; - ARTTextFrame frame; - frame.count = 0; - - NSArray *lines = [self NSArray:dict[@"lines"]]; - NSUInteger lineCount = [lines count]; - if (lineCount == 0) { - return frame; - } - - CTFontRef font = (__bridge CTFontRef)[self UIFont:dict[@"font"]]; - if (!font) { - return frame; - } - - // Create a dictionary for this font - CFDictionaryRef attributes = (__bridge CFDictionaryRef)@{ - (NSString *)kCTFontAttributeName:(__bridge id)font, - (NSString *)kCTForegroundColorFromContextAttributeName: @YES - }; - - // Set up text frame with font metrics - CGFloat size = CTFontGetSize(font); - frame.count = lineCount; - frame.baseLine = size; // estimate base line - frame.lineHeight = size * 1.1; // Base on ART canvas line height estimate - frame.lines = malloc(sizeof(CTLineRef) * lineCount); - frame.widths = malloc(sizeof(CGFloat) * lineCount); - - [lines enumerateObjectsUsingBlock:^(NSString *text, NSUInteger i, BOOL *stop) { - - CFStringRef string = (__bridge CFStringRef)text; - CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); - CTLineRef line = CTLineCreateWithAttributedString(attrString); - CFRelease(attrString); - - frame.lines[i] = line; - frame.widths[i] = CTLineGetTypographicBounds(line, NULL, NULL, NULL); - }]; - - return frame; -} - -+ (ARTCGFloatArray)ARTCGFloatArray:(id)json -{ - NSArray *arr = [self NSNumberArray:json]; - NSUInteger count = arr.count; - - ARTCGFloatArray array; - array.count = count; - array.array = NULL; - - if (count) { - // Ideally, these arrays should already use the same memory layout. - // In that case we shouldn't need this new malloc. - array.array = malloc(sizeof(CGFloat) * count); - for (NSUInteger i = 0; i < count; i++) { - array.array[i] = [arr[i] doubleValue]; - } - } - - return array; -} - -+ (ARTBrush *)ARTBrush:(id)json -{ - NSArray *arr = [self NSArray:json]; - NSUInteger type = [self NSUInteger:arr.firstObject]; - switch (type) { - case 0: // solid color - // These are probably expensive allocations since it's often the same value. - // We should memoize colors but look ups may be just as expensive. - return [[ARTSolidColor alloc] initWithArray:arr]; - case 1: // linear gradient - return [[ARTLinearGradient alloc] initWithArray:arr]; - case 2: // radial gradient - return [[ARTRadialGradient alloc] initWithArray:arr]; - case 3: // pattern - return [[ARTPattern alloc] initWithArray:arr]; - default: - RCTLogError(@"Unknown brush type: %llu", (unsigned long long)type); - return nil; - } -} - -+ (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset -{ - NSArray *arr = [self NSArray:json]; - if (arr.count < offset + 2) { - RCTLogError(@"Too few elements in array (expected at least %llu): %@", (unsigned long long)(2 + offset), arr); - return CGPointZero; - } - return (CGPoint){ - [self CGFloat:arr[offset]], - [self CGFloat:arr[offset + 1]], - }; -} - -+ (CGRect)CGRect:(id)json offset:(NSUInteger)offset -{ - NSArray *arr = [self NSArray:json]; - if (arr.count < offset + 4) { - RCTLogError(@"Too few elements in array (expected at least %llu): %@", (unsigned long long)(4 + offset), arr); - return CGRectZero; - } - return (CGRect){ - {[self CGFloat:arr[offset]], [self CGFloat:arr[offset + 1]]}, - {[self CGFloat:arr[offset + 2]], [self CGFloat:arr[offset + 3]]}, - }; -} - -+ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset -{ - NSArray *arr = [self NSArray:json]; - if (arr.count < offset + 4) { - RCTLogError(@"Too few elements in array (expected at least %llu): %@", (unsigned long long)(4 + offset), arr); - return NULL; - } - return [self CGColor:[arr subarrayWithRange:(NSRange){offset, 4}]]; -} - -+ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset -{ - NSArray *arr = [self NSArray:json]; - if (arr.count < offset) { - RCTLogError(@"Too few elements in array (expected at least %llu): %@", (unsigned long long)offset, arr); - return NULL; - } - arr = [arr subarrayWithRange:(NSRange){offset, arr.count - offset}]; - ARTCGFloatArray colorsAndOffsets = [self ARTCGFloatArray:arr]; - size_t stops = colorsAndOffsets.count / 5; - CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); - CGGradientRef gradient = CGGradientCreateWithColorComponents( - rgb, - colorsAndOffsets.array, - colorsAndOffsets.array + stops * 4, - stops - ); - CGColorSpaceRelease(rgb); - free(colorsAndOffsets.array); - return (CGGradientRef)CFAutorelease(gradient); -} - -@end diff --git a/Libraries/ART/React-ART.podspec b/Libraries/ART/React-ART.podspec deleted file mode 100644 index 1a299cbc7e782a..00000000000000 --- a/Libraries/ART/React-ART.podspec +++ /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. - -require "json" - -package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json"))) -version = package['version'] - -source = { :git => 'https://github.com/facebook/react-native.git' } -if version == '1000.0.0' - # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. - source[:commit] = `git rev-parse HEAD`.strip -else - source[:tag] = "v#{version}" -end - -Pod::Spec.new do |s| - s.name = "React-ART" - s.version = version - s.summary = "A library for drawing vector graphics." - s.homepage = "http://facebook.github.io/react-native/" - s.license = package["license"] - s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "9.0", :tvos => "9.2" } - s.source = source - s.source_files = "**/*.{m}" - s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" - s.header_dir = "ART" - - s.dependency "React-Core/ARTHeaders", version -end diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.h b/Libraries/ART/ViewManagers/ARTGroupManager.h deleted file mode 100644 index 1663c7a7fda82e..00000000000000 --- a/Libraries/ART/ViewManagers/ARTGroupManager.h +++ /dev/null @@ -1,12 +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 "ARTNodeManager.h" - -@interface ARTGroupManager : ARTNodeManager - -@end diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.m b/Libraries/ART/ViewManagers/ARTGroupManager.m deleted file mode 100644 index 5a1a1c0392eeb1..00000000000000 --- a/Libraries/ART/ViewManagers/ARTGroupManager.m +++ /dev/null @@ -1,24 +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 "RCTConvert+ART.h" - -@implementation ARTGroupManager - -RCT_EXPORT_MODULE() - -- (ARTNode *)node -{ - return [ARTGroup new]; -} - -RCT_EXPORT_VIEW_PROPERTY(clipping, CGRect) - -@end diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.h b/Libraries/ART/ViewManagers/ARTNodeManager.h deleted file mode 100644 index fd00745e6a8fcd..00000000000000 --- a/Libraries/ART/ViewManagers/ARTNodeManager.h +++ /dev/null @@ -1,16 +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 - -@class ARTNode; - -@interface ARTNodeManager : RCTViewManager - -- (ARTNode *)node; - -@end diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.m b/Libraries/ART/ViewManagers/ARTNodeManager.m deleted file mode 100644 index fe8c6e6e1e1ada..00000000000000 --- a/Libraries/ART/ViewManagers/ARTNodeManager.m +++ /dev/null @@ -1,34 +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 - -@implementation ARTNodeManager - -RCT_EXPORT_MODULE() - -- (ARTNode *)node -{ - return [ARTNode new]; -} - -- (UIView *)view -{ - return [self node]; -} - -- (RCTShadowView *)shadowView -{ - return nil; -} - -RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat) -RCT_EXPORT_VIEW_PROPERTY(transform, CGAffineTransform) - -@end diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.h b/Libraries/ART/ViewManagers/ARTRenderableManager.h deleted file mode 100644 index 72b550ebbfae05..00000000000000 --- a/Libraries/ART/ViewManagers/ARTRenderableManager.h +++ /dev/null @@ -1,15 +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 "ARTNodeManager.h" -#import "ARTRenderable.h" - -@interface ARTRenderableManager : ARTNodeManager - -- (ARTRenderable *)node; - -@end diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.m b/Libraries/ART/ViewManagers/ARTRenderableManager.m deleted file mode 100644 index 1f8d4a7814e9d2..00000000000000 --- a/Libraries/ART/ViewManagers/ARTRenderableManager.m +++ /dev/null @@ -1,28 +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 "RCTConvert+ART.h" - -@implementation ARTRenderableManager - -RCT_EXPORT_MODULE() - -- (ARTRenderable *)node -{ - return [ARTRenderable new]; -} - -RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat) -RCT_EXPORT_VIEW_PROPERTY(strokeCap, CGLineCap) -RCT_EXPORT_VIEW_PROPERTY(strokeJoin, CGLineJoin) -RCT_EXPORT_VIEW_PROPERTY(fill, ARTBrush) -RCT_EXPORT_VIEW_PROPERTY(stroke, CGColor) -RCT_EXPORT_VIEW_PROPERTY(strokeDash, ARTCGFloatArray) - -@end diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.h b/Libraries/ART/ViewManagers/ARTShapeManager.h deleted file mode 100644 index 1672c257cc642a..00000000000000 --- a/Libraries/ART/ViewManagers/ARTShapeManager.h +++ /dev/null @@ -1,12 +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 "ARTRenderableManager.h" - -@interface ARTShapeManager : ARTRenderableManager - -@end diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.m b/Libraries/ART/ViewManagers/ARTShapeManager.m deleted file mode 100644 index 05fba6fac17f06..00000000000000 --- a/Libraries/ART/ViewManagers/ARTShapeManager.m +++ /dev/null @@ -1,24 +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 "RCTConvert+ART.h" - -@implementation ARTShapeManager - -RCT_EXPORT_MODULE() - -- (ARTRenderable *)node -{ - return [ARTShape new]; -} - -RCT_EXPORT_VIEW_PROPERTY(d, CGPath) - -@end diff --git a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h deleted file mode 100644 index b021c8dd4fcbeb..00000000000000 --- a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h +++ /dev/null @@ -1,12 +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 - -@interface ARTSurfaceViewManager : RCTViewManager - -@end diff --git a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m deleted file mode 100644 index 4d6a14a21ecefb..00000000000000 --- a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m +++ /dev/null @@ -1,21 +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 - -@implementation ARTSurfaceViewManager - -RCT_EXPORT_MODULE() - -- (UIView *)view -{ - return [ARTSurfaceView new]; -} - -@end diff --git a/Libraries/ART/ViewManagers/ARTTextManager.h b/Libraries/ART/ViewManagers/ARTTextManager.h deleted file mode 100644 index cc0622eb13184e..00000000000000 --- a/Libraries/ART/ViewManagers/ARTTextManager.h +++ /dev/null @@ -1,12 +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 "ARTRenderableManager.h" - -@interface ARTTextManager : ARTRenderableManager - -@end diff --git a/Libraries/ART/ViewManagers/ARTTextManager.m b/Libraries/ART/ViewManagers/ARTTextManager.m deleted file mode 100644 index 7bea655c4d0e3e..00000000000000 --- a/Libraries/ART/ViewManagers/ARTTextManager.m +++ /dev/null @@ -1,25 +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 "RCTConvert+ART.h" - -@implementation ARTTextManager - -RCT_EXPORT_MODULE() - -- (ARTRenderable *)node -{ - return [ARTText new]; -} - -RCT_EXPORT_VIEW_PROPERTY(alignment, CTTextAlignment) -RCT_REMAP_VIEW_PROPERTY(frame, textFrame, ARTTextFrame) - -@end diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index 1bd16980344472..786839498f334f 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -8,17 +8,17 @@ * @format */ -'use strict'; - import RCTActionSheetManager from './NativeActionSheetManager'; const invariant = require('invariant'); const processColor = require('../StyleSheet/processColor'); +import type {ColorValue} from '../StyleSheet/StyleSheet'; +import type {ProcessedColorValue} from '../StyleSheet/processColor'; /** * Display action sheets and share sheets on iOS. * - * See http://facebook.github.io/react-native/docs/actionsheetios.html + * See https://reactnative.dev/docs/actionsheetios.html */ const ActionSheetIOS = { /** @@ -31,11 +31,12 @@ const ActionSheetIOS = { * - `destructiveButtonIndex` (int or array of ints) - index or indices of destructive buttons in `options` * - `title` (string) - a title to show above the action sheet * - `message` (string) - a message to show below the title + * - `disabledButtonIndices` (array of numbers) - a list of button indices which should be disabled * * The 'callback' function takes one parameter, the zero-based index * of the selected item. * - * See http://facebook.github.io/react-native/docs/actionsheetios.html#showactionsheetwithoptions + * See https://reactnative.dev/docs/actionsheetios.html#showactionsheetwithoptions */ showActionSheetWithOptions( options: {| @@ -45,7 +46,9 @@ const ActionSheetIOS = { +destructiveButtonIndex?: ?number | ?Array, +cancelButtonIndex?: ?number, +anchor?: ?number, - +tintColor?: number | string, + +tintColor?: ColorValue | ProcessedColorValue, + +userInterfaceStyle?: string, + +disabledButtonIndices?: Array, |}, callback: (buttonIndex: number) => void, ) { @@ -54,7 +57,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; @@ -65,10 +68,15 @@ const ActionSheetIOS = { destructiveButtonIndices = [destructiveButtonIndex]; } + const processedTintColor = processColor(tintColor); + invariant( + processedTintColor == null || typeof processedTintColor === 'number', + 'Unexpected color given for ActionSheetIOS.showActionSheetWithOptions tintColor', + ); RCTActionSheetManager.showActionSheetWithOptions( { ...remainingOptions, - tintColor: processColor(tintColor), + tintColor: processedTintColor, destructiveButtonIndices, }, callback, @@ -96,7 +104,7 @@ const ActionSheetIOS = { * - a boolean value signifying success or failure * - a string that, in the case of success, indicates the method of sharing * - * See http://facebook.github.io/react-native/docs/actionsheetios.html#showshareactionsheetwithoptions + * See https://reactnative.dev/docs/actionsheetios.html#showshareactionsheetwithoptions */ showShareActionSheetWithOptions( options: Object, @@ -115,7 +123,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/NativeActionSheetManager.js b/Libraries/ActionSheetIOS/NativeActionSheetManager.js index afc2b82f97af55..7c48f8e2d4bbc3 100644 --- a/Libraries/ActionSheetIOS/NativeActionSheetManager.js +++ b/Libraries/ActionSheetIOS/NativeActionSheetManager.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; @@ -24,6 +22,8 @@ export interface Spec extends TurboModule { +cancelButtonIndex?: ?number, +anchor?: ?number, +tintColor?: ?number, + +userInterfaceStyle?: ?string, + +disabledButtonIndices?: Array, |}, callback: (buttonIndex: number) => void, ) => void; @@ -35,6 +35,7 @@ export interface Spec extends TurboModule { +anchor?: ?number, +tintColor?: ?number, +excludedActivityTypes?: ?Array, + +userInterfaceStyle?: ?string, |}, failureCallback: (error: {| +domain: string, diff --git a/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec b/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec index 5a9226e5d119b4..f080a2b2ffb25f 100644 --- a/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec +++ b/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec @@ -11,7 +11,7 @@ version = package['version'] source = { :git => 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. - source[:commit] = `git rev-parse HEAD`.strip + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") else source[:tag] = "v#{version}" end @@ -20,14 +20,14 @@ Pod::Spec.new do |s| s.name = "React-RCTActionSheet" s.version = version s.summary = "An API for displaying iOS action sheets and share sheets." - s.homepage = "http://facebook.github.io/react-native/" - s.documentation_url = "https://facebook.github.io/react-native/docs/actionsheetios" + s.homepage = "https://reactnative.dev/" + s.documentation_url = "https://reactnative.dev/docs/actionsheetios" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "9.0", :tvos => "9.2" } + s.platforms = { :ios => "11.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/Alert.js b/Libraries/Alert/Alert.js index 88873c5aa60852..02a7a312e5b2ac 100644 --- a/Libraries/Alert/Alert.js +++ b/Libraries/Alert/Alert.js @@ -8,12 +8,8 @@ * @flow */ -'use strict'; - import Platform from '../Utilities/Platform'; -import NativeDialogManagerAndroid, { - type DialogOptions, -} from '../NativeModules/specs/NativeDialogManagerAndroid'; +import type {DialogOptions} from '../NativeModules/specs/NativeDialogManagerAndroid'; import RCTAlertManager from './RCTAlertManager'; export type AlertType = @@ -38,7 +34,7 @@ type Options = { /** * Launches an alert dialog with the specified title and message. * - * See http://facebook.github.io/react-native/docs/alert.html + * See https://reactnative.dev/docs/alert.html */ class Alert { static alert( @@ -50,6 +46,8 @@ class Alert { if (Platform.OS === 'ios') { Alert.prompt(title, message, buttons, 'default'); } else if (Platform.OS === 'android') { + const NativeDialogManagerAndroid = require('../NativeModules/specs/NativeDialogManagerAndroid') + .default; if (!NativeDialogManagerAndroid) { return; } @@ -111,28 +109,6 @@ class Alert { keyboardType?: string, ): void { if (Platform.OS === 'ios') { - if (typeof type === 'function') { - console.warn( - 'You passed a callback function as the "type" argument to Alert.prompt(). React Native is ' + - 'assuming you want to use the deprecated Alert.prompt(title, defaultValue, buttons, callback) ' + - 'signature. The current signature is Alert.prompt(title, message, callbackOrButtons, type, defaultValue, ' + - 'keyboardType) and the old syntax will be removed in a future version.', - ); - - const callback = type; - RCTAlertManager.alertWithArgs( - { - title: title || '', - type: 'plain-text', - defaultValue: message || '', - }, - (id, value) => { - callback(value); - }, - ); - return; - } - let callbacks = []; const buttons = []; let cancelButtonKey; diff --git a/Libraries/Alert/NativeAlertManager.js b/Libraries/Alert/NativeAlertManager.js index 452be85c5b112f..ec01b4a916e911 100644 --- a/Libraries/Alert/NativeAlertManager.js +++ b/Libraries/Alert/NativeAlertManager.js @@ -8,15 +8,13 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; export type Args = {| title?: string, message?: string, - buttons?: Array, // TODO: have a better type + buttons?: Array, // TODO(T67565166): have a better type type?: string, defaultValue?: string, cancelButtonKey?: string, diff --git a/Libraries/Alert/RCTAlertManager.android.js b/Libraries/Alert/RCTAlertManager.android.js index 43f49a94e50bb1..366692bd906d04 100644 --- a/Libraries/Alert/RCTAlertManager.android.js +++ b/Libraries/Alert/RCTAlertManager.android.js @@ -7,8 +7,6 @@ * @format */ -'use strict'; - import NativeDialogManagerAndroid from '../NativeModules/specs/NativeDialogManagerAndroid'; function emptyCallback() {} diff --git a/Libraries/Alert/RCTAlertManager.ios.js b/Libraries/Alert/RCTAlertManager.ios.js index 8ededddf4b463c..a5dbdac5daf7dc 100644 --- a/Libraries/Alert/RCTAlertManager.ios.js +++ b/Libraries/Alert/RCTAlertManager.ios.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import NativeAlertManager from './NativeAlertManager'; import type {Args} from './NativeAlertManager'; diff --git a/Libraries/Animated/Animated.js b/Libraries/Animated/Animated.js new file mode 100644 index 00000000000000..2f6ee99ae6b04e --- /dev/null +++ b/Libraries/Animated/Animated.js @@ -0,0 +1,46 @@ +/** + * 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 + */ + +import Platform from '../Utilities/Platform'; +import typeof AnimatedFlatList from './components/AnimatedFlatList'; +import typeof AnimatedImage from './components/AnimatedImage'; +import typeof AnimatedScrollView from './components/AnimatedScrollView'; +import typeof AnimatedSectionList from './components/AnimatedSectionList'; +import typeof AnimatedText from './components/AnimatedText'; +import typeof AnimatedView from './components/AnimatedView'; + +const AnimatedMock = require('./AnimatedMock'); +const AnimatedImplementation = require('./AnimatedImplementation'); + +const Animated = ((Platform.isTesting + ? AnimatedMock + : AnimatedImplementation): typeof AnimatedMock); + +module.exports = { + get FlatList(): AnimatedFlatList { + return require('./components/AnimatedFlatList'); + }, + get Image(): AnimatedImage { + return require('./components/AnimatedImage'); + }, + get ScrollView(): AnimatedScrollView { + return require('./components/AnimatedScrollView'); + }, + get SectionList(): AnimatedSectionList { + return require('./components/AnimatedSectionList'); + }, + get Text(): AnimatedText { + return require('./components/AnimatedText'); + }, + get View(): AnimatedView { + return require('./components/AnimatedView'); + }, + ...Animated, +}; diff --git a/Libraries/Animated/AnimatedEvent.js b/Libraries/Animated/AnimatedEvent.js new file mode 100644 index 00000000000000..b6de725948a961 --- /dev/null +++ b/Libraries/Animated/AnimatedEvent.js @@ -0,0 +1,234 @@ +/** + * 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'; + +const AnimatedValue = require('./nodes/AnimatedValue'); +const NativeAnimatedHelper = require('./NativeAnimatedHelper'); +const ReactNative = require('../Renderer/shims/ReactNative'); + +const invariant = require('invariant'); + +const {shouldUseNativeDriver} = require('./NativeAnimatedHelper'); + +export type Mapping = {[key: string]: Mapping, ...} | AnimatedValue; +export type EventConfig = { + listener?: ?Function, + useNativeDriver: boolean, +}; + +function attachNativeEvent( + viewRef: any, + eventName: string, + argMapping: $ReadOnlyArray, +): {detach: () => void} { + // Find animated values in `argMapping` and create an array representing their + // key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x']. + const eventMappings = []; + + const traverse = (value, path) => { + if (value instanceof AnimatedValue) { + value.__makeNative(); + + eventMappings.push({ + nativeEventPath: path, + animatedValueTag: value.__getNativeTag(), + }); + } else if (typeof value === 'object') { + for (const key in value) { + traverse(value[key], path.concat(key)); + } + } + }; + + invariant( + argMapping[0] && argMapping[0].nativeEvent, + 'Native driven events only support animated values contained inside `nativeEvent`.', + ); + + // Assume that the event containing `nativeEvent` is always the first argument. + traverse(argMapping[0].nativeEvent, []); + + const viewTag = ReactNative.findNodeHandle(viewRef); + if (viewTag != null) { + eventMappings.forEach(mapping => { + NativeAnimatedHelper.API.addAnimatedEventToView( + viewTag, + eventName, + mapping, + ); + }); + } + + return { + detach() { + if (viewTag != null) { + eventMappings.forEach(mapping => { + NativeAnimatedHelper.API.removeAnimatedEventFromView( + viewTag, + eventName, + // $FlowFixMe[incompatible-call] + mapping.animatedValueTag, + ); + }); + } + }, + }; +} + +function validateMapping(argMapping, args) { + const validate = (recMapping, recEvt, key) => { + if (recMapping instanceof AnimatedValue) { + invariant( + typeof recEvt === 'number', + 'Bad mapping of event key ' + + key + + ', should be number but got ' + + typeof recEvt, + ); + return; + } + if (typeof recEvt === 'number') { + invariant( + recMapping instanceof AnimatedValue, + 'Bad mapping of type ' + + typeof recMapping + + ' for key ' + + key + + ', event value must map to AnimatedValue', + ); + return; + } + invariant( + typeof recMapping === 'object', + 'Bad mapping of type ' + typeof recMapping + ' for key ' + key, + ); + invariant( + typeof recEvt === 'object', + 'Bad event of type ' + typeof recEvt + ' for key ' + key, + ); + for (const mappingKey in recMapping) { + validate(recMapping[mappingKey], recEvt[mappingKey], mappingKey); + } + }; + + invariant( + args.length >= argMapping.length, + 'Event has less arguments than mapping', + ); + argMapping.forEach((mapping, idx) => { + validate(mapping, args[idx], 'arg' + idx); + }); +} + +class AnimatedEvent { + _argMapping: $ReadOnlyArray; + _listeners: Array = []; + _callListeners: Function; + _attachedEvent: ?{detach: () => void, ...}; + __isNative: boolean; + + constructor(argMapping: $ReadOnlyArray, config: EventConfig) { + this._argMapping = argMapping; + + if (config == null) { + console.warn('Animated.event now requires a second argument for options'); + config = {useNativeDriver: false}; + } + + if (config.listener) { + this.__addListener(config.listener); + } + this._callListeners = this._callListeners.bind(this); + this._attachedEvent = null; + this.__isNative = shouldUseNativeDriver(config); + } + + __addListener(callback: Function): void { + this._listeners.push(callback); + } + + __removeListener(callback: Function): void { + this._listeners = this._listeners.filter(listener => listener !== callback); + } + + __attach(viewRef: any, eventName: string) { + invariant( + this.__isNative, + 'Only native driven events need to be attached.', + ); + + this._attachedEvent = attachNativeEvent( + viewRef, + eventName, + this._argMapping, + ); + } + + __detach(viewTag: any, eventName: string) { + invariant( + this.__isNative, + 'Only native driven events need to be detached.', + ); + + this._attachedEvent && this._attachedEvent.detach(); + } + + __getHandler(): any | ((...args: any) => void) { + if (this.__isNative) { + if (__DEV__) { + let validatedMapping = false; + return (...args: any) => { + if (!validatedMapping) { + validateMapping(this._argMapping, args); + validatedMapping = true; + } + this._callListeners(...args); + }; + } else { + return this._callListeners; + } + } + + let validatedMapping = false; + return (...args: any) => { + if (__DEV__ && !validatedMapping) { + validateMapping(this._argMapping, args); + validatedMapping = true; + } + + const traverse = (recMapping, recEvt, key) => { + if (recMapping instanceof AnimatedValue) { + if (typeof recEvt === 'number') { + recMapping.setValue(recEvt); + } + } else if (typeof recMapping === 'object') { + for (const mappingKey in recMapping) { + /* $FlowFixMe[prop-missing] (>=0.120.0) This comment suppresses an + * error found when Flow v0.120 was deployed. To see the error, + * delete this comment and run Flow. */ + traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); + } + } + }; + this._argMapping.forEach((mapping, idx) => { + traverse(mapping, args[idx], 'arg' + idx); + }); + + this._callListeners(...args); + }; + } + + _callListeners(...args: any) { + this._listeners.forEach(listener => listener(...args)); + } +} + +module.exports = {AnimatedEvent, attachNativeEvent}; diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/AnimatedImplementation.js similarity index 87% rename from Libraries/Animated/src/AnimatedImplementation.js rename to Libraries/Animated/AnimatedImplementation.js index a1ce333acd214e..706b87ea01e9e1 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/AnimatedImplementation.js @@ -6,7 +6,6 @@ * * @flow * @format - * @preventMunge */ 'use strict'; @@ -91,7 +90,7 @@ const diffClamp = function( const _combineCallbacks = function( callback: ?EndCallback, - config: AnimationConfig, + config: {...AnimationConfig, ...}, ) { if (callback && config.onComplete) { return (...args) => { @@ -170,9 +169,6 @@ const spring = function( _startNativeLoop: function(iterations?: number): void { const singleConfig = {...config, iterations}; - /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses - * an error found when Flow v0.111 was deployed. To see the error, - * delete this comment and run Flow. */ start(value, singleConfig); }, @@ -227,9 +223,6 @@ const timing = function( _startNativeLoop: function(iterations?: number): void { const singleConfig = {...config, iterations}; - /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses - * an error found when Flow v0.111 was deployed. To see the error, - * delete this comment and run Flow. */ start(value, singleConfig); }, @@ -272,9 +265,6 @@ const decay = function( _startNativeLoop: function(iterations?: number): void { const singleConfig = {...config, iterations}; - /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses - * an error found when Flow v0.111 was deployed. To see the error, - * delete this comment and run Flow. */ start(value, singleConfig); }, @@ -522,7 +512,10 @@ function unforkEvent( } } -const event = function(argMapping: Array, config: EventConfig): any { +const event = function( + argMapping: $ReadOnlyArray, + config: EventConfig, +): any { const animatedEvent = new AnimatedEvent(argMapping, config); if (animatedEvent.__isNative) { return animatedEvent; @@ -539,33 +532,33 @@ const event = function(argMapping: Array, config: EventConfig): any { * If additional transforms are added, be sure to include them in * AnimatedMock.js as well. * - * See http://facebook.github.io/react-native/docs/animated.html + * See https://reactnative.dev/docs/animated.html */ module.exports = { /** * Standard value class for driving animations. Typically initialized with * `new Animated.Value(0);` * - * See http://facebook.github.io/react-native/docs/animated.html#value + * See https://reactnative.dev/docs/animated.html#value */ Value: AnimatedValue, /** * 2D value class for driving 2D animations, such as pan gestures. * - * See https://facebook.github.io/react-native/docs/animatedvaluexy.html + * See https://reactnative.dev/docs/animatedvaluexy.html */ ValueXY: AnimatedValueXY, /** * Exported to use the Interpolation type in flow. * - * See http://facebook.github.io/react-native/docs/animated.html#interpolation + * See https://reactnative.dev/docs/animated.html#interpolation */ Interpolation: AnimatedInterpolation, /** * Exported for ease of type checking. All animated values derive from this * class. * - * See http://facebook.github.io/react-native/docs/animated.html#node + * See https://reactnative.dev/docs/animated.html#node */ Node: AnimatedNode, @@ -573,21 +566,21 @@ module.exports = { * Animates a value from an initial velocity to zero based on a decay * coefficient. * - * See http://facebook.github.io/react-native/docs/animated.html#decay + * See https://reactnative.dev/docs/animated.html#decay */ decay, /** * Animates a value along a timed easing curve. The Easing module has tons of * predefined curves, or you can use your own function. * - * See http://facebook.github.io/react-native/docs/animated.html#timing + * See https://reactnative.dev/docs/animated.html#timing */ timing, /** * Animates a value according to an analytical spring model based on * damped harmonic oscillation. * - * See http://facebook.github.io/react-native/docs/animated.html#spring + * See https://reactnative.dev/docs/animated.html#spring */ spring, @@ -595,7 +588,7 @@ module.exports = { * Creates a new Animated value composed from two Animated values added * together. * - * See http://facebook.github.io/react-native/docs/animated.html#add + * See https://reactnative.dev/docs/animated.html#add */ add, @@ -603,7 +596,7 @@ module.exports = { * Creates a new Animated value composed by subtracting the second Animated * value from the first Animated value. * - * See http://facebook.github.io/react-native/docs/animated.html#subtract + * See https://reactnative.dev/docs/animated.html#subtract */ subtract, @@ -611,7 +604,7 @@ module.exports = { * Creates a new Animated value composed by dividing the first Animated value * by the second Animated value. * - * See http://facebook.github.io/react-native/docs/animated.html#divide + * See https://reactnative.dev/docs/animated.html#divide */ divide, @@ -619,7 +612,7 @@ module.exports = { * Creates a new Animated value composed from two Animated values multiplied * together. * - * See http://facebook.github.io/react-native/docs/animated.html#multiply + * See https://reactnative.dev/docs/animated.html#multiply */ multiply, @@ -627,7 +620,7 @@ module.exports = { * Creates a new Animated value that is the (non-negative) modulo of the * provided Animated value. * - * See http://facebook.github.io/react-native/docs/animated.html#modulo + * See https://reactnative.dev/docs/animated.html#modulo */ modulo, @@ -636,14 +629,14 @@ module.exports = { * difference between the last value so even if the value is far from the * bounds it will start changing when the value starts getting closer again. * - * See http://facebook.github.io/react-native/docs/animated.html#diffclamp + * See https://reactnative.dev/docs/animated.html#diffclamp */ diffClamp, /** * Starts an animation after the given delay. * - * See http://facebook.github.io/react-native/docs/animated.html#delay + * See https://reactnative.dev/docs/animated.html#delay */ delay, /** @@ -651,7 +644,7 @@ module.exports = { * before starting the next. If the current running animation is stopped, no * following animations will be started. * - * See http://facebook.github.io/react-native/docs/animated.html#sequence + * See https://reactnative.dev/docs/animated.html#sequence */ sequence, /** @@ -659,21 +652,21 @@ module.exports = { * of the animations is stopped, they will all be stopped. You can override * this with the `stopTogether` flag. * - * See http://facebook.github.io/react-native/docs/animated.html#parallel + * See https://reactnative.dev/docs/animated.html#parallel */ parallel, /** * Array of animations may run in parallel (overlap), but are started in * sequence with successive delays. Nice for doing trailing effects. * - * See http://facebook.github.io/react-native/docs/animated.html#stagger + * See https://reactnative.dev/docs/animated.html#stagger */ stagger, /** * Loops a given animation continuously, so that each time it reaches the * end, it resets and begins again from the start. * - * See http://facebook.github.io/react-native/docs/animated.html#loop + * See https://reactnative.dev/docs/animated.html#loop */ loop, @@ -681,14 +674,14 @@ module.exports = { * Takes an array of mappings and extracts values from each arg accordingly, * then calls `setValue` on the mapped outputs. * - * See http://facebook.github.io/react-native/docs/animated.html#event + * See https://reactnative.dev/docs/animated.html#event */ event, /** * Make any React component Animatable. Used to create `Animated.View`, etc. * - * See http://facebook.github.io/react-native/docs/animated.html#createanimatedcomponent + * See https://reactnative.dev/docs/animated.html#createanimatedcomponent */ createAnimatedComponent, @@ -696,7 +689,7 @@ module.exports = { * Imperative API to attach an animated value to an event on a view. Prefer * using `Animated.event` with `useNativeDrive: true` if possible. * - * See http://facebook.github.io/react-native/docs/animated.html#attachnativeevent + * See https://reactnative.dev/docs/animated.html#attachnativeevent */ attachNativeEvent, @@ -704,7 +697,7 @@ module.exports = { * Advanced imperative API for snooping on animated events that are passed in * through props. Use values directly where possible. * - * See http://facebook.github.io/react-native/docs/animated.html#forkevent + * See https://reactnative.dev/docs/animated.html#forkevent */ forkEvent, unforkEvent, @@ -713,6 +706,4 @@ module.exports = { * Expose Event class, so it can be used as a type for type checkers. */ Event: AnimatedEvent, - - __PropsOnlyForTests: AnimatedProps, }; diff --git a/Libraries/Animated/src/AnimatedMock.js b/Libraries/Animated/AnimatedMock.js similarity index 93% rename from Libraries/Animated/src/AnimatedMock.js rename to Libraries/Animated/AnimatedMock.js index eecfbb884ec42b..225669b1f640c9 100644 --- a/Libraries/Animated/src/AnimatedMock.js +++ b/Libraries/Animated/AnimatedMock.js @@ -14,7 +14,6 @@ const {AnimatedEvent, attachNativeEvent} = require('./AnimatedEvent'); const AnimatedImplementation = require('./AnimatedImplementation'); const AnimatedInterpolation = require('./nodes/AnimatedInterpolation'); const AnimatedNode = require('./nodes/AnimatedNode'); -const AnimatedProps = require('./nodes/AnimatedProps'); const AnimatedValue = require('./nodes/AnimatedValue'); const AnimatedValueXY = require('./nodes/AnimatedValueXY'); @@ -24,7 +23,6 @@ import type {EndCallback} from './animations/Animation'; import type {TimingAnimationConfig} from './animations/TimingAnimation'; import type {DecayAnimationConfig} from './animations/DecayAnimation'; import type {SpringAnimationConfig} from './animations/SpringAnimation'; -import type {Mapping, EventConfig} from './AnimatedEvent'; /** * Animations are a source of flakiness in snapshot testing. This mock replaces @@ -123,10 +121,6 @@ const loop = function( return emptyAnimation; }; -const event = function(argMapping: Array, config: EventConfig): any { - return null; -}; - module.exports = { Value: AnimatedValue, ValueXY: AnimatedValueXY, @@ -146,11 +140,10 @@ module.exports = { parallel, stagger, loop, - event, + event: AnimatedImplementation.event, createAnimatedComponent, attachNativeEvent, forkEvent: AnimatedImplementation.forkEvent, unforkEvent: AnimatedImplementation.unforkEvent, Event: AnimatedEvent, - __PropsOnlyForTests: AnimatedProps, }; diff --git a/Libraries/Animated/AnimatedWeb.js b/Libraries/Animated/AnimatedWeb.js new file mode 100644 index 00000000000000..513f322b709d08 --- /dev/null +++ b/Libraries/Animated/AnimatedWeb.js @@ -0,0 +1,26 @@ +/** + * 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 AnimatedImplementation = require('./AnimatedImplementation'); + +module.exports = { + ...AnimatedImplementation, + /* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive + * types. Plain intrinsic components can't be typed like this */ + div: (AnimatedImplementation.createAnimatedComponent('div'): $FlowFixMe), + /* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive + * types. Plain intrinsic components can't be typed like this */ + span: (AnimatedImplementation.createAnimatedComponent('span'): $FlowFixMe), + /* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive + * types. Plain intrinsic components can't be typed like this */ + img: (AnimatedImplementation.createAnimatedComponent('img'): $FlowFixMe), +}; diff --git a/Libraries/Animated/src/Easing.js b/Libraries/Animated/Easing.js similarity index 100% rename from Libraries/Animated/src/Easing.js rename to Libraries/Animated/Easing.js diff --git a/Libraries/Animated/NativeAnimatedHelper.js b/Libraries/Animated/NativeAnimatedHelper.js new file mode 100644 index 00000000000000..dd1fc6f260925e --- /dev/null +++ b/Libraries/Animated/NativeAnimatedHelper.js @@ -0,0 +1,408 @@ +/** + * 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-local + * @format + */ + +import NativeAnimatedNonTurboModule from './NativeAnimatedModule'; +import NativeAnimatedTurboModule from './NativeAnimatedTurboModule'; +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; +import Platform from '../Utilities/Platform'; +import type {EventConfig} from './AnimatedEvent'; +import type { + EventMapping, + AnimatedNodeConfig, + AnimatingNodeConfig, +} from './NativeAnimatedModule'; +import type {AnimationConfig, EndCallback} from './animations/Animation'; +import type {InterpolationConfigType} from './nodes/AnimatedInterpolation'; +import invariant from 'invariant'; + +// TODO T69437152 @petetheheat - Delete this fork when Fabric ships to 100%. +const NativeAnimatedModule = + Platform.OS === 'ios' && global.RN$Bridgeless + ? NativeAnimatedTurboModule + : NativeAnimatedNonTurboModule; + +let __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */ +let __nativeAnimationIdCount = 1; /* used for started animations */ + +let nativeEventEmitter; + +let waitingForQueuedOperations = new Set(); +let queueOperations = false; +let queue: Array<() => void> = []; + +/** + * Simple wrappers around NativeAnimatedModule to provide flow and autocomplete support for + * the native module methods + */ +const API = { + getValue: function( + tag: number, + saveValueCallback: (value: number) => void, + ): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + if (NativeAnimatedModule.getValue) { + NativeAnimatedModule.getValue(tag, saveValueCallback); + } + }, + setWaitingForIdentifier: function(id: string): void { + waitingForQueuedOperations.add(id); + queueOperations = true; + }, + unsetWaitingForIdentifier: function(id: string): void { + waitingForQueuedOperations.delete(id); + + if (waitingForQueuedOperations.size === 0) { + queueOperations = false; + API.disableQueue(); + } + }, + disableQueue: function(): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + + if (Platform.OS === 'android') { + NativeAnimatedModule.startOperationBatch(); + } + for (let q = 0, l = queue.length; q < l; q++) { + queue[q](); + } + queue.length = 0; + if (Platform.OS === 'android') { + NativeAnimatedModule.finishOperationBatch(); + } + }, + queueOperation: (fn: () => void): void => { + if (queueOperations) { + queue.push(fn); + } else { + fn(); + } + }, + createAnimatedNode: function(tag: number, config: AnimatedNodeConfig): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.createAnimatedNode(tag, config), + ); + }, + startListeningToAnimatedNodeValue: function(tag: number) { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.startListeningToAnimatedNodeValue(tag), + ); + }, + stopListeningToAnimatedNodeValue: function(tag: number) { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.stopListeningToAnimatedNodeValue(tag), + ); + }, + connectAnimatedNodes: function(parentTag: number, childTag: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag), + ); + }, + disconnectAnimatedNodes: function(parentTag: number, childTag: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.disconnectAnimatedNodes(parentTag, childTag), + ); + }, + startAnimatingNode: function( + animationId: number, + nodeTag: number, + config: AnimatingNodeConfig, + endCallback: EndCallback, + ): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.startAnimatingNode( + animationId, + nodeTag, + config, + endCallback, + ), + ); + }, + stopAnimation: function(animationId: number) { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => NativeAnimatedModule.stopAnimation(animationId)); + }, + setAnimatedNodeValue: function(nodeTag: number, value: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.setAnimatedNodeValue(nodeTag, value), + ); + }, + setAnimatedNodeOffset: function(nodeTag: number, offset: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.setAnimatedNodeOffset(nodeTag, offset), + ); + }, + flattenAnimatedNodeOffset: function(nodeTag: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.flattenAnimatedNodeOffset(nodeTag), + ); + }, + extractAnimatedNodeOffset: function(nodeTag: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.extractAnimatedNodeOffset(nodeTag), + ); + }, + connectAnimatedNodeToView: function(nodeTag: number, viewTag: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.connectAnimatedNodeToView(nodeTag, viewTag), + ); + }, + disconnectAnimatedNodeFromView: function( + nodeTag: number, + viewTag: number, + ): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.disconnectAnimatedNodeFromView(nodeTag, viewTag), + ); + }, + restoreDefaultValues: function(nodeTag: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + // Backwards compat with older native runtimes, can be removed later. + if (NativeAnimatedModule.restoreDefaultValues != null) { + API.queueOperation(() => + NativeAnimatedModule.restoreDefaultValues(nodeTag), + ); + } + }, + dropAnimatedNode: function(tag: number): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => NativeAnimatedModule.dropAnimatedNode(tag)); + }, + addAnimatedEventToView: function( + viewTag: number, + eventName: string, + eventMapping: EventMapping, + ) { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.addAnimatedEventToView( + viewTag, + eventName, + eventMapping, + ), + ); + }, + removeAnimatedEventFromView( + viewTag: number, + eventName: string, + animatedNodeTag: number, + ) { + invariant(NativeAnimatedModule, 'Native animated module is not available'); + API.queueOperation(() => + NativeAnimatedModule.removeAnimatedEventFromView( + viewTag, + eventName, + animatedNodeTag, + ), + ); + }, +}; + +/** + * Styles allowed by the native animated implementation. + * + * In general native animated implementation should support any numeric property that doesn't need + * to be updated through the shadow view hierarchy (all non-layout properties). + */ +const SUPPORTED_STYLES = { + opacity: true, + transform: true, + borderRadius: true, + borderBottomEndRadius: true, + borderBottomLeftRadius: true, + borderBottomRightRadius: true, + borderBottomStartRadius: true, + borderTopEndRadius: true, + borderTopLeftRadius: true, + borderTopRightRadius: true, + borderTopStartRadius: true, + elevation: true, + zIndex: true, + /* ios styles */ + shadowOpacity: true, + shadowRadius: true, + /* legacy android transform properties */ + scaleX: true, + scaleY: true, + translateX: true, + translateY: true, +}; + +const SUPPORTED_TRANSFORMS = { + translateX: true, + translateY: true, + scale: true, + scaleX: true, + scaleY: true, + rotate: true, + rotateX: true, + rotateY: true, + rotateZ: true, + perspective: true, +}; + +const SUPPORTED_INTERPOLATION_PARAMS = { + inputRange: true, + outputRange: true, + extrapolate: true, + extrapolateRight: true, + extrapolateLeft: true, +}; + +function addWhitelistedStyleProp(prop: string): void { + SUPPORTED_STYLES[prop] = true; +} + +function addWhitelistedTransformProp(prop: string): void { + SUPPORTED_TRANSFORMS[prop] = true; +} + +function addWhitelistedInterpolationParam(param: string): void { + SUPPORTED_INTERPOLATION_PARAMS[param] = true; +} + +function validateTransform( + configs: Array< + | { + type: 'animated', + property: string, + nodeTag: ?number, + ... + } + | { + type: 'static', + property: string, + value: number | string, + ... + }, + >, +): void { + configs.forEach(config => { + if (!SUPPORTED_TRANSFORMS.hasOwnProperty(config.property)) { + throw new Error( + `Property '${config.property}' is not supported by native animated module`, + ); + } + }); +} + +function validateStyles(styles: {[key: string]: ?number, ...}): void { + for (const key in styles) { + if (!SUPPORTED_STYLES.hasOwnProperty(key)) { + throw new Error( + `Style property '${key}' is not supported by native animated module`, + ); + } + } +} + +function validateInterpolation(config: InterpolationConfigType): void { + for (const key in config) { + if (!SUPPORTED_INTERPOLATION_PARAMS.hasOwnProperty(key)) { + throw new Error( + `Interpolation property '${key}' is not supported by native animated module`, + ); + } + } +} + +function generateNewNodeTag(): number { + return __nativeAnimatedNodeTagCount++; +} + +function generateNewAnimationId(): number { + return __nativeAnimationIdCount++; +} + +function assertNativeAnimatedModule(): void { + invariant(NativeAnimatedModule, 'Native animated module is not available'); +} + +let _warnedMissingNativeAnimated = false; + +function shouldUseNativeDriver( + config: {...AnimationConfig, ...} | EventConfig, +): boolean { + if (config.useNativeDriver == null) { + console.warn( + 'Animated: `useNativeDriver` was not specified. This is a required ' + + 'option and must be explicitly set to `true` or `false`', + ); + } + + if (config.useNativeDriver === true && !NativeAnimatedModule) { + if (!_warnedMissingNativeAnimated) { + console.warn( + 'Animated: `useNativeDriver` is not supported because the native ' + + 'animated module is missing. Falling back to JS-based animation. To ' + + 'resolve this, add `RCTAnimation` module to this app, or remove ' + + '`useNativeDriver`. ' + + 'Make sure to run `pod install` first. Read more about autolinking: https://github.com/react-native-community/cli/blob/master/docs/autolinking.md', + ); + _warnedMissingNativeAnimated = true; + } + return false; + } + + return config.useNativeDriver || false; +} + +function transformDataType(value: number | string): number | string { + // Change the string type to number type so we can reuse the same logic in + // iOS and Android platform + if (typeof value !== 'string') { + return value; + } + if (/deg$/.test(value)) { + const degrees = parseFloat(value) || 0; + const radians = (degrees * Math.PI) / 180.0; + return radians; + } else { + return value; + } +} + +module.exports = { + API, + addWhitelistedStyleProp, + addWhitelistedTransformProp, + addWhitelistedInterpolationParam, + validateStyles, + validateTransform, + validateInterpolation, + generateNewNodeTag, + generateNewAnimationId, + assertNativeAnimatedModule, + shouldUseNativeDriver, + transformDataType, + // $FlowExpectedError[unsafe-getters-setters] - unsafe getter lint suppresion + // $FlowExpectedError[missing-type-arg] - unsafe getter lint suppresion + get nativeEventEmitter(): NativeEventEmitter { + if (!nativeEventEmitter) { + nativeEventEmitter = new NativeEventEmitter( + // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior + // If you want to use the native module on other platforms, please remove this condition and test its behavior + Platform.OS !== 'ios' ? null : NativeAnimatedModule, + ); + } + return nativeEventEmitter; + }, +}; diff --git a/Libraries/Animated/src/NativeAnimatedModule.js b/Libraries/Animated/NativeAnimatedModule.js similarity index 86% rename from Libraries/Animated/src/NativeAnimatedModule.js rename to Libraries/Animated/NativeAnimatedModule.js index fc76ec1953a6d5..d126bfdaff96eb 100644 --- a/Libraries/Animated/src/NativeAnimatedModule.js +++ b/Libraries/Animated/NativeAnimatedModule.js @@ -8,13 +8,12 @@ * @format */ -'use strict'; - -import type {TurboModule} from '../../TurboModule/RCTExport'; -import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; type EndResult = {finished: boolean, ...}; type EndCallback = (result: EndResult) => void; +type SaveValueCallback = (value: number) => void; export type EventMapping = {| nativeEventPath: Array, @@ -27,7 +26,10 @@ export type AnimatedNodeConfig = Object; export type AnimatingNodeConfig = Object; export interface Spec extends TurboModule { + +startOperationBatch: () => void; + +finishOperationBatch: () => void; +createAnimatedNode: (tag: number, config: AnimatedNodeConfig) => void; + +getValue: (tag: number, saveValueCallback: SaveValueCallback) => void; +startListeningToAnimatedNodeValue: (tag: number) => void; +stopListeningToAnimatedNodeValue: (tag: number) => void; +connectAnimatedNodes: (parentTag: number, childTag: number) => void; diff --git a/Libraries/Animated/NativeAnimatedTurboModule.js b/Libraries/Animated/NativeAnimatedTurboModule.js new file mode 100644 index 00000000000000..03ff0f26116560 --- /dev/null +++ b/Libraries/Animated/NativeAnimatedTurboModule.js @@ -0,0 +1,70 @@ +/** + * 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 + */ + +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; + +type EndResult = {finished: boolean, ...}; +type EndCallback = (result: EndResult) => void; +type SaveValueCallback = (value: number) => void; + +export type EventMapping = {| + nativeEventPath: Array, + animatedValueTag: ?number, +|}; + +// The config has different keys depending on the type of the Node +// TODO(T54896888): Make these types strict +export type AnimatedNodeConfig = Object; +export type AnimatingNodeConfig = Object; + +export interface Spec extends TurboModule { + +startOperationBatch: () => void; + +finishOperationBatch: () => void; + +createAnimatedNode: (tag: number, config: AnimatedNodeConfig) => void; + +getValue: (tag: number, saveValueCallback: SaveValueCallback) => void; + +startListeningToAnimatedNodeValue: (tag: number) => void; + +stopListeningToAnimatedNodeValue: (tag: number) => void; + +connectAnimatedNodes: (parentTag: number, childTag: number) => void; + +disconnectAnimatedNodes: (parentTag: number, childTag: number) => void; + +startAnimatingNode: ( + animationId: number, + nodeTag: number, + config: AnimatingNodeConfig, + endCallback: EndCallback, + ) => void; + +stopAnimation: (animationId: number) => void; + +setAnimatedNodeValue: (nodeTag: number, value: number) => void; + +setAnimatedNodeOffset: (nodeTag: number, offset: number) => void; + +flattenAnimatedNodeOffset: (nodeTag: number) => void; + +extractAnimatedNodeOffset: (nodeTag: number) => void; + +connectAnimatedNodeToView: (nodeTag: number, viewTag: number) => void; + +disconnectAnimatedNodeFromView: (nodeTag: number, viewTag: number) => void; + +restoreDefaultValues: (nodeTag: number) => void; + +dropAnimatedNode: (tag: number) => void; + +addAnimatedEventToView: ( + viewTag: number, + eventName: string, + eventMapping: EventMapping, + ) => void; + +removeAnimatedEventFromView: ( + viewTag: number, + eventName: string, + animatedNodeTag: number, + ) => void; + + // Events + +addListener: (eventName: string) => void; + +removeListeners: (count: number) => void; +} + +export default (TurboModuleRegistry.get( + 'NativeAnimatedTurboModule', +): ?Spec); diff --git a/Libraries/Animated/src/SpringConfig.js b/Libraries/Animated/SpringConfig.js similarity index 100% rename from Libraries/Animated/src/SpringConfig.js rename to Libraries/Animated/SpringConfig.js diff --git a/Libraries/Animated/src/__tests__/Animated-test.js b/Libraries/Animated/__tests__/Animated-test.js similarity index 97% rename from Libraries/Animated/src/__tests__/Animated-test.js rename to Libraries/Animated/__tests__/Animated-test.js index 061fd449925fac..2b2ffe04dfdfb8 100644 --- a/Libraries/Animated/src/__tests__/Animated-test.js +++ b/Libraries/Animated/__tests__/Animated-test.js @@ -8,12 +8,11 @@ * @emails oncall+react_native */ -'use strict'; - +import AnimatedProps from '../nodes/AnimatedProps'; import TestRenderer from 'react-test-renderer'; import * as React from 'react'; -jest.mock('../../../BatchedBridge/NativeModules', () => ({ +jest.mock('../../BatchedBridge/NativeModules', () => ({ NativeAnimatedModule: {}, PlatformConstants: { getConstants() { @@ -23,6 +22,7 @@ jest.mock('../../../BatchedBridge/NativeModules', () => ({ })); let Animated = require('../Animated'); + describe('Animated tests', () => { beforeEach(() => { jest.resetModules(); @@ -34,7 +34,7 @@ describe('Animated tests', () => { const callback = jest.fn(); - const node = new Animated.__PropsOnlyForTests( + const node = new AnimatedProps( { style: { backgroundColor: 'red', @@ -57,8 +57,6 @@ describe('Animated tests', () => { callback, ); - expect(anim.__getChildren().length).toBe(3); - expect(node.__getValue()).toEqual({ style: { backgroundColor: 'red', @@ -71,6 +69,12 @@ describe('Animated tests', () => { }, }); + expect(anim.__getChildren().length).toBe(0); + + node.__attach(); + + expect(anim.__getChildren().length).toBe(3); + anim.setValue(0.5); expect(callback).toBeCalled(); @@ -689,13 +693,13 @@ describe('Animated tests', () => { let InteractionManager; beforeEach(() => { - jest.mock('../../../Interaction/InteractionManager'); + jest.mock('../../Interaction/InteractionManager'); Animated = require('../Animated'); - InteractionManager = require('../../../Interaction/InteractionManager'); + InteractionManager = require('../../Interaction/InteractionManager'); }); afterEach(() => { - jest.unmock('../../../Interaction/InteractionManager'); + jest.unmock('../../Interaction/InteractionManager'); }); it('registers an interaction by default', () => { @@ -788,7 +792,7 @@ describe('Animated tests', () => { const callback = jest.fn(); - const node = new Animated.__PropsOnlyForTests( + const node = new AnimatedProps( { style: { opacity: vec.x.interpolate({ @@ -811,6 +815,10 @@ describe('Animated tests', () => { }, }); + node.__attach(); + + expect(callback.mock.calls.length).toBe(0); + vec.setValue({x: 42, y: 1492}); expect(callback.mock.calls.length).toBe(2); // once each for x, y @@ -892,7 +900,7 @@ describe('Animated tests', () => { const value3 = new Animated.Value(0); const value4 = Animated.add(value3, Animated.multiply(value1, value2)); const callback = jest.fn(); - const view = new Animated.__PropsOnlyForTests( + const view = new AnimatedProps( { style: { transform: [ @@ -904,6 +912,7 @@ describe('Animated tests', () => { }, callback, ); + view.__attach(); const listener = jest.fn(); const id = value4.addListener(listener); value3.setValue(137); diff --git a/Libraries/Animated/src/__tests__/AnimatedMock-test.js b/Libraries/Animated/__tests__/AnimatedMock-test.js similarity index 100% rename from Libraries/Animated/src/__tests__/AnimatedMock-test.js rename to Libraries/Animated/__tests__/AnimatedMock-test.js diff --git a/Libraries/Animated/src/__tests__/AnimatedNative-test.js b/Libraries/Animated/__tests__/AnimatedNative-test.js similarity index 93% rename from Libraries/Animated/src/__tests__/AnimatedNative-test.js rename to Libraries/Animated/__tests__/AnimatedNative-test.js index c511638be6add8..e5ecec1fb7d648 100644 --- a/Libraries/Animated/src/__tests__/AnimatedNative-test.js +++ b/Libraries/Animated/__tests__/AnimatedNative-test.js @@ -8,11 +8,9 @@ * @emails oncall+react_native */ -'use strict'; - jest .clearAllMocks() - .mock('../../../BatchedBridge/NativeModules', () => ({ + .mock('../../BatchedBridge/NativeModules', () => ({ NativeAnimatedModule: {}, PlatformConstants: { getConstants() { @@ -21,9 +19,9 @@ jest }, })) .mock('../NativeAnimatedModule') - .mock('../../../EventEmitter/NativeEventEmitter') + .mock('../../EventEmitter/NativeEventEmitter') // findNodeHandle is imported from ReactNative so mock that whole module. - .setMock('../../../Renderer/shims/ReactNative', {findNodeHandle: () => 1}); + .setMock('../../Renderer/shims/ReactNative', {findNodeHandle: () => 1}); import TestRenderer from 'react-test-renderer'; import * as React from 'react'; @@ -36,6 +34,7 @@ describe('Native Animated', () => { beforeEach(() => { Object.assign(NativeAnimatedModule, { + getValue: jest.fn(), addAnimatedEventToView: jest.fn(), connectAnimatedNodes: jest.fn(), connectAnimatedNodeToView: jest.fn(), @@ -115,6 +114,26 @@ describe('Native Animated', () => { ); }); + it('should save value on unmount', () => { + NativeAnimatedModule.getValue = jest.fn((tag, saveCallback) => { + saveCallback(1); + }); + const opacity = new Animated.Value(0); + + opacity.__makeNative(); + + const root = TestRenderer.create(); + const tag = opacity.__getNativeTag(); + + root.unmount(); + + expect(NativeAnimatedModule.getValue).toBeCalledWith( + tag, + expect.any(Function), + ); + expect(opacity.__getValue()).toBe(1); + }); + it('should extract offset', () => { const opacity = new Animated.Value(0); opacity.__makeNative(); @@ -260,9 +279,9 @@ describe('Native Animated', () => { listener, }); const handler = event.__getHandler(); - handler({foo: 42}); + handler({nativeEvent: {foo: 42}}); expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toBeCalledWith({foo: 42}); + expect(listener).toBeCalledWith({nativeEvent: {foo: 42}}); }); }); @@ -357,11 +376,19 @@ describe('Native Animated', () => { expect(additionConnectionCalls.length).toBe(2); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( additionCall[1].input[0], - {type: 'value', value: 1, offset: 0}, + { + type: 'value', + value: 1, + offset: 0, + }, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( additionCall[1].input[1], - {type: 'value', value: 2, offset: 0}, + { + type: 'value', + value: 2, + offset: 0, + }, ); }); @@ -391,11 +418,19 @@ describe('Native Animated', () => { expect(subtractionConnectionCalls.length).toBe(2); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( subtractionCall[1].input[0], - {type: 'value', value: 2, offset: 0}, + { + type: 'value', + value: 2, + offset: 0, + }, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( subtractionCall[1].input[1], - {type: 'value', value: 1, offset: 0}, + { + type: 'value', + value: 1, + offset: 0, + }, ); }); @@ -425,11 +460,19 @@ describe('Native Animated', () => { expect(multiplicationConnectionCalls.length).toBe(2); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( multiplicationCall[1].input[0], - {type: 'value', value: 2, offset: 0}, + { + type: 'value', + value: 2, + offset: 0, + }, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( multiplicationCall[1].input[1], - {type: 'value', value: 1, offset: 0}, + { + type: 'value', + value: 1, + offset: 0, + }, ); }); @@ -459,11 +502,19 @@ describe('Native Animated', () => { expect(divisionConnectionCalls.length).toBe(2); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( divisionCall[1].input[0], - {type: 'value', value: 4, offset: 0}, + { + type: 'value', + value: 4, + offset: 0, + }, ); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( divisionCall[1].input[1], - {type: 'value', value: 2, offset: 0}, + { + type: 'value', + value: 2, + offset: 0, + }, ); }); @@ -491,7 +542,11 @@ describe('Native Animated', () => { expect(moduloConnectionCalls.length).toBe(1); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( moduloCall[1].input, - {type: 'value', value: 4, offset: 0}, + { + type: 'value', + value: 4, + offset: 0, + }, ); }); @@ -588,7 +643,11 @@ describe('Native Animated', () => { expect(diffClampConnectionCalls.length).toBe(1); expect(NativeAnimatedModule.createAnimatedNode).toBeCalledWith( diffClampCall[1].input, - {type: 'value', value: 2, offset: 0}, + { + type: 'value', + value: 2, + offset: 0, + }, ); }); diff --git a/Libraries/Animated/src/__tests__/Easing-test.js b/Libraries/Animated/__tests__/Easing-test.js similarity index 100% rename from Libraries/Animated/src/__tests__/Easing-test.js rename to Libraries/Animated/__tests__/Easing-test.js diff --git a/Libraries/Animated/src/__tests__/Interpolation-test.js b/Libraries/Animated/__tests__/Interpolation-test.js similarity index 100% rename from Libraries/Animated/src/__tests__/Interpolation-test.js rename to Libraries/Animated/__tests__/Interpolation-test.js diff --git a/Libraries/Animated/src/__tests__/TimingAnimation-test.js b/Libraries/Animated/__tests__/TimingAnimation-test.js similarity index 100% rename from Libraries/Animated/src/__tests__/TimingAnimation-test.js rename to Libraries/Animated/__tests__/TimingAnimation-test.js diff --git a/Libraries/Animated/src/__tests__/bezier-test.js b/Libraries/Animated/__tests__/bezier-test.js similarity index 100% rename from Libraries/Animated/src/__tests__/bezier-test.js rename to Libraries/Animated/__tests__/bezier-test.js diff --git a/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js b/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js new file mode 100644 index 00000000000000..e82ea170a3eb95 --- /dev/null +++ b/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js @@ -0,0 +1,67 @@ +/** + * 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 createAnimatedComponent = require('../createAnimatedComponent'); +const createAnimatedComponentInjection = require('../createAnimatedComponentInjection'); +const React = require('react'); + +function injected( + Component: React.AbstractComponent, +): React.AbstractComponent { + return createAnimatedComponent(Component); +} + +beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); +}); + +test('does nothing without injection', () => { + expect(typeof createAnimatedComponent).toBe('function'); + expect(createAnimatedComponent).not.toBe(injected); +}); + +test('injection overrides `createAnimatedComponent`', () => { + createAnimatedComponentInjection.inject(injected); + + expect(createAnimatedComponent).toBe(injected); +}); + +test('injection errors if called too late', () => { + jest.spyOn(console, 'error').mockReturnValue(undefined); + + // Causes `createAnimatedComponent` to be initialized. + createAnimatedComponent; + + createAnimatedComponentInjection.inject(injected); + + expect(createAnimatedComponent).not.toBe(injected); + expect(console.error).toBeCalledWith( + 'createAnimatedComponentInjection: Must be called before `createAnimatedComponent`.', + ); +}); + +test('injection errors if called more than once', () => { + jest.spyOn(console, 'error').mockReturnValue(undefined); + + createAnimatedComponentInjection.inject(injected); + + expect(createAnimatedComponent).toBe(injected); + expect(console.error).not.toBeCalled(); + + createAnimatedComponentInjection.inject(injected); + + expect(console.error).toBeCalledWith( + 'createAnimatedComponentInjection: Cannot be called more than once.', + ); +}); diff --git a/Libraries/Animated/animations/Animation.js b/Libraries/Animated/animations/Animation.js new file mode 100644 index 00000000000000..3555bf2cc2665a --- /dev/null +++ b/Libraries/Animated/animations/Animation.js @@ -0,0 +1,87 @@ +/** + * 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'; + +const NativeAnimatedHelper = require('../NativeAnimatedHelper'); + +import type AnimatedValue from '../nodes/AnimatedValue'; + +export type EndResult = {finished: boolean, ...}; +export type EndCallback = (result: EndResult) => void; + +export type AnimationConfig = { + isInteraction?: boolean, + useNativeDriver: boolean, + onComplete?: ?EndCallback, + iterations?: number, +}; + +let startNativeAnimationNextId = 1; + +// Important note: start() and stop() will only be called at most once. +// Once an animation has been stopped or finished its course, it will +// not be reused. +class Animation { + __active: boolean; + __isInteraction: boolean; + __nativeId: number; + __onEnd: ?EndCallback; + __iterations: number; + start( + fromValue: number, + onUpdate: (value: number) => void, + onEnd: ?EndCallback, + previousAnimation: ?Animation, + animatedValue: AnimatedValue, + ): void {} + stop(): void { + if (this.__nativeId) { + NativeAnimatedHelper.API.stopAnimation(this.__nativeId); + } + } + __getNativeAnimationConfig(): any { + // Subclasses that have corresponding animation implementation done in native + // should override this method + throw new Error('This animation type cannot be offloaded to native'); + } + // Helper function for subclasses to make sure onEnd is only called once. + __debouncedOnEnd(result: EndResult): void { + const onEnd = this.__onEnd; + this.__onEnd = null; + onEnd && onEnd(result); + } + __startNativeAnimation(animatedValue: AnimatedValue): void { + const startNativeAnimationWaitId = `${startNativeAnimationNextId}:startAnimation`; + startNativeAnimationNextId += 1; + NativeAnimatedHelper.API.setWaitingForIdentifier( + startNativeAnimationWaitId, + ); + try { + animatedValue.__makeNative(); + this.__nativeId = NativeAnimatedHelper.generateNewAnimationId(); + NativeAnimatedHelper.API.startAnimatingNode( + this.__nativeId, + animatedValue.__getNativeTag(), + this.__getNativeAnimationConfig(), + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + this.__debouncedOnEnd.bind(this), + ); + } catch (e) { + throw e; + } finally { + NativeAnimatedHelper.API.unsetWaitingForIdentifier( + startNativeAnimationWaitId, + ); + } + } +} + +module.exports = Animation; diff --git a/Libraries/Animated/src/animations/DecayAnimation.js b/Libraries/Animated/animations/DecayAnimation.js similarity index 90% rename from Libraries/Animated/src/animations/DecayAnimation.js rename to Libraries/Animated/animations/DecayAnimation.js index 6f4039cf40e604..b04b911e7600c7 100644 --- a/Libraries/Animated/src/animations/DecayAnimation.js +++ b/Libraries/Animated/animations/DecayAnimation.js @@ -17,7 +17,8 @@ const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); import type AnimatedValue from '../nodes/AnimatedValue'; import type {AnimationConfig, EndCallback} from './Animation'; -export type DecayAnimationConfig = AnimationConfig & { +export type DecayAnimationConfig = { + ...AnimationConfig, velocity: | number | { @@ -26,13 +27,12 @@ export type DecayAnimationConfig = AnimationConfig & { ... }, deceleration?: number, - ... }; -export type DecayAnimationConfigSingle = AnimationConfig & { +export type DecayAnimationConfigSingle = { + ...AnimationConfig, velocity: number, deceleration?: number, - ... }; class DecayAnimation extends Animation { @@ -84,6 +84,7 @@ class DecayAnimation extends Animation { if (this._useNativeDriver) { this.__startNativeAnimation(animatedValue); } else { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); } } @@ -105,6 +106,7 @@ class DecayAnimation extends Animation { this._lastValue = value; if (this.__active) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); } } diff --git a/Libraries/Animated/src/animations/SpringAnimation.js b/Libraries/Animated/animations/SpringAnimation.js similarity index 96% rename from Libraries/Animated/src/animations/SpringAnimation.js rename to Libraries/Animated/animations/SpringAnimation.js index 61ff7cfa7cc0f5..e09db93b7c5340 100644 --- a/Libraries/Animated/src/animations/SpringAnimation.js +++ b/Libraries/Animated/animations/SpringAnimation.js @@ -12,6 +12,7 @@ const AnimatedValue = require('../nodes/AnimatedValue'); const AnimatedValueXY = require('../nodes/AnimatedValueXY'); +const AnimatedInterpolation = require('../nodes/AnimatedInterpolation'); const Animation = require('./Animation'); const SpringConfig = require('../SpringConfig'); @@ -21,7 +22,8 @@ const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); import type {AnimationConfig, EndCallback} from './Animation'; -export type SpringAnimationConfig = AnimationConfig & { +export type SpringAnimationConfig = { + ...AnimationConfig, toValue: | number | AnimatedValue @@ -30,7 +32,8 @@ export type SpringAnimationConfig = AnimationConfig & { y: number, ... } - | AnimatedValueXY, + | AnimatedValueXY + | AnimatedInterpolation, overshootClamping?: boolean, restDisplacementThreshold?: number, restSpeedThreshold?: number, @@ -49,11 +52,11 @@ export type SpringAnimationConfig = AnimationConfig & { damping?: number, mass?: number, delay?: number, - ... }; -export type SpringAnimationConfigSingle = AnimationConfig & { - toValue: number | AnimatedValue, +export type SpringAnimationConfigSingle = { + ...AnimationConfig, + toValue: number | AnimatedValue | AnimatedInterpolation, overshootClamping?: boolean, restDisplacementThreshold?: number, restSpeedThreshold?: number, @@ -66,7 +69,6 @@ export type SpringAnimationConfigSingle = AnimationConfig & { damping?: number, mass?: number, delay?: number, - ... }; class SpringAnimation extends Animation { @@ -342,6 +344,7 @@ class SpringAnimation extends Animation { this.__debouncedOnEnd({finished: true}); return; } + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); } diff --git a/Libraries/Animated/src/animations/TimingAnimation.js b/Libraries/Animated/animations/TimingAnimation.js similarity index 88% rename from Libraries/Animated/src/animations/TimingAnimation.js rename to Libraries/Animated/animations/TimingAnimation.js index f88f7207e60d88..7f239ce0102faf 100644 --- a/Libraries/Animated/src/animations/TimingAnimation.js +++ b/Libraries/Animated/animations/TimingAnimation.js @@ -12,13 +12,15 @@ const AnimatedValue = require('../nodes/AnimatedValue'); const AnimatedValueXY = require('../nodes/AnimatedValueXY'); +const AnimatedInterpolation = require('../nodes/AnimatedInterpolation'); const Animation = require('./Animation'); const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); import type {AnimationConfig, EndCallback} from './Animation'; -export type TimingAnimationConfig = AnimationConfig & { +export type TimingAnimationConfig = { + ...AnimationConfig, toValue: | number | AnimatedValue @@ -27,25 +29,26 @@ export type TimingAnimationConfig = AnimationConfig & { y: number, ... } - | AnimatedValueXY, + | AnimatedValueXY + | AnimatedInterpolation, easing?: (value: number) => number, duration?: number, delay?: number, - ... }; -export type TimingAnimationConfigSingle = AnimationConfig & { - toValue: number | AnimatedValue, +export type TimingAnimationConfigSingle = { + ...AnimationConfig, + toValue: number | AnimatedValue | AnimatedInterpolation, easing?: (value: number) => number, duration?: number, delay?: number, - ... }; let _easeInOut; function easeInOut() { if (!_easeInOut) { const Easing = require('../Easing'); + // $FlowFixMe[method-unbinding] _easeInOut = Easing.inOut(Easing.ease); } return _easeInOut; @@ -115,6 +118,7 @@ class TimingAnimation extends Animation { this.__startNativeAnimation(animatedValue); } else { this._animationFrame = requestAnimationFrame( + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.onUpdate.bind(this), ); } @@ -147,6 +151,7 @@ class TimingAnimation extends Animation { (this._toValue - this._fromValue), ); if (this.__active) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); } } diff --git a/Libraries/Animated/src/bezier.js b/Libraries/Animated/bezier.js similarity index 100% rename from Libraries/Animated/src/bezier.js rename to Libraries/Animated/bezier.js diff --git a/Libraries/Animated/components/AnimatedFlatList.js b/Libraries/Animated/components/AnimatedFlatList.js new file mode 100644 index 00000000000000..49a1ebcbd19b37 --- /dev/null +++ b/Libraries/Animated/components/AnimatedFlatList.js @@ -0,0 +1,30 @@ +/** + * 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-local + * @format + */ + +import * as React from 'react'; + +const FlatList = require('../../Lists/FlatList'); +const createAnimatedComponent = require('../createAnimatedComponent'); + +import type {AnimatedComponentType} from '../createAnimatedComponent'; + +/** + * @see https://github.com/facebook/react-native/commit/b8c8562 + */ +const FlatListWithEventThrottle = React.forwardRef((props, ref) => ( + +)); + +module.exports = (createAnimatedComponent( + FlatListWithEventThrottle, +): AnimatedComponentType< + React.ElementConfig, + React.ElementRef, +>); diff --git a/Libraries/Animated/components/AnimatedImage.js b/Libraries/Animated/components/AnimatedImage.js new file mode 100644 index 00000000000000..107d601f50c91e --- /dev/null +++ b/Libraries/Animated/components/AnimatedImage.js @@ -0,0 +1,23 @@ +/** + * 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-local + * @format + */ + +import * as React from 'react'; + +const Image = require('../../Image/Image'); +const createAnimatedComponent = require('../createAnimatedComponent'); + +import type {AnimatedComponentType} from '../createAnimatedComponent'; + +module.exports = (createAnimatedComponent((Image: $FlowFixMe), { + collapsable: false, +}): AnimatedComponentType< + React.ElementConfig, + React.ElementRef, +>); diff --git a/Libraries/Animated/components/AnimatedScrollView.js b/Libraries/Animated/components/AnimatedScrollView.js new file mode 100644 index 00000000000000..488e9d12041534 --- /dev/null +++ b/Libraries/Animated/components/AnimatedScrollView.js @@ -0,0 +1,30 @@ +/** + * 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-local + * @format + */ + +import * as React from 'react'; + +const ScrollView = require('../../Components/ScrollView/ScrollView'); +const createAnimatedComponent = require('../createAnimatedComponent'); + +import type {AnimatedComponentType} from '../createAnimatedComponent'; + +/** + * @see https://github.com/facebook/react-native/commit/b8c8562 + */ +const ScrollViewWithEventThrottle = React.forwardRef((props, ref) => ( + +)); + +module.exports = (createAnimatedComponent(ScrollViewWithEventThrottle, { + collapsable: false, +}): AnimatedComponentType< + React.ElementConfig, + React.ElementRef, +>); diff --git a/Libraries/Animated/components/AnimatedSectionList.js b/Libraries/Animated/components/AnimatedSectionList.js new file mode 100644 index 00000000000000..98ccd918460bc6 --- /dev/null +++ b/Libraries/Animated/components/AnimatedSectionList.js @@ -0,0 +1,30 @@ +/** + * 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-local + * @format + */ + +import * as React from 'react'; + +import SectionList from '../../Lists/SectionList'; +const createAnimatedComponent = require('../createAnimatedComponent'); + +import type {AnimatedComponentType} from '../createAnimatedComponent'; + +/** + * @see https://github.com/facebook/react-native/commit/b8c8562 + */ +const SectionListWithEventThrottle = React.forwardRef((props, ref) => ( + +)); + +module.exports = (createAnimatedComponent( + SectionListWithEventThrottle, +): AnimatedComponentType< + React.ElementConfig, + React.ElementRef, +>); diff --git a/Libraries/Animated/components/AnimatedText.js b/Libraries/Animated/components/AnimatedText.js new file mode 100644 index 00000000000000..dd4fb0b02378fb --- /dev/null +++ b/Libraries/Animated/components/AnimatedText.js @@ -0,0 +1,23 @@ +/** + * 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-local + * @format + */ + +import * as React from 'react'; + +const Text = require('../../Text/Text'); +const createAnimatedComponent = require('../createAnimatedComponent'); + +import type {AnimatedComponentType} from '../createAnimatedComponent'; + +module.exports = (createAnimatedComponent((Text: $FlowFixMe), { + collapsable: false, +}): AnimatedComponentType< + React.ElementConfig, + React.ElementRef, +>); diff --git a/Libraries/Animated/components/AnimatedView.js b/Libraries/Animated/components/AnimatedView.js new file mode 100644 index 00000000000000..79ebe1a0ea97cb --- /dev/null +++ b/Libraries/Animated/components/AnimatedView.js @@ -0,0 +1,23 @@ +/** + * 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-local + * @format + */ + +import * as React from 'react'; + +const View = require('../../Components/View/View'); +const createAnimatedComponent = require('../createAnimatedComponent'); + +import type {AnimatedComponentType} from '../createAnimatedComponent'; + +module.exports = (createAnimatedComponent(View, { + collapsable: true, +}): AnimatedComponentType< + React.ElementConfig, + React.ElementRef, +>); diff --git a/Libraries/Animated/createAnimatedComponent.js b/Libraries/Animated/createAnimatedComponent.js new file mode 100644 index 00000000000000..1154557c407908 --- /dev/null +++ b/Libraries/Animated/createAnimatedComponent.js @@ -0,0 +1,305 @@ +/** + * 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 * as createAnimatedComponentInjection from './createAnimatedComponentInjection'; + +const View = require('../Components/View/View'); +const {AnimatedEvent} = require('./AnimatedEvent'); +const AnimatedProps = require('./nodes/AnimatedProps'); +const React = require('react'); +const NativeAnimatedHelper = require('./NativeAnimatedHelper'); + +const invariant = require('invariant'); +const setAndForwardRef = require('../Utilities/setAndForwardRef'); + +let animatedComponentNextId = 1; + +export type AnimatedComponentType< + Props: {+[string]: mixed, ...}, + Instance, +> = React.AbstractComponent< + $ObjMap< + Props & + $ReadOnly<{ + passthroughAnimatedPropExplicitValues?: React.ElementConfig< + typeof View, + >, + }>, + () => any, + >, + Instance, +>; + +type AnimatedComponentOptions = { + collapsable?: boolean, +}; + +function createAnimatedComponent( + Component: React.AbstractComponent, + options?: AnimatedComponentOptions, +): AnimatedComponentType { + invariant( + typeof Component !== 'function' || + (Component.prototype && Component.prototype.isReactComponent), + '`createAnimatedComponent` does not support stateless functional components; ' + + 'use a class component instead.', + ); + + class AnimatedComponent extends React.Component { + _component: any; // TODO T53738161: flow type this, and the whole file + _invokeAnimatedPropsCallbackOnMount: boolean = false; + _prevComponent: any; + _propsAnimated: AnimatedProps; + _eventDetachers: Array = []; + + // Only to be used in this file, and only in Fabric. + _animatedComponentId: string = `${animatedComponentNextId++}:animatedComponent`; + + _attachNativeEvents() { + // Make sure to get the scrollable node for components that implement + // `ScrollResponder.Mixin`. + const scrollableNode = this._component?.getScrollableNode + ? this._component.getScrollableNode() + : this._component; + + for (const key in this.props) { + const prop = this.props[key]; + if (prop instanceof AnimatedEvent && prop.__isNative) { + prop.__attach(scrollableNode, key); + this._eventDetachers.push(() => prop.__detach(scrollableNode, key)); + } + } + } + + _detachNativeEvents() { + this._eventDetachers.forEach(remove => remove()); + this._eventDetachers = []; + } + + _isFabric = (): boolean => { + // When called during the first render, `_component` is always null. + // Therefore, even if a component is rendered in Fabric, we can't detect + // that until ref is set, which happens sometime after the first render. + // In cases where this value switching between "false" and "true" on Fabric + // causes issues, add an additional check for _component nullity. + if (this._component == null) { + return false; + } + return ( + // eslint-disable-next-line dot-notation + this._component['_internalInstanceHandle']?.stateNode?.canonical != + null || + // Some components have a setNativeProps function but aren't a host component + // such as lists like FlatList and SectionList. These should also use + // forceUpdate in Fabric since setNativeProps doesn't exist on the underlying + // host component. This crazy hack is essentially special casing those lists and + // ScrollView itself to use forceUpdate in Fabric. + // If these components end up using forwardRef then these hacks can go away + // as this._component would actually be the underlying host component and the above check + // would be sufficient. + (this._component.getNativeScrollRef != null && + this._component.getNativeScrollRef() != null && + // eslint-disable-next-line dot-notation + this._component.getNativeScrollRef()['_internalInstanceHandle'] + ?.stateNode?.canonical != null) || + (this._component.getScrollResponder != null && + this._component.getScrollResponder() != null && + this._component.getScrollResponder().getNativeScrollRef != null && + this._component.getScrollResponder().getNativeScrollRef() != null && + this._component.getScrollResponder().getNativeScrollRef()[ + // eslint-disable-next-line dot-notation + '_internalInstanceHandle' + ]?.stateNode?.canonical != null) + ); + }; + + _waitForUpdate = (): void => { + if (this._isFabric()) { + NativeAnimatedHelper.API.setWaitingForIdentifier( + this._animatedComponentId, + ); + } + }; + + _markUpdateComplete = (): void => { + if (this._isFabric()) { + NativeAnimatedHelper.API.unsetWaitingForIdentifier( + this._animatedComponentId, + ); + } + }; + + // The system is best designed when setNativeProps is implemented. It is + // able to avoid re-rendering and directly set the attributes that changed. + // However, setNativeProps can only be implemented on leaf native + // components. If you want to animate a composite component, you need to + // re-render it. In this case, we have a fallback that uses forceUpdate. + // This fallback is also called in Fabric. + _animatedPropsCallback = () => { + if (this._component == null) { + // AnimatedProps is created in will-mount because it's used in render. + // But this callback may be invoked before mount in async mode, + // In which case we should defer the setNativeProps() call. + // React may throw away uncommitted work in async mode, + // So a deferred call won't always be invoked. + this._invokeAnimatedPropsCallbackOnMount = true; + } else if ( + process.env.NODE_ENV === 'test' || + // For animating properties of non-leaf/non-native components + typeof this._component.setNativeProps !== 'function' || + // In Fabric, force animations to go through forceUpdate and skip setNativeProps + this._isFabric() + ) { + this.forceUpdate(); + } else if (!this._propsAnimated.__isNative) { + this._component.setNativeProps( + this._propsAnimated.__getAnimatedValue(), + ); + } else { + throw new Error( + 'Attempting to run JS driven animation on animated ' + + 'node that has been moved to "native" earlier by starting an ' + + 'animation with `useNativeDriver: true`', + ); + } + }; + + _attachProps(nextProps) { + const oldPropsAnimated = this._propsAnimated; + + this._propsAnimated = new AnimatedProps( + nextProps, + this._animatedPropsCallback, + ); + this._propsAnimated.__attach(); + + // When you call detach, it removes the element from the parent list + // of children. If it goes to 0, then the parent also detaches itself + // and so on. + // An optimization is to attach the new elements and THEN detach the old + // ones instead of detaching and THEN attaching. + // This way the intermediate state isn't to go to 0 and trigger + // this expensive recursive detaching to then re-attach everything on + // the very next operation. + if (oldPropsAnimated) { + oldPropsAnimated.__restoreDefaultValues(); + oldPropsAnimated.__detach(); + } + } + + _setComponentRef = setAndForwardRef({ + getForwardedRef: () => this.props.forwardedRef, + setLocalRef: ref => { + this._prevComponent = this._component; + this._component = ref; + }, + }); + + render() { + const {style = {}, ...props} = this._propsAnimated.__getValue() || {}; + const {style: passthruStyle = {}, ...passthruProps} = + this.props.passthroughAnimatedPropExplicitValues || {}; + const mergedStyle = {...style, ...passthruStyle}; + + // On Fabric, we always want to ensure the container Animated View is *not* + // flattened. + // Because we do not get a host component ref immediately and thus cannot + // do a proper Fabric vs non-Fabric detection immediately, we default to assuming + // that Fabric *is* enabled until we know otherwise. + // Thus, in Fabric, this view will never be flattened. In non-Fabric, the view will + // not be flattened during the initial render but may be flattened in the second render + // and onwards. + const forceNativeIdFabric = + (this._component == null && + (options?.collapsable === false || props.collapsable !== true)) || + this._isFabric(); + + const forceNativeId = + props.collapsable ?? + (this._propsAnimated.__isNative || + forceNativeIdFabric || + options?.collapsable === false); + // 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 + // go through the NativeViewHierarchyManager since it operates on the shadow + // thread. TODO: T68258846 + const collapsableProps = forceNativeId + ? { + nativeID: props.nativeID ?? 'animatedComponent', + collapsable: false, + } + : {}; + return ( + + ); + } + + UNSAFE_componentWillMount() { + this._waitForUpdate(); + this._attachProps(this.props); + } + + componentDidMount() { + if (this._invokeAnimatedPropsCallbackOnMount) { + this._invokeAnimatedPropsCallbackOnMount = false; + this._animatedPropsCallback(); + } + + this._propsAnimated.setNativeView(this._component); + this._attachNativeEvents(); + this._markUpdateComplete(); + } + + UNSAFE_componentWillReceiveProps(newProps) { + this._waitForUpdate(); + this._attachProps(newProps); + } + + componentDidUpdate(prevProps) { + if (this._component !== this._prevComponent) { + this._propsAnimated.setNativeView(this._component); + } + if (this._component !== this._prevComponent || prevProps !== this.props) { + this._detachNativeEvents(); + this._attachNativeEvents(); + } + this._markUpdateComplete(); + } + + componentWillUnmount() { + this._propsAnimated && this._propsAnimated.__detach(); + this._detachNativeEvents(); + this._markUpdateComplete(); + this._component = null; + this._prevComponent = null; + } + } + + return React.forwardRef(function AnimatedComponentWrapper(props, ref) { + return ( + + ); + }); +} + +// $FlowIgnore[incompatible-cast] - Will be compatible after refactors. +module.exports = (createAnimatedComponentInjection.recordAndRetrieve() ?? + createAnimatedComponent: typeof createAnimatedComponent); diff --git a/Libraries/Animated/createAnimatedComponentInjection.js b/Libraries/Animated/createAnimatedComponentInjection.js new file mode 100644 index 00000000000000..5a1727e959a9b7 --- /dev/null +++ b/Libraries/Animated/createAnimatedComponentInjection.js @@ -0,0 +1,48 @@ +/** + * 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-local + * @format + */ + +import * as React from 'react'; + +type createAnimatedComponent = ( + Component: React.AbstractComponent, +) => React.AbstractComponent; + +// This can be undefined, null, or the experimental implementation. If this is +// null, that means `createAnimatedComponent` has already been initialized and +// it is too late to call `inject`. +let injected: ?createAnimatedComponent; + +/** + * Call during bundle initialization to opt-in to new `createAnimatedComponent`. + */ +export function inject(newInjected: createAnimatedComponent): void { + if (injected !== undefined) { + if (__DEV__) { + console.error( + 'createAnimatedComponentInjection: ' + + (injected == null + ? 'Must be called before `createAnimatedComponent`.' + : 'Cannot be called more than once.'), + ); + } + return; + } + injected = newInjected; +} + +/** + * Only called by `createAnimatedComponent.js`. + */ +export function recordAndRetrieve(): createAnimatedComponent | null { + if (injected === undefined) { + injected = null; + } + return injected; +} diff --git a/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js b/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js new file mode 100644 index 00000000000000..a7d5f8169e5636 --- /dev/null +++ b/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js @@ -0,0 +1,48 @@ +/** + * 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-local + * @format + */ + +import useAnimatedProps from './useAnimatedProps'; +import useMergeRefs from '../Utilities/useMergeRefs'; +import StyleSheet from '../StyleSheet/StyleSheet'; +import * as React from 'react'; + +/** + * Experimental implementation of `createAnimatedComponent` that is intended to + * be compatible with concurrent rendering. + */ +export default function createAnimatedComponent( + Component: React.AbstractComponent, +): React.AbstractComponent { + return React.forwardRef((props, forwardedRef) => { + const [reducedProps, callbackRef] = useAnimatedProps( + props, + ); + const ref = useMergeRefs(callbackRef, forwardedRef); + + // Some components require explicit passthrough values for animation + // to work properly. For example, if an animated component is + // transformed and Pressable, onPress will not work after transform + // without these passthrough values. + // $FlowFixMe[prop-missing] + const {passthroughAnimatedPropExplicitValues, style} = reducedProps; + const {style: passthroughStyle, ...passthroughProps} = + passthroughAnimatedPropExplicitValues ?? {}; + const mergedStyle = StyleSheet.compose(style, passthroughStyle); + + return ( + + ); + }); +} diff --git a/Libraries/Animated/examples/demo.html b/Libraries/Animated/examples/demo.html deleted file mode 100644 index 4f95e114fa86a9..00000000000000 --- a/Libraries/Animated/examples/demo.html +++ /dev/null @@ -1,712 +0,0 @@ - - - - - - Animated - - - - - - - - - - - -
- -

Animated

-

Animations have for a long time been a weak point of the React ecosystem. The Animated library aims at solving this problem. It embraces the declarative aspect of React and obtains performance by using raw DOM manipulation behind the scenes instead of the usual diff.

- -

Animated.Value

- -

The basic building block of this library is Animated.Value. This is a variable that's going to drive the animation. You use it like a normal value in style attribute. Only animated components such as Animated.div will understand it.

- - - -

setValue

- -

As you can see, the value is being used inside of render() as you would expect. However, you don't call setState() in order to update the value. Instead, you can call setValue() directly on the value itself. We are using a form of data binding.

- -

The Animated.div component when rendered tracks which animated values it received. This way, whenever that value changes, we don't need to re-render the entire component, we can directly update the specific style attribute that changed.

- - - -

Animated.timing

- -

Now that we understand how the system works, let's play with some animations! The hello world of animations is to move the element somewhere else. To do that, we're going to animate the value from the current value 0 to the value 400.

- -

On every frame (via requestAnimationFrame), the timing animation is going to figure out the new value based on the current time, update the animated value which in turn is going to update the corresponding DOM node.

- - - -

Interrupt Animations

- -

As a developer, the mental model is that most animations are fire and forget. When the user presses the button, you want it to shrink to 80% and when she releases, you want it to go back to 100%.

- -

There are multiple challenges to implement this correctly. You need to stop the current animation, grab the current value and restart an animation from there. As this is pretty tedious to do manually, Animated will do that automatically for you.

- - - -

Animated.spring

- -

Unfortunately, the timing animation doesn't feel good. The main reason is that no matter how far you are in the animation, it will trigger a new one with always the same duration.

- -

The commonly used solution for this problem is to use the equation of a real-world spring. Imagine that you attach a spring to the target value, stretch it to the current value and let it go. The spring movement is going to be the same as the update.

- -

It turns out that this model is useful in a very wide range of animations. I highly recommend you to always start with a spring animation instead of a timing animation. It will make your interface feels much better.

- - - -

interpolate

- -

It is very common to animate multiple attributes during the same animation. The usual way to implement it is to start a separate animation for each of the attribute. The downside is that you now have to manage a different state per attribute which is not ideal.

- -

With Animated, you can use a single state variable and render it in multiple attributes. When the value is updated, all the places will reflect the change.

- -

In the following example, we're going to model the animation with a variable where 1 means fully visible and 0 means fully hidden. We can pass it directly to the scale attribute as the ranges match. But for the rotation, we need to convert [0 ; 1] range to [260deg ; 0deg]. This is where interpolate() comes handy.

- - - -

stopAnimation

- -

The reason why we can get away with not calling render() and instead modify the DOM directly on updates is because the animated values are opaque. In render, you cannot know the current value, which prevents you from being able to modify the structure of the DOM.

- -

Animated can offload the animation to a different thread (CoreAnimation, CSS transitions, main thread...) and we don't have a good way to know the real value. If you try to query the value then modify it, you are going to be out of sync and the result will look terrible.

- -

There's however one exception: when you want to stop the current animation. You need to know where it stopped in order to continue from there. We cannot know the value synchronously so we give it via a callback in stopAnimation. It will not suffer from being out of sync since the animation is no longer running.

- - - - -

Gesture-based Animations

- -

Most animations libraries only deal with time-based animations. But, as we move to mobile, a lot of animations are also gesture driven. Even more problematic, they often switch between both modes: once the gesture is over, you start a time-based animation using the same interpolations.

- -

Animated has been designed with this use case in mind. The key aspect is that there are three distinct and separate concepts: inputs, value, output. The same value can be updated either from a time-based animation or a gesture-based one. Because we use this intermediate representation for the animation, we can keep the same rendering as output.

- -

HorizontalPan

- -

The code needed to drag elements around is very messy with the DOM APIs. On mousedown, you need to register a mousemove listener on window otherwise you may drop touches if you move too fast. removeEventListener takes the same arguments as addEventListener instead of an id like clearTimeout. It's also really easy to forget to remove a listener and have a leak. And finally, you need to store the current position and value at the beginning and update only compared to it.

- -

We introduce a little helper called HorizontalPan which handles all this annoying code for us. It takes an Animated.Value as first argument and returns the event handlers required for it to work. We just have to bind this value to the left attribute and we're good to go.

- - - -

Animated.decay

- -

One of the big breakthrough of the iPhone is the fact that when you release the finger while scrolling, it will not abruptly stop but instead keep going for some time.

- -

In order to implement this effect, we are using a second real-world simulation: an object moving on an icy surface. All it needs is two values: the current velocity and a deceleration coefficient. It is implemented by Animated.decay.

- - - -

Animation Chaining

- -

The target for an animation is usually a number but sometimes it is convenient to use another value as a target. This way, the first value will track the second. Using a spring animation, we can get a nice trailing effect.

- - - -

addListener

- -

As I said earlier, if you track a spring

- - - - -

Animated.sequence

- -

It is very common to animate

- - - - - - - - - - - - - -
- - diff --git a/Libraries/Animated/examples/pic1.jpg b/Libraries/Animated/examples/pic1.jpg deleted file mode 100644 index a9f2e824fe6903..00000000000000 Binary files a/Libraries/Animated/examples/pic1.jpg and /dev/null differ diff --git a/Libraries/Animated/examples/pic2.jpg b/Libraries/Animated/examples/pic2.jpg deleted file mode 100644 index 34d30966ae9868..00000000000000 Binary files a/Libraries/Animated/examples/pic2.jpg and /dev/null differ diff --git a/Libraries/Animated/examples/pic3.jpg b/Libraries/Animated/examples/pic3.jpg deleted file mode 100644 index 47a97c80180407..00000000000000 Binary files a/Libraries/Animated/examples/pic3.jpg and /dev/null differ diff --git a/Libraries/Animated/examples/style.css b/Libraries/Animated/examples/style.css deleted file mode 100644 index 24191a56b985ca..00000000000000 --- a/Libraries/Animated/examples/style.css +++ /dev/null @@ -1,88 +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. - */ - -html, h1, h2 { - font-family: 'Roboto', sans-serif; - font-weight: 300; -} - -.container { - width: 800px; - margin: 0 auto; -} - -.circle { - margin: 2px; - width: 50px; - height: 50px; - position: absolute; - display: inline-block; - box-shadow: 0 1px 2px #999; - text-shadow: 0 1px 2px #999; - background-image: url(pic1.jpg); - background-size: cover; - line-height: 80px; - vertical-align: bottom; - text-align: center; - color: white; - font-size: 10px; -} - -.circle:nth-child(2) { - background-image: url(pic2.jpg); -} - -.circle:nth-child(3) { - background-image: url(pic3.jpg); -} - -div.code { - box-shadow: 0 1px 2px #999; - width: 600px; - padding: 5px; - position: relative; - margin: 0 auto; - margin-bottom: 40px; -} - -div.code .reset { - float: right; -} - -div.code pre { - padding: 2px; -} - -hr { - border: none; - border-bottom: 1px solid #D9D9D9; - margin: 0; -} - -button { - vertical-align: top; -} - -.example > span { - color: #333; - font-size: 13px; -} - -.example { - position: relative; - height: 60px; -} - -.code pre { - margin: 0; - font-size: 11px; - line-height: 1; -} - -.highlight { - background: rgb(228, 254, 253); -} diff --git a/Libraries/Animated/src/nodes/AnimatedAddition.js b/Libraries/Animated/nodes/AnimatedAddition.js similarity index 100% rename from Libraries/Animated/src/nodes/AnimatedAddition.js rename to Libraries/Animated/nodes/AnimatedAddition.js diff --git a/Libraries/Animated/src/nodes/AnimatedDiffClamp.js b/Libraries/Animated/nodes/AnimatedDiffClamp.js similarity index 100% rename from Libraries/Animated/src/nodes/AnimatedDiffClamp.js rename to Libraries/Animated/nodes/AnimatedDiffClamp.js diff --git a/Libraries/Animated/nodes/AnimatedDivision.js b/Libraries/Animated/nodes/AnimatedDivision.js new file mode 100644 index 00000000000000..fad47ae87d1242 --- /dev/null +++ b/Libraries/Animated/nodes/AnimatedDivision.js @@ -0,0 +1,79 @@ +/** + * 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'; + +const AnimatedInterpolation = require('./AnimatedInterpolation'); +const AnimatedNode = require('./AnimatedNode'); +const AnimatedValue = require('./AnimatedValue'); +const AnimatedWithChildren = require('./AnimatedWithChildren'); + +import type {InterpolationConfigType} from './AnimatedInterpolation'; + +class AnimatedDivision extends AnimatedWithChildren { + _a: AnimatedNode; + _b: AnimatedNode; + _warnedAboutDivideByZero: boolean = false; + + constructor(a: AnimatedNode | number, b: AnimatedNode | number) { + super(); + 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; + this._b = typeof b === 'number' ? new AnimatedValue(b) : b; + } + + __makeNative() { + this._a.__makeNative(); + this._b.__makeNative(); + super.__makeNative(); + } + + __getValue(): number { + const a = this._a.__getValue(); + const b = this._b.__getValue(); + if (b === 0) { + // Prevent spamming the console/LogBox + if (!this._warnedAboutDivideByZero) { + console.error('Detected division by zero in AnimatedDivision'); + this._warnedAboutDivideByZero = true; + } + // Passing infinity/NaN to Fabric will cause a native crash + return 0; + } + this._warnedAboutDivideByZero = false; + return a / b; + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __attach(): void { + this._a.__addChild(this); + this._b.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + this._b.__removeChild(this); + super.__detach(); + } + + __getNativeConfig(): any { + return { + type: 'division', + input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + }; + } +} + +module.exports = AnimatedDivision; diff --git a/Libraries/Animated/src/nodes/AnimatedInterpolation.js b/Libraries/Animated/nodes/AnimatedInterpolation.js similarity index 79% rename from Libraries/Animated/src/nodes/AnimatedInterpolation.js rename to Libraries/Animated/nodes/AnimatedInterpolation.js index 1bf6af4b27c3d7..ce06272b5c6d79 100644 --- a/Libraries/Animated/src/nodes/AnimatedInterpolation.js +++ b/Libraries/Animated/nodes/AnimatedInterpolation.js @@ -17,22 +17,17 @@ const AnimatedWithChildren = require('./AnimatedWithChildren'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); const invariant = require('invariant'); -const normalizeColor = require('../../../StyleSheet/normalizeColor'); +const normalizeColor = require('../../StyleSheet/normalizeColor'); type ExtrapolateType = 'extend' | 'identity' | 'clamp'; export type InterpolationConfigType = { - inputRange: Array, - /* $FlowFixMe(>=0.38.0 site=react_native_fb,react_native_oss) - Flow error - * detected during the deployment of v0.38.0. To see the error, remove this - * comment and run flow - */ - outputRange: Array | Array, + inputRange: $ReadOnlyArray, + outputRange: $ReadOnlyArray | $ReadOnlyArray, easing?: (input: number) => number, extrapolate?: ExtrapolateType, extrapolateLeft?: ExtrapolateType, extrapolateRight?: ExtrapolateType, - ... }; const linear = t => t; @@ -169,17 +164,17 @@ function interpolate( } function colorToRgba(input: string): string { - let int32Color = normalizeColor(input); - if (int32Color === null) { + let normalizedColor = normalizeColor(input); + if (normalizedColor === null || typeof normalizedColor !== 'number') { return input; } - int32Color = int32Color || 0; + normalizedColor = normalizedColor || 0; - const r = (int32Color & 0xff000000) >>> 24; - const g = (int32Color & 0x00ff0000) >>> 16; - const b = (int32Color & 0x0000ff00) >>> 8; - const a = (int32Color & 0x000000ff) / 255; + const r = (normalizedColor & 0xff000000) >>> 24; + const g = (normalizedColor & 0x00ff0000) >>> 16; + const b = (normalizedColor & 0x0000ff00) >>> 8; + const a = (normalizedColor & 0x000000ff) / 255; return `rgba(${r}, ${g}, ${b}, ${a})`; } @@ -210,24 +205,23 @@ function createInterpolationFromStringOutputRange( // [200, 250], // [0, 0.5], // ] - /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to - * guard against this possibility. - */ + /* $FlowFixMe[incompatible-use] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ const outputRanges = outputRange[0].match(stringShapeRegex).map(() => []); outputRange.forEach(value => { - /* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard - * against this possibility. - */ + /* $FlowFixMe[incompatible-use] (>=0.18.0): `value.match()` can return + * `null`. Need to guard against this possibility. */ value.match(stringShapeRegex).forEach((number, i) => { outputRanges[i].push(+number); }); }); - /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to - * guard against this possibility. - */ const interpolations = outputRange[0] .match(stringShapeRegex) + /* $FlowFixMe[incompatible-use] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ + /* $FlowFixMe[incompatible-call] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ .map((value, i) => { return createInterpolation({ ...config, @@ -258,7 +252,7 @@ function isRgbOrRgba(range) { return typeof range === 'string' && range.startsWith('rgb'); } -function checkPattern(arr: Array) { +function checkPattern(arr: $ReadOnlyArray) { const pattern = arr[0].replace(stringShapeRegex, ''); for (let i = 1; i < arr.length; ++i) { invariant( @@ -268,7 +262,7 @@ function checkPattern(arr: Array) { } } -function findRange(input: number, inputRange: Array) { +function findRange(input: number, inputRange: $ReadOnlyArray) { let i; for (i = 1; i < inputRange.length - 1; ++i) { if (inputRange[i] >= input) { @@ -278,32 +272,30 @@ function findRange(input: number, inputRange: Array) { return i - 1; } -function checkValidInputRange(arr: Array) { +function checkValidInputRange(arr: $ReadOnlyArray) { invariant(arr.length >= 2, 'inputRange must have at least 2 elements'); for (let i = 1; i < arr.length; ++i) { invariant( arr[i] >= arr[i - 1], - /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment, - * one or both of the operands may be something that doesn't cleanly - * convert to a string, like undefined, null, and object, etc. If you really - * mean this implicit string conversion, you can do something like - * String(myThing) - */ + /* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression + * below this comment, one or both of the operands may be something that + * doesn't cleanly convert to a string, like undefined, null, and object, + * etc. If you really mean this implicit string conversion, you can do + * something like String(myThing) */ 'inputRange must be monotonically non-decreasing ' + arr, ); } } -function checkInfiniteRange(name: string, arr: Array) { +function checkInfiniteRange(name: string, arr: $ReadOnlyArray) { invariant(arr.length >= 2, name + ' must have at least 2 elements'); invariant( arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity, - /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment, - * one or both of the operands may be something that doesn't cleanly convert - * to a string, like undefined, null, and object, etc. If you really mean - * this implicit string conversion, you can do something like - * String(myThing) - */ + /* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression + * below this comment, one or both of the operands may be something that + * doesn't cleanly convert to a string, like undefined, null, and object, + * etc. If you really mean this implicit string conversion, you can do + * something like String(myThing) */ name + 'cannot be ]-infinity;+infinity[ ' + arr, ); } @@ -364,6 +356,9 @@ class AnimatedInterpolation extends AnimatedWithChildren { return { inputRange: this._config.inputRange, // Only the `outputRange` can contain strings so we don't need to transform `inputRange` here + /* $FlowFixMe[incompatible-call] (>=0.38.0) - Flow error detected during + * the deployment of v0.38.0. To see the error, remove this comment and + * run flow */ outputRange: this.__transformDataType(this._config.outputRange), extrapolateLeft: this._config.extrapolateLeft || this._config.extrapolate || 'extend', diff --git a/Libraries/Animated/src/nodes/AnimatedModulo.js b/Libraries/Animated/nodes/AnimatedModulo.js similarity index 100% rename from Libraries/Animated/src/nodes/AnimatedModulo.js rename to Libraries/Animated/nodes/AnimatedModulo.js diff --git a/Libraries/Animated/src/nodes/AnimatedMultiplication.js b/Libraries/Animated/nodes/AnimatedMultiplication.js similarity index 100% rename from Libraries/Animated/src/nodes/AnimatedMultiplication.js rename to Libraries/Animated/nodes/AnimatedMultiplication.js diff --git a/Libraries/Animated/src/nodes/AnimatedNode.js b/Libraries/Animated/nodes/AnimatedNode.js similarity index 94% rename from Libraries/Animated/src/nodes/AnimatedNode.js rename to Libraries/Animated/nodes/AnimatedNode.js index 4b6a079bf732ad..7371d49fd5b355 100644 --- a/Libraries/Animated/src/nodes/AnimatedNode.js +++ b/Libraries/Animated/nodes/AnimatedNode.js @@ -65,7 +65,7 @@ class AnimatedNode { * animations. This is useful because there is no way to * synchronously read the value because it might be driven natively. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#addlistener + * See https://reactnative.dev/docs/animatedvalue.html#addlistener */ addListener(callback: (value: any) => mixed): string { const id = String(_uniqueId++); @@ -80,7 +80,7 @@ class AnimatedNode { * Unregister a listener. The `id` param shall match the identifier * previously returned by `addListener()`. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#removelistener + * See https://reactnative.dev/docs/animatedvalue.html#removelistener */ removeListener(id: string): void { delete this._listeners[id]; @@ -92,7 +92,7 @@ class AnimatedNode { /** * Remove all registered listeners. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#removealllisteners + * See https://reactnative.dev/docs/animatedvalue.html#removealllisteners */ removeAllListeners(): void { this._listeners = {}; diff --git a/Libraries/Animated/src/nodes/AnimatedProps.js b/Libraries/Animated/nodes/AnimatedProps.js similarity index 98% rename from Libraries/Animated/src/nodes/AnimatedProps.js rename to Libraries/Animated/nodes/AnimatedProps.js index be630133f1a471..6f915bb5b8fd1d 100644 --- a/Libraries/Animated/src/nodes/AnimatedProps.js +++ b/Libraries/Animated/nodes/AnimatedProps.js @@ -14,7 +14,7 @@ const {AnimatedEvent} = require('../AnimatedEvent'); const AnimatedNode = require('./AnimatedNode'); const AnimatedStyle = require('./AnimatedStyle'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); -const ReactNative = require('../../../Renderer/shims/ReactNative'); +const ReactNative = require('../../Renderer/shims/ReactNative'); const invariant = require('invariant'); @@ -33,7 +33,6 @@ class AnimatedProps extends AnimatedNode { } this._props = props; this._callback = callback; - this.__attach(); } __getValue(): Object { diff --git a/Libraries/Animated/src/nodes/AnimatedStyle.js b/Libraries/Animated/nodes/AnimatedStyle.js similarity index 98% rename from Libraries/Animated/src/nodes/AnimatedStyle.js rename to Libraries/Animated/nodes/AnimatedStyle.js index a9405edd38e0c3..6ccd56263a799d 100644 --- a/Libraries/Animated/src/nodes/AnimatedStyle.js +++ b/Libraries/Animated/nodes/AnimatedStyle.js @@ -15,7 +15,7 @@ const AnimatedTransform = require('./AnimatedTransform'); const AnimatedWithChildren = require('./AnimatedWithChildren'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); -const flattenStyle = require('../../../StyleSheet/flattenStyle'); +const flattenStyle = require('../../StyleSheet/flattenStyle'); class AnimatedStyle extends AnimatedWithChildren { _style: Object; diff --git a/Libraries/Animated/src/nodes/AnimatedSubtraction.js b/Libraries/Animated/nodes/AnimatedSubtraction.js similarity index 100% rename from Libraries/Animated/src/nodes/AnimatedSubtraction.js rename to Libraries/Animated/nodes/AnimatedSubtraction.js diff --git a/Libraries/Animated/src/nodes/AnimatedTracking.js b/Libraries/Animated/nodes/AnimatedTracking.js similarity index 100% rename from Libraries/Animated/src/nodes/AnimatedTracking.js rename to Libraries/Animated/nodes/AnimatedTracking.js diff --git a/Libraries/Animated/src/nodes/AnimatedTransform.js b/Libraries/Animated/nodes/AnimatedTransform.js similarity index 100% rename from Libraries/Animated/src/nodes/AnimatedTransform.js rename to Libraries/Animated/nodes/AnimatedTransform.js diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/nodes/AnimatedValue.js similarity index 77% rename from Libraries/Animated/src/nodes/AnimatedValue.js rename to Libraries/Animated/nodes/AnimatedValue.js index 4fa35ed27c20ab..ac3d72915b4116 100644 --- a/Libraries/Animated/src/nodes/AnimatedValue.js +++ b/Libraries/Animated/nodes/AnimatedValue.js @@ -12,7 +12,7 @@ const AnimatedInterpolation = require('./AnimatedInterpolation'); const AnimatedWithChildren = require('./AnimatedWithChildren'); -const InteractionManager = require('../../../Interaction/InteractionManager'); +const InteractionManager = require('../../Interaction/InteractionManager'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); import type Animation, {EndCallback} from '../animations/Animation'; @@ -46,9 +46,9 @@ const NativeAnimatedAPI = NativeAnimatedHelper.API; function _flush(rootNode: AnimatedValue): void { const animatedStyles = new Set(); function findAnimatedStyles(node) { - /* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.68 was deployed. To see the error delete this - * comment and run Flow. */ + /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.68 was deployed. To see the error + * delete this comment and run Flow. */ if (typeof node.update === 'function') { animatedStyles.add(node); } else { @@ -56,17 +56,28 @@ function _flush(rootNode: AnimatedValue): void { } } findAnimatedStyles(rootNode); - /* $FlowFixMe */ + // $FlowFixMe[prop-missing] animatedStyles.forEach(animatedStyle => animatedStyle.update()); } +/** + * Some operations are executed only on batch end, which is _mostly_ scheduled when + * Animated component props change. For some of the changes which require immediate execution + * (e.g. setValue), we create a separate batch in case none is scheduled. + */ +function _executeAsAnimatedBatch(id: string, operation: () => void) { + NativeAnimatedAPI.setWaitingForIdentifier(id); + operation(); + NativeAnimatedAPI.unsetWaitingForIdentifier(id); +} + /** * Standard value for driving animations. One `Animated.Value` can drive * multiple properties in a synchronized fashion, but can only be driven by one * mechanism at a time. Using a new mechanism (e.g. starting a new animation, * or calling `setValue`) will stop any previous ones. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html + * See https://reactnative.dev/docs/animatedvalue.html */ class AnimatedValue extends AnimatedWithChildren { _value: number; @@ -77,12 +88,20 @@ class AnimatedValue extends AnimatedWithChildren { constructor(value: number) { super(); + if (typeof value !== 'number') { + throw new Error('AnimatedValue: Attempting to set value to undefined'); + } this._startingValue = this._value = value; this._offset = 0; this._animation = null; } __detach() { + if (this.__isNative) { + NativeAnimatedAPI.getValue(this.__getNativeTag(), value => { + this._value = value; + }); + } this.stopAnimation(); super.__detach(); } @@ -95,7 +114,7 @@ class AnimatedValue extends AnimatedWithChildren { * Directly set the value. This will stop any animations running on the value * and update all the bound properties. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#setvalue + * See https://reactnative.dev/docs/animatedvalue.html#setvalue */ setValue(value: number): void { if (this._animation) { @@ -107,7 +126,9 @@ class AnimatedValue extends AnimatedWithChildren { !this.__isNative /* don't perform a flush for natively driven values */, ); if (this.__isNative) { - NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value); + _executeAsAnimatedBatch(this.__getNativeTag().toString(), () => { + NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value); + }); } } @@ -116,7 +137,7 @@ class AnimatedValue extends AnimatedWithChildren { * `setValue`, an animation, or `Animated.event`. Useful for compensating * things like the start of a pan gesture. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#setoffset + * See https://reactnative.dev/docs/animatedvalue.html#setoffset */ setOffset(offset: number): void { this._offset = offset; @@ -129,7 +150,7 @@ class AnimatedValue extends AnimatedWithChildren { * Merges the offset value into the base value and resets the offset to zero. * The final output of the value is unchanged. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#flattenoffset + * See https://reactnative.dev/docs/animatedvalue.html#flattenoffset */ flattenOffset(): void { this._value += this._offset; @@ -143,7 +164,7 @@ class AnimatedValue extends AnimatedWithChildren { * Sets the offset value to the base value, and resets the base value to zero. * The final output of the value is unchanged. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#extractoffset + * See https://reactnative.dev/docs/animatedvalue.html#extractoffset */ extractOffset(): void { this._offset += this._value; @@ -158,7 +179,7 @@ class AnimatedValue extends AnimatedWithChildren { * final value after stopping the animation, which is useful for updating * state to match the animation position with layout. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#stopanimation + * See https://reactnative.dev/docs/animatedvalue.html#stopanimation */ stopAnimation(callback?: ?(value: number) => void): void { this.stopTracking(); @@ -170,11 +191,17 @@ class AnimatedValue extends AnimatedWithChildren { /** * Stops any animation and resets the value to its original. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#resetanimation + * See https://reactnative.dev/docs/animatedvalue.html#resetanimation */ resetAnimation(callback?: ?(value: number) => void): void { this.stopAnimation(callback); this._value = this._startingValue; + if (this.__isNative) { + NativeAnimatedAPI.setAnimatedNodeValue( + this.__getNativeTag(), + this._startingValue, + ); + } } _onAnimatedValueUpdateReceived(value: number): void { @@ -193,7 +220,7 @@ class AnimatedValue extends AnimatedWithChildren { * Typically only used internally, but could be used by a custom Animation * class. * - * See http://facebook.github.io/react-native/docs/animatedvalue.html#animate + * See https://reactnative.dev/docs/animatedvalue.html#animate */ animate(animation: Animation, callback: ?EndCallback): void { let handle = null; @@ -239,6 +266,10 @@ class AnimatedValue extends AnimatedWithChildren { } _updateValue(value: number, flush: boolean): void { + if (value === undefined) { + throw new Error('AnimatedValue: Attempting to set value to undefined'); + } + this._value = value; if (flush) { _flush(this); diff --git a/Libraries/Animated/src/nodes/AnimatedValueXY.js b/Libraries/Animated/nodes/AnimatedValueXY.js similarity index 82% rename from Libraries/Animated/src/nodes/AnimatedValueXY.js rename to Libraries/Animated/nodes/AnimatedValueXY.js index 2e845b26219d84..28d3395db8a802 100644 --- a/Libraries/Animated/src/nodes/AnimatedValueXY.js +++ b/Libraries/Animated/nodes/AnimatedValueXY.js @@ -27,7 +27,7 @@ let _uniqueId = 1; * 2D Value for driving 2D animations, such as pan gestures. Almost identical * API to normal `Animated.Value`, but multiplexed. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html + * See https://reactnative.dev/docs/animatedvaluexy.html */ class AnimatedValueXY extends AnimatedWithChildren { x: AnimatedValue; @@ -69,7 +69,7 @@ class AnimatedValueXY extends AnimatedWithChildren { * Directly set the value. This will stop any animations running on the value * and update all the bound properties. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#setvalue + * See https://reactnative.dev/docs/animatedvaluexy.html#setvalue */ setValue(value: {x: number, y: number, ...}) { this.x.setValue(value.x); @@ -81,7 +81,7 @@ class AnimatedValueXY extends AnimatedWithChildren { * via `setValue`, an animation, or `Animated.event`. Useful for compensating * things like the start of a pan gesture. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#setoffset + * See https://reactnative.dev/docs/animatedvaluexy.html#setoffset */ setOffset(offset: {x: number, y: number, ...}) { this.x.setOffset(offset.x); @@ -92,7 +92,7 @@ class AnimatedValueXY extends AnimatedWithChildren { * Merges the offset value into the base value and resets the offset to zero. * The final output of the value is unchanged. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#flattenoffset + * See https://reactnative.dev/docs/animatedvaluexy.html#flattenoffset */ flattenOffset(): void { this.x.flattenOffset(); @@ -103,7 +103,7 @@ class AnimatedValueXY extends AnimatedWithChildren { * Sets the offset value to the base value, and resets the base value to * zero. The final output of the value is unchanged. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#extractoffset + * See https://reactnative.dev/docs/animatedvaluexy.html#extractoffset */ extractOffset(): void { this.x.extractOffset(); @@ -124,7 +124,7 @@ class AnimatedValueXY extends AnimatedWithChildren { /** * Stops any animation and resets the value to its original. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#resetanimation + * See https://reactnative.dev/docs/animatedvaluexy.html#resetanimation */ resetAnimation( callback?: (value: { @@ -143,7 +143,7 @@ class AnimatedValueXY extends AnimatedWithChildren { * final value after stopping the animation, which is useful for updating * state to match the animation position with layout. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#stopanimation + * See https://reactnative.dev/docs/animatedvaluexy.html#stopanimation */ stopAnimation( callback?: (value: { @@ -164,7 +164,7 @@ class AnimatedValueXY extends AnimatedWithChildren { * * Returns a string that serves as an identifier for the listener. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#addlistener + * See https://reactnative.dev/docs/animatedvaluexy.html#addlistener */ addListener(callback: ValueXYListenerCallback): string { const id = String(_uniqueId++); @@ -182,7 +182,7 @@ class AnimatedValueXY extends AnimatedWithChildren { * Unregister a listener. The `id` param shall match the identifier * previously returned by `addListener()`. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#removelistener + * See https://reactnative.dev/docs/animatedvaluexy.html#removelistener */ removeListener(id: string): void { this.x.removeListener(this._listeners[id].x); @@ -193,7 +193,7 @@ class AnimatedValueXY extends AnimatedWithChildren { /** * Remove all registered listeners. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#removealllisteners + * See https://reactnative.dev/docs/animatedvaluexy.html#removealllisteners */ removeAllListeners(): void { this.x.removeAllListeners(); @@ -204,7 +204,7 @@ class AnimatedValueXY extends AnimatedWithChildren { /** * Converts `{x, y}` into `{left, top}` for use in style. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#getlayout + * See https://reactnative.dev/docs/animatedvaluexy.html#getlayout */ getLayout(): {[key: string]: AnimatedValue, ...} { return { @@ -216,7 +216,7 @@ class AnimatedValueXY extends AnimatedWithChildren { /** * Converts `{x, y}` into a useable translation transform. * - * See http://facebook.github.io/react-native/docs/animatedvaluexy.html#gettranslatetransform + * See https://reactnative.dev/docs/animatedvaluexy.html#gettranslatetransform */ getTranslateTransform(): Array<{[key: string]: AnimatedValue, ...}> { return [{translateX: this.x}, {translateY: this.y}]; diff --git a/Libraries/Animated/src/nodes/AnimatedWithChildren.js b/Libraries/Animated/nodes/AnimatedWithChildren.js similarity index 95% rename from Libraries/Animated/src/nodes/AnimatedWithChildren.js rename to Libraries/Animated/nodes/AnimatedWithChildren.js index e46dbf2f629762..5f4ff1c6de1f91 100644 --- a/Libraries/Animated/src/nodes/AnimatedWithChildren.js +++ b/Libraries/Animated/nodes/AnimatedWithChildren.js @@ -76,6 +76,7 @@ class AnimatedWithChildren extends AnimatedNode { super.__callListeners(value); if (!this.__isNative) { for (const child of this._children) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters if (child.__getValue) { child.__callListeners(child.__getValue()); } diff --git a/Libraries/Animated/release/.gitignore b/Libraries/Animated/release/.gitignore deleted file mode 100644 index 3d2bc62692c4b0..00000000000000 --- a/Libraries/Animated/release/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/dist/ -/node_modules/ diff --git a/Libraries/Animated/release/gulpfile.js b/Libraries/Animated/release/gulpfile.js deleted file mode 100644 index 291f3236303d87..00000000000000 --- a/Libraries/Animated/release/gulpfile.js +++ /dev/null @@ -1,155 +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 - */ - -'use strict'; - -var babel = require('gulp-babel'); -var babelPluginDEV = require('fbjs-scripts/babel/dev-expression'); -var babelPluginModules = require('fbjs-scripts/babel/rewrite-modules'); -var del = require('del'); -var derequire = require('gulp-derequire'); -var flatten = require('gulp-flatten'); -var gulp = require('gulp'); -var gulpUtil = require('gulp-util'); -var header = require('gulp-header'); -var objectAssign = require('object-assign'); -var runSequence = require('run-sequence'); -var webpackStream = require('webpack-stream'); - -var DEVELOPMENT_HEADER = - ['/**', ' * Animated v<%= version %>', ' */'].join('\n') + '\n'; -var PRODUCTION_HEADER = - [ - '/**', - ' * Animated v<%= version %>', - ' *', - ' * Copyright (c) 2013-present, Facebook, Inc.', - ' *', - ' * This source code is licensed under the MIT license found in the', - ' * LICENSE file in the root directory of this source tree.', - ' */', - ].join('\n') + '\n'; - -var babelOpts = { - nonStandard: true, - loose: ['es6.classes'], - stage: 1, - plugins: [babelPluginDEV, babelPluginModules], - _moduleMap: objectAssign({}, require('fbjs/module-map'), { - React: 'react', - }), -}; - -var buildDist = function(opts) { - var webpackOpts = { - debug: opts.debug, - externals: { - react: 'React', - }, - module: { - loaders: [{test: /\.js$/, loader: 'babel'}], - }, - output: { - filename: opts.output, - library: 'Animated', - }, - plugins: [ - new webpackStream.webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify( - opts.debug ? 'development' : 'production', - ), - }), - new webpackStream.webpack.optimize.OccurenceOrderPlugin(), - new webpackStream.webpack.optimize.DedupePlugin(), - ], - }; - if (!opts.debug) { - webpackOpts.plugins.push( - new webpackStream.webpack.optimize.UglifyJsPlugin({ - compress: { - hoist_vars: true, - screw_ie8: true, - warnings: false, - }, - }), - ); - } - return webpackStream(webpackOpts, null, function(err, stats) { - if (err) { - throw new gulpUtil.PluginError('webpack', err); - } - if (stats.compilation.errors.length) { - throw new gulpUtil.PluginError('webpack', stats.toString()); - } - }); -}; - -var paths = { - dist: 'dist', - entry: 'lib/AnimatedWeb.js', - lib: 'lib', - src: [ - '*src/**/*.js', - '!src/**/__tests__/**/*.js', - '!src/**/__mocks__/**/*.js', - ], -}; - -gulp.task('clean', function(cb) { - del([paths.dist, paths.lib], cb); -}); - -gulp.task('modules', function() { - return gulp - .src(paths.src, {cwd: '../'}) - .pipe(babel(babelOpts)) - .pipe(flatten()) - .pipe(gulp.dest(paths.lib)); -}); - -gulp.task('dist', ['modules'], function() { - var distOpts = { - debug: true, - output: 'animated.js', - }; - return gulp - .src(paths.entry) - .pipe(buildDist(distOpts)) - .pipe(derequire()) - .pipe( - header(DEVELOPMENT_HEADER, { - version: process.env.npm_package_version, - }), - ) - .pipe(gulp.dest(paths.dist)); -}); - -gulp.task('dist:min', ['modules'], function() { - var distOpts = { - debug: false, - output: 'animated.min.js', - }; - return gulp - .src(paths.entry) - .pipe(buildDist(distOpts)) - .pipe( - header(PRODUCTION_HEADER, { - version: process.env.npm_package_version, - }), - ) - .pipe(gulp.dest(paths.dist)); -}); - -gulp.task('watch', function() { - gulp.watch(paths.src, ['modules']); -}); - -gulp.task('default', function(cb) { - runSequence('clean', 'modules', ['dist', 'dist:min'], cb); -}); diff --git a/Libraries/Animated/release/package.json b/Libraries/Animated/release/package.json deleted file mode 100644 index ec7fec05fd599f..00000000000000 --- a/Libraries/Animated/release/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "react-animated", - "description": "Animated provides powerful mechanisms for animating your React views", - "version": "0.1.0", - "keywords": [ - "react", - "animated", - "animation" - ], - "license": "MIT", - "main": "Animated.js", - "dependencies": { - "fbjs": "^1.0.0" - }, - "scripts": { - "build": "gulp" - }, - "devDependencies": { - "babel-core": "^5.8.25", - "babel-loader": "^5.3.2", - "del": "^1.2.0", - "fbjs-scripts": "^1.1.0", - "gulp": "^3.9.0", - "gulp-babel": "^5.1.0", - "gulp-derequire": "^2.1.0", - "gulp-flatten": "^0.1.0", - "gulp-header": "^1.2.2", - "gulp-util": "^3.0.6", - "object-assign": "^3.0.0", - "run-sequence": "^1.1.2", - "webpack": "1.11.0", - "webpack-stream": "^2.1.0" - } -} diff --git a/Libraries/Animated/src/Animated.js b/Libraries/Animated/src/Animated.js deleted file mode 100644 index a42c0d395ba25c..00000000000000 --- a/Libraries/Animated/src/Animated.js +++ /dev/null @@ -1,48 +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. - * - * @flow - * @format - */ - -'use strict'; - -import Platform from '../../Utilities/Platform'; -const View = require('../../Components/View/View'); -const React = require('react'); -import type {AnimatedComponentType} from './createAnimatedComponent'; - -const AnimatedMock = require('./AnimatedMock'); -const AnimatedImplementation = require('./AnimatedImplementation'); - -const Animated = ((Platform.isTesting - ? AnimatedMock - : AnimatedImplementation): typeof AnimatedMock); - -module.exports = { - get FlatList(): any { - return require('./components/AnimatedFlatList'); - }, - get Image(): any { - return require('./components/AnimatedImage'); - }, - get ScrollView(): any { - return require('./components/AnimatedScrollView'); - }, - get SectionList(): any { - return require('./components/AnimatedSectionList'); - }, - get Text(): any { - return require('./components/AnimatedText'); - }, - get View(): AnimatedComponentType< - React.ElementConfig, - React.ElementRef, - > { - return require('./components/AnimatedView'); - }, - ...Animated, -}; diff --git a/Libraries/Animated/src/AnimatedEvent.js b/Libraries/Animated/src/AnimatedEvent.js deleted file mode 100644 index e3c0b814cf8888..00000000000000 --- a/Libraries/Animated/src/AnimatedEvent.js +++ /dev/null @@ -1,204 +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. - * - * @flow - * @format - */ - -'use strict'; - -const AnimatedValue = require('./nodes/AnimatedValue'); -const NativeAnimatedHelper = require('./NativeAnimatedHelper'); -const ReactNative = require('../../Renderer/shims/ReactNative'); - -const invariant = require('invariant'); - -const {shouldUseNativeDriver} = require('./NativeAnimatedHelper'); - -export type Mapping = {[key: string]: Mapping, ...} | AnimatedValue; -export type EventConfig = { - listener?: ?Function, - useNativeDriver: boolean, - ... -}; - -function attachNativeEvent( - viewRef: any, - eventName: string, - argMapping: Array, -): {|detach: () => void|} { - // Find animated values in `argMapping` and create an array representing their - // key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x']. - const eventMappings = []; - - const traverse = (value, path) => { - if (value instanceof AnimatedValue) { - value.__makeNative(); - - eventMappings.push({ - nativeEventPath: path, - animatedValueTag: value.__getNativeTag(), - }); - } else if (typeof value === 'object') { - for (const key in value) { - traverse(value[key], path.concat(key)); - } - } - }; - - invariant( - argMapping[0] && argMapping[0].nativeEvent, - 'Native driven events only support animated values contained inside `nativeEvent`.', - ); - - // Assume that the event containing `nativeEvent` is always the first argument. - traverse(argMapping[0].nativeEvent, []); - - const viewTag = ReactNative.findNodeHandle(viewRef); - - if (viewTag != null) { - eventMappings.forEach(mapping => { - NativeAnimatedHelper.API.addAnimatedEventToView( - viewTag, - eventName, - mapping, - ); - }); - } - - return { - detach() { - if (viewTag != null) { - eventMappings.forEach(mapping => { - NativeAnimatedHelper.API.removeAnimatedEventFromView( - viewTag, - eventName, - mapping.animatedValueTag, - ); - }); - } - }, - }; -} - -class AnimatedEvent { - _argMapping: Array; - _listeners: Array = []; - _callListeners: Function; - _attachedEvent: ?{detach: () => void, ...}; - __isNative: boolean; - - constructor(argMapping: Array, config: EventConfig) { - this._argMapping = argMapping; - - if (config == null) { - console.warn('Animated.event now requires a second argument for options'); - config = {}; - } - - if (config.listener) { - this.__addListener(config.listener); - } - this._callListeners = this._callListeners.bind(this); - this._attachedEvent = null; - this.__isNative = shouldUseNativeDriver(config); - - if (__DEV__) { - this._validateMapping(); - } - } - - __addListener(callback: Function): void { - this._listeners.push(callback); - } - - __removeListener(callback: Function): void { - this._listeners = this._listeners.filter(listener => listener !== callback); - } - - __attach(viewRef: any, eventName: string) { - invariant( - this.__isNative, - 'Only native driven events need to be attached.', - ); - - this._attachedEvent = attachNativeEvent( - viewRef, - eventName, - this._argMapping, - ); - } - - __detach(viewTag: any, eventName: string) { - invariant( - this.__isNative, - 'Only native driven events need to be detached.', - ); - - this._attachedEvent && this._attachedEvent.detach(); - } - - __getHandler(): any | ((...args: any) => void) { - if (this.__isNative) { - return this._callListeners; - } - - return (...args: any) => { - const traverse = (recMapping, recEvt, key) => { - if (typeof recEvt === 'number' && recMapping instanceof AnimatedValue) { - recMapping.setValue(recEvt); - } else if (typeof recMapping === 'object') { - for (const mappingKey in recMapping) { - /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This - * comment suppresses an error when upgrading Flow's support for - * React. To see the error delete this comment and run Flow. */ - traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); - } - } - }; - - if (!this.__isNative) { - this._argMapping.forEach((mapping, idx) => { - traverse(mapping, args[idx], 'arg' + idx); - }); - } - this._callListeners(...args); - }; - } - - _callListeners(...args: any) { - this._listeners.forEach(listener => listener(...args)); - } - - _validateMapping() { - const traverse = (recMapping, recEvt, key) => { - if (typeof recEvt === 'number') { - invariant( - recMapping instanceof AnimatedValue, - 'Bad mapping of type ' + - typeof recMapping + - ' for key ' + - key + - ', event value must map to AnimatedValue', - ); - return; - } - invariant( - typeof recMapping === 'object', - 'Bad mapping of type ' + typeof recMapping + ' for key ' + key, - ); - invariant( - typeof recEvt === 'object', - 'Bad event of type ' + typeof recEvt + ' for key ' + key, - ); - for (const mappingKey in recMapping) { - traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); - } - }; - } -} - -module.exports = {AnimatedEvent, attachNativeEvent}; diff --git a/Libraries/Animated/src/AnimatedWeb.js b/Libraries/Animated/src/AnimatedWeb.js deleted file mode 100644 index 3f3313fe6c69e2..00000000000000 --- a/Libraries/Animated/src/AnimatedWeb.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 AnimatedImplementation = require('./AnimatedImplementation'); - -module.exports = { - ...AnimatedImplementation, - // $FlowFixMe createAnimatedComponent expects to receive types. Plain intrinsic components can't be typed like this - div: (AnimatedImplementation.createAnimatedComponent('div'): $FlowFixMe), - // $FlowFixMe createAnimatedComponent expects to receive types. Plain intrinsic components can't be typed like this - span: (AnimatedImplementation.createAnimatedComponent('span'): $FlowFixMe), - // $FlowFixMe createAnimatedComponent expects to receive types. Plain intrinsic components can't be typed like this - img: (AnimatedImplementation.createAnimatedComponent('img'): $FlowFixMe), -}; diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js deleted file mode 100644 index 25fde53b6286a6..00000000000000 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ /dev/null @@ -1,339 +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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import NativeEventEmitter from '../../EventEmitter/NativeEventEmitter'; -import type {EventConfig} from './AnimatedEvent'; -import NativeAnimatedModule from './NativeAnimatedModule'; -import type { - EventMapping, - AnimatedNodeConfig, - AnimatingNodeConfig, -} from './NativeAnimatedModule'; -import type {AnimationConfig, EndCallback} from './animations/Animation'; -import type {InterpolationConfigType} from './nodes/AnimatedInterpolation'; -import invariant from 'invariant'; - -let __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */ -let __nativeAnimationIdCount = 1; /* used for started animations */ - -let nativeEventEmitter; - -let queueConnections = false; -let queue = []; - -/** - * Simple wrappers around NativeAnimatedModule to provide flow and autocomplete support for - * the native module methods - */ -const API = { - enableQueue: function(): void { - queueConnections = true; - }, - disableQueue: function(): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - queueConnections = false; - for (let q = 0, l = queue.length; q < l; q++) { - const args = queue[q]; - NativeAnimatedModule.connectAnimatedNodes(args[0], args[1]); - } - queue.length = 0; - }, - createAnimatedNode: function(tag: number, config: AnimatedNodeConfig): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.createAnimatedNode(tag, config); - }, - startListeningToAnimatedNodeValue: function(tag: number) { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.startListeningToAnimatedNodeValue(tag); - }, - stopListeningToAnimatedNodeValue: function(tag: number) { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.stopListeningToAnimatedNodeValue(tag); - }, - connectAnimatedNodes: function(parentTag: number, childTag: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - if (queueConnections) { - queue.push([parentTag, childTag]); - return; - } - NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag); - }, - disconnectAnimatedNodes: function(parentTag: number, childTag: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.disconnectAnimatedNodes(parentTag, childTag); - }, - startAnimatingNode: function( - animationId: number, - nodeTag: number, - config: AnimatingNodeConfig, - endCallback: EndCallback, - ): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.startAnimatingNode( - animationId, - nodeTag, - config, - endCallback, - ); - }, - stopAnimation: function(animationId: number) { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.stopAnimation(animationId); - }, - setAnimatedNodeValue: function(nodeTag: number, value: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.setAnimatedNodeValue(nodeTag, value); - }, - setAnimatedNodeOffset: function(nodeTag: number, offset: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.setAnimatedNodeOffset(nodeTag, offset); - }, - flattenAnimatedNodeOffset: function(nodeTag: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.flattenAnimatedNodeOffset(nodeTag); - }, - extractAnimatedNodeOffset: function(nodeTag: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.extractAnimatedNodeOffset(nodeTag); - }, - connectAnimatedNodeToView: function(nodeTag: number, viewTag: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.connectAnimatedNodeToView(nodeTag, viewTag); - }, - disconnectAnimatedNodeFromView: function( - nodeTag: number, - viewTag: number, - ): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.disconnectAnimatedNodeFromView(nodeTag, viewTag); - }, - restoreDefaultValues: function(nodeTag: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - // Backwards compat with older native runtimes, can be removed later. - if (NativeAnimatedModule.restoreDefaultValues != null) { - NativeAnimatedModule.restoreDefaultValues(nodeTag); - } - }, - dropAnimatedNode: function(tag: number): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.dropAnimatedNode(tag); - }, - addAnimatedEventToView: function( - viewTag: number, - eventName: string, - eventMapping: EventMapping, - ) { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.addAnimatedEventToView( - viewTag, - eventName, - eventMapping, - ); - }, - removeAnimatedEventFromView( - viewTag: number, - eventName: string, - animatedNodeTag: number, - ) { - invariant(NativeAnimatedModule, 'Native animated module is not available'); - NativeAnimatedModule.removeAnimatedEventFromView( - viewTag, - eventName, - animatedNodeTag, - ); - }, -}; - -/** - * Styles allowed by the native animated implementation. - * - * In general native animated implementation should support any numeric property that doesn't need - * to be updated through the shadow view hierarchy (all non-layout properties). - */ -const STYLES_WHITELIST = { - opacity: true, - transform: true, - borderRadius: true, - borderBottomEndRadius: true, - borderBottomLeftRadius: true, - borderBottomRightRadius: true, - borderBottomStartRadius: true, - borderTopEndRadius: true, - borderTopLeftRadius: true, - borderTopRightRadius: true, - borderTopStartRadius: true, - elevation: true, - /* ios styles */ - shadowOpacity: true, - shadowRadius: true, - /* legacy android transform properties */ - scaleX: true, - scaleY: true, - translateX: true, - translateY: true, -}; - -const TRANSFORM_WHITELIST = { - translateX: true, - translateY: true, - scale: true, - scaleX: true, - scaleY: true, - rotate: true, - rotateX: true, - rotateY: true, - rotateZ: true, - perspective: true, -}; - -const SUPPORTED_INTERPOLATION_PARAMS = { - inputRange: true, - outputRange: true, - extrapolate: true, - extrapolateRight: true, - extrapolateLeft: true, -}; - -function addWhitelistedStyleProp(prop: string): void { - STYLES_WHITELIST[prop] = true; -} - -function addWhitelistedTransformProp(prop: string): void { - TRANSFORM_WHITELIST[prop] = true; -} - -function addWhitelistedInterpolationParam(param: string): void { - SUPPORTED_INTERPOLATION_PARAMS[param] = true; -} - -function validateTransform( - configs: Array< - | { - type: 'animated', - property: string, - nodeTag: ?number, - ... - } - | { - type: 'static', - property: string, - value: number | string, - ... - }, - >, -): void { - configs.forEach(config => { - if (!TRANSFORM_WHITELIST.hasOwnProperty(config.property)) { - throw new Error( - `Property '${ - config.property - }' is not supported by native animated module`, - ); - } - }); -} - -function validateStyles(styles: {[key: string]: ?number, ...}): void { - for (const key in styles) { - if (!STYLES_WHITELIST.hasOwnProperty(key)) { - throw new Error( - `Style property '${key}' is not supported by native animated module`, - ); - } - } -} - -function validateInterpolation(config: InterpolationConfigType): void { - for (const key in config) { - if (!SUPPORTED_INTERPOLATION_PARAMS.hasOwnProperty(key)) { - throw new Error( - `Interpolation property '${key}' is not supported by native animated module`, - ); - } - } -} - -function generateNewNodeTag(): number { - return __nativeAnimatedNodeTagCount++; -} - -function generateNewAnimationId(): number { - return __nativeAnimationIdCount++; -} - -function assertNativeAnimatedModule(): void { - invariant(NativeAnimatedModule, 'Native animated module is not available'); -} - -let _warnedMissingNativeAnimated = false; - -function shouldUseNativeDriver(config: AnimationConfig | EventConfig): boolean { - if (config.useNativeDriver == null) { - console.warn( - 'Animated: `useNativeDriver` was not specified. This is a required ' + - 'option and must be explicitly set to `true` or `false`', - ); - } - - if (config.useNativeDriver === true && !NativeAnimatedModule) { - if (!_warnedMissingNativeAnimated) { - console.warn( - 'Animated: `useNativeDriver` is not supported because the native ' + - 'animated module is missing. Falling back to JS-based animation. To ' + - 'resolve this, add `RCTAnimation` module to this app, or remove ' + - '`useNativeDriver`. ' + - 'More info: https://github.com/facebook/react-native/issues/11094#issuecomment-263240420', - ); - _warnedMissingNativeAnimated = true; - } - return false; - } - - return config.useNativeDriver || false; -} - -function transformDataType(value: number | string): number | string { - // Change the string type to number type so we can reuse the same logic in - // iOS and Android platform - if (typeof value !== 'string') { - return value; - } - if (/deg$/.test(value)) { - const degrees = parseFloat(value) || 0; - const radians = (degrees * Math.PI) / 180.0; - return radians; - } else { - return value; - } -} - -module.exports = { - API, - addWhitelistedStyleProp, - addWhitelistedTransformProp, - addWhitelistedInterpolationParam, - validateStyles, - validateTransform, - validateInterpolation, - generateNewNodeTag, - generateNewAnimationId, - assertNativeAnimatedModule, - shouldUseNativeDriver, - transformDataType, - // $FlowExpectedError - unsafe getter lint suppresion - get nativeEventEmitter(): NativeEventEmitter { - if (!nativeEventEmitter) { - nativeEventEmitter = new NativeEventEmitter(NativeAnimatedModule); - } - return nativeEventEmitter; - }, -}; diff --git a/Libraries/Animated/src/animations/Animation.js b/Libraries/Animated/src/animations/Animation.js deleted file mode 100644 index 4d2ac80752330a..00000000000000 --- a/Libraries/Animated/src/animations/Animation.js +++ /dev/null @@ -1,74 +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. - * - * @flow - * @format - */ - -'use strict'; - -const NativeAnimatedHelper = require('../NativeAnimatedHelper'); - -import type AnimatedValue from '../nodes/AnimatedValue'; - -export type EndResult = {finished: boolean, ...}; -export type EndCallback = (result: EndResult) => void; - -export type AnimationConfig = { - isInteraction?: boolean, - useNativeDriver: boolean, - onComplete?: ?EndCallback, - iterations?: number, - ... -}; - -// Important note: start() and stop() will only be called at most once. -// Once an animation has been stopped or finished its course, it will -// not be reused. -class Animation { - __active: boolean; - __isInteraction: boolean; - __nativeId: number; - __onEnd: ?EndCallback; - __iterations: number; - start( - fromValue: number, - onUpdate: (value: number) => void, - onEnd: ?EndCallback, - previousAnimation: ?Animation, - animatedValue: AnimatedValue, - ): void {} - stop(): void { - if (this.__nativeId) { - NativeAnimatedHelper.API.stopAnimation(this.__nativeId); - } - } - __getNativeAnimationConfig(): any { - // Subclasses that have corresponding animation implementation done in native - // should override this method - throw new Error('This animation type cannot be offloaded to native'); - } - // Helper function for subclasses to make sure onEnd is only called once. - __debouncedOnEnd(result: EndResult): void { - const onEnd = this.__onEnd; - this.__onEnd = null; - onEnd && onEnd(result); - } - __startNativeAnimation(animatedValue: AnimatedValue): void { - NativeAnimatedHelper.API.enableQueue(); - animatedValue.__makeNative(); - NativeAnimatedHelper.API.disableQueue(); - this.__nativeId = NativeAnimatedHelper.generateNewAnimationId(); - NativeAnimatedHelper.API.startAnimatingNode( - this.__nativeId, - animatedValue.__getNativeTag(), - this.__getNativeAnimationConfig(), - this.__debouncedOnEnd.bind(this), - ); - } -} - -module.exports = Animation; diff --git a/Libraries/Animated/src/components/AnimatedFlatList.js b/Libraries/Animated/src/components/AnimatedFlatList.js deleted file mode 100644 index a17a13e9e9fcc4..00000000000000 --- a/Libraries/Animated/src/components/AnimatedFlatList.js +++ /dev/null @@ -1,28 +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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import * as React from 'react'; - -const FlatList = require('../../../Lists/FlatList'); - -const createAnimatedComponent = require('../createAnimatedComponent'); - -/** - * @see https://github.com/facebook/react-native/commit/b8c8562 - */ -const FlatListWithEventThrottle = React.forwardRef((props, ref) => ( - -)); - -module.exports = (createAnimatedComponent( - FlatListWithEventThrottle, -): $FlowFixMe); diff --git a/Libraries/Animated/src/components/AnimatedImage.js b/Libraries/Animated/src/components/AnimatedImage.js deleted file mode 100644 index 89fee4d29ea8da..00000000000000 --- a/Libraries/Animated/src/components/AnimatedImage.js +++ /dev/null @@ -1,17 +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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -const Image = require('../../../Image/Image'); - -const createAnimatedComponent = require('../createAnimatedComponent'); - -module.exports = (createAnimatedComponent(Image): $FlowFixMe); diff --git a/Libraries/Animated/src/components/AnimatedScrollView.js b/Libraries/Animated/src/components/AnimatedScrollView.js deleted file mode 100644 index 9938b594868881..00000000000000 --- a/Libraries/Animated/src/components/AnimatedScrollView.js +++ /dev/null @@ -1,28 +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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import * as React from 'react'; - -const ScrollView = require('../../../Components/ScrollView/ScrollView'); - -const createAnimatedComponent = require('../createAnimatedComponent'); - -/** - * @see https://github.com/facebook/react-native/commit/b8c8562 - */ -const ScrollViewWithEventThrottle = React.forwardRef((props, ref) => ( - -)); - -module.exports = (createAnimatedComponent( - ScrollViewWithEventThrottle, -): $FlowFixMe); diff --git a/Libraries/Animated/src/components/AnimatedSectionList.js b/Libraries/Animated/src/components/AnimatedSectionList.js deleted file mode 100644 index 49e95e4adc7431..00000000000000 --- a/Libraries/Animated/src/components/AnimatedSectionList.js +++ /dev/null @@ -1,28 +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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import * as React from 'react'; - -const SectionList = require('../../../Lists/SectionList'); - -const createAnimatedComponent = require('../createAnimatedComponent'); - -/** - * @see https://github.com/facebook/react-native/commit/b8c8562 - */ -const SectionListWithEventThrottle = React.forwardRef((props, ref) => ( - -)); - -module.exports = (createAnimatedComponent( - SectionListWithEventThrottle, -): $FlowFixMe); diff --git a/Libraries/Animated/src/components/AnimatedText.js b/Libraries/Animated/src/components/AnimatedText.js deleted file mode 100644 index 81b016cca18d41..00000000000000 --- a/Libraries/Animated/src/components/AnimatedText.js +++ /dev/null @@ -1,17 +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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -const Text = require('../../../Text/Text'); - -const createAnimatedComponent = require('../createAnimatedComponent'); - -module.exports = (createAnimatedComponent(Text): $FlowFixMe); diff --git a/Libraries/Animated/src/components/AnimatedView.js b/Libraries/Animated/src/components/AnimatedView.js deleted file mode 100644 index 3396337eda3f50..00000000000000 --- a/Libraries/Animated/src/components/AnimatedView.js +++ /dev/null @@ -1,24 +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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -const View = require('../../../Components/View/View'); - -const createAnimatedComponent = require('../createAnimatedComponent'); - -const React = require('react'); - -import type {AnimatedComponentType} from '../createAnimatedComponent'; - -module.exports = (createAnimatedComponent(View): AnimatedComponentType< - React.ElementConfig, - React.ElementRef, ->); diff --git a/Libraries/Animated/src/createAnimatedComponent.js b/Libraries/Animated/src/createAnimatedComponent.js deleted file mode 100644 index 0672962354a97f..00000000000000 --- a/Libraries/Animated/src/createAnimatedComponent.js +++ /dev/null @@ -1,202 +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. - * - * @flow - * @format - */ - -'use strict'; - -const {AnimatedEvent} = require('./AnimatedEvent'); -const AnimatedProps = require('./nodes/AnimatedProps'); -const React = require('react'); - -const invariant = require('invariant'); -const setAndForwardRef = require('../../Utilities/setAndForwardRef'); - -export type AnimatedComponentType< - Props: {+[string]: mixed, ...}, - Instance, -> = React.AbstractComponent<$ObjMap any>, Instance>; - -function createAnimatedComponent( - Component: React.AbstractComponent, -): AnimatedComponentType { - invariant( - typeof Component !== 'function' || - (Component.prototype && Component.prototype.isReactComponent), - '`createAnimatedComponent` does not support stateless functional components; ' + - 'use a class component instead.', - ); - - class AnimatedComponent extends React.Component { - _component: any; // TODO T53738161: flow type this, and the whole file - _invokeAnimatedPropsCallbackOnMount: boolean = false; - _prevComponent: any; - _propsAnimated: AnimatedProps; - _eventDetachers: Array = []; - - _attachNativeEvents() { - // Make sure to get the scrollable node for components that implement - // `ScrollResponder.Mixin`. - const scrollableNode = this._component?.getScrollableNode - ? this._component.getScrollableNode() - : this._component; - - for (const key in this.props) { - const prop = this.props[key]; - if (prop instanceof AnimatedEvent && prop.__isNative) { - prop.__attach(scrollableNode, key); - this._eventDetachers.push(() => prop.__detach(scrollableNode, key)); - } - } - } - - _detachNativeEvents() { - this._eventDetachers.forEach(remove => remove()); - this._eventDetachers = []; - } - - // The system is best designed when setNativeProps is implemented. It is - // able to avoid re-rendering and directly set the attributes that changed. - // However, setNativeProps can only be implemented on leaf native - // components. If you want to animate a composite component, you need to - // re-render it. In this case, we have a fallback that uses forceUpdate. - _animatedPropsCallback = () => { - if (this._component == null) { - // AnimatedProps is created in will-mount because it's used in render. - // But this callback may be invoked before mount in async mode, - // In which case we should defer the setNativeProps() call. - // React may throw away uncommitted work in async mode, - // So a deferred call won't always be invoked. - this._invokeAnimatedPropsCallbackOnMount = true; - } else if ( - process.env.NODE_ENV === 'test' || - // For animating properties of non-leaf/non-native components - typeof this._component.setNativeProps !== 'function' || - // In Fabric, force animations to go through forceUpdate and skip setNativeProps - // eslint-disable-next-line dot-notation - this._component['_internalInstanceHandle']?.stateNode?.canonical != null - ) { - this.forceUpdate(); - } else if (!this._propsAnimated.__isNative) { - this._component.setNativeProps( - this._propsAnimated.__getAnimatedValue(), - ); - } else { - throw new Error( - 'Attempting to run JS driven animation on animated ' + - 'node that has been moved to "native" earlier by starting an ' + - 'animation with `useNativeDriver: true`', - ); - } - }; - - _attachProps(nextProps) { - const oldPropsAnimated = this._propsAnimated; - - this._propsAnimated = new AnimatedProps( - nextProps, - this._animatedPropsCallback, - ); - - // When you call detach, it removes the element from the parent list - // of children. If it goes to 0, then the parent also detaches itself - // and so on. - // An optimization is to attach the new elements and THEN detach the old - // ones instead of detaching and THEN attaching. - // This way the intermediate state isn't to go to 0 and trigger - // this expensive recursive detaching to then re-attach everything on - // the very next operation. - if (oldPropsAnimated) { - oldPropsAnimated.__restoreDefaultValues(); - oldPropsAnimated.__detach(); - } - } - - _setComponentRef = setAndForwardRef({ - getForwardedRef: () => this.props.forwardedRef, - setLocalRef: ref => { - this._prevComponent = this._component; - this._component = ref; - - // TODO: Delete this in a future release. - if (ref != null && ref.getNode == null) { - ref.getNode = () => { - console.warn( - '%s: Calling `getNode()` on the ref of an Animated component ' + - 'is no longer necessary. You can now directly use the ref ' + - 'instead. This method will be removed in a future release.', - ref.constructor.name ?? '<>', - ); - return ref; - }; - } - }, - }); - - render() { - const props = this._propsAnimated.__getValue(); - return ( - - ); - } - - UNSAFE_componentWillMount() { - this._attachProps(this.props); - } - - componentDidMount() { - if (this._invokeAnimatedPropsCallbackOnMount) { - this._invokeAnimatedPropsCallbackOnMount = false; - this._animatedPropsCallback(); - } - - this._propsAnimated.setNativeView(this._component); - this._attachNativeEvents(); - } - - UNSAFE_componentWillReceiveProps(newProps) { - this._attachProps(newProps); - } - - componentDidUpdate(prevProps) { - if (this._component !== this._prevComponent) { - this._propsAnimated.setNativeView(this._component); - } - if (this._component !== this._prevComponent || prevProps !== this.props) { - this._detachNativeEvents(); - this._attachNativeEvents(); - } - } - - componentWillUnmount() { - this._propsAnimated && this._propsAnimated.__detach(); - this._detachNativeEvents(); - } - } - - return React.forwardRef(function AnimatedComponentWrapper(props, ref) { - return ( - - ); - }); -} - -module.exports = createAnimatedComponent; diff --git a/Libraries/Animated/src/nodes/AnimatedDivision.js b/Libraries/Animated/src/nodes/AnimatedDivision.js deleted file mode 100644 index e7b52f242b9f55..00000000000000 --- a/Libraries/Animated/src/nodes/AnimatedDivision.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. - * - * @flow - * @format - */ - -'use strict'; - -const AnimatedInterpolation = require('./AnimatedInterpolation'); -const AnimatedNode = require('./AnimatedNode'); -const AnimatedValue = require('./AnimatedValue'); -const AnimatedWithChildren = require('./AnimatedWithChildren'); - -import type {InterpolationConfigType} from './AnimatedInterpolation'; - -class AnimatedDivision extends AnimatedWithChildren { - _a: AnimatedNode; - _b: AnimatedNode; - - constructor(a: AnimatedNode | number, b: AnimatedNode | number) { - super(); - this._a = typeof a === 'number' ? new AnimatedValue(a) : a; - this._b = typeof b === 'number' ? new AnimatedValue(b) : b; - } - - __makeNative() { - this._a.__makeNative(); - this._b.__makeNative(); - super.__makeNative(); - } - - __getValue(): number { - const a = this._a.__getValue(); - const b = this._b.__getValue(); - if (b === 0) { - console.error('Detected division by zero in AnimatedDivision'); - } - return a / b; - } - - interpolate(config: InterpolationConfigType): AnimatedInterpolation { - return new AnimatedInterpolation(this, config); - } - - __attach(): void { - this._a.__addChild(this); - this._b.__addChild(this); - } - - __detach(): void { - this._a.__removeChild(this); - this._b.__removeChild(this); - super.__detach(); - } - - __getNativeConfig(): any { - return { - type: 'division', - input: [this._a.__getNativeTag(), this._b.__getNativeTag()], - }; - } -} - -module.exports = AnimatedDivision; diff --git a/Libraries/Animated/src/polyfills/InteractionManager.js b/Libraries/Animated/src/polyfills/InteractionManager.js deleted file mode 100644 index c2247c762a3da0..00000000000000 --- a/Libraries/Animated/src/polyfills/InteractionManager.js +++ /dev/null @@ -1,15 +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 - */ - -'use strict'; - -module.exports = { - createInteractionHandle: function() {}, - clearInteractionHandle: function() {}, -}; diff --git a/Libraries/Animated/src/polyfills/Set.js b/Libraries/Animated/src/polyfills/Set.js deleted file mode 100644 index 39205b8f9f9920..00000000000000 --- a/Libraries/Animated/src/polyfills/Set.js +++ /dev/null @@ -1,26 +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 - */ - -'use strict'; - -function SetPolyfill() { - this._cache = []; -} - -SetPolyfill.prototype.add = function(e) { - if (this._cache.indexOf(e) === -1) { - this._cache.push(e); - } -}; - -SetPolyfill.prototype.forEach = function(cb) { - this._cache.forEach(cb); -}; - -module.exports = SetPolyfill; diff --git a/Libraries/Animated/src/polyfills/flattenStyle.js b/Libraries/Animated/src/polyfills/flattenStyle.js deleted file mode 100644 index b50057af28435f..00000000000000 --- a/Libraries/Animated/src/polyfills/flattenStyle.js +++ /dev/null @@ -1,13 +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 - */ - -'use strict'; -module.exports = function(style) { - return style; -}; diff --git a/Libraries/Animated/useAnimatedProps.js b/Libraries/Animated/useAnimatedProps.js new file mode 100644 index 00000000000000..142faedb1b2396 --- /dev/null +++ b/Libraries/Animated/useAnimatedProps.js @@ -0,0 +1,211 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import AnimatedProps from './nodes/AnimatedProps'; +import {AnimatedEvent} from './AnimatedEvent'; +import useRefEffect from '../Utilities/useRefEffect'; +import NativeAnimatedHelper from './NativeAnimatedHelper'; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useReducer, + useRef, + useState, +} from 'react'; + +type ReducedProps = { + ...TProps, + collapsable: boolean, + ... +}; +type CallbackRef = T => mixed; + +let animatedComponentNextId = 1; + +export default function useAnimatedProps( + props: TProps, +): [ReducedProps, CallbackRef] { + const [, scheduleUpdate] = useReducer(count => count + 1, 0); + const onUpdateRef = useRef void>(null); + + // TODO: Only invalidate `node` if animated props or `style` change. In the + // previous implementation, we permitted `style` to override props with the + // same name property name as styles, so we can probably continue doing that. + // The ordering of other props *should* not matter. + const node = useMemo( + () => new AnimatedProps(props, () => onUpdateRef.current?.()), + [props], + ); + useAnimatedPropsLifecycle(node); + + // TODO: This "effect" does three things: + // + // 1) Call `setNativeView`. + // 2) Update `onUpdateRef`. + // 3) Update listeners for `AnimatedEvent` props. + // + // Ideally, each of these would be separat "effects" so that they are not + // unnecessarily re-run when irrelevant dependencies change. For example, we + // should be able to hoist all `AnimatedEvent` props and only do #3 if either + // the `AnimatedEvent` props change or `instance` changes. + // + // But there is no way to transparently compose three separate callback refs, + // so we just combine them all into one for now. + const refEffect = useCallback( + instance => { + // NOTE: This may be called more often than necessary (e.g. when `props` + // changes), but `setNativeView` already optimizes for that. + node.setNativeView(instance); + + // NOTE: This callback is only used by the JavaScript animation driver. + onUpdateRef.current = () => { + if ( + process.env.NODE_ENV === 'test' || + typeof instance !== 'object' || + typeof instance?.setNativeProps !== 'function' || + isFabricInstance(instance) + ) { + // Schedule an update for this component to update `reducedProps`, + // but do not compute it immediately. If a parent also updated, we + // need to merge those new props in before updating. + scheduleUpdate(); + } else if (!node.__isNative) { + // $FlowIgnore[not-a-function] - Assume it's still a function. + instance.setNativeProps(node.__getAnimatedValue()); + } else { + throw new Error( + 'Attempting to run JS driven animation on animated node ' + + 'that has been moved to "native" earlier by starting an ' + + 'animation with `useNativeDriver: true`', + ); + } + }; + + const target = getEventTarget(instance); + const events = []; + + for (const propName in props) { + const propValue = props[propName]; + if (propValue instanceof AnimatedEvent && propValue.__isNative) { + propValue.__attach(target, propName); + events.push([propName, propValue]); + } + } + + return () => { + onUpdateRef.current = null; + + for (const [propName, propValue] of events) { + propValue.__detach(target, propName); + } + }; + }, + [props, node], + ); + const callbackRef = useRefEffect(refEffect); + + return [reduceAnimatedProps(node), callbackRef]; +} + +function reduceAnimatedProps( + node: AnimatedProps, +): ReducedProps { + // Force `collapsable` to be false so that the native view is not flattened. + // Flattened views cannot be accurately referenced by the native driver. + return { + ...node.__getValue(), + collapsable: false, + }; +} + +/** + * Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach` + * and `__detach`. However, this is more complicated because `AnimatedProps` + * uses reference counting to determine when to recursively detach its children + * nodes. So in order to optimize this, we avoid detaching until the next attach + * unless we are unmounting. + */ +function useAnimatedPropsLifecycle(node: AnimatedProps): void { + const prevNodeRef = useRef(null); + const isUnmountingRef = useRef(false); + + const [animatedComponentId] = useState( + () => `${animatedComponentNextId++}:animatedComponent`, + ); + + useLayoutEffect(() => { + NativeAnimatedHelper.API.setWaitingForIdentifier(animatedComponentId); + }); + + useEffect(() => { + NativeAnimatedHelper.API.unsetWaitingForIdentifier(animatedComponentId); + }); + + useLayoutEffect(() => { + isUnmountingRef.current = false; + return () => { + isUnmountingRef.current = true; + }; + }, []); + + useLayoutEffect(() => { + node.__attach(); + if (prevNodeRef.current != null) { + const prevNode = prevNodeRef.current; + // TODO: Stop restoring default values (unless `reset` is called). + prevNode.__restoreDefaultValues(); + prevNode.__detach(); + prevNodeRef.current = null; + } + return () => { + if (isUnmountingRef.current) { + // NOTE: Do not restore default values on unmount, see D18197735. + node.__detach(); + } else { + prevNodeRef.current = node; + } + }; + }, [node]); +} + +function getEventTarget(instance: TInstance): TInstance { + return typeof instance === 'object' && + typeof instance?.getScrollableNode === 'function' + ? // $FlowFixMe[incompatible-use] - Legacy instance assumptions. + instance.getScrollableNode() + : instance; +} + +// $FlowFixMe[unclear-type] - Legacy instance assumptions. +function isFabricInstance(instance: any): boolean { + return ( + hasFabricHandle(instance) || + // Some components have a setNativeProps function but aren't a host component + // such as lists like FlatList and SectionList. These should also use + // forceUpdate in Fabric since setNativeProps doesn't exist on the underlying + // host component. This crazy hack is essentially special casing those lists and + // ScrollView itself to use forceUpdate in Fabric. + // If these components end up using forwardRef then these hacks can go away + // as instance would actually be the underlying host component and the above check + // would be sufficient. + hasFabricHandle(instance?.getNativeScrollRef?.()) || + hasFabricHandle(instance?.getScrollResponder?.()?.getNativeScrollRef?.()) + ); +} + +// $FlowFixMe[unclear-type] - Legacy instance assumptions. +function hasFabricHandle(instance: any): boolean { + // eslint-disable-next-line dot-notation + return instance?.['_internalInstanceHandle']?.stateNode?.canonical != null; +} diff --git a/Libraries/AppState/AppState.js b/Libraries/AppState/AppState.js index 30a0808f4400b3..303c97ed28e2e1 100644 --- a/Libraries/AppState/AppState.js +++ b/Libraries/AppState/AppState.js @@ -4,64 +4,81 @@ * 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 */ -'use strict'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; +import logError from '../Utilities/logError'; +import NativeAppState from './NativeAppState'; +import Platform from '../Utilities/Platform'; -const EventEmitter = require('../vendor/emitter/EventEmitter'); -const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter'); +export type AppStateValues = 'inactive' | 'background' | 'active'; -const invariant = require('invariant'); -const logError = require('../Utilities/logError'); +type AppStateEventDefinitions = { + change: [AppStateValues], + memoryWarning: [], + blur: [], + focus: [], +}; -import NativeAppState from './NativeAppState'; +type NativeAppStateEventDefinitions = { + appStateDidChange: [{app_state: AppStateValues}], + appStateFocusChange: [boolean], + memoryWarning: [], +}; /** * `AppState` can tell you if the app is in the foreground or background, * and notify you when the state changes. * - * See http://facebook.github.io/react-native/docs/appstate.html + * See https://reactnative.dev/docs/appstate.html */ -class AppState extends NativeEventEmitter { - _eventHandlers: Object; - _supportedEvents = ['change', 'memoryWarning', 'blur', 'focus']; - currentState: ?string; +class AppState { + currentState: ?string = null; isAvailable: boolean; - constructor() { - super(NativeAppState); - - this.isAvailable = true; - this._eventHandlers = this._supportedEvents.reduce((handlers, key) => { - handlers[key] = new Map(); - return handlers; - }, {}); + _emitter: ?NativeEventEmitter; - this.currentState = NativeAppState.getConstants().initialAppState; - - let eventUpdated = false; - - // TODO: this is a terrible solution - in order to ensure `currentState` - // prop is up to date, we have to register an observer that updates it - // whenever the state changes, even if nobody cares. We should just - // deprecate the `currentState` property and get rid of this. - this.addListener('appStateDidChange', appStateData => { - eventUpdated = true; - this.currentState = appStateData.app_state; - }); - - // TODO: see above - this request just populates the value of `currentState` - // when the module is first initialized. Would be better to get rid of the - // prop and expose `getCurrentAppState` method directly. - NativeAppState.getCurrentAppState(appStateData => { - // It's possible that the state will have changed here & listeners need to be notified - if (!eventUpdated && this.currentState !== appStateData.app_state) { + constructor() { + if (NativeAppState == null) { + this.isAvailable = false; + } else { + this.isAvailable = true; + + const emitter: NativeEventEmitter = new NativeEventEmitter( + // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior + // If you want to use the native module on other platforms, please remove this condition and test its behavior + Platform.OS !== 'ios' ? null : NativeAppState, + ); + this._emitter = emitter; + + this.currentState = NativeAppState.getConstants().initialAppState; + + let eventUpdated = false; + + // TODO: this is a terrible solution - in order to ensure `currentState` + // prop is up to date, we have to register an observer that updates it + // whenever the state changes, even if nobody cares. We should just + // deprecate the `currentState` property and get rid of this. + emitter.addListener('appStateDidChange', appStateData => { + eventUpdated = true; this.currentState = appStateData.app_state; - this.emit('appStateDidChange', appStateData); - } - }, logError); + }); + + // TODO: see above - this request just populates the value of `currentState` + // when the module is first initialized. Would be better to get rid of the + // prop and expose `getCurrentAppState` method directly. + // $FlowExpectedError[incompatible-call] + NativeAppState.getCurrentAppState(appStateData => { + // It's possible that the state will have changed here & listeners need to be notified + if (!eventUpdated && this.currentState !== appStateData.app_state) { + this.currentState = appStateData.app_state; + emitter.emit('appStateDidChange', appStateData); + } + }, logError); + } } // TODO: now that AppState is a subclass of NativeEventEmitter, we could @@ -73,109 +90,74 @@ class AppState extends NativeEventEmitter { * Add a handler to AppState changes by listening to the `change` event type * and providing the handler. * - * See http://facebook.github.io/react-native/docs/appstate.html#addeventlistener + * See https://reactnative.dev/docs/appstate.html#addeventlistener */ - addEventListener(type: string, handler: Function) { - invariant( - this._supportedEvents.indexOf(type) !== -1, - 'Trying to subscribe to unknown event: "%s"', - type, - ); - + addEventListener>( + type: K, + handler: (...$ElementType) => void, + ): EventSubscription { + const emitter = this._emitter; + if (emitter == null) { + throw new Error('Cannot use AppState when `isAvailable` is false.'); + } switch (type) { - case 'change': { - this._eventHandlers[type].set( - handler, - this.addListener('appStateDidChange', appStateData => { - handler(appStateData.app_state); - }), - ); - break; - } - case 'memoryWarning': { - this._eventHandlers[type].set( - handler, - this.addListener('memoryWarning', handler), - ); - break; - } - + case 'change': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + const changeHandler: AppStateValues => void = handler; + return emitter.addListener('appStateDidChange', appStateData => { + changeHandler(appStateData.app_state); + }); + case 'memoryWarning': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + const memoryWarningHandler: () => void = handler; + return emitter.addListener('memoryWarning', memoryWarningHandler); case 'blur': - case 'focus': { - this._eventHandlers[type].set( - handler, - this.addListener('appStateFocusChange', hasFocus => { - if (type === 'blur' && !hasFocus) { - handler(); - } - if (type === 'focus' && hasFocus) { - handler(); - } - }), - ); - } + case 'focus': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + const focusOrBlurHandler: () => void = handler; + return emitter.addListener('appStateFocusChange', hasFocus => { + if (type === 'blur' && !hasFocus) { + focusOrBlurHandler(); + } + if (type === 'focus' && hasFocus) { + focusOrBlurHandler(); + } + }); } + throw new Error('Trying to subscribe to unknown event: ' + type); } /** - * Remove a handler by passing the `change` event type and the handler. - * - * See http://facebook.github.io/react-native/docs/appstate.html#removeeventlistener + * @deprecated Use `remove` on the EventSubscription from `addEventListener`. */ - removeEventListener(type: string, handler: Function) { - invariant( - this._supportedEvents.indexOf(type) !== -1, - 'Trying to remove listener for unknown event: "%s"', - type, - ); - if (!this._eventHandlers[type].has(handler)) { - return; + removeEventListener>( + type: K, + listener: (...$ElementType) => mixed, + ): void { + const emitter = this._emitter; + if (emitter == null) { + throw new Error('Cannot use AppState when `isAvailable` is false.'); } - this._eventHandlers[type].get(handler).remove(); - this._eventHandlers[type].delete(handler); - } -} - -function throwMissingNativeModule() { - invariant( - false, - 'Cannot use AppState module when native RCTAppState is not included in the build.\n' + - 'Either include it, or check AppState.isAvailable before calling any methods.', - ); -} - -class MissingNativeAppStateShim extends EventEmitter { - // AppState - isAvailable: boolean = false; - currentState: ?string = null; - - addEventListener(type: string, handler: Function) { - throwMissingNativeModule(); - } - - removeEventListener(type: string, handler: Function) { - throwMissingNativeModule(); - } - - // EventEmitter - addListener() { - throwMissingNativeModule(); - } - - removeAllListeners() { - throwMissingNativeModule(); - } - - removeSubscription() { - throwMissingNativeModule(); + // NOTE: This will report a deprecation notice via `console.error`. + switch (type) { + case 'change': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + // $FlowIssue[incompatible-call] + emitter.removeListener('appStateDidChange', listener); + return; + case 'memoryWarning': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + emitter.removeListener('memoryWarning', listener); + return; + case 'blur': + case 'focus': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + // $FlowIssue[incompatible-call] + emitter.addListener('appStateFocusChange', listener); + return; + } + throw new Error('Trying to unsubscribe from unknown event: ' + type); } } -// This module depends on the native `RCTAppState` module. If you don't include it, -// `AppState.isAvailable` will return `false`, and any method calls will throw. -// We reassign the class variable to keep the autodoc generator happy. -const AppStateInstance: AppState | MissingNativeAppStateShim = NativeAppState - ? new AppState() - : new MissingNativeAppStateShim(); - -module.exports = AppStateInstance; +module.exports = (new AppState(): AppState); diff --git a/Libraries/AppState/NativeAppState.js b/Libraries/AppState/NativeAppState.js index 76f3b71619ef2c..b08fe5ca81a5c4 100644 --- a/Libraries/AppState/NativeAppState.js +++ b/Libraries/AppState/NativeAppState.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/BUCK b/Libraries/BUCK new file mode 100644 index 00000000000000..a2f403115fbc2e --- /dev/null +++ b/Libraries/BUCK @@ -0,0 +1,26 @@ +# 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. + +load( + "//tools/build_defs/oss:rn_codegen_defs.bzl", + "rn_codegen", +) + +rn_codegen( + name = "FBReactNativeSpec", + android_package_name = "com.facebook.fbreact.specs", + codegen_modules = True, + ios_assume_nonnull = False, + library_labels = ["supermodule:xplat/default/public.react_native.infra"], + native_module_spec_name = "FBReactNativeSpec", +) + +# TODO: Merge this into FBReactNativeSpec +rn_codegen( + name = "FBReactNativeComponentSpec", + codegen_components = True, + ios_assume_nonnull = False, + library_labels = ["supermodule:xplat/default/public.react_native.infra"], +) diff --git a/Libraries/BatchedBridge/BatchedBridge.js b/Libraries/BatchedBridge/BatchedBridge.js index b68e9873e393ba..5b928edcc6b3f2 100644 --- a/Libraries/BatchedBridge/BatchedBridge.js +++ b/Libraries/BatchedBridge/BatchedBridge.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow strict-local + * @flow strict */ 'use strict'; diff --git a/Libraries/BatchedBridge/MessageQueue.js b/Libraries/BatchedBridge/MessageQueue.js index 8c35ee9f8eaa95..a91621ca2b5878 100644 --- a/Libraries/BatchedBridge/MessageQueue.js +++ b/Libraries/BatchedBridge/MessageQueue.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow + * @flow strict * @format */ @@ -15,14 +15,14 @@ const Systrace = require('../Performance/Systrace'); const deepFreezeAndThrowOnMutationInDev = require('../Utilities/deepFreezeAndThrowOnMutationInDev'); const invariant = require('invariant'); -const stringifySafe = require('../Utilities/stringifySafe'); +const stringifySafe = require('../Utilities/stringifySafe').default; const warnOnce = require('../Utilities/warnOnce'); export type SpyData = { type: number, module: ?string, method: string | number, - args: any[], + args: mixed[], ... }; @@ -40,10 +40,10 @@ const TRACE_TAG_REACT_APPS = 1 << 17; const DEBUG_INFO_LIMIT = 32; class MessageQueue { - _lazyCallableModules: {[key: string]: (void) => Object, ...}; - _queue: [number[], number[], any[], number]; - _successCallbacks: Map; - _failureCallbacks: Map; + _lazyCallableModules: {[key: string]: (void) => {...}, ...}; + _queue: [number[], number[], mixed[], number]; + _successCallbacks: Map void>; + _failureCallbacks: Map void>; _callID: number; _lastFlush: number; _eventLoopStartTime: number; @@ -71,14 +71,18 @@ class MessageQueue { this._remoteMethodTable = {}; } - (this: any).callFunctionReturnFlushedQueue = this.callFunctionReturnFlushedQueue.bind( + // $FlowFixMe[cannot-write] + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + this.callFunctionReturnFlushedQueue = this.callFunctionReturnFlushedQueue.bind( this, ); - (this: any).callFunctionReturnResultAndFlushedQueue = this.callFunctionReturnResultAndFlushedQueue.bind( - this, - ); - (this: any).flushedQueue = this.flushedQueue.bind(this); - (this: any).invokeCallbackAndReturnFlushedQueue = this.invokeCallbackAndReturnFlushedQueue.bind( + // $FlowFixMe[cannot-write] + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + this.flushedQueue = this.flushedQueue.bind(this); + + // $FlowFixMe[cannot-write] + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + this.invokeCallbackAndReturnFlushedQueue = this.invokeCallbackAndReturnFlushedQueue.bind( this, ); } @@ -92,7 +96,7 @@ class MessageQueue { MessageQueue.prototype.__spy = info => { console.log( `${info.type === TO_JS ? 'N->JS' : 'JS->N'} : ` + - `${info.module ? info.module + '.' : ''}${info.method}` + + `${info.module != null ? info.module + '.' : ''}${info.method}` + `(${JSON.stringify(info.args)})`, ); }; @@ -106,8 +110,8 @@ class MessageQueue { callFunctionReturnFlushedQueue( module: string, method: string, - args: any[], - ): null | [Array, Array, Array, number] { + args: mixed[], + ): null | [Array, Array, Array, number] { this.__guard(() => { this.__callFunction(module, method, args); }); @@ -115,23 +119,10 @@ class MessageQueue { return this.flushedQueue(); } - callFunctionReturnResultAndFlushedQueue( - module: string, - method: string, - args: any[], - ): $TEMPORARY$array, Array, Array, number]> { - let result; - this.__guard(() => { - result = this.__callFunction(module, method, args); - }); - - return [result, this.flushedQueue()]; - } - invokeCallbackAndReturnFlushedQueue( cbID: number, - args: any[], - ): null | [Array, Array, Array, number] { + args: mixed[], + ): null | [Array, Array, Array, number] { this.__guard(() => { this.__invokeCallback(cbID, args); }); @@ -139,7 +130,7 @@ class MessageQueue { return this.flushedQueue(); } - flushedQueue(): null | [Array, Array, Array, number] { + flushedQueue(): null | [Array, Array, Array, number] { this.__guard(() => { this.__callImmediates(); }); @@ -153,23 +144,25 @@ class MessageQueue { return Date.now() - this._eventLoopStartTime; } - registerCallableModule(name: string, module: Object) { + registerCallableModule(name: string, module: {...}) { this._lazyCallableModules[name] = () => module; } - registerLazyCallableModule(name: string, factory: void => Object) { - let module: Object; - let getValue: ?(void) => Object = factory; + registerLazyCallableModule(name: string, factory: void => interface {}) { + let module: interface {}; + let getValue: ?(void) => interface {} = factory; this._lazyCallableModules[name] = () => { if (getValue) { module = getValue(); getValue = null; } + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ return module; }; } - getCallableModule(name: string): any | null { + getCallableModule(name: string): {...} | null { const getValue = this._lazyCallableModules[name]; return getValue ? getValue() : null; } @@ -177,10 +170,10 @@ class MessageQueue { callNativeSyncHook( moduleID: number, methodID: number, - params: any[], - onFail: ?Function, - onSucc: ?Function, - ): any { + params: mixed[], + onFail: ?(...mixed[]) => void, + onSucc: ?(...mixed[]) => void, + ): mixed { if (__DEV__) { invariant( global.nativeCallSyncHook, @@ -197,10 +190,10 @@ class MessageQueue { processCallbacks( moduleID: number, methodID: number, - params: any[], - onFail: ?Function, - onSucc: ?Function, - ) { + params: mixed[], + onFail: ?(...mixed[]) => void, + onSucc: ?(...mixed[]) => void, + ): void { if (onFail || onSucc) { if (__DEV__) { this._debugInfo[this._callID] = [moduleID, methodID]; @@ -248,9 +241,9 @@ class MessageQueue { enqueueNativeCall( moduleID: number, methodID: number, - params: any[], - onFail: ?Function, - onSucc: ?Function, + params: mixed[], + onFail: ?(...mixed[]) => void, + onSucc: ?(...mixed[]) => void, ) { this.processCallbacks(moduleID, methodID, params, onFail, onSucc); @@ -263,30 +256,34 @@ class MessageQueue { // function it is permitted here, and special-cased in the // conversion. const isValidArgument = val => { - const t = typeof val; - if ( - t === 'undefined' || - t === 'null' || - t === 'boolean' || - t === 'string' - ) { - return true; - } - if (t === 'number') { - return isFinite(val); - } - if (t === 'function' || t !== 'object') { - return false; - } - if (Array.isArray(val)) { - return val.every(isValidArgument); - } - for (const k in val) { - if (typeof val[k] !== 'function' && !isValidArgument(val[k])) { + switch (typeof val) { + case 'undefined': + case 'boolean': + case 'string': + return true; + case 'number': + return isFinite(val); + case 'object': + if (val == null) { + return true; + } + + if (Array.isArray(val)) { + return val.every(isValidArgument); + } + + for (const k in val) { + if (typeof val[k] !== 'function' && !isValidArgument(val[k])) { + return false; + } + } + + return true; + case 'function': + return false; + default: return false; - } } - return true; }; // Replacement allows normally non-JSON-convertible values to be @@ -311,7 +308,7 @@ class MessageQueue { ); // The params object should not be mutated after being queued - deepFreezeAndThrowOnMutationInDev((params: any)); + deepFreezeAndThrowOnMutationInDev(params); } this._queue[PARAMS].push(params); @@ -384,7 +381,7 @@ class MessageQueue { // can be configured by the VM or any Inspector __shouldPauseOnThrow(): boolean { return ( - // $FlowFixMe + // $FlowFixMe[cannot-resolve-name] typeof DebuggerInternal !== 'undefined' && DebuggerInternal.shouldPauseOnThrow === true // eslint-disable-line no-undef ); @@ -398,7 +395,7 @@ class MessageQueue { Systrace.endEvent(); } - __callFunction(module: string, method: string, args: any[]): any { + __callFunction(module: string, method: string, args: mixed[]): void { this._lastFlush = Date.now(); this._eventLoopStartTime = this._lastFlush; if (__DEV__ || this.__spy) { @@ -412,22 +409,18 @@ class MessageQueue { const moduleMethods = this.getCallableModule(module); invariant( !!moduleMethods, - 'Module %s is not a registered callable module (calling %s)', - module, - method, + `Module ${module} is not a registered callable module (calling ${method}). A frequent cause of the error is that the application entry file path is incorrect. + This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.`, ); invariant( !!moduleMethods[method], - 'Method %s does not exist on module %s', - method, - module, + `Method ${method} does not exist on module ${module}`, ); - const result = moduleMethods[method].apply(moduleMethods, args); + moduleMethods[method].apply(moduleMethods, args); Systrace.endEvent(); - return result; } - __invokeCallback(cbID: number, args: any[]) { + __invokeCallback(cbID: number, args: mixed[]) { this._lastFlush = Date.now(); this._eventLoopStartTime = this._lastFlush; diff --git a/Libraries/BatchedBridge/NativeModules.js b/Libraries/BatchedBridge/NativeModules.js index 541012582c091c..257436199d3484 100644 --- a/Libraries/BatchedBridge/NativeModules.js +++ b/Libraries/BatchedBridge/NativeModules.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict */ 'use strict'; @@ -14,11 +14,11 @@ const BatchedBridge = require('./BatchedBridge'); const invariant = require('invariant'); -import type {ExtendedError} from '../Core/Devtools/parseErrorStack'; +import type {ExtendedError} from '../Core/ExtendedError'; export type ModuleConfig = [ string /* name */, - ?Object /* constants */, + ?{...} /* constants */, ?$ReadOnlyArray /* functions */, ?$ReadOnlyArray /* promise method IDs */, ?$ReadOnlyArray /* sync method IDs */, @@ -31,7 +31,7 @@ function genModule( moduleID: number, ): ?{ name: string, - module?: Object, + module?: {...}, ... } { if (!config) { @@ -55,8 +55,9 @@ function genModule( methods && methods.forEach((methodName, methodID) => { const isPromise = - promiseMethods && arrayContains(promiseMethods, methodID); - const isSync = syncMethods && arrayContains(syncMethods, methodID); + (promiseMethods && arrayContains(promiseMethods, methodID)) || false; + const isSync = + (syncMethods && arrayContains(syncMethods, methodID)) || false; invariant( !isPromise || !isSync, 'Cannot have a method that is both async and a sync hook', @@ -85,7 +86,7 @@ function genModule( // export this method as a global so we can call it from native global.__fbGenNativeModule = genModule; -function loadModule(name: string, moduleID: number): ?Object { +function loadModule(name: string, moduleID: number): ?{...} { invariant( global.nativeRequireModuleConfig, "Can't lazily create module without nativeRequireModuleConfig", @@ -98,8 +99,10 @@ function loadModule(name: string, moduleID: number): ?Object { function genMethod(moduleID: number, methodID: number, type: MethodType) { let fn = null; if (type === 'promise') { - fn = function promiseMethodWrapper(...args: Array) { + fn = function promiseMethodWrapper(...args: Array) { // In case we reject, capture a useful stack trace here. + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ const enqueueingFrameError: ExtendedError = new Error(); return new Promise((resolve, reject) => { BatchedBridge.enqueueNativeCall( @@ -108,12 +111,17 @@ function genMethod(moduleID: number, methodID: number, type: MethodType) { args, data => resolve(data), errorData => - reject(updateErrorWithErrorData(errorData, enqueueingFrameError)), + reject( + updateErrorWithErrorData( + (errorData: $FlowFixMe), + enqueueingFrameError, + ), + ), ); }); }; } else { - fn = function nonPromiseMethodWrapper(...args: Array) { + fn = function nonPromiseMethodWrapper(...args: Array) { const lastArg = args.length > 0 ? args[args.length - 1] : null; const secondLastArg = args.length > 1 ? args[args.length - 2] : null; const hasSuccessCallback = typeof lastArg === 'function'; @@ -123,15 +131,17 @@ function genMethod(moduleID: number, methodID: number, type: MethodType) { hasSuccessCallback, 'Cannot have a non-function arg after a function arg.', ); - const onSuccess = hasSuccessCallback ? lastArg : null; - const onFail = hasErrorCallback ? secondLastArg : null; + // $FlowFixMe[incompatible-type] + const onSuccess: ?(mixed) => void = hasSuccessCallback ? lastArg : null; + // $FlowFixMe[incompatible-type] + const onFail: ?(mixed) => void = hasErrorCallback ? secondLastArg : null; const callbackCount = hasSuccessCallback + hasErrorCallback; - args = args.slice(0, args.length - callbackCount); + const newArgs = args.slice(0, args.length - callbackCount); if (type === 'sync') { return BatchedBridge.callNativeSyncHook( moduleID, methodID, - args, + newArgs, onFail, onSuccess, ); @@ -139,7 +149,7 @@ function genMethod(moduleID: number, methodID: number, type: MethodType) { BatchedBridge.enqueueNativeCall( moduleID, methodID, - args, + newArgs, onFail, onSuccess, ); @@ -158,10 +168,12 @@ function updateErrorWithErrorData( errorData: {message: string, ...}, error: ExtendedError, ): ExtendedError { + /* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ return Object.assign(error, errorData || {}); } -let NativeModules: {[moduleName: string]: Object, ...} = {}; +let NativeModules: {[moduleName: string]: $FlowFixMe, ...} = {}; if (global.nativeModuleProxy) { NativeModules = global.nativeModuleProxy; } else if (!global.nativeExtensions) { diff --git a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js index 42abd597dd867e..e1b7fd84d32411 100644 --- a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js +++ b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js @@ -81,13 +81,25 @@ describe('MessageQueue', function() { }); it('should throw when calling the same callback twice', () => { - queue.enqueueNativeCall(0, 1, [], () => {}, () => {}); + queue.enqueueNativeCall( + 0, + 1, + [], + () => {}, + () => {}, + ); queue.__invokeCallback(1, []); expect(() => queue.__invokeCallback(1, [])).toThrow(); }); it('should throw when calling both success and failure callback', () => { - queue.enqueueNativeCall(0, 1, [], () => {}, () => {}); + queue.enqueueNativeCall( + 0, + 1, + [], + () => {}, + () => {}, + ); queue.__invokeCallback(1, []); expect(() => queue.__invokeCallback(0, [])).toThrow(); }); @@ -96,7 +108,8 @@ describe('MessageQueue', function() { const unknownModule = 'UnknownModule', unknownMethod = 'UnknownMethod'; expect(() => queue.__callFunction(unknownModule, unknownMethod)).toThrow( - `Module ${unknownModule} is not a registered callable module (calling ${unknownMethod})`, + `Module ${unknownModule} is not a registered callable module (calling ${unknownMethod}). A frequent cause of the error is that the application entry file path is incorrect. + This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.`, ); }); diff --git a/Libraries/Blob/Blob.js b/Libraries/Blob/Blob.js index 2af35f2bcb0ac2..99d5e59b8e58ab 100644 --- a/Libraries/Blob/Blob.js +++ b/Libraries/Blob/Blob.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow + * @flow strict-local * @format */ @@ -67,10 +67,12 @@ class Blob { * the data in the specified range of bytes of the source Blob. * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice */ + // $FlowFixMe[unsafe-getters-setters] set data(data: ?BlobData) { this._data = data; } + // $FlowFixMe[unsafe-getters-setters] get data(): BlobData { if (!this._data) { throw new Error('Blob has been closed and is no longer available'); @@ -85,6 +87,7 @@ class Blob { if (typeof start === 'number') { if (start > size) { + // $FlowFixMe[reassign-const] start = size; } offset += start; @@ -92,6 +95,7 @@ class Blob { if (typeof end === 'number') { if (end < 0) { + // $FlowFixMe[reassign-const] end = this.size + end; } size = end - start; @@ -125,6 +129,7 @@ class Blob { /** * Size of the data contained in the Blob object, in bytes. */ + // $FlowFixMe[unsafe-getters-setters] get size(): number { return this.data.size; } @@ -133,6 +138,7 @@ class Blob { * String indicating the MIME type of the data contained in the Blob. * If the type is unknown, this string is empty. */ + // $FlowFixMe[unsafe-getters-setters] get type(): string { return this.data.type || ''; } diff --git a/Libraries/Blob/BlobManager.js b/Libraries/Blob/BlobManager.js index e12512392a3e78..4f4310adddb582 100644 --- a/Libraries/Blob/BlobManager.js +++ b/Libraries/Blob/BlobManager.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - const Blob = require('./Blob'); const BlobRegistry = require('./BlobRegistry'); diff --git a/Libraries/Blob/FileReader.js b/Libraries/Blob/FileReader.js index fc769f8a544a72..1d2ca758eadfd9 100644 --- a/Libraries/Blob/FileReader.js +++ b/Libraries/Blob/FileReader.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - const Blob = require('./Blob'); const EventTarget = require('event-target-shim'); @@ -85,9 +83,15 @@ class FileReader extends (EventTarget(...READER_EVENTS): any) { throw new Error('FileReader.readAsArrayBuffer is not implemented'); } - readAsDataURL(blob: Blob) { + readAsDataURL(blob: ?Blob) { this._aborted = false; + if (blob == null) { + throw new TypeError( + "Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'", + ); + } + NativeFileReaderModule.readAsDataURL(blob.data).then( (text: string) => { if (this._aborted) { @@ -106,9 +110,15 @@ class FileReader extends (EventTarget(...READER_EVENTS): any) { ); } - readAsText(blob: Blob, encoding: string = 'UTF-8') { + readAsText(blob: ?Blob, encoding: string = 'UTF-8') { this._aborted = false; + if (blob == null) { + throw new TypeError( + "Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'", + ); + } + NativeFileReaderModule.readAsText(blob.data, encoding).then( (text: string) => { if (this._aborted) { diff --git a/Libraries/Blob/NativeBlobModule.js b/Libraries/Blob/NativeBlobModule.js index ab2572bd0ea8b7..d43c8bc3e3b21f 100644 --- a/Libraries/Blob/NativeBlobModule.js +++ b/Libraries/Blob/NativeBlobModule.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; @@ -23,4 +21,38 @@ export interface Spec extends TurboModule { +release: (blobId: string) => void; } -export default (TurboModuleRegistry.get('BlobModule'): ?Spec); +const NativeModule = TurboModuleRegistry.get('BlobModule'); + +let constants = null; +let NativeBlobModule = null; + +if (NativeModule != null) { + NativeBlobModule = { + getConstants(): {|BLOB_URI_SCHEME: ?string, BLOB_URI_HOST: ?string|} { + if (constants == null) { + constants = NativeModule.getConstants(); + } + return constants; + }, + addNetworkingHandler(): void { + NativeModule.addNetworkingHandler(); + }, + addWebSocketHandler(id: number): void { + NativeModule.addWebSocketHandler(id); + }, + removeWebSocketHandler(id: number): void { + NativeModule.removeWebSocketHandler(id); + }, + sendOverSocket(blob: Object, socketID: number): void { + NativeModule.sendOverSocket(blob, socketID); + }, + createFromParts(parts: Array, withId: string): void { + NativeModule.createFromParts(parts, withId); + }, + release(blobId: string): void { + NativeModule.release(blobId); + }, + }; +} + +export default (NativeBlobModule: ?Spec); diff --git a/Libraries/Blob/NativeFileReaderModule.js b/Libraries/Blob/NativeFileReaderModule.js index ac135db7394d19..a33d5d9b7754dd 100644 --- a/Libraries/Blob/NativeFileReaderModule.js +++ b/Libraries/Blob/NativeFileReaderModule.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Blob/RCTBlobManager.h b/Libraries/Blob/RCTBlobManager.h index 96c8f0722f02c9..04b905ccb92a24 100755 --- a/Libraries/Blob/RCTBlobManager.h +++ b/Libraries/Blob/RCTBlobManager.h @@ -8,8 +8,9 @@ #import #import #import +#import -@interface RCTBlobManager : NSObject +@interface RCTBlobManager : NSObject - (NSString *)store:(NSData *)data; diff --git a/Libraries/Blob/RCTBlobManager.mm b/Libraries/Blob/RCTBlobManager.mm index c4ab554f088faf..425dc73fb6c077 100755 --- a/Libraries/Blob/RCTBlobManager.mm +++ b/Libraries/Blob/RCTBlobManager.mm @@ -37,12 +37,11 @@ @implementation RCTBlobManager RCT_EXPORT_MODULE(BlobModule) @synthesize bridge = _bridge; +@synthesize moduleRegistry = _moduleRegistry; @synthesize methodQueue = _methodQueue; -- (void)setBridge:(RCTBridge *)bridge +- (void)initialize { - _bridge = bridge; - std::lock_guard lock(_blobsMutex); _blobs = [NSMutableDictionary new]; @@ -139,31 +138,39 @@ - (void)remove:(NSString *)blobId RCT_EXPORT_METHOD(addNetworkingHandler) { - dispatch_async(_bridge.networking.methodQueue, ^{ - [self->_bridge.networking addRequestHandler:self]; - [self->_bridge.networking addResponseHandler:self]; + RCTNetworking *const networking = [_moduleRegistry moduleForName:"Networking"]; + + // TODO(T63516227): Why can methodQueue be nil here? + // We don't want to do anything when methodQueue is nil. + if (!networking.methodQueue) { + return; + } + + dispatch_async(networking.methodQueue, ^{ + [networking addRequestHandler:self]; + [networking addResponseHandler:self]; }); } RCT_EXPORT_METHOD(addWebSocketHandler:(double)socketID) { - dispatch_async(_bridge.webSocketModule.methodQueue, ^{ - [self->_bridge.webSocketModule setContentHandler:self forSocketID:[NSNumber numberWithDouble:socketID]]; + dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{ + [[self->_moduleRegistry moduleForName:"WebSocketModule"] setContentHandler:self forSocketID:[NSNumber numberWithDouble:socketID]]; }); } RCT_EXPORT_METHOD(removeWebSocketHandler:(double)socketID) { - dispatch_async(_bridge.webSocketModule.methodQueue, ^{ - [self->_bridge.webSocketModule setContentHandler:nil forSocketID:[NSNumber numberWithDouble:socketID]]; + dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{ + [[self->_moduleRegistry moduleForName:"WebSocketModule"] setContentHandler:nil forSocketID:[NSNumber numberWithDouble:socketID]]; }); } // @lint-ignore FBOBJCUNTYPEDCOLLECTION1 RCT_EXPORT_METHOD(sendOverSocket:(NSDictionary *)blob socketID:(double)socketID) { - dispatch_async(_bridge.webSocketModule.methodQueue, ^{ - [self->_bridge.webSocketModule sendData:[self resolve:blob] forSocketID:[NSNumber numberWithDouble:socketID]]; + dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{ + [[self->_moduleRegistry moduleForName:"WebSocketModule"] sendData:[self resolve:blob] forSocketID:[NSNumber numberWithDouble:socketID]]; }); } @@ -301,9 +308,9 @@ - (id)processWebsocketMessage:(id)message }; } -- (std::shared_ptr)getTurboModuleWithJsInvoker:(std::shared_ptr)jsInvoker +- (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(self, jsInvoker); + return std::make_shared(params); } @end diff --git a/Libraries/Blob/RCTBlobPlugins.mm b/Libraries/Blob/RCTBlobPlugins.mm index d06f40e5d21120..289094f35d43a0 100644 --- a/Libraries/Blob/RCTBlobPlugins.mm +++ b/Libraries/Blob/RCTBlobPlugins.mm @@ -17,13 +17,14 @@ #import Class RCTBlobClassProvider(const char *name) { - static std::unordered_map sCoreModuleClassMap = { + // Intentionally leak to avoid crashing after static destructors are run. + static const auto sCoreModuleClassMap = new const std::unordered_map{ {"FileReaderModule", RCTFileReaderModuleCls}, {"BlobModule", RCTBlobManagerCls}, }; - auto p = sCoreModuleClassMap.find(name); - if (p != sCoreModuleClassMap.end()) { + auto p = sCoreModuleClassMap->find(name); + if (p != sCoreModuleClassMap->end()) { auto classFunc = p->second; return classFunc(); } diff --git a/Libraries/Blob/RCTFileReaderModule.mm b/Libraries/Blob/RCTFileReaderModule.mm index 5cf41adc991fd5..2525de0c201b2b 100644 --- a/Libraries/Blob/RCTFileReaderModule.mm +++ b/Libraries/Blob/RCTFileReaderModule.mm @@ -23,14 +23,14 @@ @implementation RCTFileReaderModule RCT_EXPORT_MODULE(FileReaderModule) -@synthesize bridge = _bridge; +@synthesize moduleRegistry = _moduleRegistry; RCT_EXPORT_METHOD(readAsText:(NSDictionary *)blob encoding:(NSString *)encoding resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - RCTBlobManager *blobManager = [[self bridge] moduleForClass:[RCTBlobManager class]]; + RCTBlobManager *blobManager = [_moduleRegistry moduleForName:"BlobModule"]; NSData *data = [blobManager resolve:blob]; if (data == nil) { @@ -55,7 +55,7 @@ @implementation RCTFileReaderModule resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - RCTBlobManager *blobManager = [[self bridge] moduleForClass:[RCTBlobManager class]]; + RCTBlobManager *blobManager = [_moduleRegistry moduleForName:"BlobModule"]; NSData *data = [blobManager resolve:blob]; if (data == nil) { @@ -71,10 +71,9 @@ @implementation RCTFileReaderModule } } -- (std::shared_ptr)getTurboModuleWithJsInvoker: - (std::shared_ptr)jsInvoker +- (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(self, jsInvoker); + return std::make_shared(params); } @end diff --git a/Libraries/Blob/React-RCTBlob.podspec b/Libraries/Blob/React-RCTBlob.podspec index 19dc87b1fa029e..ad8de5c22d5849 100644 --- a/Libraries/Blob/React-RCTBlob.podspec +++ b/Libraries/Blob/React-RCTBlob.podspec @@ -11,36 +11,37 @@ version = package['version'] source = { :git => 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. - source[:commit] = `git rev-parse HEAD`.strip + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") else source[:tag] = "v#{version}" end folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' -folly_version = '2018.10.22.00' +folly_version = '2021.04.26.00' Pod::Spec.new do |s| s.name = "React-RCTBlob" s.version = version s.summary = "An API for displaying iOS action sheets and share sheets." - s.homepage = "http://facebook.github.io/react-native/" + s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "9.0", :tvos => "9.2" } + s.platforms = { :ios => "11.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 = { "USE_HEADERMAP" => "YES", "CLANG_CXX_LANGUAGE_STANDARD" => "c++14", - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Folly\"" + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/RCT-Folly\"" } - s.dependency "Folly", folly_version + s.dependency "RCT-Folly", folly_version s.dependency "FBReactNativeSpec", version s.dependency "ReactCommon/turbomodule/core", version + s.dependency "React-jsi", version s.dependency "React-Core/RCTBlobHeaders", version s.dependency "React-Core/RCTWebSocket", version s.dependency "React-RCTNetwork", version diff --git a/Libraries/Blob/URL.js b/Libraries/Blob/URL.js index f2b6f7f277cb7e..e839f39ff0d577 100644 --- a/Libraries/Blob/URL.js +++ b/Libraries/Blob/URL.js @@ -7,8 +7,6 @@ * @format */ -'use strict'; - const Blob = require('./Blob'); import NativeBlobModule from './NativeBlobModule'; @@ -67,27 +65,27 @@ export class URLSearchParams { } delete(name) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.delete is not implemented'); } get(name) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.get is not implemented'); } getAll(name) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.getAll is not implemented'); } has(name) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.has is not implemented'); } set(name, value) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.set is not implemented'); } sort() { - throw new Error('not implemented'); + throw new Error('URLSearchParams.sort is not implemented'); } [Symbol.iterator]() { @@ -107,7 +105,7 @@ export class URLSearchParams { function validateBaseUrl(url: string) { // from this MIT-licensed gist: https://gist.github.com/dperini/729294 - return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( + return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)*(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/.test( url, ); } @@ -119,9 +117,7 @@ export class URL { if (BLOB_URL_PREFIX === null) { throw new Error('Cannot create URL for blob!'); } - return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${ - blob.data.offset - }&size=${blob.size}`; + return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${blob.data.offset}&size=${blob.size}`; } static revokeObjectURL(url: string) { @@ -158,15 +154,15 @@ export class URL { } get hash() { - throw new Error('not implemented'); + throw new Error('URL.hash is not implemented'); } get host() { - throw new Error('not implemented'); + throw new Error('URL.host is not implemented'); } get hostname() { - throw new Error('not implemented'); + throw new Error('URL.hostname is not implemented'); } get href(): string { @@ -174,27 +170,27 @@ export class URL { } get origin() { - throw new Error('not implemented'); + throw new Error('URL.origin is not implemented'); } get password() { - throw new Error('not implemented'); + throw new Error('URL.password is not implemented'); } get pathname() { - throw new Error('not implemented'); + throw new Error('URL.pathname not implemented'); } get port() { - throw new Error('not implemented'); + throw new Error('URL.port is not implemented'); } get protocol() { - throw new Error('not implemented'); + throw new Error('URL.protocol is not implemented'); } get search() { - throw new Error('not implemented'); + throw new Error('URL.search is not implemented'); } get searchParams(): URLSearchParams { @@ -217,6 +213,6 @@ export class URL { } get username() { - throw new Error('not implemented'); + throw new Error('URL.username is not implemented'); } } diff --git a/Libraries/BugReporting/BugReporting.js b/Libraries/BugReporting/BugReporting.js index 001c44c17b2ae9..d0f8dc2b18c73e 100644 --- a/Libraries/BugReporting/BugReporting.js +++ b/Libraries/BugReporting/BugReporting.js @@ -8,13 +8,10 @@ * @flow strict-local */ -'use strict'; - -const RCTDeviceEventEmitter = require('../EventEmitter/RCTDeviceEventEmitter'); - -import type EmitterSubscription from '../vendor/emitter/EmitterSubscription'; -import NativeBugReporting from './NativeBugReporting'; +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; import NativeRedBox from '../NativeModules/specs/NativeRedBox'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; +import NativeBugReporting from './NativeBugReporting'; type ExtraData = {[key: string]: string, ...}; type SourceCallback = () => string; @@ -39,13 +36,14 @@ function defaultExtras() { class BugReporting { static _extraSources: Map = new Map(); static _fileSources: Map = new Map(); - static _subscription: ?EmitterSubscription = null; - static _redboxSubscription: ?EmitterSubscription = null; + static _subscription: ?EventSubscription = null; + static _redboxSubscription: ?EventSubscription = null; static _maybeInit() { if (!BugReporting._subscription) { BugReporting._subscription = RCTDeviceEventEmitter.addListener( 'collectBugExtraData', + // $FlowFixMe[method-unbinding] BugReporting.collectExtraData, null, ); @@ -55,6 +53,7 @@ class BugReporting { if (!BugReporting._redboxSubscription) { BugReporting._redboxSubscription = RCTDeviceEventEmitter.addListener( 'collectRedBoxExtraData', + // $FlowFixMe[method-unbinding] BugReporting.collectExtraData, null, ); diff --git a/Libraries/BugReporting/getReactData.js b/Libraries/BugReporting/getReactData.js index cc9bc55f0cc394..103f8ed86029ff 100644 --- a/Libraries/BugReporting/getReactData.js +++ b/Libraries/BugReporting/getReactData.js @@ -162,7 +162,7 @@ function copyWithSetImpl(obj, path, idx, value) { } const key = path[idx]; const updated = Array.isArray(obj) ? obj.slice() : {...obj}; - // $FlowFixMe number or string is fine here + // $FlowFixMe[incompatible-use] number or string is fine here updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value); return updated; } diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h deleted file mode 100644 index 6aec090ea92560..00000000000000 --- a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h +++ /dev/null @@ -1,15 +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 - -@class PHPhotoLibrary; - -@interface RCTAssetsLibraryRequestHandler : NSObject - -@end diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.mm b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.mm deleted file mode 100644 index 5da65489421aca..00000000000000 --- a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.mm +++ /dev/null @@ -1,172 +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 "RCTAssetsLibraryRequestHandler.h" - -#import -#import -#import -#import - -#import -#import - -#import -#import -#import -#import - -#import "RCTCameraRollPlugins.h" - -@interface RCTAssetsLibraryRequestHandler() -@end - -@implementation RCTAssetsLibraryRequestHandler - -RCT_EXPORT_MODULE() - -#pragma mark - RCTURLRequestHandler - -- (BOOL)canHandleRequest:(NSURLRequest *)request -{ - if (![PHAsset class]) { - return NO; - } - - return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame - || [request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame - || [request.URL.scheme caseInsensitiveCompare:RCTNetworkingPHUploadHackScheme] == NSOrderedSame; -} - -- (id)sendRequest:(NSURLRequest *)request - withDelegate:(id)delegate -{ - auto cancelled = std::make_shared>(false); - void (^cancellationBlock)(void) = ^{ - cancelled->store(true); - }; - - NSURL *requestURL = request.URL; - BOOL isPHUpload = [requestURL.scheme caseInsensitiveCompare:RCTNetworkingPHUploadHackScheme] == NSOrderedSame; - if (isPHUpload) { - requestURL = [NSURL URLWithString:[@"ph" stringByAppendingString:[requestURL.absoluteString substringFromIndex:RCTNetworkingPHUploadHackScheme.length]]]; - } - - if (!requestURL) { - NSString *const msg = [NSString stringWithFormat:@"Cannot send request without URL"]; - [delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)]; - return cancellationBlock; - } - - PHFetchResult *fetchResult; - - if ([requestURL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) { - // Fetch assets using PHAsset localIdentifier (recommended) - NSString *const localIdentifier = [requestURL.absoluteString substringFromIndex:@"ph://".length]; - fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil]; - } else if ([requestURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) { - // This is the older, deprecated way of fetching assets from assets-library - // using the "assets-library://" protocol - fetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[requestURL] options:nil]; - } else { - NSString *const msg = [NSString stringWithFormat:@"Cannot send request with unknown protocol: %@", requestURL]; - [delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)]; - return cancellationBlock; - } - - if (![fetchResult firstObject]) { - NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset" - " at URL %@ with no error message.", requestURL]; - NSError *error = RCTErrorWithMessage(errorMessage); - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - return cancellationBlock; - } - - if (cancelled->load()) { - return cancellationBlock; - } - - PHAsset *const _Nonnull asset = [fetchResult firstObject]; - - // When we're uploading a video, provide the full data but in any other case, - // provide only the thumbnail of the video. - if (asset.mediaType == PHAssetMediaTypeVideo && isPHUpload) { - PHVideoRequestOptions *const requestOptions = [PHVideoRequestOptions new]; - requestOptions.networkAccessAllowed = YES; - [[PHImageManager defaultManager] requestAVAssetForVideo:asset options:requestOptions resultHandler:^(AVAsset * _Nullable avAsset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) { - NSError *error = [info objectForKey:PHImageErrorKey]; - if (error) { - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - return; - } - - if (![avAsset isKindOfClass:[AVURLAsset class]]) { - error = [NSError errorWithDomain:RCTErrorDomain code:0 userInfo: - @{ - NSLocalizedDescriptionKey: @"Unable to load AVURLAsset", - }]; - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - return; - } - - NSData *data = [NSData dataWithContentsOfURL:((AVURLAsset *)avAsset).URL - options:(NSDataReadingOptions)0 - error:&error]; - if (data) { - NSURLResponse *const response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:nil expectedContentLength:data.length textEncodingName:nil]; - [delegate URLRequest:cancellationBlock didReceiveResponse:response]; - [delegate URLRequest:cancellationBlock didReceiveData:data]; - } - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - }]; - } else { - // By default, allow downloading images from iCloud - PHImageRequestOptions *const requestOptions = [PHImageRequestOptions new]; - requestOptions.networkAccessAllowed = YES; - - [[PHImageManager defaultManager] requestImageDataForAsset:asset - options:requestOptions - resultHandler:^(NSData * _Nullable imageData, - NSString * _Nullable dataUTI, - UIImageOrientation orientation, - NSDictionary * _Nullable info) { - NSError *const error = [info objectForKey:PHImageErrorKey]; - if (error) { - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - return; - } - - NSInteger const length = [imageData length]; - CFStringRef const dataUTIStringRef = (__bridge CFStringRef _Nonnull)(dataUTI); - CFStringRef const mimeType = UTTypeCopyPreferredTagWithClass(dataUTIStringRef, kUTTagClassMIMEType); - - NSURLResponse *const response = [[NSURLResponse alloc] initWithURL:request.URL - MIMEType:(__bridge NSString *)(mimeType) - expectedContentLength:length - textEncodingName:nil]; - CFRelease(mimeType); - - [delegate URLRequest:cancellationBlock didReceiveResponse:response]; - - [delegate URLRequest:cancellationBlock didReceiveData:imageData]; - [delegate URLRequest:cancellationBlock didCompleteWithError:nil]; - }]; - } - - return cancellationBlock; -} - -- (void)cancelRequest:(id)requestToken -{ - ((void (^)(void))requestToken)(); -} - -@end - -Class RCTAssetsLibraryRequestHandlerCls(void) { - return RCTAssetsLibraryRequestHandler.class; -} diff --git a/Libraries/CameraRoll/RCTCameraRollManager.h b/Libraries/CameraRoll/RCTCameraRollManager.h deleted file mode 100644 index 2c4e0d74d702b0..00000000000000 --- a/Libraries/CameraRoll/RCTCameraRollManager.h +++ /dev/null @@ -1,22 +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 - -@interface RCTConvert (PHFetchOptions) - -+ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType; - -@end - - -@interface RCTCameraRollManager : NSObject - -@end diff --git a/Libraries/CameraRoll/RCTCameraRollManager.mm b/Libraries/CameraRoll/RCTCameraRollManager.mm deleted file mode 100644 index f7480ea4914840..00000000000000 --- a/Libraries/CameraRoll/RCTCameraRollManager.mm +++ /dev/null @@ -1,361 +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 "RCTCameraRollManager.h" - -#import -#import -#import -#import -#import -#import -#import -#import - -#import -#import -#import -#import -#import - -#import "RCTCameraRollPlugins.h" -#import "RCTAssetsLibraryRequestHandler.h" - -@implementation RCTConvert (PHAssetCollectionSubtype) - -RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{ - @"album": @(PHAssetCollectionSubtypeAny), - @"all": @(PHAssetCollectionSubtypeSmartAlbumUserLibrary), - @"event": @(PHAssetCollectionSubtypeAlbumSyncedEvent), - @"faces": @(PHAssetCollectionSubtypeAlbumSyncedFaces), - @"library": @(PHAssetCollectionSubtypeSmartAlbumUserLibrary), - @"photo-stream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), // incorrect, but legacy - @"photostream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), - @"saved-photos": @(PHAssetCollectionSubtypeAny), // incorrect, but legacy - @"savedphotos": @(PHAssetCollectionSubtypeAny), // This was ALAssetsGroupSavedPhotos, seems to have no direct correspondence in PHAssetCollectionSubtype -}), PHAssetCollectionSubtypeAny, integerValue) - - -@end - -@implementation RCTConvert (PHFetchOptions) - -+ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType -{ - // This is not exhaustive in terms of supported media type predicates; more can be added in the future - NSString *const lowercase = [mediaType lowercaseString]; - - if ([lowercase isEqualToString:@"photos"]) { - PHFetchOptions *const options = [PHFetchOptions new]; - options.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeImage]; - return options; - } else if ([lowercase isEqualToString:@"videos"]) { - PHFetchOptions *const options = [PHFetchOptions new]; - options.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeVideo]; - return options; - } else { - if (![lowercase isEqualToString:@"all"]) { - RCTLogError(@"Invalid filter option: '%@'. Expected one of 'photos'," - "'videos' or 'all'.", mediaType); - } - // This case includes the "all" mediatype - return nil; - } -} - -@end - -@interface RCTCameraRollManager() -@end - -@implementation RCTCameraRollManager - -RCT_EXPORT_MODULE() - -@synthesize bridge = _bridge; - -static NSString *const kErrorUnableToSave = @"E_UNABLE_TO_SAVE"; -static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; - -static NSString *const kErrorAuthRestricted = @"E_PHOTO_LIBRARY_AUTH_RESTRICTED"; -static NSString *const kErrorAuthDenied = @"E_PHOTO_LIBRARY_AUTH_DENIED"; - -typedef void (^PhotosAuthorizedBlock)(void); - -static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthorizedBlock authorizedBlock) { - PHAuthorizationStatus authStatus = [PHPhotoLibrary authorizationStatus]; - if (authStatus == PHAuthorizationStatusRestricted) { - reject(kErrorAuthRestricted, @"Access to photo library is restricted", nil); - } else if (authStatus == PHAuthorizationStatusAuthorized) { - authorizedBlock(); - } else if (authStatus == PHAuthorizationStatusNotDetermined) { - [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - requestPhotoLibraryAccess(reject, authorizedBlock); - }]; - } else { - reject(kErrorAuthDenied, @"Access to photo library was denied", nil); - } -} - -RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request - type:(NSString *)type - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) -{ - __block PHObjectPlaceholder *placeholder; - - // We load images and videos differently. - // Images have many custom loaders which can load images from ALAssetsLibrary URLs, PHPhotoLibrary - // URLs, `data:` URIs, etc. Video URLs are passed directly through for now; it may be nice to support - // more ways of loading videos in the future. - __block NSURL *inputURI = nil; - __block UIImage *inputImage = nil; - - void (^saveBlock)(void) = ^void() { - // performChanges and the completionHandler are called on - // arbitrary threads, not the main thread - this is safe - // for now since all JS is queued and executed on a single thread. - // We should reevaluate this if that assumption changes. - [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ - PHAssetChangeRequest *changeRequest; - - // Defaults to "photo". `type` is an optional param. - if ([type isEqualToString:@"video"]) { - changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:inputURI]; - } else { - changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:inputImage]; - } - - placeholder = [changeRequest placeholderForCreatedAsset]; - } completionHandler:^(BOOL success, NSError * _Nullable error) { - if (success) { - NSString *uri = [NSString stringWithFormat:@"ph://%@", [placeholder localIdentifier]]; - resolve(uri); - } else { - reject(kErrorUnableToSave, nil, error); - } - }]; - }; - - void (^loadBlock)(void) = ^void() { - if ([type isEqualToString:@"video"]) { - inputURI = request.URL; - saveBlock(); - } else { - [[self.bridge moduleForClass:[RCTImageLoader class]] loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) { - if (error) { - reject(kErrorUnableToLoad, nil, error); - return; - } - - inputImage = image; - saveBlock(); - }]; - } - }; - - requestPhotoLibraryAccess(reject, loadBlock); -} - -static void RCTResolvePromise(RCTPromiseResolveBlock resolve, - NSArray *> *assets, - BOOL hasNextPage) -{ - if (!assets.count) { - resolve(@{ - @"edges": assets, - @"page_info": @{ - @"has_next_page": @NO, - } - }); - return; - } - resolve(@{ - @"edges": assets, - @"page_info": @{ - @"start_cursor": assets[0][@"node"][@"image"][@"uri"], - @"end_cursor": assets[assets.count - 1][@"node"][@"image"][@"uri"], - @"has_next_page": @(hasNextPage), - } - }); -} - -RCT_EXPORT_METHOD(getPhotos:(JS::NativeCameraRollManager::GetPhotosParams &)params - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) -{ - checkPhotoLibraryConfig(); - - NSUInteger const first = [RCTConvert NSInteger:[NSNumber numberWithDouble:params.first()]]; - NSString *const afterCursor = [RCTConvert NSString:params.after()]; - NSString *const groupName = [RCTConvert NSString:params.groupName()]; - NSString *const groupTypes = [[RCTConvert NSString:params.groupTypes()] lowercaseString]; - NSString *const mediaType = [RCTConvert NSString:params.assetType()]; - NSArray *const mimeTypes = [RCTConvert NSStringArray:RCTConvertOptionalVecToArray(params.mimeTypes())]; - - // If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all - // other groupTypes values require the "album" collection type. - PHAssetCollectionType const collectionType = ([groupTypes isEqualToString:@"all"] - ? PHAssetCollectionTypeSmartAlbum - : PHAssetCollectionTypeAlbum); - PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes]; - - // Predicate for fetching assets within a collection - PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType]; - assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; - - BOOL __block foundAfter = NO; - BOOL __block hasNextPage = NO; - BOOL __block resolvedPromise = NO; - NSMutableArray *> *assets = [NSMutableArray new]; - - // Filter collection name ("group") - PHFetchOptions *const collectionFetchOptions = [PHFetchOptions new]; - collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]]; - if (groupName != nil) { - collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:@"localizedTitle == '%@'", groupName]]; - } - - requestPhotoLibraryAccess(reject, ^{ - PHFetchResult *const assetCollectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithType:collectionType subtype:collectionSubtype options:collectionFetchOptions]; - [assetCollectionFetchResult enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull assetCollection, NSUInteger collectionIdx, BOOL * _Nonnull stopCollections) { - // Enumerate assets within the collection - PHFetchResult *const assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:assetFetchOptions]; - - [assetsFetchResult enumerateObjectsUsingBlock:^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) { - NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]]; - if (afterCursor && !foundAfter) { - if ([afterCursor isEqualToString:uri]) { - foundAfter = YES; - } - return; // skip until we get to the first one - } - - // Get underlying resources of an asset - this includes files as well as details about edited PHAssets - if ([mimeTypes count] > 0) { - NSArray *const assetResources = [PHAssetResource assetResourcesForAsset:asset]; - if (![assetResources firstObject]) { - return; - } - - PHAssetResource *const _Nonnull resource = [assetResources firstObject]; - CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier); - NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)); - - BOOL __block mimeTypeFound = NO; - [mimeTypes enumerateObjectsUsingBlock:^(NSString * _Nonnull mimeTypeFilter, NSUInteger idx, BOOL * _Nonnull stop) { - if ([mimeType isEqualToString:mimeTypeFilter]) { - mimeTypeFound = YES; - *stop = YES; - } - }]; - - if (!mimeTypeFound) { - return; - } - } - - // If we've accumulated enough results to resolve a single promise - if (first == assets.count) { - *stopAssets = YES; - *stopCollections = YES; - hasNextPage = YES; - RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results."); - RCTResolvePromise(resolve, assets, hasNextPage); - resolvedPromise = YES; - return; - } - - NSString *const assetMediaTypeLabel = (asset.mediaType == PHAssetMediaTypeVideo - ? @"video" - : (asset.mediaType == PHAssetMediaTypeImage - ? @"image" - : (asset.mediaType == PHAssetMediaTypeAudio - ? @"audio" - : @"unknown"))); - CLLocation *const loc = asset.location; - - // A note on isStored: in the previous code that used ALAssets, isStored - // was always set to YES, probably because iCloud-synced images were never returned (?). - // To get the "isStored" information and filename, we would need to actually request the - // image data from the image manager. Those operations could get really expensive and - // would definitely utilize the disk too much. - // Thus, this field is actually not reliable. - // Note that Android also does not return the `isStored` field at all. - [assets addObject:@{ - @"node": @{ - @"type": assetMediaTypeLabel, // TODO: switch to mimeType? - @"group_name": [assetCollection localizedTitle], - @"image": @{ - @"uri": uri, - @"height": @([asset pixelHeight]), - @"width": @([asset pixelWidth]), - @"isStored": @YES, // this field doesn't seem to exist on android - @"playableDuration": @([asset duration]) // fractional seconds - }, - @"timestamp": @(asset.creationDate.timeIntervalSince1970), - @"location": (loc ? @{ - @"latitude": @(loc.coordinate.latitude), - @"longitude": @(loc.coordinate.longitude), - @"altitude": @(loc.altitude), - @"heading": @(loc.course), - @"speed": @(loc.speed), // speed in m/s - } : @{}) - } - }]; - }]; - }]; - - // If we get this far and haven't resolved the promise yet, we reached the end of the list of photos - if (!resolvedPromise) { - hasNextPage = NO; - RCTResolvePromise(resolve, assets, hasNextPage); - resolvedPromise = YES; - } - }); -} - -RCT_EXPORT_METHOD(deletePhotos:(NSArray*)assets - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject) -{ - NSArray *assets_ = [RCTConvert NSURLArray:assets]; - [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ - PHFetchResult *fetched = - [PHAsset fetchAssetsWithALAssetURLs:assets_ options:nil]; - [PHAssetChangeRequest deleteAssets:fetched]; - } - completionHandler:^(BOOL success, NSError *error) { - if (success == YES) { - resolve(@(success)); - } - else { - reject(@"Couldn't delete", @"Couldn't delete assets", error); - } - } - ]; -} - -static void checkPhotoLibraryConfig() -{ -#if RCT_DEV - if (![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSPhotoLibraryUsageDescription"]) { - RCTLogError(@"NSPhotoLibraryUsageDescription key must be present in Info.plist to use camera roll."); - } -#endif -} - -- (std::shared_ptr)getTurboModuleWithJsInvoker:(std::shared_ptr)jsInvoker -{ - return std::make_shared(self, jsInvoker); -} - -@end - -Class RCTCameraRollManagerCls(void) { - return RCTCameraRollManager.class; -} diff --git a/Libraries/CameraRoll/RCTCameraRollPlugins.h b/Libraries/CameraRoll/RCTCameraRollPlugins.h deleted file mode 100644 index 0f4c987c3abc77..00000000000000 --- a/Libraries/CameraRoll/RCTCameraRollPlugins.h +++ /dev/null @@ -1,43 +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. - * - * @generated by an internal plugin build system - */ - -#ifdef RN_DISABLE_OSS_PLUGIN_HEADER - -// FB Internal: FBRCTCameraRollPlugins.h is autogenerated by the build system. -#import - -#else - -// OSS-compatibility layer - -#import - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wreturn-type-c-linkage" - -#ifdef __cplusplus -extern "C" { -#endif - -// RCTTurboModuleManagerDelegate should call this to resolve module classes. -Class RCTCameraRollClassProvider(const char *name); - -// Lookup functions -Class RCTAssetsLibraryRequestHandlerCls(void) __attribute__((used)); -Class RCTCameraRollManagerCls(void) __attribute__((used)); -Class RCTImagePickerManagerCls(void) __attribute__((used)); -Class RCTPhotoLibraryImageLoaderCls(void) __attribute__((used)); - -#ifdef __cplusplus -} -#endif - -#pragma GCC diagnostic pop - -#endif // RN_DISABLE_OSS_PLUGIN_HEADER diff --git a/Libraries/CameraRoll/RCTCameraRollPlugins.mm b/Libraries/CameraRoll/RCTCameraRollPlugins.mm deleted file mode 100644 index f2510796e0d2e5..00000000000000 --- a/Libraries/CameraRoll/RCTCameraRollPlugins.mm +++ /dev/null @@ -1,35 +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. - * - * @generated by an internal plugin build system - */ - -#ifndef RN_DISABLE_OSS_PLUGIN_HEADER - -// OSS-compatibility layer - -#import "RCTCameraRollPlugins.h" - -#import -#import - -Class RCTCameraRollClassProvider(const char *name) { - static std::unordered_map sCoreModuleClassMap = { - {"AssetsLibraryRequestHandler", RCTAssetsLibraryRequestHandlerCls}, - {"CameraRollManager", RCTCameraRollManagerCls}, - {"ImagePickerIOS", RCTImagePickerManagerCls}, - {"PhotoLibraryImageLoader", RCTPhotoLibraryImageLoaderCls}, - }; - - auto p = sCoreModuleClassMap.find(name); - if (p != sCoreModuleClassMap.end()) { - auto classFunc = p->second; - return classFunc(); - } - return nil; -} - -#endif // RN_DISABLE_OSS_PLUGIN_HEADER diff --git a/Libraries/CameraRoll/RCTImagePickerManager.h b/Libraries/CameraRoll/RCTImagePickerManager.h deleted file mode 100644 index d47fcda36ecbf8..00000000000000 --- a/Libraries/CameraRoll/RCTImagePickerManager.h +++ /dev/null @@ -1,12 +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 - -@interface RCTImagePickerManager : NSObject - -@end diff --git a/Libraries/CameraRoll/RCTImagePickerManager.mm b/Libraries/CameraRoll/RCTImagePickerManager.mm deleted file mode 100644 index 893a33cd3200bb..00000000000000 --- a/Libraries/CameraRoll/RCTImagePickerManager.mm +++ /dev/null @@ -1,261 +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 "RCTImagePickerManager.h" - -#import -#import -#import - -#import -#import -#import -#import - -#import "RCTCameraRollPlugins.h" - -@interface RCTImagePickerController : UIImagePickerController - -@property (nonatomic, assign) BOOL unmirrorFrontFacingCamera; - -@end - -@implementation RCTImagePickerController - -@end - -@interface RCTImagePickerManager () -@end - -@implementation RCTImagePickerManager -{ - NSMutableArray *_pickers; - NSMutableArray *_pickerCallbacks; - NSMutableArray *_pickerCancelCallbacks; - NSMutableDictionary *> *_pendingVideoInfo; -} - -RCT_EXPORT_MODULE(ImagePickerIOS); - -@synthesize bridge = _bridge; - -- (id)init -{ - if (self = [super init]) { - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(cameraChanged:) - name:@"AVCaptureDeviceDidStartRunningNotification" - object:nil]; - } - return self; -} - -+ (BOOL)requiresMainQueueSetup -{ - return NO; -} - -- (dispatch_queue_t)methodQueue -{ - return dispatch_get_main_queue(); -} - -RCT_EXPORT_METHOD(canRecordVideos:(RCTResponseSenderBlock)callback) -{ - NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; - callback(@[@([availableMediaTypes containsObject:(NSString *)kUTTypeMovie])]); -} - -RCT_EXPORT_METHOD(canUseCamera:(RCTResponseSenderBlock)callback) -{ - callback(@[@([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])]); -} - -RCT_EXPORT_METHOD(openCameraDialog:(JS::NativeImagePickerIOS::SpecOpenCameraDialogConfig &)config - successCallback:(RCTResponseSenderBlock)callback - cancelCallback:(RCTResponseSenderBlock)cancelCallback) -{ - if (RCTRunningInAppExtension()) { - cancelCallback(@[@"Camera access is unavailable in an app extension"]); - return; - } - - RCTImagePickerController *imagePicker = [RCTImagePickerController new]; - imagePicker.delegate = self; - imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; - NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; - imagePicker.mediaTypes = availableMediaTypes; - imagePicker.unmirrorFrontFacingCamera = config.unmirrorFrontFacingCamera() ? YES : NO; - - if (config.videoMode()) { - imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo; - } - - [self _presentPicker:imagePicker - successCallback:callback - cancelCallback:cancelCallback]; -} - -RCT_EXPORT_METHOD(openSelectDialog:(JS::NativeImagePickerIOS::SpecOpenSelectDialogConfig &)config - successCallback:(RCTResponseSenderBlock)callback - cancelCallback:(RCTResponseSenderBlock)cancelCallback) -{ - if (RCTRunningInAppExtension()) { - cancelCallback(@[@"Image picker is currently unavailable in an app extension"]); - return; - } - - UIImagePickerController *imagePicker = [UIImagePickerController new]; - imagePicker.delegate = self; - imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - - NSMutableArray *allowedTypes = [NSMutableArray new]; - if (config.showImages()) { - [allowedTypes addObject:(NSString *)kUTTypeImage]; - } - if (config.showVideos()) { - [allowedTypes addObject:(NSString *)kUTTypeMovie]; - } - - imagePicker.mediaTypes = allowedTypes; - - [self _presentPicker:imagePicker - successCallback:callback - cancelCallback:cancelCallback]; -} - -// In iOS 13, the URLs provided when selecting videos from the library are only valid while the -// info object provided by the delegate is retained. -// This method provides a way to clear out all retained pending info objects. -RCT_EXPORT_METHOD(clearAllPendingVideos) -{ - [_pendingVideoInfo removeAllObjects]; - _pendingVideoInfo = [NSMutableDictionary new]; -} - -// In iOS 13, the URLs provided when selecting videos from the library are only valid while the -// info object provided by the delegate is retained. -// This method provides a way to release the info object for a particular file url when the application -// is done with it, for example after the video has been uploaded or copied locally. -RCT_EXPORT_METHOD(removePendingVideo:(NSString *)url) -{ - [_pendingVideoInfo removeObjectForKey:url]; -} - -- (void)imagePickerController:(UIImagePickerController *)picker -didFinishPickingMediaWithInfo:(NSDictionary *)info -{ - NSString *mediaType = info[UIImagePickerControllerMediaType]; - BOOL isMovie = [mediaType isEqualToString:(NSString *)kUTTypeMovie]; - NSString *key = isMovie ? UIImagePickerControllerMediaURL : UIImagePickerControllerReferenceURL; - NSURL *imageURL = info[key]; - UIImage *image = info[UIImagePickerControllerOriginalImage]; - NSNumber *width = 0; - NSNumber *height = 0; - if (image) { - height = @(image.size.height); - width = @(image.size.width); - } - if (imageURL) { - NSString *imageURLString = imageURL.absoluteString; - // In iOS 13, video URLs are only valid while info dictionary is retained - if (@available(iOS 13.0, *)) { - if (isMovie) { - _pendingVideoInfo[imageURLString] = info; - } - } - - [self _dismissPicker:picker args:@[imageURLString, RCTNullIfNil(height), RCTNullIfNil(width)]]; - return; - } - - // This is a newly taken image, and doesn't have a URL yet. - // We need to save it to the image store first. - UIImage *originalImage = info[UIImagePickerControllerOriginalImage]; - - // WARNING: Using ImageStoreManager may cause a memory leak because the - // image isn't automatically removed from store once we're done using it. - [_bridge.imageStoreManager storeImage:originalImage withBlock:^(NSString *tempImageTag) { - [self _dismissPicker:picker args:tempImageTag ? @[tempImageTag, RCTNullIfNil(height), RCTNullIfNil(width)] : nil]; - }]; -} - -- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker -{ - [self _dismissPicker:picker args:nil]; -} - -- (void)_presentPicker:(UIImagePickerController *)imagePicker - successCallback:(RCTResponseSenderBlock)callback - cancelCallback:(RCTResponseSenderBlock)cancelCallback -{ - if (!_pickers) { - _pickers = [NSMutableArray new]; - _pickerCallbacks = [NSMutableArray new]; - _pickerCancelCallbacks = [NSMutableArray new]; - _pendingVideoInfo = [NSMutableDictionary new]; - } - - [_pickers addObject:imagePicker]; - [_pickerCallbacks addObject:callback]; - [_pickerCancelCallbacks addObject:cancelCallback]; - - UIViewController *rootViewController = RCTPresentedViewController(); - [rootViewController presentViewController:imagePicker animated:YES completion:nil]; -} - -- (void)_dismissPicker:(UIImagePickerController *)picker args:(NSArray *)args -{ - NSUInteger index = [_pickers indexOfObject:picker]; - if (index == NSNotFound) { - // This happens if the user selects multiple items in succession. - return; - } - - RCTResponseSenderBlock successCallback = _pickerCallbacks[index]; - RCTResponseSenderBlock cancelCallback = _pickerCancelCallbacks[index]; - - [_pickers removeObjectAtIndex:index]; - [_pickerCallbacks removeObjectAtIndex:index]; - [_pickerCancelCallbacks removeObjectAtIndex:index]; - - UIViewController *rootViewController = RCTPresentedViewController(); - [rootViewController dismissViewControllerAnimated:YES completion:nil]; - - if (args) { - successCallback(args); - } else { - cancelCallback(@[]); - } -} - -- (void)cameraChanged:(NSNotification *)notification -{ - for (UIImagePickerController *picker in _pickers) { - if (picker.sourceType != UIImagePickerControllerSourceTypeCamera) { - continue; - } - if ([picker isKindOfClass:[RCTImagePickerController class]] - && ((RCTImagePickerController *)picker).unmirrorFrontFacingCamera - && picker.cameraDevice == UIImagePickerControllerCameraDeviceFront) { - picker.cameraViewTransform = CGAffineTransformScale(CGAffineTransformIdentity, -1, 1); - } else { - picker.cameraViewTransform = CGAffineTransformIdentity; - } - } -} - -- (std::shared_ptr)getTurboModuleWithJsInvoker:(std::shared_ptr)jsInvoker -{ - return std::make_shared(self, jsInvoker); -} - -@end - -Class RCTImagePickerManagerCls(void) { - return RCTImagePickerManager.class; -} diff --git a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h deleted file mode 100644 index e4206a7842d1d7..00000000000000 --- a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h +++ /dev/null @@ -1,12 +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 - -@interface RCTPhotoLibraryImageLoader : NSObject - -@end diff --git a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.mm b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.mm deleted file mode 100644 index f489de1d4974f0..00000000000000 --- a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.mm +++ /dev/null @@ -1,121 +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 "RCTPhotoLibraryImageLoader.h" - -#import -#import -#import -#import - -#import "RCTCameraRollPlugins.h" - -@interface RCTPhotoLibraryImageLoader () -@end - -@implementation RCTPhotoLibraryImageLoader - -RCT_EXPORT_MODULE() - -@synthesize bridge = _bridge; - -#pragma mark - RCTImageLoader - -- (BOOL)canLoadImageURL:(NSURL *)requestURL -{ - if (![PHAsset class]) { - return NO; - } - return [requestURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame || - [requestURL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame; -} - -- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL - size:(CGSize)size - scale:(CGFloat)scale - resizeMode:(RCTResizeMode)resizeMode - progressHandler:(RCTImageLoaderProgressBlock)progressHandler - partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler - completionHandler:(RCTImageLoaderCompletionBlock)completionHandler -{ - // Using PhotoKit for iOS 8+ - // The 'ph://' prefix is used by FBMediaKit to differentiate between - // assets-library. It is prepended to the local ID so that it is in the - // form of an NSURL which is what assets-library uses. - NSString *assetID = @""; - PHFetchResult *results; - if (!imageURL) { - completionHandler(RCTErrorWithMessage(@"Cannot load a photo library asset with no URL"), nil); - return ^{}; - } else if ([imageURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) { - assetID = [imageURL absoluteString]; - results = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil]; - } else { - assetID = [imageURL.absoluteString substringFromIndex:@"ph://".length]; - results = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetID] options:nil]; - } - if (results.count == 0) { - NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", assetID]; - completionHandler(RCTErrorWithMessage(errorText), nil); - return ^{}; - } - - PHAsset *asset = [results firstObject]; - PHImageRequestOptions *imageOptions = [PHImageRequestOptions new]; - - // Allow PhotoKit to fetch images from iCloud - imageOptions.networkAccessAllowed = YES; - - if (progressHandler) { - imageOptions.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { - static const double multiplier = 1e6; - progressHandler(progress * multiplier, multiplier); - }; - } - - // Note: PhotoKit defaults to a deliveryMode of PHImageRequestOptionsDeliveryModeOpportunistic - // which means it may call back multiple times - we probably don't want that - imageOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; - - BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); - CGSize targetSize; - if (useMaximumSize) { - targetSize = PHImageManagerMaximumSize; - imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone; - } else { - targetSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale)); - imageOptions.resizeMode = PHImageRequestOptionsResizeModeFast; - } - - PHImageContentMode contentMode = PHImageContentModeAspectFill; - if (resizeMode == RCTResizeModeContain) { - contentMode = PHImageContentModeAspectFit; - } - - PHImageRequestID requestID = - [[PHImageManager defaultManager] requestImageForAsset:asset - targetSize:targetSize - contentMode:contentMode - options:imageOptions - resultHandler:^(UIImage *result, NSDictionary *info) { - if (result) { - completionHandler(nil, result); - } else { - completionHandler(info[PHImageErrorKey], nil); - } - }]; - - return ^{ - [[PHImageManager defaultManager] cancelImageRequest:requestID]; - }; -} - -@end - -Class RCTPhotoLibraryImageLoaderCls(void) { - return RCTPhotoLibraryImageLoader.class; -} diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js deleted file mode 100644 index 1840dfce692a0d..00000000000000 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js +++ /dev/null @@ -1,162 +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'; - -const RCTDeviceEventEmitter = require('../../EventEmitter/RCTDeviceEventEmitter'); -const UIManager = require('../../ReactNative/UIManager'); - -import NativeAccessibilityInfo from './NativeAccessibilityInfo'; - -const REDUCE_MOTION_EVENT = 'reduceMotionDidChange'; -const TOUCH_EXPLORATION_EVENT = 'touchExplorationDidChange'; - -type ChangeEventName = $Keys<{ - change: string, - reduceMotionChanged: string, - screenReaderChanged: string, - ... -}>; - -const _subscriptions = new Map(); - -/** - * Sometimes it's useful to know whether or not the device has a screen reader - * that is currently active. The `AccessibilityInfo` API is designed for this - * purpose. You can use it to query the current state of the screen reader as - * well as to register to be notified when the state of the screen reader - * changes. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html - */ - -const AccessibilityInfo = { - /** - * iOS only - */ - isBoldTextEnabled: function(): Promise { - return Promise.resolve(false); - }, - - /** - * iOS only - */ - isGrayscaleEnabled: function(): Promise { - return Promise.resolve(false); - }, - - /** - * iOS only - */ - isInvertColorsEnabled: function(): Promise { - return Promise.resolve(false); - }, - - isReduceMotionEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityInfo) { - NativeAccessibilityInfo.isReduceMotionEnabled(resolve); - } else { - reject(false); - } - }); - }, - - /** - * iOS only - */ - isReduceTransparencyEnabled: function(): Promise { - return Promise.resolve(false); - }, - - isScreenReaderEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityInfo) { - NativeAccessibilityInfo.isTouchExplorationEnabled(resolve); - } else { - reject(false); - } - }); - }, - - /** - * Deprecated - * - * Same as `isScreenReaderEnabled` - */ - get fetch(): () => Promise { - console.warn( - 'AccessibilityInfo.fetch is deprecated, call Accessibility.isScreenReaderEnabled instead', - ); - return this.isScreenReaderEnabled; - }, - - addEventListener: function( - eventName: ChangeEventName, - handler: Function, - ): void { - let listener; - - if (eventName === 'change' || eventName === 'screenReaderChanged') { - listener = RCTDeviceEventEmitter.addListener( - TOUCH_EXPLORATION_EVENT, - enabled => { - handler(enabled); - }, - ); - } else if (eventName === 'reduceMotionChanged') { - listener = RCTDeviceEventEmitter.addListener( - REDUCE_MOTION_EVENT, - enabled => { - handler(enabled); - }, - ); - } - - _subscriptions.set(handler, listener); - }, - - removeEventListener: function( - eventName: ChangeEventName, - handler: Function, - ): void { - const listener = _subscriptions.get(handler); - if (!listener) { - return; - } - listener.remove(); - _subscriptions.delete(handler); - }, - - /** - * Set accessibility focus to a react component. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#setaccessibilityfocus - */ - setAccessibilityFocus: function(reactTag: number): void { - UIManager.sendAccessibilityEvent( - reactTag, - UIManager.getConstants().AccessibilityEventTypes.typeViewFocused, - ); - }, - - /** - * Post a string to be announced by the screen reader. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#announceforaccessibility - */ - announceForAccessibility: function(announcement: string): void { - if (NativeAccessibilityInfo) { - NativeAccessibilityInfo.announceForAccessibility(announcement); - } - }, -}; - -module.exports = AccessibilityInfo; diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js deleted file mode 100644 index 4627b70d2ae003..00000000000000 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js +++ /dev/null @@ -1,271 +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'; - -const Promise = require('../../Promise'); -const RCTDeviceEventEmitter = require('../../EventEmitter/RCTDeviceEventEmitter'); - -import NativeAccessibilityManager from './NativeAccessibilityManager'; - -const CHANGE_EVENT_NAME = { - announcementFinished: 'announcementFinished', - boldTextChanged: 'boldTextChanged', - grayscaleChanged: 'grayscaleChanged', - invertColorsChanged: 'invertColorsChanged', - reduceMotionChanged: 'reduceMotionChanged', - reduceTransparencyChanged: 'reduceTransparencyChanged', - screenReaderChanged: 'screenReaderChanged', -}; - -type ChangeEventName = $Keys<{ - announcementFinished: string, - boldTextChanged: string, - change: string, - grayscaleChanged: string, - invertColorsChanged: string, - reduceMotionChanged: string, - reduceTransparencyChanged: string, - screenReaderChanged: string, - ... -}>; - -const _subscriptions = new Map(); - -/** - * Sometimes it's useful to know whether or not the device has a screen reader - * that is currently active. The `AccessibilityInfo` API is designed for this - * purpose. You can use it to query the current state of the screen reader as - * well as to register to be notified when the state of the screen reader - * changes. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html - */ -const AccessibilityInfo = { - /** - * Query whether bold text is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when bold text is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isBoldTextEnabled - */ - isBoldTextEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentBoldTextState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether grayscale is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when grayscale is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isGrayscaleEnabled - */ - isGrayscaleEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentGrayscaleState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether inverted colors are currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when invert color is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isInvertColorsEnabled - */ - isInvertColorsEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentInvertColorsState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether reduced motion is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a reduce motion is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isReduceMotionEnabled - */ - isReduceMotionEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentReduceMotionState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether reduced transparency is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a reduce transparency is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isReduceTransparencyEnabled - */ - isReduceTransparencyEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentReduceTransparencyState( - resolve, - reject, - ); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether a screen reader is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a screen reader is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isScreenReaderEnabled - */ - isScreenReaderEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentVoiceOverState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Deprecated - * - * Same as `isScreenReaderEnabled` - */ - get fetch(): $FlowFixMe { - console.warn( - 'AccessibilityInfo.fetch is deprecated, call Accessibility.isScreenReaderEnabled instead', - ); - return this.isScreenReaderEnabled; - }, - - /** - * Add an event handler. Supported events: - * - * - `boldTextChanged`: iOS-only event. Fires when the state of the bold text toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a bold text - * is enabled and `false` otherwise. - * - `grayscaleChanged`: iOS-only event. Fires when the state of the gray scale toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a gray scale - * is enabled and `false` otherwise. - * - `invertColorsChanged`: iOS-only event. Fires when the state of the invert colors toggle - * changes. The argument to the event handler is a boolean. The boolean is `true` when a invert - * colors is enabled and `false` otherwise. - * - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a reduce - * motion is enabled (or when "Transition Animation Scale" in "Developer options" is - * "Animation off") and `false` otherwise. - * - `reduceTransparencyChanged`: iOS-only event. Fires when the state of the reduce transparency - * toggle changes. The argument to the event handler is a boolean. The boolean is `true` - * when a reduce transparency is enabled and `false` otherwise. - * - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument - * to the event handler is a boolean. The boolean is `true` when a screen - * reader is enabled and `false` otherwise. - * - `announcementFinished`: iOS-only event. Fires when the screen reader has - * finished making an announcement. The argument to the event handler is a - * dictionary with these keys: - * - `announcement`: The string announced by the screen reader. - * - `success`: A boolean indicating whether the announcement was - * successfully made. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#addeventlistener - */ - addEventListener: function( - eventName: ChangeEventName, - handler: Function, - ): Object { - let listener; - - if (eventName === 'change') { - listener = RCTDeviceEventEmitter.addListener( - CHANGE_EVENT_NAME.screenReaderChanged, - handler, - ); - } else if (CHANGE_EVENT_NAME[eventName]) { - listener = RCTDeviceEventEmitter.addListener(eventName, handler); - } - - _subscriptions.set(handler, listener); - return { - remove: AccessibilityInfo.removeEventListener.bind( - null, - eventName, - handler, - ), - }; - }, - - /** - * Set accessibility focus to a react component. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#setaccessibilityfocus - */ - setAccessibilityFocus: function(reactTag: number): void { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.setAccessibilityFocus(reactTag); - } - }, - - /** - * Post a string to be announced by the screen reader. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#announceforaccessibility - */ - announceForAccessibility: function(announcement: string): void { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.announceForAccessibility(announcement); - } - }, - - /** - * Remove an event handler. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#removeeventlistener - */ - removeEventListener: function( - eventName: ChangeEventName, - handler: Function, - ): void { - const listener = _subscriptions.get(handler); - if (!listener) { - return; - } - listener.remove(); - _subscriptions.delete(handler); - }, -}; - -module.exports = AccessibilityInfo; diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js new file mode 100644 index 00000000000000..3933de41bc9d95 --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -0,0 +1,350 @@ +/** + * 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-local + * @format + */ + +import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; +import {sendAccessibilityEvent} from '../../Renderer/shims/ReactNative'; +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import Platform from '../../Utilities/Platform'; +import type EventEmitter from '../../vendor/emitter/EventEmitter'; +import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; +import NativeAccessibilityInfoAndroid from './NativeAccessibilityInfo'; +import NativeAccessibilityManagerIOS from './NativeAccessibilityManager'; +import legacySendAccessibilityEvent from './legacySendAccessibilityEvent'; +import type {ElementRef} from 'react'; + +// Events that are only supported on iOS. +type AccessibilityEventDefinitionsIOS = { + announcementFinished: [{announcement: string, success: boolean}], + boldTextChanged: [boolean], + grayscaleChanged: [boolean], + invertColorsChanged: [boolean], + reduceTransparencyChanged: [boolean], +}; + +type AccessibilityEventDefinitions = { + ...AccessibilityEventDefinitionsIOS, + change: [boolean], // screenReaderChanged + reduceMotionChanged: [boolean], + screenReaderChanged: [boolean], +}; + +type AccessibilityEventTypes = 'click' | 'focus'; + +// Mapping of public event names to platform-specific event names. +const EventNames: Map<$Keys, string> = + Platform.OS === 'android' + ? new Map([ + ['change', 'touchExplorationDidChange'], + ['reduceMotionChanged', 'reduceMotionDidChange'], + ['screenReaderChanged', 'touchExplorationDidChange'], + ]) + : new Map([ + ['announcementFinished', 'announcementFinished'], + ['boldTextChanged', 'boldTextChanged'], + ['change', 'screenReaderChanged'], + ['grayscaleChanged', 'grayscaleChanged'], + ['invertColorsChanged', 'invertColorsChanged'], + ['reduceMotionChanged', 'reduceMotionChanged'], + ['reduceTransparencyChanged', 'reduceTransparencyChanged'], + ['screenReaderChanged', 'screenReaderChanged'], + ]); + +/** + * Sometimes it's useful to know whether or not the device has a screen reader + * that is currently active. The `AccessibilityInfo` API is designed for this + * purpose. You can use it to query the current state of the screen reader as + * well as to register to be notified when the state of the screen reader + * changes. + * + * See https://reactnative.dev/docs/accessibilityinfo.html + */ +const AccessibilityInfo = { + /** + * Query whether bold text is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when bold text is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isBoldTextEnabled + */ + isBoldTextEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerIOS != null) { + NativeAccessibilityManagerIOS.getCurrentBoldTextState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } + }, + + /** + * Query whether grayscale is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when grayscale is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isGrayscaleEnabled + */ + isGrayscaleEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerIOS != null) { + NativeAccessibilityManagerIOS.getCurrentGrayscaleState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } + }, + + /** + * Query whether inverted colors are currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when invert color is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isInvertColorsEnabled + */ + isInvertColorsEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerIOS != null) { + NativeAccessibilityManagerIOS.getCurrentInvertColorsState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } + }, + + /** + * Query whether reduced motion is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when a reduce motion is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isReduceMotionEnabled + */ + isReduceMotionEnabled(): Promise { + return new Promise((resolve, reject) => { + if (Platform.OS === 'android') { + if (NativeAccessibilityInfoAndroid != null) { + NativeAccessibilityInfoAndroid.isReduceMotionEnabled(resolve); + } else { + reject(null); + } + } else { + if (NativeAccessibilityManagerIOS != null) { + NativeAccessibilityManagerIOS.getCurrentReduceMotionState( + resolve, + reject, + ); + } else { + reject(null); + } + } + }); + }, + + /** + * Query whether reduced transparency is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when a reduce transparency is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isReduceTransparencyEnabled + */ + isReduceTransparencyEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerIOS != null) { + NativeAccessibilityManagerIOS.getCurrentReduceTransparencyState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } + }, + + /** + * Query whether a screen reader is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when a screen reader is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isScreenReaderEnabled + */ + isScreenReaderEnabled(): Promise { + return new Promise((resolve, reject) => { + if (Platform.OS === 'android') { + if (NativeAccessibilityInfoAndroid != null) { + NativeAccessibilityInfoAndroid.isTouchExplorationEnabled(resolve); + } else { + reject(null); + } + } else { + if (NativeAccessibilityManagerIOS != null) { + NativeAccessibilityManagerIOS.getCurrentVoiceOverState( + resolve, + reject, + ); + } else { + reject(null); + } + } + }); + }, + + /** + * Add an event handler. Supported events: + * + * - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a reduce + * motion is enabled (or when "Transition Animation Scale" in "Developer options" is + * "Animation off") and `false` otherwise. + * - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument + * to the event handler is a boolean. The boolean is `true` when a screen + * reader is enabled and `false` otherwise. + * + * These events are only supported on iOS: + * + * - `boldTextChanged`: iOS-only event. Fires when the state of the bold text toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a bold text + * is enabled and `false` otherwise. + * - `grayscaleChanged`: iOS-only event. Fires when the state of the gray scale toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a gray scale + * is enabled and `false` otherwise. + * - `invertColorsChanged`: iOS-only event. Fires when the state of the invert colors toggle + * changes. The argument to the event handler is a boolean. The boolean is `true` when a invert + * colors is enabled and `false` otherwise. + * - `reduceTransparencyChanged`: iOS-only event. Fires when the state of the reduce transparency + * toggle changes. The argument to the event handler is a boolean. The boolean is `true` + * when a reduce transparency is enabled and `false` otherwise. + * - `announcementFinished`: iOS-only event. Fires when the screen reader has + * finished making an announcement. The argument to the event handler is a + * dictionary with these keys: + * - `announcement`: The string announced by the screen reader. + * - `success`: A boolean indicating whether the announcement was + * successfully made. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#addeventlistener + */ + addEventListener>( + eventName: K, + handler: (...$ElementType) => void, + ): EventSubscription { + const deviceEventName = EventNames.get(eventName); + return deviceEventName == null + ? {remove(): void {}} + : RCTDeviceEventEmitter.addListener(deviceEventName, handler); + }, + + /** + * Set accessibility focus to a React component. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#setaccessibilityfocus + */ + setAccessibilityFocus(reactTag: number): void { + legacySendAccessibilityEvent(reactTag, 'focus'); + }, + + /** + * Send a named accessibility event to a HostComponent. + */ + sendAccessibilityEvent_unstable( + handle: ElementRef>, + eventType: AccessibilityEventTypes, + ) { + // iOS only supports 'focus' event types + if (Platform.OS === 'ios' && eventType === 'click') { + return; + } + // route through React renderer to distinguish between Fabric and non-Fabric handles + sendAccessibilityEvent(handle, eventType); + }, + + /** + * Post a string to be announced by the screen reader. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#announceforaccessibility + */ + announceForAccessibility(announcement: string): void { + if (Platform.OS === 'android') { + NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement); + } else { + NativeAccessibilityManagerIOS?.announceForAccessibility(announcement); + } + }, + + /** + * @deprecated Use `remove` on the EventSubscription from `addEventListener`. + */ + removeEventListener>( + eventName: K, + handler: (...$ElementType) => void, + ): void { + // NOTE: This will report a deprecation notice via `console.error`. + const deviceEventName = EventNames.get(eventName); + if (deviceEventName != null) { + // $FlowIgnore[incompatible-cast] + (RCTDeviceEventEmitter: EventEmitter<$FlowFixMe>).removeListener( + 'deviceEventName', + // $FlowFixMe[invalid-tuple-arity] + handler, + ); + } + }, + + /** + * Get the recommended timeout for changes to the UI needed by this user. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#getrecommendedtimeoutmillis + */ + getRecommendedTimeoutMillis(originalTimeout: number): Promise { + if (Platform.OS === 'android') { + return new Promise((resolve, reject) => { + if (NativeAccessibilityInfoAndroid?.getRecommendedTimeoutMillis) { + NativeAccessibilityInfoAndroid.getRecommendedTimeoutMillis( + originalTimeout, + resolve, + ); + } else { + resolve(originalTimeout); + } + }); + } else { + return Promise.resolve(originalTimeout); + } + }, +}; + +export default AccessibilityInfo; diff --git a/Libraries/Components/AccessibilityInfo/NativeAccessibilityInfo.js b/Libraries/Components/AccessibilityInfo/NativeAccessibilityInfo.js index 10aa0f110cdb20..916667aa5c2c0a 100644 --- a/Libraries/Components/AccessibilityInfo/NativeAccessibilityInfo.js +++ b/Libraries/Components/AccessibilityInfo/NativeAccessibilityInfo.js @@ -4,12 +4,10 @@ * 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 + * @flow strict * @format */ -'use strict'; - import type {TurboModule} from '../../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; @@ -22,6 +20,10 @@ export interface Spec extends TurboModule { ) => void; +setAccessibilityFocus: (reactTag: number) => void; +announceForAccessibility: (announcement: string) => void; + +getRecommendedTimeoutMillis?: ( + mSec: number, + onSuccess: (recommendedTimeoutMillis: number) => void, + ) => void; } export default (TurboModuleRegistry.get('AccessibilityInfo'): ?Spec); diff --git a/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js b/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js index 0a1abe5792492b..9ee2403e372b91 100644 --- a/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js +++ b/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js new file mode 100644 index 00000000000000..3bb1c554950d50 --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js @@ -0,0 +1,35 @@ +/** + * 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 + */ + +import UIManager from '../../ReactNative/UIManager'; + +/** + * This is a function exposed to the React Renderer that can be used by the + * pre-Fabric renderer to emit accessibility events to pre-Fabric nodes. + */ +function legacySendAccessibilityEvent( + reactTag: number, + eventType: string, +): void { + if (eventType === 'focus') { + UIManager.sendAccessibilityEvent( + reactTag, + UIManager.getConstants().AccessibilityEventTypes.typeViewFocused, + ); + } + if (eventType === 'click') { + UIManager.sendAccessibilityEvent( + reactTag, + UIManager.getConstants().AccessibilityEventTypes.typeViewClicked, + ); + } +} + +module.exports = legacySendAccessibilityEvent; diff --git a/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js new file mode 100644 index 00000000000000..950336d1101c44 --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js @@ -0,0 +1,26 @@ +/** + * 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 + */ + +import NativeAccessibilityManager from './NativeAccessibilityManager'; + +/** + * This is a function exposed to the React Renderer that can be used by the + * pre-Fabric renderer to emit accessibility events to pre-Fabric nodes. + */ +function legacySendAccessibilityEvent( + reactTag: number, + eventType: string, +): void { + if (eventType === 'focus' && NativeAccessibilityManager) { + NativeAccessibilityManager.setAccessibilityFocus(reactTag); + } +} + +module.exports = legacySendAccessibilityEvent; diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/Libraries/Components/ActivityIndicator/ActivityIndicator.js index 6026cf16737b88..9100cf6483de2a 100644 --- a/Libraries/Components/ActivityIndicator/ActivityIndicator.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicator.js @@ -6,14 +6,14 @@ * * @format * @flow + * @generate-docs */ 'use strict'; - -const Platform = require('../../Utilities/Platform'); -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); -const View = require('../View/View'); +import * as React from 'react'; +import Platform from '../../Utilities/Platform'; +import StyleSheet, {type ColorValue} from '../../StyleSheet/StyleSheet'; +import View from '../View/View'; import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; @@ -28,10 +28,10 @@ type IndicatorSize = number | 'small' | 'large'; type IOSProps = $ReadOnly<{| /** - * Whether the indicator should hide when not animating (true by default). - * - * See http://facebook.github.io/react-native/docs/activityindicator.html#hideswhenstopped - */ + Whether the indicator should hide when not animating. + + @platform ios + */ hidesWhenStopped?: ?boolean, |}>; type Props = $ReadOnly<{| @@ -39,35 +39,39 @@ type Props = $ReadOnly<{| ...IOSProps, /** - * Whether to show the indicator (true, the default) or hide it (false). - * - * See http://facebook.github.io/react-native/docs/activityindicator.html#animating + Whether to show the indicator (`true`) or hide it (`false`). */ animating?: ?boolean, /** - * The foreground color of the spinner (default is gray). - * - * See http://facebook.github.io/react-native/docs/activityindicator.html#color - */ - color?: ?string, + The foreground color of the spinner. + + @default {@platform android} `null` (system accent default color) + @default {@platform ios} '#999999' + */ + color?: ?ColorValue, /** - * Size of the indicator (default is 'small'). - * Passing a number to the size prop is only supported on Android. - * - * See http://facebook.github.io/react-native/docs/activityindicator.html#size - */ + Size of the indicator. + + @type enum(`'small'`, `'large'`) + @type {@platform android} number + */ size?: ?IndicatorSize, |}>; -/** - * Displays a circular loading indicator. - * - * See http://facebook.github.io/react-native/docs/activityindicator.html - */ -const ActivityIndicator = (props: Props, forwardedRef?: any) => { - const {onLayout, style, size, ...restProps} = props; +const ActivityIndicator = ( + { + animating = true, + color = Platform.OS === 'ios' ? GRAY : null, + hidesWhenStopped = true, + onLayout, + size = 'small', + style, + ...restProps + }: Props, + forwardedRef?: any, +) => { let sizeStyle; let sizeProp; @@ -81,11 +85,14 @@ const ActivityIndicator = (props: Props, forwardedRef?: any) => { sizeProp = 'large'; break; default: - sizeStyle = {height: props.size, width: props.size}; + sizeStyle = {height: size, width: size}; break; } const nativeProps = { + animating, + color, + hidesWhenStopped, ...restProps, ref: forwardedRef, style: sizeStyle, @@ -100,15 +107,12 @@ const ActivityIndicator = (props: Props, forwardedRef?: any) => { return ( + style={StyleSheet.compose(styles.container, style)}> {Platform.OS === 'android' ? ( - // $FlowFixMe Flow doesn't know when this is the android component + // $FlowFixMe[prop-missing] Flow doesn't know when this is the android component ) : ( - /* $FlowFixMe(>=0.106.0 site=react_native_android_fb) This comment + /* $FlowFixMe[prop-missing] (>=0.106.0 site=react_native_android_fb) This comment * suppresses an error found when Flow v0.106 was deployed. To see the * error, delete this comment and run Flow. */ @@ -117,22 +121,74 @@ const ActivityIndicator = (props: Props, forwardedRef?: any) => { ); }; +/** + Displays a circular loading indicator. + + ```SnackPlayer name=ActivityIndicator%20Function%20Component%20Example + import React from "react"; + import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; + + const App = () => ( + + + + + + + ); + + const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center" + }, + horizontal: { + flexDirection: "row", + justifyContent: "space-around", + padding: 10 + } + }); + export default App; + ``` + + ```SnackPlayer name=ActivityIndicator%20Class%20Component%20Example + import React, { Component } from "react"; + import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; + + class App extends Component { + render() { + return ( + + + + + + + ); + } + } + + const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center" + }, + horizontal: { + flexDirection: "row", + justifyContent: "space-around", + padding: 10 + } + }); + export default App; + ``` +*/ + const ActivityIndicatorWithRef: React.AbstractComponent< Props, HostComponent, > = React.forwardRef(ActivityIndicator); ActivityIndicatorWithRef.displayName = 'ActivityIndicator'; -/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an error - * found when Flow v0.89 was deployed. To see the error, delete this comment - * and run Flow. */ -ActivityIndicatorWithRef.defaultProps = { - animating: true, - color: Platform.OS === 'ios' ? GRAY : null, - hidesWhenStopped: true, - size: 'small', -}; - const styles = StyleSheet.create({ container: { alignItems: 'center', diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicatorViewNativeComponent.js b/Libraries/Components/ActivityIndicator/ActivityIndicatorViewNativeComponent.js index a91460f31fed94..3475cebeddeabd 100644 --- a/Libraries/Components/ActivityIndicator/ActivityIndicatorViewNativeComponent.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicatorViewNativeComponent.js @@ -8,11 +8,9 @@ * @flow strict-local */ -'use strict'; - import type {WithDefault} from '../../Types/CodegenTypes'; -import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; +import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {ViewProps} from '../View/ViewPropTypes'; import codegenNativeComponent from '../../Utilities/codegenNativeComponent'; @@ -24,21 +22,21 @@ type NativeProps = $ReadOnly<{| /** * Whether the indicator should hide when not animating (true by default). * - * See http://facebook.github.io/react-native/docs/activityindicator.html#hideswhenstopped + * See https://reactnative.dev/docs/activityindicator.html#hideswhenstopped */ hidesWhenStopped?: WithDefault, /** * Whether to show the indicator (true, the default) or hide it (false). * - * See http://facebook.github.io/react-native/docs/activityindicator.html#animating + * See https://reactnative.dev/docs/activityindicator.html#animating */ animating?: WithDefault, /** * The foreground color of the spinner (default is gray). * - * See http://facebook.github.io/react-native/docs/activityindicator.html#color + * See https://reactnative.dev/docs/activityindicator.html#color */ color?: ?ColorValue, @@ -46,7 +44,7 @@ type NativeProps = $ReadOnly<{| * Size of the indicator (default is 'small'). * Passing a number to the size prop is only supported on Android. * - * See http://facebook.github.io/react-native/docs/activityindicator.html#size + * See https://reactnative.dev/docs/activityindicator.html#size */ size?: WithDefault<'small' | 'large', 'small'>, |}>; diff --git a/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap b/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap index 1d6d1e70b30ea6..4326d7f8ae20f3 100644 --- a/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap +++ b/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap @@ -2,9 +2,7 @@ exports[` should render as expected: should deep render when mocked (please verify output manually) 1`] = ` `; @@ -34,19 +32,15 @@ exports[` should render as expected: should deep render whe `; exports[` should render as expected: should shallow render as when mocked 1`] = ` - `; exports[` should render as expected: should shallow render as when not mocked 1`] = ` - `; diff --git a/Libraries/Components/AppleTV/NativeTVNavigationEventEmitter.js b/Libraries/Components/AppleTV/NativeTVNavigationEventEmitter.js deleted file mode 100644 index 3cecc6e84e7198..00000000000000 --- a/Libraries/Components/AppleTV/NativeTVNavigationEventEmitter.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. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import type {TurboModule} from '../../TurboModule/RCTExport'; -import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; - -export interface Spec extends TurboModule { - +addListener: (eventName: string) => void; - +removeListeners: (count: number) => void; -} - -export default (TurboModuleRegistry.get( - 'TVNavigationEventEmitter', -): ?Spec); diff --git a/Libraries/Components/AppleTV/TVEventHandler.js b/Libraries/Components/AppleTV/TVEventHandler.js deleted file mode 100644 index d5cd47afdce59f..00000000000000 --- a/Libraries/Components/AppleTV/TVEventHandler.js +++ /dev/null @@ -1,52 +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'; - -const Platform = require('../../Utilities/Platform'); -const NativeEventEmitter = require('../../EventEmitter/NativeEventEmitter'); - -import NativeTVNavigationEventEmitter from './NativeTVNavigationEventEmitter'; -import type EmitterSubscription from '../../vendor/emitter/EmitterSubscription'; - -class TVEventHandler { - __nativeTVNavigationEventListener: ?EmitterSubscription = 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/Button.js b/Libraries/Components/Button.js index 6a2b8244f27266..d598a499f25325 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -6,125 +6,256 @@ * * @format * @flow + * @generate-docs */ 'use strict'; -const Platform = require('../Utilities/Platform'); -const React = require('react'); -const StyleSheet = require('../StyleSheet/StyleSheet'); -const Text = require('../Text/Text'); -const TouchableNativeFeedback = require('./Touchable/TouchableNativeFeedback'); -const TouchableOpacity = require('./Touchable/TouchableOpacity'); -const View = require('./View/View'); - -const invariant = require('invariant'); +import * as React from 'react'; +import Platform from '../Utilities/Platform'; +import StyleSheet, {type ColorValue} from '../StyleSheet/StyleSheet'; +import Text from '../Text/Text'; +import TouchableNativeFeedback from './Touchable/TouchableNativeFeedback'; +import TouchableOpacity from './Touchable/TouchableOpacity'; +import View from './View/View'; +import invariant from 'invariant'; +import type { + AccessibilityState, + AccessibilityActionEvent, + AccessibilityActionInfo, +} from './View/ViewAccessibility'; import type {PressEvent} from '../Types/CoreEventTypes'; type ButtonProps = $ReadOnly<{| /** - * Text to display inside the button + Text to display inside the button. On Android the given title will be + converted to the uppercased form. */ title: string, /** - * Handler to be called when the user taps the button + Handler to be called when the user taps the button. The first function + argument is an event in form of [PressEvent](pressevent). */ onPress: (event?: PressEvent) => mixed, /** - * If true, doesn't play system sound on touch (Android Only) - **/ + If `true`, doesn't play system sound on touch. + + @platform android + + @default false + */ touchSoundDisabled?: ?boolean, /** - * Color of the text (iOS), or background color of the button (Android) + Color of the text (iOS), or background color of the button (Android). + + @default {@platform android} '#2196F3' + @default {@platform ios} '#007AFF' */ - color?: ?string, + color?: ?ColorValue, /** - * TV preferred focus (see documentation for the View component). + TV preferred focus. + + @platform tv + + @default false */ hasTVPreferredFocus?: ?boolean, /** - * TV next focus down (see documentation for the View component). - * - * @platform android + Designates the next view to receive focus when the user navigates down. See + the [Android documentation][android:nextFocusDown]. + + [android:nextFocusDown]: + https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusDown + + @platform android, tv */ nextFocusDown?: ?number, /** - * TV next focus forward (see documentation for the View component). - * - * @platform android + Designates the next view to receive focus when the user navigates forward. + See the [Android documentation][android:nextFocusForward]. + + [android:nextFocusForward]: + https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusForward + + @platform android, tv */ nextFocusForward?: ?number, /** - * TV next focus left (see documentation for the View component). - * - * @platform android + Designates the next view to receive focus when the user navigates left. See + the [Android documentation][android:nextFocusLeft]. + + [android:nextFocusLeft]: + https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusLeft + + @platform android, tv */ nextFocusLeft?: ?number, /** - * TV next focus right (see documentation for the View component). - * - * @platform android + Designates the next view to receive focus when the user navigates right. See + the [Android documentation][android:nextFocusRight]. + + [android:nextFocusRight]: + https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusRight + + @platform android, tv */ nextFocusRight?: ?number, /** - * TV next focus up (see documentation for the View component). - * - * @platform android + Designates the next view to receive focus when the user navigates up. See + the [Android documentation][android:nextFocusUp]. + + [android:nextFocusUp]: + https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusUp + + @platform android, tv */ nextFocusUp?: ?number, /** - * Text to display for blindness accessibility features + Text to display for blindness accessibility features. */ accessibilityLabel?: ?string, /** - * If true, disable all interactions for this component. + If `true`, disable all interactions for this component. + + @default false */ disabled?: ?boolean, /** - * Used to locate this view in end-to-end tests. + Used to locate this view in end-to-end tests. */ testID?: ?string, + + /** + * Accessibility props. + */ + accessible?: ?boolean, + accessibilityActions?: ?$ReadOnlyArray, + onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, + accessibilityState?: ?AccessibilityState, |}>; /** - * A basic button component that should render nicely on any platform. Supports - * a minimal level of customization. - * - *
- * - * If this button doesn't look right for your app, you can build your own - * button using [TouchableOpacity](docs/touchableopacity.html) - * or [TouchableNativeFeedback](docs/touchablenativefeedback.html). - * For inspiration, look at the [source code for this button component](https://github.com/facebook/react-native/blob/master/Libraries/Components/Button.js). - * Or, take a look at the [wide variety of button components built by the community](https://js.coach/react-native?search=button). - * - * Example usage: - * - * ``` - * import { Button } from 'react-native'; - * ... - * - *