diff --git a/.appveyor/config.yml b/.appveyor/config.yml index 79e38b1bd495e8..3753f3eba4783a 100644 --- a/.appveyor/config.yml +++ b/.appveyor/config.yml @@ -2,7 +2,7 @@ environment: ANDROID_HOME: "C:\\android-sdk-windows" ANDROID_NDK: "C:\\android-sdk-windows\\android-ndk-r17c" ANDROID_BUILD_VERSION: 28 - ANDROID_TOOLS_VERSION: 28.0.2 + ANDROID_TOOLS_VERSION: 28.0.3 GRADLE_OPTS: -Dorg.gradle.daemon=false @@ -21,11 +21,11 @@ install: - set PATH=%PATH%;"%ANDROID_HOME%\tools\bin" - yes 2> nul | sdkmanager --licenses > nul - - sdkmanager "system-images;android-19;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" + - 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 diff --git a/.circleci/config.yml b/.circleci/config.yml index 905966fa5094fb..a230fee2ea99a9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,14 +26,6 @@ aliases: - node_modules key: v1-analysis-dependencies-{{ arch }}-{{ checksum "package.json" }}{{ checksum "bots/package.json" }} - - &restore-cache-android-packages - keys: - - v1-android-sdkmanager-packages-api-28-alpha-{{ checksum "scripts/.tests.env" }} - - &save-cache-android-packages - paths: - - /opt/android/sdk - key: v1-android-sdkmanager-packages-api-28-alpha-{{ checksum "scripts/.tests.env" }} - - &restore-cache-gradle keys: - v1-gradle-{{ .Branch }}-{{ checksum "build.gradle" }}-{{ checksum "ReactAndroid/build.gradle" }} @@ -47,23 +39,15 @@ aliases: - ~/.gradle key: v1-gradle-{{ .Branch }}-{{ checksum "build.gradle" }}-{{ checksum "ReactAndroid/build.gradle" }} - - &restore-cache-ndk - keys: - - v3-android-ndk-r17c-{{ checksum "scripts/android-setup.sh" }} - - &save-cache-ndk - paths: - - /opt/ndk - key: v3-android-ndk-r17c-{{ checksum "scripts/android-setup.sh" }} - - &restore-cache-downloads-buck keys: - - v3-buck-v2018.10.29.01-{{ checksum "scripts/circleci/buck_fetch.sh" }}} - - v3-buck-v2018.10.29.01- + - v3-buck-v2019.01.10.01-{{ checksum "scripts/circleci/buck_fetch.sh" }}} + - v3-buck-v2019.01.10.01- - &save-cache-downloads-buck paths: - ~/buck - ~/okbuck - key: v3-buck-v2018.10.29.01-{{ checksum "scripts/circleci/buck_fetch.sh" }} + key: v3-buck-v2019.01.10.01-{{ checksum "scripts/circleci/buck_fetch.sh" }} - &restore-cache-watchman keys: @@ -115,11 +99,6 @@ aliases: - /.*-stable/ - gh-pages - # Dependency Management - - &install-ndk - name: Install Android NDK - command: source scripts/android-setup.sh && getAndroidNDK - - &yarn name: Run Yarn command: | @@ -129,20 +108,9 @@ aliases: yarn install --non-interactive --cache-folder ~/.cache/yarn fi - - &install-yarn - name: Install Yarn - command: | - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - sudo apt-get update && sudo apt-get install yarn - - &install-buck name: Install BUCK command: | - if [[ ! -e ~/buck ]]; then - git clone https://github.com/facebook/buck.git ~/buck --branch v2018.10.29.01 --depth=1 - fi - cd ~/buck && ant buck --version # Install related tooling if [[ ! -e ~/okbuck ]]; then @@ -150,31 +118,6 @@ aliases: fi mkdir -p ~/react-native/tooling/junit cp -R ~/okbuck/tooling/junit/* ~/react-native/tooling/junit/. - - - &create-ndk-directory - name: Create Android NDK Directory - command: | - if [[ ! -e /opt/ndk ]]; then - sudo mkdir /opt/ndk - fi - sudo chown ${USER:=$(/usr/bin/id -run)}:$USER /opt/ndk - - # CircleCI does not support interpolating env variables in the environment - # https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables - - &configure-android-path - name: Configure Environment Variables - command: | - echo 'export PATH=${ANDROID_NDK}:~/buck/bin:$PATH' >> $BASH_ENV - source $BASH_ENV - - - &install-android-packages - name: Install Android SDK Packages - command: source scripts/android-setup.sh && getAndroidPackages - - - &install-android-build-dependencies - name: Install Android Build Dependencies - command: ./scripts/circleci/apt-get-android-deps.sh - - &validate-android-sdk name: Validate Android SDK Install command: ./scripts/validate-android-sdk.sh @@ -248,7 +191,7 @@ aliases: - &compile-native-libs name: Compile Native Libs for Unit and Integration Tests - command: ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=$BUILD_THREADS -Pcom.android.build.threadPoolSize=1 + command: ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=$BUILD_THREADS no_output_timeout: 6m - &run-android-unit-tests @@ -265,7 +208,7 @@ aliases: - &build-android-rntester-app name: Build Android RNTester App - command: ./gradlew RNTester:android:app:assembleRelease -Pjobs=$BUILD_THREADS + command: ./gradlew RNTester:android:app:assembleRelease - &collect-android-test-results name: Collect Test Results @@ -340,14 +283,13 @@ js_defaults: &js_defaults android_defaults: &android_defaults <<: *defaults docker: - - image: circleci/android:api-28-node8-alpha + - image: reactnativecommunity/react-native-android resource_class: "large" environment: - TERM: "dumb" - ADB_INSTALL_TIMEOUT: 10 - _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" - GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-XX:+HeapDumpOnOutOfMemoryError"' - - ANDROID_NDK: '/opt/ndk/android-ndk-r17c' - BUILD_THREADS: 2 macos_defaults: &macos_defaults @@ -472,15 +414,6 @@ jobs: - attach_workspace: at: ~/react-native - # Configure Android SDK and related dependencies - - run: *configure-android-path - # Android build deps install from the network faster than cache - - run: *install-android-build-dependencies - - - restore-cache: *restore-cache-android-packages - - run: *install-android-packages - - save-cache: *save-cache-android-packages - # Validate Android SDK installation and packages - run: *validate-android-sdk @@ -490,12 +423,6 @@ jobs: # Keep configuring Android dependencies while AVD boots up - # Install Android NDK - - run: *create-ndk-directory - - restore-cache: *restore-cache-ndk - - run: *install-ndk - - save-cache: *save-cache-ndk - # Install Buck - restore-cache: *restore-cache-downloads-buck - run: *install-buck @@ -623,21 +550,8 @@ jobs: - restore-cache: *restore-yarn-cache - run: *yarn - # Configure Android SDK and related dependencies - - run: *configure-android-path - - run: *install-android-build-dependencies - - - restore-cache: *restore-cache-android-packages - - run: *install-android-packages - - # Install Android NDK - - run: *create-ndk-directory - - restore-cache: *restore-cache-ndk - - run: *install-ndk - # Fetch dependencies using Buck - restore-cache: *restore-cache-downloads-buck - - run: *install-buck - run: *download-dependencies-buck # Fetch dependencies using Gradle diff --git a/.flowconfig b/.flowconfig index c4ff4b76b7ab6e..e70c184db7c04c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -26,6 +26,9 @@ ; require from fbjs/lib instead: require('fbjs/lib/warning') .*/node_modules/warning/.* +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* + [include] [libs] @@ -85,6 +88,7 @@ sketchy-null-mixed=warn # This is noisy for now. We *do* still want to warn on importing types # from untyped files, which is covered by untyped-type-import untyped-import=off +deprecated-utility=error [strict] deprecated-type @@ -96,4 +100,4 @@ untyped-import untyped-type-import [version] -^0.89.0 +^0.92.0 diff --git a/.flowconfig.android b/.flowconfig.android index 53165f2fa3a8ac..3eae482e141c2b 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -26,6 +26,9 @@ ; require from fbjs/lib instead: require('fbjs/lib/warning') .*/node_modules/warning/.* +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* + [include] [libs] @@ -85,6 +88,7 @@ sketchy-null-mixed=warn # This is noisy for now. We *do* still want to warn on importing types # from untyped files, which is covered by untyped-type-import untyped-import=off +deprecated-utility=error [strict] deprecated-type @@ -96,4 +100,4 @@ untyped-import untyped-type-import [version] -^0.89.0 +^0.92.0 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d31f3a36ccac03..3a342453133dc8 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,4 @@ -GitHub Issues in the `facebook/react-native` repository are used exclusively for tracking bugs in React Native. +👉 Please follow one of these issue templates: +- https://github.com/facebook/react-native/issues/new/choose -Please take a look at the issue templates at https://github.com/facebook/react-native/issues/new/choose before submitting a new issue. Following one of the issue templates will ensure maintainers can route your request efficiently. Thanks! +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 85c66cde5f6281..d3f49e39442f33 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,18 +1,32 @@ --- -name: 🐛 Bug Report -about: Report a reproducible bug or regression in the core React Native library. +name: I want to report an issue with React Native. +about: You want to report a reproducible bug or regression in React Native. +labels: "Bug Report" --- - - - [ ] Review the documentation: https://facebook.github.io/react-native - - [ ] Search for existing issues: https://github.com/facebook/react-native/issues - - [ ] Use the latest React Native release: https://github.com/facebook/react-native/releases + ## Environment -Run `react-native info` in your terminal and paste its contents here. + ## Description -Describe your issue in detail. Include screenshots if needed. If this is a regression, let us know. + ## Reproducible Demo -Let us know how to reproduce the issue. Include a code sample, share a project, or share an app that reproduces the issue using https://snack.expo.io/. Please follow the guidelines for providing a MCVE: https://stackoverflow.com/help/mcve + diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md index 4fb4e92ec15749..23e29479a29c29 100644 --- a/.github/ISSUE_TEMPLATE/discussion.md +++ b/.github/ISSUE_TEMPLATE/discussion.md @@ -1,20 +1,7 @@ --- -name: 🗣 Start a Discussion -about: Use https://github.com/react-native-community/discussions-and-proposals to propose changes or discuss feature requests. -title: [Discussion] -labels: "For Discussion" +name: I want to discuss or propose a change to React Native. +about: You have an idea that could make React Native better, or you want to discuss some aspect of the framework. --- -Use https://github.com/react-native-community/discussions-and-proposals to propose changes or discuss feature requests. This helps us ensure bug reports and regressions are given the priority they require. - -You may also use https://discuss.reactjs.org/ for discussions on topics that are not necessarily served by the Bug Report template. - ---- - -# For Discussion - - +The React Native community has set up a separate repository to track discussions and proposals: +- https://github.com/react-native-community/discussions-and-proposals diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md index 8db3110ed622d9..80ac6d7891064d 100644 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -1,9 +1,10 @@ --- -name: 📖 Documentation Issue -about: Report issues with the docs at https://github.com/facebook/react-native-website/issues. +name: I found a problem with the documentation. +about: You want to report something that is wrong or missing from the documentation. labels: "🚫Docs" --- -GitHub Issues in the `facebook/react-native` repository are used exclusively for tracking bugs in React Native. - -If you would like to report an issue in the React Native documentation, or anything related to the React Native website, please visit https://github.com/facebook/react-native-website/issues. +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 index 1a489b014dd623..6b7212c27345f9 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,28 +1,17 @@ --- -name: 💬 Question -about: If you need help with your React Native app, the right place to go depends on the type of help that you need. +name: I need help using React Native. +about: You need help writing your React Native app. labels: "Question" --- -GitHub Issues in the `facebook/react-native` repository are used exclusively for tracking bugs in React Native. GitHub may not be the ideal place to ask questions about using React Native, but we have compiled a list of resources that should help. - -### Get help with your React Native app or ask code-level questions - -Many members of the community use Stack Overflow to ask questions. +GitHub Issues in the `facebook/react-native` repository are used exclusively for tracking bugs +in the React Native framework. Please do not submit support requests through GitHub. +Many members of the community use Stack Overflow to ask questions: * Read through the existing questions tagged with **react-native**: http://stackoverflow.com/questions/tagged/react-native * Ask your own: http://stackoverflow.com/questions/ask?tags=react-native -### Talk about best practices or propose changes to React Native - -For longer-form conversations about React Native, we’ve set up a discussion forum: -https://discuss.reactjs.org - -This forum is a great place for discussion about best practices and application architecture as well as the future of React Native. - -### Chat with React and React Native community members - If you need an answer right away, check out the Reactiflux Discord community at https://discord.gg/0ZcbPKXt5bZjGY5n. There are usually a number of React Native experts there who can help out or point you to somewhere you might want to look. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f6e97493a62515..25f56477f52f92 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,15 @@ -Thank you for sending the PR! We appreciate you spending the time to work on these changes. -Help us understand your motivation by explaining why you decided to make this change: + +## Summary -Changelog: ----------- + -Help reviewers and the release process by writing your own changelog entry. See http://facebook.github.io/react-native/docs/contributing#changelog for an example. +## Changelog -[CATEGORY] [TYPE] - Message + +[CATEGORY] [TYPE] - Message -Test Plan: ----------- +## Test Plan -Write your test plan here (**REQUIRED**). If you changed any code, please provide us with clear instructions on how you verified your changes work. Bonus points for screenshots and videos! Increase test coverage whenever possible. + diff --git a/ContainerShip/Dockerfile.android b/ContainerShip/Dockerfile.android index 39b02062d152c4..28a923710d6a0d 100644 --- a/ContainerShip/Dockerfile.android +++ b/ContainerShip/Dockerfile.android @@ -9,7 +9,7 @@ # image. Ideally, this image would be rebuilt with any new commit to the master # branch. Doing so will catch issues such as BUCK failing to fetch dependencies # or run tests, as well as unit test failures. -FROM reactnativeci/android-base:latest +FROM react-native-community/react-native LABEL Description="This image prepares and runs React Native's Android tests." LABEL maintainer="Héctor Ramos " @@ -47,10 +47,10 @@ ADD build.gradle /app/build.gradle ADD react.gradle /app/react.gradle # run gradle downloads -RUN ./gradlew :ReactAndroid:downloadBoost :ReactAndroid:downloadDoubleConversion :ReactAndroid:downloadFolly :ReactAndroid:downloadGlog :ReactAndroid:downloadJSCHeaders +RUN ./gradlew :ReactAndroid:downloadBoost :ReactAndroid:downloadDoubleConversion :ReactAndroid:downloadFolly :ReactAndroid:downloadGlog :ReactAndroid:downloadJSC # compile native libs with Gradle script, we need bridge for unit and integration tests -RUN ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=1 -Pcom.android.build.threadPoolSize=1 +RUN ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=1 # add all react-native code ADD . /app diff --git a/ContainerShip/Dockerfile.android-base b/ContainerShip/Dockerfile.android-base deleted file mode 100644 index e54b3bbfc81889..00000000000000 --- a/ContainerShip/Dockerfile.android-base +++ /dev/null @@ -1,83 +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. - -# React Native Base Android Development Environment -# -# This image provides a base Android development environment for React Native, -# including, but not limited to, the Android SDK, Android NDK, Node, and BUCK. -# These are required in order to run React Native's Android unit and integration -# tests. -# -# This image is not currently built automatically as part of React Native's CI -# infrastructure. It should not be necessary to rebuild this image while the -# Android dependencies (Android SDK version, build tools version, etc) remain -# equal. The operations performed to build this image are those that tend to -# remain stable across commits in any given React Native release. - -FROM library/ubuntu:16.04 - -LABEL Description="This image provides a base Android development environment for React Native, and may be used to run tests." -LABEL maintainer="Héctor Ramos " - -# set default build arguments -ARG SDK_VERSION=sdk-tools-linux-3859397.zip -ARG ANDROID_BUILD_VERSION=28 -ARG ANDROID_TOOLS_VERSION=28.0.2 -ARG BUCK_VERSION=v2018.10.29.01 -ARG NDK_VERSION=17c -ARG NODE_VERSION=8.10.0 -ARG WATCHMAN_VERSION=4.9.0 - -# set default environment variables -ENV ADB_INSTALL_TIMEOUT=10 -ENV PATH=${PATH}:/opt/buck/bin/ -ENV ANDROID_HOME=/opt/android -ENV ANDROID_SDK_HOME=${ANDROID_HOME} -ENV PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools -ENV ANDROID_NDK=/opt/ndk/android-ndk-r$NDK_VERSION -ENV PATH=${PATH}:${ANDROID_NDK} - -# install system dependencies -RUN apt-get update && apt-get install ant autoconf automake curl g++ gcc git libqt5widgets5 lib32z1 lib32stdc++6 make maven npm openjdk-8* python-dev python3-dev qml-module-qtquick-controls qtdeclarative5-dev unzip -y - -# configure npm -RUN npm config set spin=false && \ - npm config set progress=false - -# install node -RUN npm install n -g -RUN n $NODE_VERSION - -# download buck -RUN git clone https://github.com/facebook/buck.git /opt/buck --branch $BUCK_VERSION --depth=1 -WORKDIR /opt/buck - -# build buck -RUN ant - -# Full reference at https://dl.google.com/android/repository/repository2-1.xml -# download and unpack android -RUN mkdir /opt/android && \ - cd /opt/android && \ - curl --silent https://dl.google.com/android/repository/${SDK_VERSION} > android.zip && \ - unzip android.zip && \ - rm android.zip - -# download and unpack NDK -RUN mkdir /opt/ndk && \ - cd /opt/ndk && \ - curl --silent https://dl.google.com/android/repository/android-ndk-r$NDK_VERSION-linux-x86_64.zip > ndk.zip && \ - unzip ndk.zip && \ - rm ndk.zip - -# Add android SDK tools -RUN sdkmanager "system-images;android-19;google_apis;armeabi-v7a" \ - "platforms;android-$ANDROID_BUILD_VERSION" \ - "build-tools;$ANDROID_TOOLS_VERSION" \ - "add-ons;addon-google_apis-google-23" \ - "extras;android;m2repository" - -# clean up unnecessary directories -RUN rm -rf /opt/android/system-images/android-19/default/x86 diff --git a/ContainerShip/Dockerfile.javascript b/ContainerShip/Dockerfile.javascript deleted file mode 100644 index 5e077f972318c9..00000000000000 --- a/ContainerShip/Dockerfile.javascript +++ /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. - -FROM library/node:6.9.2 - -ENV YARN_VERSION=0.27.5 - -# install dependencies -RUN apt-get update && apt-get install ocaml libelf-dev -y -RUN npm install yarn@$YARN_VERSION -g - -# add code -RUN mkdir /app -ADD . /app - -WORKDIR /app -RUN yarn install --ignore-engines - -WORKDIR website -RUN yarn install --ignore-engines --ignore-platform - -WORKDIR /app diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index 348d0867e73e97..de88f1d6056f17 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -10,6 +10,7 @@ 'use strict'; +require('InitializeCore'); const React = require('react'); const ReactNative = require('react-native'); const { diff --git a/Libraries/ART/ARTCGFloatArray.h b/Libraries/ART/ARTCGFloatArray.h index a607904e0be1ed..b26bcb1b7dae96 100644 --- a/Libraries/ART/ARTCGFloatArray.h +++ b/Libraries/ART/ARTCGFloatArray.h @@ -7,7 +7,7 @@ // 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 dealloced. +// 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. diff --git a/Libraries/ART/ARTTextFrame.h b/Libraries/ART/ARTTextFrame.h index 1c02d3e04260b0..00e61ce263d76f 100644 --- a/Libraries/ART/ARTTextFrame.h +++ b/Libraries/ART/ARTTextFrame.h @@ -9,7 +9,7 @@ // 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 dealloced. +// 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. diff --git a/Libraries/ART/RCTConvert+ART.h b/Libraries/ART/RCTConvert+ART.h index ffc1deafe4e6e7..5fe3b4edb281f9 100644 --- a/Libraries/ART/RCTConvert+ART.h +++ b/Libraries/ART/RCTConvert+ART.h @@ -15,7 +15,7 @@ @interface RCTConvert (ART) -+ (CGPathRef)CGPath:(id)json; ++ (CGPathRef)CGPath:(id)json CF_RETURNS_NOT_RETAINED; + (CTTextAlignment)CTTextAlignment:(id)json; + (ARTTextFrame)ARTTextFrame:(id)json; + (ARTCGFloatArray)ARTCGFloatArray:(id)json; @@ -23,7 +23,7 @@ + (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset; + (CGRect)CGRect:(id)json offset:(NSUInteger)offset; -+ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset; -+ (CGGradientRef)CGGradient:(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/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index 7e4dba6c854fcc..129de677d8b17d 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -27,7 +27,7 @@ const ActionSheetIOS = { * * - `options` (array of strings) - a list of button titles (required) * - `cancelButtonIndex` (int) - index of cancel button in `options` - * - `destructiveButtonIndex` (int) - index of destructive button in `options` + * - `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 * diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m index af1e202c962409..9eca118126ca16 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.m +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -32,20 +32,21 @@ - (dispatch_queue_t)methodQueue return dispatch_get_main_queue(); } -/* - * The `anchor` option takes a view to set as the anchor for the share - * popup to point to, on iPads running iOS 8. If it is not passed, it - * defaults to centering the share popup on screen without any arrows. - */ -- (CGRect)sourceRectInView:(UIView *)sourceView - anchorViewTag:(NSNumber *)anchorViewTag +- (void)presentViewController:(UIViewController *)alertController + onParentViewController:(UIViewController *)parentViewController + anchorViewTag:(NSNumber *)anchorViewTag { + alertController.modalPresentationStyle = UIModalPresentationPopover; + UIView *sourceView = parentViewController.view; + if (anchorViewTag) { - UIView *anchorView = [self.bridge.uiManager viewForReactTag:anchorViewTag]; - return [anchorView convertRect:anchorView.bounds toView:sourceView]; + sourceView = [self.bridge.uiManager viewForReactTag:anchorViewTag]; } else { - return (CGRect){sourceView.center, {1, 1}}; + alertController.popoverPresentationController.permittedArrowDirections = 0; } + alertController.popoverPresentationController.sourceView = sourceView; + alertController.popoverPresentationController.sourceRect = sourceView.bounds; + [parentViewController presentViewController:alertController animated:YES completion:nil]; } RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options @@ -63,8 +64,14 @@ - (CGRect)sourceRectInView:(UIView *)sourceView NSString *title = [RCTConvert NSString:options[@"title"]]; NSString *message = [RCTConvert NSString:options[@"message"]]; NSArray *buttons = [RCTConvert NSStringArray:options[@"options"]]; - NSInteger destructiveButtonIndex = options[@"destructiveButtonIndex"] ? [RCTConvert NSInteger:options[@"destructiveButtonIndex"]] : -1; NSInteger cancelButtonIndex = options[@"cancelButtonIndex"] ? [RCTConvert NSInteger:options[@"cancelButtonIndex"]] : -1; + NSArray *destructiveButtonIndices; + if ([options[@"destructiveButtonIndex"] isKindOfClass:[NSArray class]]) { + destructiveButtonIndices = [RCTConvert NSArray:options[@"destructiveButtonIndex"]]; + } else { + NSNumber *destructiveButtonIndex = options[@"destructiveButtonIndex"] ? [RCTConvert NSNumber:options[@"destructiveButtonIndex"]] : @-1; + destructiveButtonIndices = @[destructiveButtonIndex]; + } UIViewController *controller = RCTPresentedViewController(); @@ -79,9 +86,7 @@ - (CGRect)sourceRectInView:(UIView *)sourceView * defaults to centering the share popup on screen without any arrows. */ NSNumber *anchorViewTag = [RCTConvert NSNumber:options[@"anchor"]]; - UIView *sourceView = controller.view; - CGRect sourceRect = [self sourceRectInView:sourceView anchorViewTag:anchorViewTag]; - + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message @@ -90,7 +95,7 @@ - (CGRect)sourceRectInView:(UIView *)sourceView NSInteger index = 0; for (NSString *option in buttons) { UIAlertActionStyle style = UIAlertActionStyleDefault; - if (index == destructiveButtonIndex) { + if ([destructiveButtonIndices containsObject:@(index)]) { style = UIAlertActionStyleDestructive; } else if (index == cancelButtonIndex) { style = UIAlertActionStyleCancel; @@ -106,15 +111,8 @@ - (CGRect)sourceRectInView:(UIView *)sourceView index++; } - alertController.modalPresentationStyle = UIModalPresentationPopover; - alertController.popoverPresentationController.sourceView = sourceView; - alertController.popoverPresentationController.sourceRect = sourceRect; - if (!anchorViewTag) { - alertController.popoverPresentationController.permittedArrowDirections = 0; - } - [controller presentViewController:alertController animated:YES completion:nil]; - alertController.view.tintColor = [RCTConvert UIColor:options[@"tintColor"]]; + [self presentViewController:alertController onParentViewController:controller anchorViewTag:anchorViewTag]; } RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options @@ -173,17 +171,10 @@ - (CGRect)sourceRectInView:(UIView *)sourceView } }; - shareController.modalPresentationStyle = UIModalPresentationPopover; NSNumber *anchorViewTag = [RCTConvert NSNumber:options[@"anchor"]]; - if (!anchorViewTag) { - shareController.popoverPresentationController.permittedArrowDirections = 0; - } - shareController.popoverPresentationController.sourceView = controller.view; - shareController.popoverPresentationController.sourceRect = [self sourceRectInView:controller.view anchorViewTag:anchorViewTag]; - - [controller presentViewController:shareController animated:YES completion:nil]; - shareController.view.tintColor = [RCTConvert UIColor:options[@"tintColor"]]; + + [self presentViewController:shareController onParentViewController:controller anchorViewTag:anchorViewTag]; } @end diff --git a/Libraries/Animated/src/AnimatedEvent.js b/Libraries/Animated/src/AnimatedEvent.js index 029aa4aa3430d7..72ef2af980bfb0 100644 --- a/Libraries/Animated/src/AnimatedEvent.js +++ b/Libraries/Animated/src/AnimatedEvent.js @@ -158,7 +158,7 @@ class AnimatedEvent { }; } - _callListeners(...args) { + _callListeners(...args: any) { this._listeners.forEach(listener => listener(...args)); } diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 57e34ce6e9eee3..d14ac612ee7f51 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -685,5 +685,10 @@ module.exports = { forkEvent, unforkEvent, + /** + * Expose Event class, so it can be used as a type for type checkers. + */ + Event: AnimatedEvent, + __PropsOnlyForTests: AnimatedProps, }; diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js index 79e76db4cf3d5c..2ac39f024ab63a 100644 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -260,6 +260,22 @@ function shouldUseNativeDriver(config: AnimationConfig | EventConfig): boolean { return config.useNativeDriver || false; } +function transformDataType(value: any): number { + // 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 { + // Assume radians + return parseFloat(value) || 0; + } +} + module.exports = { API, addWhitelistedStyleProp, @@ -272,6 +288,7 @@ module.exports = { generateNewAnimationId, assertNativeAnimatedModule, shouldUseNativeDriver, + transformDataType, get nativeEventEmitter() { if (!nativeEventEmitter) { nativeEventEmitter = new NativeEventEmitter(NativeAnimatedModule); diff --git a/Libraries/Animated/src/nodes/AnimatedInterpolation.js b/Libraries/Animated/src/nodes/AnimatedInterpolation.js index d426a2931defe2..1ba304d331ad5f 100644 --- a/Libraries/Animated/src/nodes/AnimatedInterpolation.js +++ b/Libraries/Animated/src/nodes/AnimatedInterpolation.js @@ -347,21 +347,7 @@ class AnimatedInterpolation extends AnimatedWithChildren { } __transformDataType(range: Array) { - // Change the string array type to number array - // So we can reuse the same logic in iOS and Android platform - return range.map(function(value) { - 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 { - // Assume radians - return parseFloat(value) || 0; - } - }); + return range.map(NativeAnimatedHelper.transformDataType); } __getNativeConfig(): any { diff --git a/Libraries/Animated/src/nodes/AnimatedTransform.js b/Libraries/Animated/src/nodes/AnimatedTransform.js index 703fa51ceb4e1b..e3b3034675055c 100644 --- a/Libraries/Animated/src/nodes/AnimatedTransform.js +++ b/Libraries/Animated/src/nodes/AnimatedTransform.js @@ -103,7 +103,7 @@ class AnimatedTransform extends AnimatedWithChildren { transConfigs.push({ type: 'static', property: key, - value, + value: NativeAnimatedHelper.transformDataType(value), }); } } diff --git a/Libraries/Blob/URL.js b/Libraries/Blob/URL.js index 36dbe0ffa02b50..049a91ec03026a 100644 --- a/Libraries/Blob/URL.js +++ b/Libraries/Blob/URL.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow strict-local */ 'use strict'; @@ -47,11 +46,71 @@ if (BlobModule && typeof BlobModule.BLOB_URI_SCHEME === 'string') { * * ``` */ -class URL { - constructor() { - throw new Error('Creating URL objects is not supported yet.'); + +// Small subset from whatwg-url: https://github.com/jsdom/whatwg-url/tree/master/lib +// The reference code bloat comes from Unicode issues with URLs, so those won't work here. +export class URLSearchParams { + _searchParams = []; + + constructor(params: any) { + if (typeof params === 'object') { + Object.keys(params).forEach(key => this.append(key, params[key])); + } + } + + append(key: string, value: string) { + this._searchParams.push([key, value]); + } + + delete(name) { + throw new Error('not implemented'); + } + + get(name) { + throw new Error('not implemented'); + } + + getAll(name) { + throw new Error('not implemented'); + } + + has(name) { + throw new Error('not implemented'); + } + + set(name, value) { + throw new Error('not implemented'); } + sort() { + throw new Error('not implemented'); + } + + [Symbol.iterator]() { + return this._searchParams[Symbol.iterator](); + } + + toString() { + if (this._searchParams.length === 0) { + return ''; + } + const last = this._searchParams.length - 1; + return this._searchParams.reduce((acc, curr, index) => { + return acc + curr.join('=') + (index === last ? '' : '&'); + }, ''); + } +} + +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( + url, + ); +} + +export class URL { + _searchParamsInstance = null; + static createObjectURL(blob: Blob) { if (BLOB_URL_PREFIX === null) { throw new Error('Cannot create URL for blob!'); @@ -64,6 +123,93 @@ class URL { static revokeObjectURL(url: string) { // Do nothing. } -} -module.exports = URL; + constructor(url: string, base: string) { + let baseUrl = null; + if (base) { + if (typeof base === 'string') { + baseUrl = base; + if (!validateBaseUrl(baseUrl)) { + throw new TypeError(`Invalid base URL: ${baseUrl}`); + } + } else if (typeof base === 'object') { + baseUrl = base.toString(); + } + if (baseUrl.endsWith('/') && url.startsWith('/')) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (baseUrl.endsWith(url)) { + url = ''; + } + this._url = `${baseUrl}${url}`; + } else { + this._url = url; + if (!this._url.endsWith('/')) { + this._url += '/'; + } + } + } + + get hash() { + throw new Error('not implemented'); + } + + get host() { + throw new Error('not implemented'); + } + + get hostname() { + throw new Error('not implemented'); + } + + get href(): string { + return this.toString(); + } + + get origin() { + throw new Error('not implemented'); + } + + get password() { + throw new Error('not implemented'); + } + + get pathname() { + throw new Error('not implemented'); + } + + get port() { + throw new Error('not implemented'); + } + + get protocol() { + throw new Error('not implemented'); + } + + get search() { + throw new Error('not implemented'); + } + + get searchParams(): URLSearchParams { + if (this._searchParamsInstance == null) { + this._searchParamsInstance = new URLSearchParams(); + } + return this._searchParamsInstance; + } + + toJSON(): string { + return this.toString(); + } + + toString(): string { + if (this._searchParamsInstance === null) { + return this._url; + } + const separator = this._url.indexOf('?') > -1 ? '&' : '?'; + return this._url + separator + this._searchParamsInstance.toString(); + } + + get username() { + throw new Error('not implemented'); + } +} diff --git a/Libraries/Blob/__tests__/URL-test.js b/Libraries/Blob/__tests__/URL-test.js new file mode 100644 index 00000000000000..051ec9a724e164 --- /dev/null +++ b/Libraries/Blob/__tests__/URL-test.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 + * @emails oncall+react_native + */ +'use strict'; + +const URL = require('URL').URL; + +describe('URL', function() { + it('should pass Mozilla Dev Network examples', () => { + const a = new URL('/', 'https://developer.mozilla.org'); + expect(a.href).toBe('https://developer.mozilla.org/'); + const b = new URL('https://developer.mozilla.org'); + expect(b.href).toBe('https://developer.mozilla.org/'); + const c = new URL('en-US/docs', b); + expect(c.href).toBe('https://developer.mozilla.org/en-US/docs'); + const d = new URL('/en-US/docs', b); + expect(d.href).toBe('https://developer.mozilla.org/en-US/docs'); + const f = new URL('/en-US/docs', d); + expect(f.href).toBe('https://developer.mozilla.org/en-US/docs'); + // from original test suite, but requires complex implementation + // const g = new URL( + // '/en-US/docs', + // 'https://developer.mozilla.org/fr-FR/toto', + // ); + // expect(g.href).toBe('https://developer.mozilla.org/en-US/docs'); + const h = new URL('/en-US/docs', a); + expect(h.href).toBe('https://developer.mozilla.org/en-US/docs'); + }); +}); diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h index 7ccba500f59f0b..7a0f0635ee7b75 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h +++ b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h @@ -8,17 +8,8 @@ #import #import -@class ALAssetsLibrary; +@class PHPhotoLibrary; @interface RCTAssetsLibraryRequestHandler : NSObject @end - -@interface RCTBridge (RCTAssetsLibraryImageLoader) - -/** - * The shared asset library instance. - */ -@property (nonatomic, readonly) ALAssetsLibrary *assetsLibrary; - -@end diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m index 663598ceaea0b7..cdc6cb9f203ebc 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m +++ b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m @@ -11,41 +11,26 @@ #import #import -#import +#import #import #import #import @implementation RCTAssetsLibraryRequestHandler -{ - ALAssetsLibrary *_assetsLibrary; -} RCT_EXPORT_MODULE() -@synthesize bridge = _bridge; -static Class _ALAssetsLibrary = nil; -static void ensureAssetsLibLoaded(void) -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - void * handle = dlopen("/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary", RTLD_LAZY); -#pragma unused(handle) - _ALAssetsLibrary = objc_getClass("ALAssetsLibrary"); - }); -} -- (ALAssetsLibrary *)assetsLibrary -{ - ensureAssetsLibLoaded(); - return _assetsLibrary ?: (_assetsLibrary = [_ALAssetsLibrary new]); -} - #pragma mark - RCTURLRequestHandler - (BOOL)canHandleRequest:(NSURLRequest *)request { - return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame; + if (![PHAsset class]) { + return NO; + } + + return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame + || [request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame; } - (id)sendRequest:(NSURLRequest *)request @@ -55,58 +40,75 @@ - (id)sendRequest:(NSURLRequest *)request void (^cancellationBlock)(void) = ^{ atomic_store(&cancelled, YES); }; - - [[self assetsLibrary] assetForURL:request.URL resultBlock:^(ALAsset *asset) { - if (atomic_load(&cancelled)) { - return; - } - - if (asset) { - - ALAssetRepresentation *representation = [asset defaultRepresentation]; - NSInteger length = (NSInteger)representation.size; - CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef _Nonnull)(representation.UTI), kUTTagClassMIMEType); - - NSURLResponse *response = - [[NSURLResponse alloc] initWithURL:request.URL - MIMEType:(__bridge NSString *)(MIMEType) - expectedContentLength:length - textEncodingName:nil]; - - [delegate URLRequest:cancellationBlock didReceiveResponse:response]; - - NSError *error = nil; - uint8_t *buffer = (uint8_t *)malloc((size_t)length); - if ([representation getBytes:buffer - fromOffset:0 - length:length - error:&error]) { - - NSData *data = [[NSData alloc] initWithBytesNoCopy:buffer - length:length - freeWhenDone:YES]; - - [delegate URLRequest:cancellationBlock didReceiveData:data]; - [delegate URLRequest:cancellationBlock didCompleteWithError:nil]; - - } else { - free(buffer); - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - } - - } else { - NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset" - " at URL %@ with no error message.", request.URL]; - NSError *error = RCTErrorWithMessage(errorMessage); + + if (!request.URL) { + NSString *const msg = [NSString stringWithFormat:@"Cannot send request without URL"]; + [delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)]; + return cancellationBlock; + } + + PHFetchResult *fetchResult; + + if ([request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) { + // Fetch assets using PHAsset localIdentifier (recommended) + NSString *const localIdentifier = [request.URL.absoluteString substringFromIndex:@"ph://".length]; + fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil]; + } else if ([request.URL.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:@[request.URL] options:nil]; + } else { + NSString *const msg = [NSString stringWithFormat:@"Cannot send request with unknown protocol: %@", request.URL]; + [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.", request.URL]; + NSError *error = RCTErrorWithMessage(errorMessage); + [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + return cancellationBlock; + } + + if (atomic_load(&cancelled)) { + return cancellationBlock; + } + + PHAsset *const _Nonnull asset = [fetchResult firstObject]; + + // 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]; - } - } failureBlock:^(NSError *loadError) { - if (atomic_load(&cancelled)) { return; } - [delegate URLRequest:cancellationBlock didCompleteWithError:loadError]; - }]; + 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; } @@ -116,12 +118,3 @@ - (void)cancelRequest:(id)requestToken } @end - -@implementation RCTBridge (RCTAssetsLibraryImageLoader) - -- (ALAssetsLibrary *)assetsLibrary -{ - return [[self moduleForClass:[RCTAssetsLibraryRequestHandler class]] assetsLibrary]; -} - -@end diff --git a/Libraries/CameraRoll/RCTCameraRollManager.h b/Libraries/CameraRoll/RCTCameraRollManager.h index 3341c539b0e9dc..b5d25a04d1eebd 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.h +++ b/Libraries/CameraRoll/RCTCameraRollManager.h @@ -5,18 +5,18 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import #import #import -@interface RCTConvert (ALAssetGroup) +@interface RCTConvert (PHFetchOptions) -+ (ALAssetsGroupType)ALAssetsGroupType:(id)json; -+ (ALAssetsFilter *)ALAssetsFilter:(id)json; ++ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType; @end + @interface RCTCameraRollManager : NSObject @end diff --git a/Libraries/CameraRoll/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m index 96e117c16a3783..245546a8f12564 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.m +++ b/Libraries/CameraRoll/RCTCameraRollManager.m @@ -13,6 +13,7 @@ #import #import #import +#import #import #import @@ -22,85 +23,46 @@ #import "RCTAssetsLibraryRequestHandler.h" -@implementation RCTConvert (ALAssetGroup) - -RCT_ENUM_CONVERTER(ALAssetsGroupType, (@{ - - // New values - @"album": @(ALAssetsGroupAlbum), - @"all": @(ALAssetsGroupAll), - @"event": @(ALAssetsGroupEvent), - @"faces": @(ALAssetsGroupFaces), - @"library": @(ALAssetsGroupLibrary), - @"photo-stream": @(ALAssetsGroupPhotoStream), - @"saved-photos": @(ALAssetsGroupSavedPhotos), - - // Legacy values - @"Album": @(ALAssetsGroupAlbum), - @"All": @(ALAssetsGroupAll), - @"Event": @(ALAssetsGroupEvent), - @"Faces": @(ALAssetsGroupFaces), - @"Library": @(ALAssetsGroupLibrary), - @"PhotoStream": @(ALAssetsGroupPhotoStream), - @"SavedPhotos": @(ALAssetsGroupSavedPhotos), - -}), ALAssetsGroupSavedPhotos, integerValue) - -static Class _ALAssetsFilter = nil; -static NSString *_ALAssetsGroupPropertyName = nil; -static NSString *_ALAssetPropertyAssetURL = nil; -static NSString *_ALAssetPropertyLocation = nil; -static NSString *_ALAssetPropertyDate = nil; -static NSString *_ALAssetPropertyType = nil; -static NSString *_ALAssetPropertyDuration = nil; -static NSString *_ALAssetTypeVideo = nil; -static NSString *lookupNSString(void * handle, const char * name) -{ - void ** sym = dlsym(handle, name); - return (__bridge NSString *)(sym ? *sym : nil); -} -static void ensureAssetsLibLoaded(void) -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - void * handle = dlopen("/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary", RTLD_LAZY); - RCTAssert(handle != NULL, @"Unable to load AssetsLibrary.framework."); - _ALAssetsFilter = objc_getClass("ALAssetsFilter"); - _ALAssetsGroupPropertyName = lookupNSString(handle, "ALAssetsGroupPropertyName"); - _ALAssetPropertyAssetURL = lookupNSString(handle, "ALAssetPropertyAssetURL"); - _ALAssetPropertyLocation = lookupNSString(handle, "ALAssetPropertyLocation"); - _ALAssetPropertyDate = lookupNSString(handle, "ALAssetPropertyDate"); - _ALAssetPropertyType = lookupNSString(handle, "ALAssetPropertyType"); - _ALAssetPropertyDuration = lookupNSString(handle, "ALAssetPropertyDuration"); - _ALAssetTypeVideo = lookupNSString(handle, "ALAssetTypeVideo"); - }); -} +@implementation RCTConvert (PHAssetCollectionSubtype) + +RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{ + @"album": @(PHAssetCollectionSubtypeAny), + @"all": @(PHAssetCollectionSubtypeAny), + @"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) + -+ (ALAssetsFilter *)ALAssetsFilter:(id)json +@end + +@implementation RCTConvert (PHFetchOptions) + ++ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType { - static NSDictionary *options; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ensureAssetsLibLoaded(); - options = @{ - // New values - @"photos": [_ALAssetsFilter allPhotos], - @"videos": [_ALAssetsFilter allVideos], - @"all": [_ALAssetsFilter allAssets], - - // Legacy values - @"Photos": [_ALAssetsFilter allPhotos], - @"Videos": [_ALAssetsFilter allVideos], - @"All": [_ALAssetsFilter allAssets], - }; - }); + // This is not exhaustive in terms of supported media type predicates; more can be added in the future + NSString *const lowercase = [mediaType lowercaseString]; - ALAssetsFilter *filter = options[json ?: @"photos"]; - if (!filter) { - RCTLogError(@"Invalid filter option: '%@'. Expected one of 'photos'," - "'videos' or 'all'.", json); + 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; } - return filter ?: [_ALAssetsFilter allPhotos]; } @end @@ -111,45 +73,87 @@ @implementation RCTCameraRollManager @synthesize bridge = _bridge; -static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; 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) { - if ([type isEqualToString:@"video"]) { - // It's unclear if writeVideoAtPathToSavedPhotosAlbum is thread-safe - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_bridge.assetsLibrary writeVideoAtPathToSavedPhotosAlbum:request.URL completionBlock:^(NSURL *assetURL, NSError *saveError) { - if (saveError) { - reject(kErrorUnableToSave, nil, saveError); - } else { - resolve(assetURL.absoluteString); - } - }]; - }); - } else { - [_bridge.imageLoader loadImageWithURLRequest:request - callback:^(NSError *loadError, UIImage *loadedImage) { - if (loadError) { - reject(kErrorUnableToLoad, nil, loadError); - return; + __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); } - // It's unclear if writeImageToSavedPhotosAlbum is thread-safe - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { - if (saveError) { - RCTLogWarn(@"Error saving cropped image: %@", saveError); - reject(kErrorUnableToSave, nil, saveError); - } else { - resolve(assetURL.absoluteString); - } - }]; - }); }]; - } + }; + + void (^loadBlock)(void) = ^void() { + if ([type isEqualToString:@"video"]) { + inputURI = request.URL; + saveBlock(); + } else { + [self.bridge.imageLoader 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, @@ -181,89 +185,133 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve, { checkPhotoLibraryConfig(); - ensureAssetsLibLoaded(); - NSUInteger first = [RCTConvert NSInteger:params[@"first"]]; - NSString *afterCursor = [RCTConvert NSString:params[@"after"]]; - NSString *groupName = [RCTConvert NSString:params[@"groupName"]]; - ALAssetsFilter *assetType = [RCTConvert ALAssetsFilter:params[@"assetType"]]; - ALAssetsGroupType groupTypes = [RCTConvert ALAssetsGroupType:params[@"groupTypes"]]; + NSUInteger const first = [RCTConvert NSInteger: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: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]; - [_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { - if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:_ALAssetsGroupPropertyName]])) { - - [group setAssetsFilter:assetType]; - [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) { - if (result) { - NSString *uri = ((NSURL *)[result valueForProperty:_ALAssetPropertyAssetURL]).absoluteString; - if (afterCursor && !foundAfter) { - if ([afterCursor isEqualToString:uri]) { - foundAfter = YES; - } - return; // Skip until we get to the first one + // 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; } - if (first == assets.count) { - *stopAssets = YES; - *stopGroups = YES; - hasNextPage = YES; - RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results."); - RCTResolvePromise(resolve, assets, hasNextPage); - resolvedPromise = 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; } - CGSize dimensions = [result defaultRepresentation].dimensions; - CLLocation *loc = [result valueForProperty:_ALAssetPropertyLocation]; - NSDate *date = [result valueForProperty:_ALAssetPropertyDate]; - NSString *filename = [result defaultRepresentation].filename; - int64_t duration = 0; - if ([[result valueForProperty:_ALAssetPropertyType] isEqualToString:_ALAssetTypeVideo]) { - duration = [[result valueForProperty:_ALAssetPropertyDuration] intValue]; - } - - [assets addObject:@{ - @"node": @{ - @"type": [result valueForProperty:_ALAssetPropertyType], - @"group_name": [group valueForProperty:_ALAssetsGroupPropertyName], - @"image": @{ - @"uri": uri, - @"filename" : filename ?: [NSNull null], - @"height": @(dimensions.height), - @"width": @(dimensions.width), - @"isStored": @YES, - @"playableDuration": @(duration), - }, - @"timestamp": @(date.timeIntervalSince1970), - @"location": loc ? @{ - @"latitude": @(loc.coordinate.latitude), - @"longitude": @(loc.coordinate.longitude), - @"altitude": @(loc.altitude), - @"heading": @(loc.course), - @"speed": @(loc.speed), - } : @{}, + + 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; } - - if (!group) { - // Sometimes the enumeration continues even if we set stop above, so we guard against resolving the promise - // multiple times here. - if (!resolvedPromise) { - RCTResolvePromise(resolve, assets, hasNextPage); - resolvedPromise = YES; - } - } - } failureBlock:^(NSError *error) { - if (error.code != ALAssetsLibraryAccessUserDeniedError) { - RCTLogError(@"Failure while iterating through asset groups %@", error); - } - reject(kErrorUnableToLoad, nil, error); - }]; + }); } RCT_EXPORT_METHOD(deletePhotos:(NSArray*)assets diff --git a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m index e6c191e93b5099..ccdf3c27f73087 100644 --- a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m +++ b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m @@ -39,7 +39,7 @@ - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL // 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. + // form of an NSURL which is what assets-library uses. NSString *assetID = @""; PHFetchResult *results; if (!imageURL) { diff --git a/React/Views/RCTTabBarManager.h b/Libraries/CameraRoll/__mocks__/CameraRoll.js similarity index 68% rename from React/Views/RCTTabBarManager.h rename to Libraries/CameraRoll/__mocks__/CameraRoll.js index 7e0b49d3435821..5094bdc7290099 100644 --- a/React/Views/RCTTabBarManager.h +++ b/Libraries/CameraRoll/__mocks__/CameraRoll.js @@ -3,10 +3,12 @@ * * 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 - -@interface RCTTabBarManager : RCTViewManager +const CameraRoll = {}; -@end +export default CameraRoll; diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/Libraries/Components/ActivityIndicator/ActivityIndicator.js index 9bb86373165494..7d8b9cb77ce863 100644 --- a/Libraries/Components/ActivityIndicator/ActivityIndicator.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicator.js @@ -15,7 +15,7 @@ const React = require('React'); const StyleSheet = require('StyleSheet'); const View = require('View'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTActivityIndicatorViewNativeComponent = require('RCTActivityIndicatorViewNativeComponent'); import type {NativeComponent} from 'ReactNative'; import type {ViewProps} from 'ViewPropTypes'; @@ -23,7 +23,7 @@ import type {ViewProps} from 'ViewPropTypes'; const RCTActivityIndicator = Platform.OS === 'android' ? require('ProgressBarAndroid') - : requireNativeComponent('RCTActivityIndicatorView'); + : RCTActivityIndicatorViewNativeComponent; const GRAY = '#999999'; @@ -69,10 +69,7 @@ type Props = $ReadOnly<{| * * See http://facebook.github.io/react-native/docs/activityindicator.html */ -const ActivityIndicator = ( - props: Props, - forwardedRef?: ?React.Ref<'RCTActivityIndicatorView'>, -) => { +const ActivityIndicator = (props: Props, forwardedRef?: any) => { const {onLayout, style, ...restProps} = props; let sizeStyle; diff --git a/Libraries/Components/ActivityIndicator/RCTActivityIndicatorViewNativeComponent.js b/Libraries/Components/ActivityIndicator/RCTActivityIndicatorViewNativeComponent.js new file mode 100644 index 00000000000000..461ee0e51d3392 --- /dev/null +++ b/Libraries/Components/ActivityIndicator/RCTActivityIndicatorViewNativeComponent.js @@ -0,0 +1,60 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {ViewStyleProp} from 'StyleSheet'; +import type {NativeComponent} from 'ReactNative'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * Whether the indicator should hide when not animating (true by default). + * + * See http://facebook.github.io/react-native/docs/activityindicator.html#hideswhenstopped + */ + hidesWhenStopped?: ?boolean, + + /** + * Whether to show the indicator (true, the default) or hide it (false). + * + * See http://facebook.github.io/react-native/docs/activityindicator.html#animating + */ + animating?: ?boolean, + + /** + * The foreground color of the spinner (default is gray). + * + * See http://facebook.github.io/react-native/docs/activityindicator.html#color + */ + color?: ?string, + + /** + * 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?: ?(number | 'small' | 'large'), + + style?: ?ViewStyleProp, + styleAttr?: ?string, + indeterminate?: ?boolean, +|}>; + +type ActivityIndicatorNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTActivityIndicatorView', +): any): ActivityIndicatorNativeType); diff --git a/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js b/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js new file mode 100644 index 00000000000000..14c84b069f8531 --- /dev/null +++ b/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js @@ -0,0 +1,51 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {NativeComponent} from 'ReactNative'; + +type CheckBoxEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + value: boolean, + |}>, +>; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * Used in case the props change removes the component. + */ + onChange?: ?(event: CheckBoxEvent) => mixed, + + /** + * Invoked with the new value when the value changes. + */ + onValueChange?: ?(value: boolean) => mixed, + + /** + * Used to locate this view in end-to-end tests. + */ + testID?: ?string, + + on?: ?boolean, + enabled?: boolean, +|}>; + +type CheckBoxNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidCheckBox', +): any): CheckBoxNativeType); diff --git a/Libraries/Components/CheckBox/CheckBox.android.js b/Libraries/Components/CheckBox/CheckBox.android.js index b8a7d3d765e522..e288562a0be0e3 100644 --- a/Libraries/Components/CheckBox/CheckBox.android.js +++ b/Libraries/Components/CheckBox/CheckBox.android.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 */ 'use strict'; @@ -12,7 +12,7 @@ const React = require('React'); const StyleSheet = require('StyleSheet'); -const requireNativeComponent = require('requireNativeComponent'); +const AndroidCheckBoxNativeComponent = require('AndroidCheckBoxNativeComponent'); const nullthrows = require('nullthrows'); const setAndForwardRef = require('setAndForwardRef'); @@ -76,10 +76,6 @@ type Props = $ReadOnly<{| forwardedRef?: ?React.Ref, |}>; -const RCTCheckBox = ((requireNativeComponent( - 'AndroidCheckBox', -): any): CheckBoxNativeType); - /** * Renders a boolean input (Android only). * @@ -169,7 +165,7 @@ class CheckBox extends React.Component { }; return ( - { mode: 'datetime', }; - // $FlowFixMe How to type a native component to be able to call setNativeProps - _picker: ?React.ElementRef = null; + _picker: ?React.ElementRef = null; componentDidUpdate() { if (this.props.date) { @@ -147,7 +145,7 @@ class DatePickerIOS extends React.Component { ); return ( - { this._picker = picker; diff --git a/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js b/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js new file mode 100644 index 00000000000000..137d88351314c7 --- /dev/null +++ b/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js @@ -0,0 +1,41 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type Event = SyntheticEvent< + $ReadOnly<{| + timestamp: number, + |}>, +>; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + date?: ?number, + initialDate?: ?Date, + locale?: ?string, + maximumDate?: ?number, + minimumDate?: ?number, + minuteInterval?: ?(1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30), + mode?: ?('date' | 'time' | 'datetime'), + onChange?: ?(event: Event) => void, + timeZoneOffsetInMinutes?: ?number, +|}>; +type RCTDatePickerNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTDatePicker', +): any): RCTDatePickerNativeType); diff --git a/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js b/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js new file mode 100644 index 00000000000000..ba5ec7698802e9 --- /dev/null +++ b/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js @@ -0,0 +1,121 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {NativeComponent} from 'ReactNative'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ViewStyleProp} from 'StyleSheet'; +import type React from 'React'; + +type ColorValue = null | string; + +type DrawerStates = 'Idle' | 'Dragging' | 'Settling'; + +type DrawerStateEvent = SyntheticEvent< + $ReadOnly<{| + drawerState: number, + |}>, +>; + +type DrawerSlideEvent = SyntheticEvent< + $ReadOnly<{| + offset: number, + |}>, +>; + +type NativeProps = $ReadOnly<{| + /** + * Determines whether the keyboard gets dismissed in response to a drag. + * - 'none' (the default), drags do not dismiss the keyboard. + * - 'on-drag', the keyboard is dismissed when a drag begins. + */ + keyboardDismissMode?: ?('none' | 'on-drag'), + + /** + * Specifies the background color of the drawer. The default value is white. + * If you want to set the opacity of the drawer, use rgba. Example: + * + * ``` + * return ( + * + * + * ); + * ``` + */ + drawerBackgroundColor: ColorValue, + + /** + * Specifies the side of the screen from which the drawer will slide in. + */ + drawerPosition: ?number, + + /** + * Specifies the width of the drawer, more precisely the width of the view that be pulled in + * from the edge of the window. + */ + drawerWidth?: ?number, + + /** + * Specifies the lock mode of the drawer. The drawer can be locked in 3 states: + * - unlocked (default), meaning that the drawer will respond (open/close) to touch gestures. + * - locked-closed, meaning that the drawer will stay closed and not respond to gestures. + * - locked-open, meaning that the drawer will stay opened and not respond to gestures. + * The drawer may still be opened and closed programmatically (`openDrawer`/`closeDrawer`). + */ + drawerLockMode?: ?('unlocked' | 'locked-closed' | 'locked-open'), + + /** + * Function called whenever there is an interaction with the navigation view. + */ + onDrawerSlide?: ?(event: DrawerSlideEvent) => mixed, + + /** + * Function called when the drawer state has changed. The drawer can be in 3 states: + * - Idle, meaning there is no interaction with the navigation view happening at the time + * - Dragging, meaning there is currently an interaction with the navigation view + * - Settling, meaning that there was an interaction with the navigation view, and the + * navigation view is now finishing its closing or opening animation + */ + onDrawerStateChanged?: ?(state: DrawerStateEvent) => mixed, + + /** + * Function called whenever the navigation view has been opened. + */ + onDrawerOpen?: ?() => mixed, + + /** + * Function called whenever the navigation view has been closed. + */ + onDrawerClose?: ?() => mixed, + + /** + * The navigation view that will be rendered to the side of the screen and can be pulled in. + */ + renderNavigationView: () => React.Element, + + /** + * Make the drawer take the entire screen and draw the background of the + * status bar to allow it to open over the status bar. It will only have an + * effect on API 21+. + */ + statusBarBackgroundColor?: ?ColorValue, + + children?: React.Node, + style?: ?ViewStyleProp, +|}>; + +type AndroidDrawerLayoutNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidDrawerLayout', +): any): AndroidDrawerLayoutNativeType); diff --git a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js index c6a94f59450d32..2c1e970ca5fe9c 100644 --- a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js +++ b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js @@ -23,7 +23,7 @@ const DrawerConsts = UIManager.getViewManagerConfig('AndroidDrawerLayout') .Constants; const dismissKeyboard = require('dismissKeyboard'); -const requireNativeComponent = require('requireNativeComponent'); +const AndroidDrawerLayoutNativeComponent = require('AndroidDrawerLayoutNativeComponent'); const DRAWER_STATES = ['Idle', 'Dragging', 'Settling']; @@ -131,23 +131,10 @@ type Props = $ReadOnly<{| style?: ?ViewStyleProp, |}>; -type NativeProps = $ReadOnly<{| - ...$Diff< - Props, - $ReadOnly<{onDrawerStateChanged?: ?(state: DrawerStates) => mixed}>, - >, - onDrawerStateChanged?: ?(state: DrawerStateEvent) => mixed, -|}>; - type State = {| statusBarBackgroundColor: ColorValue, |}; -// The View that contains both the actual drawer and the main view -const AndroidDrawerLayout = ((requireNativeComponent( - 'AndroidDrawerLayout', -): any): Class>); - /** * React component that wraps the platform `DrawerLayout` (Android only). The * Drawer (typically used for navigation) is rendered with `renderNavigationView` @@ -185,9 +172,7 @@ class DrawerLayoutAndroid extends React.Component { drawerBackgroundColor: 'white', }; - _nativeRef = React.createRef< - Class>, - >(); + _nativeRef = React.createRef>>(); state = {statusBarBackgroundColor: null}; @@ -229,7 +214,7 @@ class DrawerLayoutAndroid extends React.Component { ); return ( - =0.87.0 site=react_native_android_fb) This comment * suppresses an error found when Flow v0.87 was deployed. To see the @@ -245,7 +230,7 @@ class DrawerLayoutAndroid extends React.Component { onDrawerStateChanged={this._onDrawerStateChanged}> {childrenWrapper} {drawerViewWrapper} - + ); } diff --git a/Libraries/Components/Keyboard/Keyboard.js b/Libraries/Components/Keyboard/Keyboard.js index 4cd19ca398139d..6930a0578fbc12 100644 --- a/Libraries/Components/Keyboard/Keyboard.js +++ b/Libraries/Components/Keyboard/Keyboard.js @@ -37,6 +37,7 @@ export type KeyboardEvent = $ReadOnly<{| easing?: string, endCoordinates: ScreenRect, startCoordinates?: ScreenRect, + isEventFromThisApp?: boolean, |}>; type KeyboardEventListener = (e: KeyboardEvent) => void; diff --git a/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/Libraries/Components/Keyboard/KeyboardAvoidingView.js index 6bfa6df2cc54c1..b8b123c86bb2fa 100644 --- a/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -52,8 +52,6 @@ type State = {| bottom: number, |}; -const viewRef = 'VIEW'; - /** * View that moves out of the way when the keyboard appears by automatically * adjusting its height, position, or bottom padding. @@ -66,10 +64,13 @@ class KeyboardAvoidingView extends React.Component { _frame: ?ViewLayout = null; _subscriptions: Array = []; + viewRef: {current: React.ElementRef | null}; - state = { - bottom: 0, - }; + constructor(props: Props) { + super(props); + this.state = {bottom: 0}; + this.viewRef = React.createRef(); + } _relativeKeyboardHeight(keyboardFrame): number { const frame = this._frame; @@ -159,7 +160,7 @@ class KeyboardAvoidingView extends React.Component { switch (behavior) { case 'height': let heightStyle; - if (this._frame != null) { + if (this._frame != null && this.state.bottom > 0) { // Note that we only apply a height change when there is keyboard present, // i.e. this.state.bottom is greater than 0. If we remove that condition, // this.frame.height will never go back to its original value. @@ -171,7 +172,7 @@ class KeyboardAvoidingView extends React.Component { } return ( { case 'position': return ( @@ -204,7 +205,7 @@ class KeyboardAvoidingView extends React.Component { case 'padding': return ( { default: return ( diff --git a/Libraries/Components/Keyboard/__tests__/Keyboard-test.js b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js new file mode 100644 index 00000000000000..98f8b363521196 --- /dev/null +++ b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js @@ -0,0 +1,93 @@ +/** + * 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 + * @emails oncall+react_native + */ + +'use strict'; + +const Keyboard = require('Keyboard'); +const dismissKeyboard = require('dismissKeyboard'); +const LayoutAnimation = require('LayoutAnimation'); + +const NativeEventEmitter = require('NativeEventEmitter'); +const NativeModules = require('NativeModules'); + +jest.mock('LayoutAnimation'); + +describe('Keyboard', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('exposes KeyboardEventEmitter methods', () => { + const KeyboardObserver = NativeModules.KeyboardObserver; + const KeyboardEventEmitter = new NativeEventEmitter(KeyboardObserver); + + // $FlowFixMe + expect(Keyboard._subscriber).toBe(KeyboardEventEmitter._subscriber); + expect(Keyboard._nativeModule).toBe(KeyboardEventEmitter._nativeModule); + }); + + it('uses dismissKeyboard utility', () => { + expect(Keyboard.dismiss).toBe(dismissKeyboard); + }); + + describe('scheduling layout animation', () => { + const scheduleLayoutAnimation = ( + duration: number | null, + easing: string | null, + ): void => Keyboard.scheduleLayoutAnimation({duration, easing}); + + it('triggers layout animation', () => { + scheduleLayoutAnimation(12, 'spring'); + expect(LayoutAnimation.configureNext).toHaveBeenCalledWith({ + duration: 12, + update: { + duration: 12, + type: 'spring', + }, + }); + }); + + it('does not trigger animation when duration is null', () => { + scheduleLayoutAnimation(null, 'spring'); + expect(LayoutAnimation.configureNext).not.toHaveBeenCalled(); + }); + + it('does not trigger animation when duration is 0', () => { + scheduleLayoutAnimation(0, 'spring'); + expect(LayoutAnimation.configureNext).not.toHaveBeenCalled(); + }); + + describe('animation update type', () => { + const assertAnimationUpdateType = type => + expect(LayoutAnimation.configureNext).toHaveBeenCalledWith( + expect.objectContaining({ + duration: expect.anything(), + update: {duration: expect.anything(), type}, + }), + ); + + it('retrieves type from LayoutAnimation', () => { + scheduleLayoutAnimation(12, 'linear'); + assertAnimationUpdateType('linear'); + }); + + it("defaults to 'keyboard' when key in LayoutAnimation is not found", () => { + scheduleLayoutAnimation(12, 'some-unknown-animation-type'); + assertAnimationUpdateType('keyboard'); + }); + + it("defaults to 'keyboard' when easing is null", () => { + scheduleLayoutAnimation(12, null); + assertAnimationUpdateType('keyboard'); + }); + }); + }); +}); diff --git a/Libraries/Components/MaskedView/MaskedViewIOS.ios.js b/Libraries/Components/MaskedView/MaskedViewIOS.ios.js index ce860ac2eeab40..b8af448da8dc2b 100644 --- a/Libraries/Components/MaskedView/MaskedViewIOS.ios.js +++ b/Libraries/Components/MaskedView/MaskedViewIOS.ios.js @@ -11,13 +11,10 @@ const React = require('React'); const StyleSheet = require('StyleSheet'); const View = require('View'); - -const requireNativeComponent = require('requireNativeComponent'); +const RCTMaskedViewNativeComponent = require('RCTMaskedViewNativeComponent'); import type {ViewProps} from 'ViewPropTypes'; -const RCTMaskedView = requireNativeComponent('RCTMaskedView'); - type Props = $ReadOnly<{| ...ViewProps, @@ -83,12 +80,12 @@ class MaskedViewIOS extends React.Component { } return ( - + {maskElement} {children} - + ); } } diff --git a/Libraries/Components/MaskedView/RCTMaskedViewNativeComponent.js b/Libraries/Components/MaskedView/RCTMaskedViewNativeComponent.js new file mode 100644 index 00000000000000..b2cb472f47b110 --- /dev/null +++ b/Libraries/Components/MaskedView/RCTMaskedViewNativeComponent.js @@ -0,0 +1,24 @@ +/** + * 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 + */ + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, +|}>; + +type RCTMaskedViewNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTMaskedView', +): any): RCTMaskedViewNativeType); diff --git a/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js b/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js new file mode 100644 index 00000000000000..89900c4fac7cfc --- /dev/null +++ b/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js @@ -0,0 +1,47 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {TextStyleProp} from 'StyleSheet'; +import type {NativeComponent} from 'ReactNative'; + +type PickerAndroidChangeEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type Item = $ReadOnly<{| + label: string, + value: ?(number | string), + color?: ?number, +|}>; + +type NativeProps = $ReadOnly<{| + enabled?: ?boolean, + items: $ReadOnlyArray, + mode?: ?('dialog' | 'dropdown'), + onSelect?: (event: PickerAndroidChangeEvent) => void, + selected: number, + prompt?: ?string, + testID?: string, + style?: ?TextStyleProp, + accessibilityLabel?: ?string, +|}>; + +type DialogPickerNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidDialogPicker', +): any): DialogPickerNativeType); diff --git a/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js b/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js new file mode 100644 index 00000000000000..a39f6a223e16dd --- /dev/null +++ b/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js @@ -0,0 +1,47 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {TextStyleProp} from 'StyleSheet'; +import type {NativeComponent} from 'ReactNative'; + +type PickerAndroidChangeEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type Item = $ReadOnly<{| + label: string, + value: ?(number | string), + color?: ?number, +|}>; + +type NativeProps = $ReadOnly<{| + enabled?: ?boolean, + items: $ReadOnlyArray, + mode?: ?('dialog' | 'dropdown'), + onSelect?: (event: PickerAndroidChangeEvent) => void, + selected: number, + prompt?: ?string, + testID?: string, + style?: ?TextStyleProp, + accessibilityLabel?: ?string, +|}>; + +type DropdownPickerNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidDropdownPicker', +): any): DropdownPickerNativeType); diff --git a/Libraries/Components/Picker/PickerAndroid.android.js b/Libraries/Components/Picker/PickerAndroid.android.js index 2c1ae760e27775..af72f74459230b 100644 --- a/Libraries/Components/Picker/PickerAndroid.android.js +++ b/Libraries/Components/Picker/PickerAndroid.android.js @@ -10,14 +10,12 @@ 'use strict'; +const AndroidDropdownPickerNativeComponent = require('AndroidDropdownPickerNativeComponent'); +const AndroidDialogPickerNativeComponent = require('AndroidDialogPickerNativeComponent'); const React = require('React'); const StyleSheet = require('StyleSheet'); const processColor = require('processColor'); -const requireNativeComponent = require('requireNativeComponent'); - -const DropdownPicker = requireNativeComponent('AndroidDropdownPicker'); -const DialogPicker = requireNativeComponent('AndroidDialogPicker'); const REF_PICKER = 'picker'; const MODE_DROPDOWN = 'dropdown'; @@ -49,7 +47,6 @@ type Item = $ReadOnly<{| |}>; type PickerAndroidState = {| - initialSelectedIndex: number, selectedIndex: number, items: $ReadOnlyArray, |}; @@ -62,26 +59,9 @@ class PickerAndroid extends React.Component< PickerAndroidProps, PickerAndroidState, > { - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - constructor(props, context) { - super(props, context); - const state = this._stateFromProps(props); - - this.state = { - ...state, - initialSelectedIndex: state.selectedIndex, - }; - } - - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - UNSAFE_componentWillReceiveProps(nextProps) { - this.setState(this._stateFromProps(nextProps)); - } - - // Translate prop and children into stuff that the native picker understands. - _stateFromProps = props => { + static getDerivedStateFromProps( + props: PickerAndroidProps, + ): PickerAndroidState { let selectedIndex = 0; const items = React.Children.map(props.children, (child, index) => { if (child.props.value === props.selectedValue) { @@ -99,11 +79,15 @@ class PickerAndroid extends React.Component< return childProps; }); return {selectedIndex, items}; - }; + } + + state = PickerAndroid.getDerivedStateFromProps(this.props); render() { const Picker = - this.props.mode === MODE_DROPDOWN ? DropdownPicker : DialogPicker; + this.props.mode === MODE_DROPDOWN + ? AndroidDropdownPickerNativeComponent + : AndroidDialogPickerNativeComponent; const nativeProps = { enabled: this.props.enabled, @@ -111,7 +95,7 @@ class PickerAndroid extends React.Component< mode: this.props.mode, onSelect: this._onChange, prompt: this.props.prompt, - selected: this.state.initialSelectedIndex, + selected: this.state.selectedIndex, testID: this.props.testID, style: [styles.pickerAndroid, this.props.style], /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found @@ -135,19 +119,7 @@ class PickerAndroid extends React.Component< this.props.onValueChange(null, position); } } - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - this._lastNativePosition = event.nativeEvent.position; - this.forceUpdate(); - }; - - componentDidMount() { - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - this._lastNativePosition = this.state.initialSelectedIndex; - } - componentDidUpdate() { // The picker is a controlled component. This means we expect the // on*Change handlers to be in charge of updating our // `selectedValue` prop. That way they can also @@ -156,18 +128,13 @@ class PickerAndroid extends React.Component< // truth, not the native component. if ( this.refs[REF_PICKER] && - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - this.state.selectedIndex !== this._lastNativePosition + this.state.selectedIndex !== event.nativeEvent.position ) { this.refs[REF_PICKER].setNativeProps({ selected: this.state.selectedIndex, }); - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - this._lastNativePosition = this.state.selectedIndex; } - } + }; } const styles = StyleSheet.create({ diff --git a/Libraries/Components/Picker/PickerIOS.ios.js b/Libraries/Components/Picker/PickerIOS.ios.js index 787126356a4974..1f47a53a97a11e 100644 --- a/Libraries/Components/Picker/PickerIOS.ios.js +++ b/Libraries/Components/Picker/PickerIOS.ios.js @@ -18,7 +18,7 @@ const ReactNative = require('ReactNative'); const StyleSheet = require('StyleSheet'); const View = require('View'); const processColor = require('processColor'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTPickerNativeComponent = require('RCTPickerNativeComponent'); import type {SyntheticEvent} from 'CoreEventTypes'; import type {ColorValue} from 'StyleSheetTypes'; @@ -52,10 +52,6 @@ type RCTPickerIOSType = Class< >, >; -const RCTPickerIOS: RCTPickerIOSType = (requireNativeComponent( - 'RCTPicker', -): any); - type Label = Stringish | number; type Props = $ReadOnly<{| @@ -111,7 +107,7 @@ class PickerIOS extends React.Component { render() { return ( - { this._picker = picker; }} diff --git a/Libraries/Components/Picker/RCTPickerNativeComponent.js b/Libraries/Components/Picker/RCTPickerNativeComponent.js new file mode 100644 index 00000000000000..2a3e8b5e0d6af0 --- /dev/null +++ b/Libraries/Components/Picker/RCTPickerNativeComponent.js @@ -0,0 +1,47 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {TextStyleProp} from 'StyleSheet'; +import type {NativeComponent} from 'ReactNative'; + +type PickerIOSChangeEvent = SyntheticEvent< + $ReadOnly<{| + newValue: number | string, + newIndex: number, + |}>, +>; + +type RCTPickerIOSItemType = $ReadOnly<{| + label: ?Label, + value: ?(number | string), + textColor: ?number, +|}>; + +type Label = Stringish | number; + +type RCTPickerIOSType = Class< + NativeComponent< + $ReadOnly<{| + items: $ReadOnlyArray, + onChange: (event: PickerIOSChangeEvent) => void, + onResponderTerminationRequest: () => boolean, + onStartShouldSetResponder: () => boolean, + selectedIndex: number, + style?: ?TextStyleProp, + testID?: ?string, + |}>, + >, +>; + +module.exports = ((requireNativeComponent('RCTPicker'): any): RCTPickerIOSType); diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index efddfdd3b3ab32..616dfc7ed7e07c 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -12,13 +12,10 @@ const React = require('React'); -const requireNativeComponent = require('requireNativeComponent'); +const ProgressBarAndroidNativeComponent = require('ProgressBarAndroidNativeComponent'); -import type {NativeComponent} from 'ReactNative'; import type {ViewProps} from 'ViewPropTypes'; -const AndroidProgressBar = requireNativeComponent('AndroidProgressBar'); - export type ProgressBarAndroidProps = $ReadOnly<{| ...ViewProps, @@ -84,9 +81,9 @@ export type ProgressBarAndroidProps = $ReadOnly<{| */ const ProgressBarAndroid = ( props: ProgressBarAndroidProps, - forwardedRef: ?React.Ref<'AndroidProgressBar'>, + forwardedRef: ?React.Ref, ) => { - return ; + return ; }; const ProgressBarAndroidToExport = React.forwardRef(ProgressBarAndroid); @@ -103,6 +100,4 @@ ProgressBarAndroidToExport.defaultProps = { /* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an * error found when Flow v0.89 was deployed. To see the error, delete this * comment and run Flow. */ -module.exports = (ProgressBarAndroidToExport: Class< - NativeComponent, ->); +module.exports = (ProgressBarAndroidToExport: ProgressBarAndroidNativeComponent); diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js new file mode 100644 index 00000000000000..8e897540f7543d --- /dev/null +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + styleAttr?: string, + typeAttr?: string, + indeterminate: boolean, + progress?: number, + animating?: ?boolean, + color?: ?string, + testID?: ?string, +|}>; + +type ProgressBarAndroidType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidProgressBar', +): any): ProgressBarAndroidType); diff --git a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js index 0c8a7708b5e80e..c54e48a82bac0a 100644 --- a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js +++ b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js @@ -13,9 +13,8 @@ const React = require('React'); const StyleSheet = require('StyleSheet'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTProgressViewNativeComponent = require('RCTProgressViewNativeComponent'); -import type {NativeComponent} from 'ReactNative'; import type {ImageSource} from 'ImageSource'; import type {ColorValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; @@ -54,20 +53,14 @@ type Props = $ReadOnly<{| trackImage?: ?ImageSource, |}>; -type NativeProgressViewIOS = Class>; - -const RCTProgressView = ((requireNativeComponent( - 'RCTProgressView', -): any): NativeProgressViewIOS); - /** * Use `ProgressViewIOS` to render a UIProgressView on iOS. */ const ProgressViewIOS = ( props: Props, - forwardedRef?: ?React.Ref, + forwardedRef?: ?React.Ref, ) => ( - =0.89.0 site=react_native_ios_fb) This comment suppresses an * error found when Flow v0.89 was deployed. To see the error, delete this * comment and run Flow. */ -module.exports = (ProgressViewIOSWithRef: NativeProgressViewIOS); +module.exports = (ProgressViewIOSWithRef: RCTProgressViewNativeComponent); diff --git a/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js b/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js new file mode 100644 index 00000000000000..32b2975a59940a --- /dev/null +++ b/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js @@ -0,0 +1,34 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {NativeComponent} from 'ReactNative'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + progressViewStyle?: ?('default' | 'bar'), + progress?: ?number, + progressTintColor?: ?ColorValue, + trackTintColor?: ?ColorValue, + progressImage?: ?ImageSource, + trackImage?: ?ImageSource, +|}>; + +type NativeProgressViewIOS = Class>; + +module.exports = ((requireNativeComponent( + 'RCTProgressView', +): any): NativeProgressViewIOS); diff --git a/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js b/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js new file mode 100644 index 00000000000000..78558aac78b808 --- /dev/null +++ b/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +const AndroidSwipeRefreshLayout = require('UIManager').getViewManagerConfig( + 'AndroidSwipeRefreshLayout', +); +const RefreshLayoutConsts = AndroidSwipeRefreshLayout + ? AndroidSwipeRefreshLayout.Constants + : {SIZE: {}}; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * Whether the pull to refresh functionality is enabled. + */ + enabled?: ?boolean, + /** + * The colors (at least one) that will be used to draw the refresh indicator. + */ + colors?: ?$ReadOnlyArray, + /** + * The background color of the refresh indicator. + */ + progressBackgroundColor?: ?ColorValue, + /** + * Size of the refresh indicator, see RefreshControl.SIZE. + */ + size?: ?( + | typeof RefreshLayoutConsts.SIZE.DEFAULT + | typeof RefreshLayoutConsts.SIZE.LARGE + ), + /** + * Progress view top offset + */ + progressViewOffset?: ?number, + + /** + * Called when the view starts refreshing. + */ + onRefresh?: ?() => mixed, + + /** + * Whether the view should be indicating an active refresh. + */ + refreshing: boolean, +|}>; + +type AndroidSwipeRefreshLayoutNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidSwipeRefreshLayout', +): any): AndroidSwipeRefreshLayoutNativeType); diff --git a/Libraries/Components/RefreshControl/RCTRefreshControlNativeComponent.js b/Libraries/Components/RefreshControl/RCTRefreshControlNativeComponent.js new file mode 100644 index 00000000000000..a3262d46436e60 --- /dev/null +++ b/Libraries/Components/RefreshControl/RCTRefreshControlNativeComponent.js @@ -0,0 +1,50 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +export type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * The color of the refresh indicator. + */ + tintColor?: ?ColorValue, + /** + * Title color. + */ + titleColor?: ?ColorValue, + /** + * The title displayed under the refresh indicator. + */ + title?: ?string, + + /** + * Called when the view starts refreshing. + */ + onRefresh?: ?() => mixed, + + /** + * Whether the view should be indicating an active refresh. + */ + refreshing: boolean, +|}>; + +type RCTRefreshControlNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTRefreshControl', +): any): RCTRefreshControlNativeType); diff --git a/Libraries/Components/RefreshControl/RefreshControl.js b/Libraries/Components/RefreshControl/RefreshControl.js index ac6853e2addcb5..bbb3246a47d50e 100644 --- a/Libraries/Components/RefreshControl/RefreshControl.js +++ b/Libraries/Components/RefreshControl/RefreshControl.js @@ -14,7 +14,8 @@ const Platform = require('Platform'); const React = require('React'); const {NativeComponent} = require('ReactNative'); -const requireNativeComponent = require('requireNativeComponent'); +const AndroidSwipeRefreshLayoutNativeComponent = require('AndroidSwipeRefreshLayoutNativeComponent'); +const RCTRefreshControlNativeComponent = require('RCTRefreshControlNativeComponent'); const nullthrows = require('nullthrows'); import type {ColorValue} from 'StyleSheetTypes'; @@ -31,12 +32,6 @@ if (Platform.OS === 'android') { } else { RefreshLayoutConsts = {SIZE: {}}; } -type NativeRefreshControlType = Class>; - -const NativeRefreshControl: NativeRefreshControlType = - Platform.OS === 'ios' - ? (requireNativeComponent('RCTRefreshControl'): any) - : (requireNativeComponent('AndroidSwipeRefreshLayout'): any); type IOSProps = $ReadOnly<{| /** @@ -143,7 +138,7 @@ export type RefreshControlProps = $ReadOnly<{| class RefreshControl extends React.Component { static SIZE = RefreshLayoutConsts.SIZE; - _nativeRef: ?React.ElementRef = null; + _setNativePropsOnRef: ?({refreshing: boolean}) => void; _lastNativeRefreshing = false; componentDidMount() { @@ -156,8 +151,11 @@ class RefreshControl extends React.Component { // the js value. if (this.props.refreshing !== prevProps.refreshing) { this._lastNativeRefreshing = this.props.refreshing; - } else if (this.props.refreshing !== this._lastNativeRefreshing) { - nullthrows(this._nativeRef).setNativeProps({ + } else if ( + this.props.refreshing !== this._lastNativeRefreshing && + this._setNativePropsOnRef + ) { + this._setNativePropsOnRef({ refreshing: this.props.refreshing, }); this._lastNativeRefreshing = this.props.refreshing; @@ -165,15 +163,34 @@ class RefreshControl extends React.Component { } render() { - return ( - { - this._nativeRef = ref; - }} - onRefresh={this._onRefresh} - /> - ); + const setRef = ref => + (this._setNativePropsOnRef = ref ? ref.setNativeProps.bind(ref) : null); + if (Platform.OS === 'ios') { + const { + enabled, + colors, + progressBackgroundColor, + size, + progressViewOffset, + ...props + } = this.props; + return ( + + ); + } else { + const {tintColor, titleColor, title, ...props} = this.props; + return ( + + ); + } } _onRefresh = () => { diff --git a/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js b/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js new file mode 100644 index 00000000000000..250c2cd5c255be --- /dev/null +++ b/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js @@ -0,0 +1,25 @@ +/** + * 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 + */ + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + emulateUnlessSupported?: boolean, +|}>; + +type RCTSafeAreaViewNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTSafeAreaView', +): any): RCTSafeAreaViewNativeType); diff --git a/Libraries/Components/SafeAreaView/SafeAreaView.js b/Libraries/Components/SafeAreaView/SafeAreaView.js index 6f4ecf3ac867b4..916852aa42607d 100644 --- a/Libraries/Components/SafeAreaView/SafeAreaView.js +++ b/Libraries/Components/SafeAreaView/SafeAreaView.js @@ -11,7 +11,6 @@ const Platform = require('Platform'); const React = require('React'); const View = require('View'); -const requireNativeComponent = require('requireNativeComponent'); import type {ViewProps} from 'ViewPropTypes'; @@ -39,10 +38,15 @@ if (Platform.OS === 'android') { } }; } else { - const RCTSafeAreaView = requireNativeComponent('RCTSafeAreaView'); + const RCTSafeAreaViewNativeComponent = require('RCTSafeAreaViewNativeComponent'); exported = class SafeAreaView extends React.Component { render(): React.Node { - return ; + return ( + + ); } }; } diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 8a238edaebf735..c237e0a6a4a96d 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -403,8 +403,8 @@ export type Props = $ReadOnly<{| * - `false`, deprecated, use 'never' instead * - `true`, deprecated, use 'always' instead */ - /* $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 + /* $FlowFixMe(>=0.92.0 site=react_native_fb) This comment suppresses an error + * found when Flow v0.92 was deployed. To see the error, delete this comment * and run Flow. */ keyboardShouldPersistTaps?: ?('always' | 'never' | 'handled' | false | true), /** @@ -599,7 +599,11 @@ class ScrollView extends React.Component { * * 3. Mixin methods access other mixin methods via dynamic dispatch using * this. Since mixin methods are bound to the component instance, we need - * to copy all mixin methods to the component instance. + * to copy all mixin methods to the component instance. This is also + * necessary because getScrollResponder() is a public method that returns + * an object that can be used to execute all scrollResponder methods. + * Since the object returned from that method is the ScrollView instance, + * we need to bind all mixin methods to the ScrollView instance. */ for (const key in ScrollResponder.Mixin) { if ( @@ -672,8 +676,14 @@ class ScrollView extends React.Component { * implement this method so that they can be composed while providing access * to the underlying scroll responder's methods. */ - getScrollResponder(): ScrollView { - return this; + getScrollResponder(): { + ...typeof ScrollView, + ...typeof ScrollResponder.Mixin, + } { + return ((this: any): { + ...typeof ScrollView, + ...typeof ScrollResponder.Mixin, + }); } getScrollableNode(): any { @@ -751,7 +761,6 @@ class ScrollView extends React.Component { } _getKeyForIndex(index, childArray) { - // $FlowFixMe Invalid prop usage const child = childArray[index]; return child && child.key; } diff --git a/Libraries/Components/SegmentedControlIOS/RCTSegmentedControlNativeComponent.js b/Libraries/Components/SegmentedControlIOS/RCTSegmentedControlNativeComponent.js new file mode 100644 index 00000000000000..37b944d186fc0a --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/RCTSegmentedControlNativeComponent.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. + * + * @flow + * @format + */ +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {NativeComponent} from 'ReactNative'; + +type Event = SyntheticEvent< + $ReadOnly<{| + value: number, + selectedSegmentIndex: number, + |}>, +>; + +type SegmentedControlIOSProps = $ReadOnly<{| + ...ViewProps, + /** + * The labels for the control's segment buttons, in order. + */ + values?: $ReadOnlyArray, + /** + * The index in `props.values` of the segment to be (pre)selected. + */ + selectedIndex?: ?number, + /** + * Callback that is called when the user taps a segment; + * passes the segment's value as an argument + */ + onValueChange?: ?(value: number) => mixed, + /** + * Callback that is called when the user taps a segment; + * passes the event as an argument + */ + onChange?: ?(event: Event) => mixed, + /** + * If false the user won't be able to interact with the control. + * Default value is true. + */ + enabled?: boolean, + /** + * Accent color of the control. + */ + tintColor?: ?string, + /** + * If true, then selecting a segment won't persist visually. + * The `onValueChange` callback will still work as expected. + */ + momentary?: ?boolean, +|}>; + +type NativeSegmentedControlIOS = Class< + NativeComponent, +>; + +module.exports = ((requireNativeComponent( + 'RCTSegmentedControl', +): any): NativeSegmentedControlIOS); diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js index aa447cf76fa05e..8dc88c1c7af478 100644 --- a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js @@ -13,11 +13,10 @@ const React = require('React'); const StyleSheet = require('StyleSheet'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTSegmentedControlNativeComponent = require('RCTSegmentedControlNativeComponent'); import type {SyntheticEvent} from 'CoreEventTypes'; import type {ViewProps} from 'ViewPropTypes'; -import type {NativeComponent} from 'ReactNative'; type Event = SyntheticEvent< $ReadOnly<{| @@ -64,13 +63,9 @@ type SegmentedControlIOSProps = $ReadOnly<{| type Props = $ReadOnly<{| ...SegmentedControlIOSProps, - forwardedRef: ?React.Ref, + forwardedRef: ?React.Ref, |}>; -type NativeSegmentedControlIOS = Class< - NativeComponent, ->; - /** * Use `SegmentedControlIOS` to render a UISegmentedControl iOS. * @@ -92,10 +87,6 @@ type NativeSegmentedControlIOS = Class< * ```` */ -const RCTSegmentedControl = ((requireNativeComponent( - 'RCTSegmentedControl', -): any): NativeSegmentedControlIOS); - class SegmentedControlIOS extends React.Component { static defaultProps = { values: [], @@ -111,7 +102,7 @@ class SegmentedControlIOS extends React.Component { render() { const {forwardedRef, ...props} = this.props; return ( - , + forwardedRef: ?React.Ref, ) => { return ; }, diff --git a/Libraries/Components/Slider/RCTSliderNativeComponent.js b/Libraries/Components/Slider/RCTSliderNativeComponent.js new file mode 100644 index 00000000000000..c718c67ee2e2d9 --- /dev/null +++ b/Libraries/Components/Slider/RCTSliderNativeComponent.js @@ -0,0 +1,52 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {ColorValue} from 'StyleSheetTypes'; +import type {ImageSource} from 'ImageSource'; +import type {NativeComponent} from 'ReactNative'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {ViewStyleProp} from 'StyleSheet'; + +type Event = SyntheticEvent< + $ReadOnly<{| + value: number, + fromUser?: boolean, + |}>, +>; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + disabled?: ?boolean, + enabled?: ?boolean, + maximumTrackImage?: ?ImageSource, + maximumTrackTintColor?: ?ColorValue, + maximumValue?: ?number, + minimumTrackImage?: ?ImageSource, + minimumTrackTintColor?: ?ColorValue, + minimumValue?: ?number, + onChange?: ?(event: Event) => void, + onSlidingComplete?: ?(event: Event) => void, + onValueChange?: ?(event: Event) => void, + step?: ?number, + testID?: ?string, + thumbImage?: ?ImageSource, + thumbTintColor?: ?ColorValue, + trackImage?: ?ImageSource, + value?: ?number, +|}>; + +type RCTSliderType = Class>; + +module.exports = ((requireNativeComponent('RCTSlider'): any): RCTSliderType); diff --git a/Libraries/Components/Slider/Slider.js b/Libraries/Components/Slider/Slider.js index e768d5f8ff2d8d..34391e6a63b5ff 100644 --- a/Libraries/Components/Slider/Slider.js +++ b/Libraries/Components/Slider/Slider.js @@ -5,26 +5,23 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow strict-local + * @flow */ 'use strict'; -const ReactNative = require('ReactNative'); const Platform = require('Platform'); +const RCTSliderNativeComponent = require('RCTSliderNativeComponent'); const React = require('React'); +const ReactNative = require('ReactNative'); const StyleSheet = require('StyleSheet'); -const requireNativeComponent = require('requireNativeComponent'); - import type {ImageSource} from 'ImageSource'; import type {ViewStyleProp} from 'StyleSheet'; import type {ColorValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; import type {SyntheticEvent} from 'CoreEventTypes'; -const RCTSlider = requireNativeComponent('RCTSlider'); - type Event = SyntheticEvent< $ReadOnly<{| value: number, @@ -60,18 +57,9 @@ type IOSProps = $ReadOnly<{| thumbImage?: ?ImageSource, |}>; -type AndroidProps = $ReadOnly<{| - /** - * Color of the foreground switch grip. - * @platform android - */ - thumbTintColor?: ?ColorValue, -|}>; - type Props = $ReadOnly<{| ...ViewProps, ...IOSProps, - ...AndroidProps, /** * Used to style and layout the `Slider`. See `StyleSheet.js` and @@ -117,6 +105,11 @@ type Props = $ReadOnly<{| * Overrides the default blue gradient image on iOS. */ maximumTrackTintColor?: ?ColorValue, + /** + * The color used to tint the default thumb images on iOS, or the + * color of the foreground switch grip on Android. + */ + thumbTintColor?: ?ColorValue, /** * If true the user won't be able to move the slider. @@ -204,45 +197,43 @@ type Props = $ReadOnly<{| */ const Slider = ( props: Props, - forwardedRef?: ?React.Ref<'RCTActivityIndicatorView'>, + forwardedRef?: ?React.Ref, ) => { const style = StyleSheet.compose( styles.slider, props.style, ); - const onValueChange = - props.onValueChange && - ((event: Event) => { - let userEvent = true; - if (Platform.OS === 'android') { - // On Android there's a special flag telling us the user is - // dragging the slider. - userEvent = - event.nativeEvent.fromUser != null && event.nativeEvent.fromUser; + const {onValueChange, onSlidingComplete, ...localProps} = props; + + const onValueChangeEvent = onValueChange + ? (event: Event) => { + let userEvent = true; + if (Platform.OS === 'android') { + // On Android there's a special flag telling us the user is + // dragging the slider. + userEvent = + event.nativeEvent.fromUser != null && event.nativeEvent.fromUser; + } + userEvent && onValueChange(event.nativeEvent.value); } - props.onValueChange && - userEvent && - props.onValueChange(event.nativeEvent.value); - }); - - const onChange = onValueChange; + : null; - const onSlidingComplete = - props.onSlidingComplete && - ((event: Event) => { - props.onSlidingComplete && - props.onSlidingComplete(event.nativeEvent.value); - }); + const onChangeEvent = onValueChangeEvent; + const onSlidingCompleteEvent = onSlidingComplete + ? (event: Event) => { + onSlidingComplete(event.nativeEvent.value); + } + : null; return ( - true} onResponderTerminationRequest={() => false} diff --git a/Libraries/Components/StatusBar/StatusBar.js b/Libraries/Components/StatusBar/StatusBar.js index 52e6a764d2ca74..0ee780d25bdb99 100644 --- a/Libraries/Components/StatusBar/StatusBar.js +++ b/Libraries/Components/StatusBar/StatusBar.js @@ -182,11 +182,34 @@ function createStackEntry(props: any): any { * * ### Imperative API * - * For cases where using a component is not ideal, there is also an imperative - * API exposed as static functions on the component. It is however not recommended - * to use the static API and the component for the same prop because any value - * set by the static API will get overriden by the one set by the component in - * the next render. + * For cases where using a component is not ideal, there are static methods + * to manipulate the `StatusBar` display stack. These methods have the same + * behavior as mounting and unmounting a `StatusBar` component. + * + * For example, you can call `StatusBar.pushStackEntry` to update the status bar + * before launching a third-party native UI component, and then call + * `StatusBar.popStackEntry` when completed. + * + * ``` + * const openThirdPartyBugReporter = async () => { + * // The bug reporter has a dark background, so we push a new status bar style. + * const stackEntry = StatusBar.pushStackEntry({barStyle: 'light-content'}); + * + * // `open` returns a promise that resolves when the UI is dismissed. + * await BugReporter.open(); + * + * // Don't forget to call `popStackEntry` when you're done. + * StatusBar.popStackEntry(stackEntry); + * }; + * ``` + * + * There is a legacy imperative API that enables you to manually update the + * status bar styles. However, the legacy API does not update the internal + * `StatusBar` display stack, which means that any changes will be overridden + * whenever a `StatusBar` component is mounted or unmounted. + * + * It is strongly advised that you use `pushStackEntry`, `popStackEntry`, or + * `replaceStackEntry` instead of the static methods beginning with `set`. * * ### Constants * @@ -198,7 +221,10 @@ class StatusBar extends React.Component { static _defaultProps = createStackEntry({ animated: false, showHideTransition: 'fade', - backgroundColor: 'black', + backgroundColor: Platform.select({ + android: StatusBarManager.DEFAULT_BACKGROUND_COLOR ?? 'black', + ios: 'black', + }), barStyle: 'default', translucent: false, hidden: false, @@ -297,6 +323,48 @@ class StatusBar extends React.Component { StatusBarManager.setTranslucent(translucent); } + /** + * Push a StatusBar entry onto the stack. + * The return value should be passed to `popStackEntry` when complete. + * + * @param props Object containing the StatusBar props to use in the stack entry. + */ + static pushStackEntry(props: any) { + const entry = createStackEntry(props); + StatusBar._propsStack.push(entry); + StatusBar._updatePropsStack(); + return entry; + } + + /** + * Pop a StatusBar entry from the stack. + * + * @param entry Entry returned from `pushStackEntry`. + */ + static popStackEntry(entry: any) { + const index = StatusBar._propsStack.indexOf(entry); + if (index !== -1) { + StatusBar._propsStack.splice(index, 1); + } + StatusBar._updatePropsStack(); + } + + /** + * Replace an existing StatusBar stack entry with new props. + * + * @param entry Entry returned from `pushStackEntry` to replace. + * @param props Object containing the StatusBar props to use in the replacement stack entry. + */ + static replaceStackEntry(entry: any, props: any) { + const newEntry = createStackEntry(props); + const index = StatusBar._propsStack.indexOf(entry); + if (index !== -1) { + StatusBar._propsStack[index] = newEntry; + } + StatusBar._updatePropsStack(); + return newEntry; + } + static defaultProps = { animated: false, showHideTransition: 'fade', @@ -308,33 +376,27 @@ class StatusBar extends React.Component { // Every time a StatusBar component is mounted, we push it's prop to a stack // and always update the native status bar with the props from the top of then // stack. This allows having multiple StatusBar components and the one that is - // added last or is deeper in the view hierarchy will have priority. - this._stackEntry = createStackEntry(this.props); - StatusBar._propsStack.push(this._stackEntry); - this._updatePropsStack(); + // added last or is deeper in the view hierachy will have priority. + this._stackEntry = StatusBar.pushStackEntry(this.props); } componentWillUnmount() { // When a StatusBar is unmounted, remove itself from the stack and update // the native bar with the next props. - const index = StatusBar._propsStack.indexOf(this._stackEntry); - StatusBar._propsStack.splice(index, 1); - - this._updatePropsStack(); + StatusBar.popStackEntry(this._stackEntry); } componentDidUpdate() { - const index = StatusBar._propsStack.indexOf(this._stackEntry); - this._stackEntry = createStackEntry(this.props); - StatusBar._propsStack[index] = this._stackEntry; - - this._updatePropsStack(); + this._stackEntry = StatusBar.replaceStackEntry( + this._stackEntry, + this.props, + ); } /** * Updates the native status bar with the props from the stack. */ - _updatePropsStack = () => { + static _updatePropsStack = () => { // Send the update to the native module only once at the end of the frame. clearImmediate(StatusBar._updateImmediate); StatusBar._updateImmediate = setImmediate(() => { @@ -352,7 +414,7 @@ class StatusBar extends React.Component { ) { StatusBarManager.setStyle( mergedProps.barStyle.value, - mergedProps.barStyle.animated, + mergedProps.barStyle.animated || false, ); } if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) { diff --git a/Libraries/Components/Switch/SwitchNativeComponent.js b/Libraries/Components/Switch/SwitchNativeComponent.js index 4b43fc3e96cea1..fd46bf64ea236d 100644 --- a/Libraries/Components/Switch/SwitchNativeComponent.js +++ b/Libraries/Components/Switch/SwitchNativeComponent.js @@ -18,25 +18,33 @@ const requireNativeComponent = require('requireNativeComponent'); import type {SwitchChangeEvent} from 'CoreEventTypes'; import type {ViewProps} from 'ViewPropTypes'; +type SwitchProps = $ReadOnly<{| + ...ViewProps, + disabled?: ?boolean, + onChange?: ?(event: SwitchChangeEvent) => mixed, + thumbColor?: ?string, + trackColorForFalse?: ?string, + trackColorForTrue?: ?string, + value?: ?boolean, +|}>; + // @see ReactSwitchManager.java export type NativeAndroidProps = $ReadOnly<{| - ...ViewProps, + ...SwitchProps, + enabled?: ?boolean, on?: ?boolean, - onChange?: ?(event: SwitchChangeEvent) => mixed, thumbTintColor?: ?string, trackTintColor?: ?string, |}>; // @see RCTSwitchManager.m export type NativeIOSProps = $ReadOnly<{| - ...ViewProps, - disabled?: ?boolean, - onChange?: ?(event: SwitchChangeEvent) => mixed, + ...SwitchProps, + onTintColor?: ?string, thumbTintColor?: ?string, tintColor?: ?string, - value?: ?boolean, |}>; type SwitchNativeComponentType = Class< diff --git a/Libraries/Components/Switch/SwitchSchema.js b/Libraries/Components/Switch/SwitchSchema.js new file mode 100644 index 00000000000000..de37583f6175a8 --- /dev/null +++ b/Libraries/Components/Switch/SwitchSchema.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +import type {SchemaType} from '../../../codegen/src/CodegenSchema.js'; + +const SwitchSchema: SchemaType = { + modules: { + Switch: { + components: { + Switch: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + type: 'BooleanTypeAnnotation', + name: 'value', + optional: false, + }, + ], + }, + }, + }, + ], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + { + name: 'value', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + { + name: 'tintColor', + optional: true, + typeAnnotation: { + type: 'NativePrimitiveTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'onTintColor', + optional: true, + typeAnnotation: { + type: 'NativePrimitiveTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'thumbTintColor', + optional: true, + typeAnnotation: { + type: 'NativePrimitiveTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + ], + }, + }, + }, + }, +}; + +module.exports = SwitchSchema; diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.android.js b/Libraries/Components/TabBarIOS/TabBarIOS.android.js deleted file mode 100644 index 81047bfe95d2cb..00000000000000 --- a/Libraries/Components/TabBarIOS/TabBarIOS.android.js +++ /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. - * - * @format - * @flow - */ - -'use strict'; - -const React = require('React'); -const StyleSheet = require('StyleSheet'); -const TabBarItemIOS = require('TabBarItemIOS'); -const View = require('View'); - -let showedDeprecationWarning = false; - -class DummyTabBarIOS extends React.Component<$FlowFixMeProps> { - static Item = TabBarItemIOS; - - componentDidMount() { - if (!showedDeprecationWarning) { - console.warn( - 'TabBarIOS and TabBarItemIOS are deprecated and will be removed in a future release. ' + - 'Please use react-native-tab-view instead.', - ); - - showedDeprecationWarning = true; - } - } - - render() { - return ( - - {this.props.children} - - ); - } -} - -const styles = StyleSheet.create({ - tabGroup: { - flex: 1, - }, -}); - -module.exports = DummyTabBarIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js deleted file mode 100644 index 883ed1bd6e11c3..00000000000000 --- a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +++ /dev/null @@ -1,110 +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 React = require('React'); -const StyleSheet = require('StyleSheet'); -const TabBarItemIOS = require('TabBarItemIOS'); - -const requireNativeComponent = require('requireNativeComponent'); - -import type {ViewProps} from 'ViewPropTypes'; -import type {ColorValue} from 'StyleSheetTypes'; - -const RCTTabBar = requireNativeComponent('RCTTabBar'); - -type Props = $ReadOnly<{| - ...ViewProps, - - /** - * Color of text on unselected tabs - */ - unselectedTintColor?: ColorValue, - - /** - * Color of the currently selected tab icon - */ - tintColor?: ColorValue, - - /** - * Color of unselected tab icons. Available since iOS 10. - */ - unselectedItemTintColor?: ColorValue, - - /** - * Background color of the tab bar - */ - barTintColor?: ColorValue, - - /** - * The style of the tab bar. Supported values are 'default', 'black'. - * Use 'black' instead of setting `barTintColor` to black. This produces - * a tab bar with the native iOS style with higher translucency. - */ - barStyle?: ?('default' | 'black'), - - /** - * A Boolean value that indicates whether the tab bar is translucent - */ - translucent?: ?boolean, - - /** - * Specifies tab bar item positioning. Available values are: - * - fill - distributes items across the entire width of the tab bar - * - center - centers item in the available tab bar space - * - auto (default) - distributes items dynamically according to the - * user interface idiom. In a horizontally compact environment (e.g. iPhone 5) - * this value defaults to `fill`, in a horizontally regular one (e.g. iPad) - * it defaults to center. - */ - itemPositioning?: ?('fill' | 'center' | 'auto'), -|}>; - -let showedDeprecationWarning = false; - -class TabBarIOS extends React.Component { - static Item = TabBarItemIOS; - - componentDidMount() { - if (!showedDeprecationWarning) { - console.warn( - 'TabBarIOS and TabBarItemIOS are deprecated and will be removed in a future release. ' + - 'Please use react-native-tab-view instead.', - ); - - showedDeprecationWarning = true; - } - } - - render() { - return ( - - {this.props.children} - - ); - } -} - -const styles = StyleSheet.create({ - tabGroup: { - flex: 1, - }, -}); - -module.exports = TabBarIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js deleted file mode 100644 index 424978672f78a2..00000000000000 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js +++ /dev/null @@ -1,53 +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'; - -const React = require('React'); -const View = require('View'); -const StyleSheet = require('StyleSheet'); - -let showedDeprecationWarning = false; - -class DummyTab extends React.Component { - componentDidMount() { - if (!showedDeprecationWarning) { - console.warn( - 'TabBarIOS and TabBarItemIOS are deprecated and will be removed in a future release. ' + - 'Please use react-native-tab-view instead.', - ); - - showedDeprecationWarning = true; - } - } - - render() { - if (!this.props.selected) { - return ; - } - return ( - {this.props.children} - ); - } -} - -const styles = StyleSheet.create({ - tab: { - // TODO(5405356): Implement overflow: visible so position: absolute isn't useless - // position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - borderColor: 'red', - borderWidth: 1, - }, -}); - -module.exports = DummyTab; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js deleted file mode 100644 index 873573ad8a025c..00000000000000 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +++ /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. - * - * @format - * @noflow - */ - -'use strict'; - -const React = require('React'); -const StaticContainer = require('StaticContainer.react'); -const StyleSheet = require('StyleSheet'); -const View = require('View'); - -const requireNativeComponent = require('requireNativeComponent'); - -import type {ViewProps} from 'ViewPropTypes'; -import type {ColorValue} from 'StyleSheetTypes'; -import type {SyntheticEvent} from 'CoreEventTypes'; -import type {ImageSource} from 'ImageSource'; - -type Props = $ReadOnly<{| - ...ViewProps, - - /** - * Little red bubble that sits at the top right of the icon. - */ - badge?: ?(string | number), - - /** - * Background color for the badge. Available since iOS 10. - */ - badgeColor?: ColorValue, - - /** - * Items comes with a few predefined system icons. Note that if you are - * using them, the title and selectedIcon will be overridden with the - * system ones. - */ - systemIcon?: ?( - | 'bookmarks' - | 'contacts' - | 'downloads' - | 'favorites' - | 'featured' - | 'history' - | 'more' - | 'most-recent' - | 'most-viewed' - | 'recents' - | 'search' - | 'top-rated' - ), - - /** - * A custom icon for the tab. It is ignored when a system icon is defined. - */ - icon?: ?ImageSource, - - /** - * A custom icon when the tab is selected. It is ignored when a system - * icon is defined. If left empty, the icon will be tinted in blue. - */ - selectedIcon?: ?ImageSource, - - /** - * Callback when this tab is being selected, you should change the state of your - * component to set selected={true}. - */ - onPress?: ?(event: SyntheticEvent) => mixed, - - /** - * If set to true it renders the image as original, - * it defaults to being displayed as a template - */ - renderAsOriginal?: ?boolean, - - /** - * It specifies whether the children are visible or not. If you see a - * blank content, you probably forgot to add a selected one. - */ - selected?: ?boolean, - - /** - * Text that appears under the icon. It is ignored when a system icon - * is defined. - */ - title?: ?string, - - /** - * *(Apple TV only)* When set to true, this view will be focusable - * and navigable using the Apple TV remote. - * - * @platform ios - */ - isTVSelectable?: ?boolean, -|}>; - -type State = {| - hasBeenSelected: boolean, -|}; - -let showedDeprecationWarning = false; - -class TabBarItemIOS extends React.Component { - state = { - hasBeenSelected: false, - }; - - UNSAFE_componentWillMount() { - if (this.props.selected) { - this.setState({hasBeenSelected: true}); - } - } - - UNSAFE_componentWillReceiveProps(nextProps: Props) { - if (this.state.hasBeenSelected || nextProps.selected) { - this.setState({hasBeenSelected: true}); - } - } - - componentDidMount() { - if (!showedDeprecationWarning) { - console.warn( - 'TabBarIOS and TabBarItemIOS are deprecated and will be removed in a future release. ' + - 'Please use react-native-tab-view instead.', - ); - - showedDeprecationWarning = true; - } - } - - render() { - const {style, children, ...props} = this.props; - - // if the tab has already been shown once, always continue to show it so we - // preserve state between tab transitions - let tabContents; - if (this.state.hasBeenSelected) { - tabContents = ( - - {children} - - ); - } else { - tabContents = ; - } - - return ( - - {tabContents} - - ); - } -} - -const styles = StyleSheet.create({ - tab: { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - }, -}); - -const RCTTabBarItem = requireNativeComponent('RCTTabBarItem'); - -module.exports = TabBarItemIOS; diff --git a/Libraries/Components/TextInput/InputAccessoryView.js b/Libraries/Components/TextInput/InputAccessoryView.js index 2e7f906096c3b0..1272dc0f1cc20c 100644 --- a/Libraries/Components/TextInput/InputAccessoryView.js +++ b/Libraries/Components/TextInput/InputAccessoryView.js @@ -14,9 +14,7 @@ const Platform = require('Platform'); const React = require('React'); const StyleSheet = require('StyleSheet'); -const requireNativeComponent = require('requireNativeComponent'); - -const RCTInputAccessoryView = requireNativeComponent('RCTInputAccessoryView'); +const RCTInputAccessoryViewNativeComponent = require('RCTInputAccessoryViewNativeComponent'); import type {ViewStyleProp} from 'StyleSheet'; @@ -100,12 +98,12 @@ class InputAccessoryView extends React.Component { } return ( - {this.props.children} - + ); } } diff --git a/Libraries/Components/TextInput/RCTInputAccessoryViewNativeComponent.js b/Libraries/Components/TextInput/RCTInputAccessoryViewNativeComponent.js new file mode 100644 index 00000000000000..6a220e9b468ac7 --- /dev/null +++ b/Libraries/Components/TextInput/RCTInputAccessoryViewNativeComponent.js @@ -0,0 +1,34 @@ +/** + * 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 type {NativeComponent} from 'ReactNative'; +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewStyleProp} from 'StyleSheet'; + +const React = require('React'); +const requireNativeComponent = require('requireNativeComponent'); + +type NativeProps = $ReadOnly<{| + +children: React.Node, + /** + * An ID which is used to associate this `InputAccessoryView` to + * specified TextInput(s). + */ + nativeID?: ?string, + style?: ?ViewStyleProp, + backgroundColor?: ?ColorValue, +|}>; + +type NativeInputAccessoryView = Class>; + +module.exports = ((requireNativeComponent( + 'RCTInputAccessoryView', +): any): NativeInputAccessoryView); diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 523c25751be33b..296b6ec72dc029 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -782,6 +782,15 @@ const TextInput = createReactClass({ */ inlineImagePadding: PropTypes.number, + /** + * If `true`, allows TextInput to pass touch events to the parent component. + * This allows components such as SwipeableListView to be swipeable from the TextInput on iOS, + * as is the case on Android by default. + * If `false`, TextInput always asks to handle the input (except when disabled). + * @platform ios + */ + rejectResponderTermination: PropTypes.bool, + /** * Determines the types of data converted to clickable URLs in the text input. * Only valid if `multiline={true}` and `editable={false}`. @@ -859,6 +868,7 @@ const TextInput = createReactClass({ getDefaultProps() { return { allowFontScaling: true, + rejectResponderTermination: true, underlineColorAndroid: 'transparent', }; }, @@ -891,6 +901,10 @@ const TextInput = createReactClass({ // tag is null only in unit tests TextInputState.registerInput(tag); } + + if (this.props.autoFocus) { + this._rafId = requestAnimationFrame(this.focus); + } }, componentWillUnmount: function() { @@ -1075,7 +1089,7 @@ const TextInput = createReactClass({ { + async open(options: TimePickerOptions): Promise { return Promise.reject({ message: 'TimePickerAndroid is not supported on this platform.', }); diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js index 2a489a3fa84bb3..e659449be9ac0d 100644 --- a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js +++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js @@ -13,7 +13,7 @@ const React = require('React'); const UIManager = require('UIManager'); -const requireNativeComponent = require('requireNativeComponent'); +const ToolbarAndroidNativeComponent = require('ToolbarAndroidNativeComponent'); const resolveAssetSource = require('resolveAssetSource'); import type {SyntheticEvent} from 'CoreEventTypes'; @@ -58,8 +58,6 @@ import type {NativeComponent} from 'ReactNative'; * [0]: https://developer.android.com/reference/android/support/v7/widget/Toolbar.html */ -const NativeToolbar = requireNativeComponent('ToolbarAndroid'); - type Action = $ReadOnly<{| title: string, icon?: ?ImageSource, @@ -163,7 +161,7 @@ type ToolbarAndroidProps = $ReadOnly<{| type Props = $ReadOnly<{| ...ToolbarAndroidProps, - forwardedRef: ?React.Ref, + forwardedRef: ?React.Ref, |}>; class ToolbarAndroid extends React.Component { @@ -227,7 +225,7 @@ class ToolbarAndroid extends React.Component { } return ( - { const ToolbarAndroidToExport = React.forwardRef( ( props: ToolbarAndroidProps, - forwardedRef: ?React.Ref, + forwardedRef: ?React.Ref, ) => { return ; }, diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroidNativeComponent.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroidNativeComponent.js new file mode 100644 index 00000000000000..f58a58083a8255 --- /dev/null +++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroidNativeComponent.js @@ -0,0 +1,133 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type Action = $ReadOnly<{| + title: string, + icon?: ?ImageSource, + show?: 'always' | 'ifRoom' | 'never', + showWithText?: boolean, +|}>; + +type ToolbarAndroidChangeEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type NativeProps = $ReadOnly<{| + onSelect: (event: ToolbarAndroidChangeEvent) => mixed, + nativeActions?: Array, +|}>; + +type ColorValue = null | string; + +type ToolbarAndroidProps = $ReadOnly<{| + ...ViewProps, + ...NativeProps, + /** + * or text on the right side of the widget. If they don't fit they are placed in an 'overflow' + * Sets possible actions on the toolbar as part of the action menu. These are displayed as icons + * menu. + * + * This property takes an array of objects, where each object has the following keys: + * + * * `title`: **required**, the title of this action + * * `icon`: the icon for this action, e.g. `require('./some_icon.png')` + * * `show`: when to show this action as an icon or hide it in the overflow menu: `always`, + * `ifRoom` or `never` + * * `showWithText`: boolean, whether to show text alongside the icon or not + */ + actions?: ?Array, + /** + * Sets the toolbar logo. + */ + logo?: ?ImageSource, + /** + * Sets the navigation icon. + */ + navIcon?: ?ImageSource, + /** + * Callback that is called when an action is selected. The only argument that is passed to the + * callback is the position of the action in the actions array. + */ + onActionSelected?: ?(position: number) => void, + /** + * Callback called when the icon is selected. + */ + onIconClicked?: ?() => void, + /** + * Sets the overflow icon. + */ + overflowIcon?: ?ImageSource, + /** + * Sets the toolbar subtitle. + */ + subtitle?: ?string, + /** + * Sets the toolbar subtitle color. + */ + subtitleColor?: ?ColorValue, + /** + * Sets the toolbar title. + */ + title?: ?Stringish, + /** + * Sets the toolbar title color. + */ + titleColor?: ?ColorValue, + /** + * Sets the content inset for the toolbar starting edge. + * + * The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for + * these components and can be used to effectively align Toolbar content + * along well-known gridlines. + */ + contentInsetStart?: ?number, + /** + * Sets the content inset for the toolbar ending edge. + * + * The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for + * these components and can be used to effectively align Toolbar content + * along well-known gridlines. + */ + contentInsetEnd?: ?number, + /** + * Used to set the toolbar direction to RTL. + * In addition to this property you need to add + * + * android:supportsRtl="true" + * + * to your application AndroidManifest.xml and then call + * `setLayoutDirection(LayoutDirection.RTL)` in your MainActivity + * `onCreate` method. + */ + rtl?: ?boolean, + /** + * Used to locate this view in end-to-end tests. + */ + testID?: ?string, +|}>; + +type NativeToolbarAndroidProps = Class>; + +module.exports = ((requireNativeComponent( + 'ToolbarAndroid', +): any): NativeToolbarAndroidProps); diff --git a/Libraries/Components/View/ViewNativeComponent.js b/Libraries/Components/View/ViewNativeComponent.js index 0f7d1977b9e690..432040ad3c3a4c 100644 --- a/Libraries/Components/View/ViewNativeComponent.js +++ b/Libraries/Components/View/ViewNativeComponent.js @@ -16,9 +16,6 @@ const requireNativeComponent = require('requireNativeComponent'); import type {ViewProps} from 'ViewPropTypes'; -/* $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. */ type ViewNativeComponentType = Class>; const NativeViewComponent = requireNativeComponent('RCTView'); diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index f1e5cbe79f0618..259dd91ea91da0 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict-local */ 'use strict'; @@ -69,7 +69,7 @@ type DirectEventProps = $ReadOnly<{| * * See http://facebook.github.io/react-native/docs/view.html#onaccessibilityescape */ - onAccessibilityEscape?: ?Function, + onAccessibilityEscape?: ?() => void, |}>; type TouchEventProps = $ReadOnly<{| @@ -205,9 +205,22 @@ type GestureResponderEventProps = $ReadOnly<{| onStartShouldSetResponderCapture?: ?(e: PressEvent) => boolean, |}>; +type AndroidDrawableThemeAttr = $ReadOnly<{| + type: 'ThemeAttrAndroid', + attribute: string, +|}>; + +type AndroidDrawableRipple = $ReadOnly<{| + type: 'RippleAndroid', + color?: ?number, + borderless?: ?boolean, +|}>; + +type AndroidDrawable = AndroidDrawableThemeAttr | AndroidDrawableRipple; + type AndroidViewProps = $ReadOnly<{| - nativeBackgroundAndroid?: ?Object, - nativeForegroundAndroid?: ?Object, + nativeBackgroundAndroid?: ?AndroidDrawable, + nativeForegroundAndroid?: ?AndroidDrawable, /** * Whether this `View` should render itself (and all of its children) into a diff --git a/Libraries/Components/ViewPager/AndroidViewPagerNativeComponent.js b/Libraries/Components/ViewPager/AndroidViewPagerNativeComponent.js new file mode 100644 index 00000000000000..a76092794d144c --- /dev/null +++ b/Libraries/Components/ViewPager/AndroidViewPagerNativeComponent.js @@ -0,0 +1,112 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {NativeComponent} from 'ReactNative'; +import type {Node} from 'React'; +import type {ViewStyleProp} from 'StyleSheet'; + +type PageScrollState = 'idle' | 'dragging' | 'settling'; + +type PageScrollEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + offset: number, + |}>, +>; + +type PageScrollStateChangedEvent = SyntheticEvent< + $ReadOnly<{| + pageScrollState: PageScrollState, + |}>, +>; + +type PageSelectedEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type NativeProps = $ReadOnly<{| + /** + * Index of initial page that should be selected. Use `setPage` method to + * update the page, and `onPageSelected` to monitor page changes + */ + initialPage?: ?number, + + /** + * Executed when transitioning between pages (ether because of animation for + * the requested page change or when user is swiping/dragging between pages) + * The `event.nativeEvent` object for this callback will carry following data: + * - position - index of first page from the left that is currently visible + * - offset - value from range [0,1) describing stage between page transitions. + * Value x means that (1 - x) fraction of the page at "position" index is + * visible, and x fraction of the next page is visible. + */ + onPageScroll?: ?(e: PageScrollEvent) => void, + + /** + * Function called when the page scrolling state has changed. + * The page scrolling state can be in 3 states: + * - idle, meaning there is no interaction with the page scroller happening at the time + * - dragging, meaning there is currently an interaction with the page scroller + * - settling, meaning that there was an interaction with the page scroller, and the + * page scroller is now finishing it's closing or opening animation + */ + onPageScrollStateChanged?: ?(e: PageScrollStateChangedEvent) => void, + + /** + * This callback will be called once ViewPager finish navigating to selected page + * (when user swipes between pages). The `event.nativeEvent` object passed to this + * callback will have following fields: + * - position - index of page that has been selected + */ + onPageSelected?: ?(e: PageSelectedEvent) => void, + + /** + * Blank space to show between pages. This is only visible while scrolling, pages are still + * edge-to-edge. + */ + pageMargin?: ?number, + + /** + * Whether enable showing peekFraction or not. If this is true, the preview of + * last and next page will show in current screen. Defaults to false. + */ + + peekEnabled?: ?boolean, + + /** + * Determines whether the keyboard gets dismissed in response to a drag. + * - 'none' (the default), drags do not dismiss the keyboard. + * - 'on-drag', the keyboard is dismissed when a drag begins. + */ + keyboardDismissMode?: ?('none' | 'on-drag'), + + /** + * When false, the content does not scroll. + * The default value is true. + */ + scrollEnabled?: ?boolean, + + children?: Node, + + style?: ?ViewStyleProp, +|}>; + +type ViewPagerNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidViewPager', +): any): ViewPagerNativeType); diff --git a/Libraries/Components/ViewPager/ViewPagerAndroid.android.js b/Libraries/Components/ViewPager/ViewPagerAndroid.android.js index 8531d7f166a1ee..0c4b2ad41ac5d3 100644 --- a/Libraries/Components/ViewPager/ViewPagerAndroid.android.js +++ b/Libraries/Components/ViewPager/ViewPagerAndroid.android.js @@ -15,9 +15,8 @@ const ReactNative = require('ReactNative'); const UIManager = require('UIManager'); const dismissKeyboard = require('dismissKeyboard'); -const requireNativeComponent = require('requireNativeComponent'); -const NativeAndroidViewPager = requireNativeComponent('AndroidViewPager'); +const NativeAndroidViewPager = require('AndroidViewPagerNativeComponent'); import type {SyntheticEvent} from 'CoreEventTypes'; import type {ViewStyleProp} from 'StyleSheet'; @@ -77,7 +76,7 @@ type Props = $ReadOnly<{| * - settling, meaning that there was an interaction with the page scroller, and the * page scroller is now finishing it's closing or opening animation */ - onPageScrollStateChanged?: ?(e: PageScrollState) => void, + onPageScrollStateChanged?: ?(e: PageScrollStateChangedEvent) => void, /** * This callback will be called once ViewPager finish navigating to selected page @@ -225,7 +224,7 @@ class ViewPagerAndroid extends React.Component { _onPageScrollStateChanged = (e: PageScrollStateChangedEvent) => { if (this.props.onPageScrollStateChanged) { - this.props.onPageScrollStateChanged(e.nativeEvent.pageScrollState); + this.props.onPageScrollStateChanged(e); } }; diff --git a/Libraries/Core/__tests__/MapAndSetPolyfills-test.js b/Libraries/Core/__tests__/MapAndSetPolyfills-test.js new file mode 100644 index 00000000000000..79a73a968789cc --- /dev/null +++ b/Libraries/Core/__tests__/MapAndSetPolyfills-test.js @@ -0,0 +1,104 @@ +/** + * 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 + * @emails oncall+react_native + */ +'use strict'; + +// Save these methods so that we can restore them afterward. +const {freeze, seal, preventExtensions} = Object; + +function setup() { + jest.setMock('../../vendor/core/_shouldPolyfillES6Collection', () => true); + jest.unmock('_wrapObjectFreezeAndFriends'); + require('_wrapObjectFreezeAndFriends'); +} + +function cleanup() { + Object.assign(Object, {freeze, seal, preventExtensions}); +} + +describe('Map polyfill', () => { + setup(); + + const Map = require('Map'); + + it('is not native', () => { + const getCode = Function.prototype.toString.call(Map.prototype.get); + expect(getCode).not.toContain('[native code]'); + expect(getCode).toContain('getIndex'); + }); + + it('should tolerate non-extensible object keys', () => { + const map = new Map(); + const key = Object.create(null); + Object.freeze(key); + map.set(key, key); + expect(map.size).toBe(1); + expect(map.has(key)).toBe(true); + map.delete(key); + expect(map.size).toBe(0); + expect(map.has(key)).toBe(false); + }); + + it('should not get confused by prototypal inheritance', () => { + const map = new Map(); + const proto = Object.create(null); + const base = Object.create(proto); + map.set(proto, proto); + expect(map.size).toBe(1); + expect(map.has(proto)).toBe(true); + expect(map.has(base)).toBe(false); + map.set(base, base); + expect(map.size).toBe(2); + expect(map.get(proto)).toBe(proto); + expect(map.get(base)).toBe(base); + }); + + afterAll(cleanup); +}); + +describe('Set polyfill', () => { + setup(); + + const Set = require('Set'); + + it('is not native', () => { + const addCode = Function.prototype.toString.call(Set.prototype.add); + expect(addCode).not.toContain('[native code]'); + }); + + it('should tolerate non-extensible object elements', () => { + const set = new Set(); + const elem = Object.create(null); + Object.freeze(elem); + set.add(elem); + expect(set.size).toBe(1); + expect(set.has(elem)).toBe(true); + set.add(elem); + expect(set.size).toBe(1); + set.delete(elem); + expect(set.size).toBe(0); + expect(set.has(elem)).toBe(false); + }); + + it('should not get confused by prototypal inheritance', () => { + const set = new Set(); + const proto = Object.create(null); + const base = Object.create(proto); + set.add(proto); + expect(set.size).toBe(1); + expect(set.has(proto)).toBe(true); + expect(set.has(base)).toBe(false); + set.add(base); + expect(set.size).toBe(2); + expect(set.has(proto)).toBe(true); + expect(set.has(base)).toBe(true); + }); + + afterAll(cleanup); +}); diff --git a/Libraries/Core/polyfillES6Collections.js b/Libraries/Core/polyfillES6Collections.js index ca1ee798c73f4c..afbb24ab23ba4e 100644 --- a/Libraries/Core/polyfillES6Collections.js +++ b/Libraries/Core/polyfillES6Collections.js @@ -18,8 +18,10 @@ const {polyfillGlobal} = require('PolyfillFunctions'); */ const _shouldPolyfillCollection = require('_shouldPolyfillES6Collection'); if (_shouldPolyfillCollection('Map')) { + require('_wrapObjectFreezeAndFriends'); polyfillGlobal('Map', () => require('Map')); } if (_shouldPolyfillCollection('Set')) { + require('_wrapObjectFreezeAndFriends'); polyfillGlobal('Set', () => require('Set')); } diff --git a/Libraries/Core/setUpXHR.js b/Libraries/Core/setUpXHR.js index ac528db6d7d50e..ec7baad8af3dcc 100644 --- a/Libraries/Core/setUpXHR.js +++ b/Libraries/Core/setUpXHR.js @@ -28,4 +28,5 @@ polyfillGlobal('WebSocket', () => require('WebSocket')); polyfillGlobal('Blob', () => require('Blob')); polyfillGlobal('File', () => require('File')); polyfillGlobal('FileReader', () => require('FileReader')); -polyfillGlobal('URL', () => require('URL')); +polyfillGlobal('URL', () => require('URL').URL); // flowlint-line untyped-import:off +polyfillGlobal('URLSearchParams', () => require('URL').URLSearchParams); // flowlint-line untyped-import:off diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js index 82e1f7a0ae5e47..0ad96e4d226afc 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js @@ -37,6 +37,10 @@ class SwipeableQuickActionButton extends React.Component<{ * found when Flow v0.82 was deployed. To see the error delete this comment * and run Flow. */ style?: ?DeprecatedViewPropTypes.style, + /* $FlowFixMe(>=0.82.0 site=react_native_fb) This comment suppresses an error + * found when Flow v0.82 was deployed. To see the error delete this comment + * and run Flow. */ + containerStyle?: ?DeprecatedViewPropTypes.style, testID?: string, text?: ?(string | Object | Array), /* $FlowFixMe(>=0.82.0 site=react_native_fb) This comment suppresses an error @@ -64,8 +68,9 @@ class SwipeableQuickActionButton extends React.Component<{ - {mainView} + underlayColor="transparent" + style={this.props.containerStyle}> + {mainView} ); } diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 904538d8a64110..09cec2b3ff53e9 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -353,11 +353,6 @@ - (void)locationManager:(CLLocationManager *)manager [_locationManager stopUpdatingLocation]; } - // Reset location accuracy if desiredAccuracy is changed. - // Otherwise update accuracy will force triggering didUpdateLocations, watchPosition would keeping receiving location updates, even there's no location changes. - if (ABS(_locationManager.desiredAccuracy - RCT_DEFAULT_LOCATION_ACCURACY) > 0.000001) { - _locationManager.desiredAccuracy = RCT_DEFAULT_LOCATION_ACCURACY; - } } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error @@ -389,11 +384,6 @@ - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError * } [_pendingRequests removeAllObjects]; - // Reset location accuracy if desiredAccuracy is changed. - // Otherwise update accuracy will force triggering didUpdateLocations, watchPosition would keeping receiving location updates, even there's no location changes. - if (ABS(_locationManager.desiredAccuracy - RCT_DEFAULT_LOCATION_ACCURACY) > 0.000001) { - _locationManager.desiredAccuracy = RCT_DEFAULT_LOCATION_ACCURACY; - } } static void checkLocationConfig() diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 188d0ef13a1b18..cb3ce45c82d6e9 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -23,9 +23,10 @@ const ImageViewManager = NativeModules.ImageViewManager; const RCTImageView = requireNativeComponent('RCTImageView'); -import type {ImageStyleProp} from 'StyleSheet'; import type {ImageProps as ImagePropsType} from 'ImageProps'; +import type {ImageStyleProp} from 'StyleSheet'; + function getSize( uri: string, success: (width: number, height: number) => void, diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index 0943493c9a283c..a0afd43f4da45a 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -31,8 +31,12 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); + if (!imageSource) { + completionHandler(nil, nil); + return ^{}; + } NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL); - NSUInteger loopCount = 0; + CGFloat loopCount = 0; if ([[properties[(id)kCGImagePropertyGIFDictionary] allKeys] containsObject:(id)kCGImagePropertyGIFLoopCount]) { loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; if (loopCount == 0) { @@ -43,7 +47,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData loopCount += 1; } } - + UIImage *image = nil; size_t imageCount = CGImageSourceGetCount(imageSource); if (imageCount > 1) { @@ -54,6 +58,9 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData for (size_t i = 0; i < imageCount; i++) { CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); + if (!imageRef) { + continue; + } if (!image) { image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; } @@ -64,10 +71,10 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData const NSTimeInterval kDelayTimeIntervalDefault = 0.1; NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime]; if (delayTime == nil) { - if (i == 0) { + if (delays.count == 0) { delayTime = @(kDelayTimeIntervalDefault); } else { - delayTime = delays[i - 1]; + delayTime = delays.lastObject; } } @@ -77,8 +84,8 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData } duration += delayTime.doubleValue; - delays[i] = delayTime; - images[i] = (__bridge_transfer id)imageRef; + [delays addObject:delayTime]; + [images addObject:(__bridge_transfer id)imageRef]; } CFRelease(imageSource); diff --git a/Libraries/Image/RCTImageCache.m b/Libraries/Image/RCTImageCache.m index ca116b5d03af68..9a941f1b889e1a 100644 --- a/Libraries/Image/RCTImageCache.m +++ b/Libraries/Image/RCTImageCache.m @@ -36,18 +36,20 @@ @implementation RCTImageCache - (instancetype)init { - _decodedImageCache = [NSCache new]; - _decodedImageCache.totalCostLimit = 20 * 1024 * 1024; // 20 MB - _cacheStaleTimes = [[NSMutableDictionary alloc] init]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(clearCache) - name:UIApplicationDidReceiveMemoryWarningNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(clearCache) - name:UIApplicationWillResignActiveNotification - object:nil]; + if (self = [super init]) { + _decodedImageCache = [NSCache new]; + _decodedImageCache.totalCostLimit = 20 * 1024 * 1024; // 20 MB + _cacheStaleTimes = [[NSMutableDictionary alloc] init]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(clearCache) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(clearCache) + name:UIApplicationWillResignActiveNotification + object:nil]; + } return self; } @@ -71,7 +73,7 @@ - (void)addImageToCache:(UIImage *)image if (!image) { return; } - CGFloat bytes = image.size.width * image.size.height * image.scale * image.scale * 4; + NSInteger bytes = image.reactDecodedImageBytes; if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) { [self->_decodedImageCache setObject:image forKey:cacheKey diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index 57a6db76da3086..0d611b1b5164dd 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -52,6 +52,11 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock; @property (nonatomic, copy) CAKeyframeAnimation *reactKeyframeAnimation; +/** + * Memory bytes of the image with the default calculation of static image or GIF. Custom calculations of decoded bytes can be assigned manually. + */ +@property (nonatomic, assign) NSInteger reactDecodedImageBytes; + @end @interface RCTImageLoader : NSObject diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index d296637c1fe18e..bb04437a997ee6 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -20,6 +20,17 @@ #import "RCTImageCache.h" #import "RCTImageUtils.h" +static NSInteger RCTImageBytesForImage(UIImage *image) +{ + CAKeyframeAnimation *keyFrameAnimation = [image reactKeyframeAnimation]; + NSInteger singleImageBytes = image.size.width * image.size.height * image.scale * image.scale * 4; + if (keyFrameAnimation) { + return keyFrameAnimation.values.count * singleImageBytes; + } else { + return image.images ? image.images.count * singleImageBytes : singleImageBytes; + } +} + @implementation UIImage (React) - (CAKeyframeAnimation *)reactKeyframeAnimation @@ -32,6 +43,20 @@ - (void)setReactKeyframeAnimation:(CAKeyframeAnimation *)reactKeyframeAnimation objc_setAssociatedObject(self, @selector(reactKeyframeAnimation), reactKeyframeAnimation, OBJC_ASSOCIATION_COPY_NONATOMIC); } +- (NSInteger)reactDecodedImageBytes +{ + NSNumber *imageBytes = objc_getAssociatedObject(self, _cmd); + if (!imageBytes) { + imageBytes = @(RCTImageBytesForImage(self)); + } + return [imageBytes integerValue]; +} + +- (void)setReactDecodedImageBytes:(NSInteger)bytes +{ + objc_setAssociatedObject(self, @selector(reactDecodedImageBytes), @(bytes), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + @end @implementation RCTImageLoader @@ -236,7 +261,7 @@ - (void)setImageCache:(id)cache return image; } -- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest +- (RCTImageLoaderCancellationBlock) loadImageWithURLRequest:(NSURLRequest *)imageURLRequest callback:(RCTImageLoaderCompletionBlock)callback { return [self loadImageWithURLRequest:imageURLRequest diff --git a/Libraries/Image/assetPathUtils.js b/Libraries/Image/assetPathUtils.js index 0b49343518d620..402680cef72779 100644 --- a/Libraries/Image/assetPathUtils.js +++ b/Libraries/Image/assetPathUtils.js @@ -12,26 +12,25 @@ import type {PackagerAsset} from './AssetRegistry'; +const androidScaleSuffix = { + '0.75': 'ldpi', + '1': 'mdpi', + '1.5': 'hdpi', + '2': 'xhdpi', + '3': 'xxhdpi', + '4': 'xxxhdpi', +}; + /** * FIXME: using number to represent discrete scale numbers is fragile in essence because of * floating point numbers imprecision. */ function getAndroidAssetSuffix(scale: number): string { - switch (scale) { - case 0.75: - return 'ldpi'; - case 1: - return 'mdpi'; - case 1.5: - return 'hdpi'; - case 2: - return 'xhdpi'; - case 3: - return 'xxhdpi'; - case 4: - return 'xxxhdpi'; + if (scale.toString() in androidScaleSuffix) { + return androidScaleSuffix[scale.toString()]; } - throw new Error('no such scale'); + + throw new Error('no such scale ' + scale.toString()); } // See https://developer.android.com/guide/topics/resources/drawable-resource.html @@ -52,8 +51,12 @@ function getAndroidResourceFolderName(asset: PackagerAsset, scale: number) { var suffix = getAndroidAssetSuffix(scale); if (!suffix) { throw new Error( - "Don't know which android drawable suffix to use for asset: " + - JSON.stringify(asset), + "Don't know which android drawable suffix to use for scale: " + + scale + + '\nAsset: ' + + JSON.stringify(asset, null, '\t') + + '\nPossible scales are:' + + JSON.stringify(androidScaleSuffix, null, '\t'), ); } const androidFolder = 'drawable-' + suffix; diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index 89cf1af9df877e..56d23c16b1a3d8 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -47,14 +47,20 @@ function findRenderers(): $ReadOnlyArray { function getInspectorDataForViewTag(touchedViewTag: number) { for (let i = 0; i < renderers.length; i++) { const renderer = renderers[i]; - const inspectorData = renderer.getInspectorDataForViewTag(touchedViewTag); - if (inspectorData.hierarchy.length > 0) { - return inspectorData; + if ( + Object.prototype.hasOwnProperty.call( + renderer, + 'getInspectorDataForViewTag', + ) + ) { + const inspectorData = renderer.getInspectorDataForViewTag(touchedViewTag); + if (inspectorData.hierarchy.length > 0) { + return inspectorData; + } } } throw new Error('Expected to find at least one React renderer.'); } - class Inspector extends React.Component< { inspectedViewTag: ?number, @@ -112,9 +118,6 @@ class Inspector extends React.Component< attachToDevtools = (agent: Object) => { let _hideWait = null; const hlSub = agent.sub('highlight', ({node, name, props}) => { - /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.63 was deployed. To see the error delete this - * comment and run Flow. */ clearTimeout(_hideWait); if (typeof node !== 'number') { diff --git a/Libraries/Interaction/__tests__/InteractionManager-test.js b/Libraries/Interaction/__tests__/InteractionManager-test.js index 27647b7a642b07..2d1e0c73984851 100644 --- a/Libraries/Interaction/__tests__/InteractionManager-test.js +++ b/Libraries/Interaction/__tests__/InteractionManager-test.js @@ -260,7 +260,7 @@ describe('promise tasks', () => { expectToBeCalledOnce(task2); }); - const bigAsyncTest = resolve => { + const bigAsyncTest = resolveTest => { jest.useRealTimers(); const task1 = createSequenceTask(1); @@ -298,7 +298,7 @@ describe('promise tasks', () => { expectToBeCalledOnce(task5); expectToBeCalledOnce(task6); - resolve(); + resolveTest(); }, 100); }; diff --git a/Libraries/LayoutAnimation/LayoutAnimation.js b/Libraries/LayoutAnimation/LayoutAnimation.js index 6f0fed1c0690d2..0b84dd5fe1cf20 100644 --- a/Libraries/LayoutAnimation/LayoutAnimation.js +++ b/Libraries/LayoutAnimation/LayoutAnimation.js @@ -10,6 +10,7 @@ 'use strict'; +import Platform from 'Platform'; const UIManager = require('UIManager'); type Type = @@ -42,13 +43,15 @@ function configureNext( config: LayoutAnimationConfig, onAnimationDidEnd?: Function, ) { - UIManager.configureNextLayoutAnimation( - config, - onAnimationDidEnd ?? function() {}, - function() { - /* unused */ - }, - ); + if (!Platform.isTesting) { + UIManager.configureNextLayoutAnimation( + config, + onAnimationDidEnd ?? function() {}, + function() { + /* unused */ + }, + ); + } } function create( diff --git a/Libraries/Lists/ViewabilityHelper.js b/Libraries/Lists/ViewabilityHelper.js index b40718e09fc14d..741fe56b9b895c 100644 --- a/Libraries/Lists/ViewabilityHelper.js +++ b/Libraries/Lists/ViewabilityHelper.js @@ -121,10 +121,13 @@ class ViewabilityHelper { } let firstVisible = -1; const {first, last} = renderRange || {first: 0, last: itemCount - 1}; - invariant( - last < itemCount, - 'Invalid render range ' + JSON.stringify({renderRange, itemCount}), - ); + if (last >= itemCount) { + console.warn( + 'Invalid render range computing viewability ' + + JSON.stringify({renderRange, itemCount}), + ); + return []; + } for (let idx = first; idx <= last; idx++) { const metrics = getFrameMetrics(idx); if (!metrics) { diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index 2e2256287fc604..e314871c4ee13a 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -9,6 +9,7 @@ */ 'use strict'; +const Platform = require('Platform'); const React = require('React'); const View = require('View'); const VirtualizedList = require('VirtualizedList'); @@ -145,7 +146,7 @@ class VirtualizedSectionList extends React.PureComponent< sectionIndex: number, viewPosition?: number, }) { - let index = params.itemIndex + 1; + let index = Platform.OS === 'ios' ? params.itemIndex : params.itemIndex - 1; for (let ii = 0; ii < params.sectionIndex; ii++) { index += this.props.sections[ii].data.length + 2; } diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/Libraries/Lists/__tests__/VirtualizedList-test.js index e6dbe5a7d25a71..a4406fe80ee678 100644 --- a/Libraries/Lists/__tests__/VirtualizedList-test.js +++ b/Libraries/Lists/__tests__/VirtualizedList-test.js @@ -174,9 +174,9 @@ describe('VirtualizedList', () => { const props = { data, renderItem: ({item}) => , - getItem: (data, index) => data[index], - getItemCount: data => data.length, - getItemLayout: (data, index) => ({ + getItem: (items, index) => items[index], + getItemCount: items => items.length, + getItemLayout: (items, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index e39005e7324c16..08a5d74ea857d2 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -20,9 +20,7 @@ const PropTypes = require('prop-types'); const StyleSheet = require('StyleSheet'); const View = require('View'); -const requireNativeComponent = require('requireNativeComponent'); - -const RCTModalHostView = requireNativeComponent('RCTModalHostView'); +const RCTModalHostView = require('RCTModalHostViewNativeComponent'); const ModalEventEmitter = Platform.OS === 'ios' && NativeModules.ModalManager diff --git a/Libraries/Modal/RCTModalHostViewNativeComponent.js b/Libraries/Modal/RCTModalHostViewNativeComponent.js new file mode 100644 index 00000000000000..45c8235bea772f --- /dev/null +++ b/Libraries/Modal/RCTModalHostViewNativeComponent.js @@ -0,0 +1,131 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {NativeComponent} from 'ReactNative'; + +type OrientationChangeEvent = SyntheticEvent< + $ReadOnly<{| + orientation: 'portrait' | 'landscape', + |}>, +>; + +type ModalNativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * The `animationType` prop controls how the modal animates. + * + * See https://facebook.github.io/react-native/docs/modal.html#animationtype + */ + animationType?: ?('none' | 'slide' | 'fade'), + + /** + * The `presentationStyle` prop controls how the modal appears. + * + * See https://facebook.github.io/react-native/docs/modal.html#presentationstyle + */ + presentationStyle?: ?( + | 'fullScreen' + | 'pageSheet' + | 'formSheet' + | 'overFullScreen' + ), + + /** + * The `transparent` prop determines whether your modal will fill the + * entire view. + * + * See https://facebook.github.io/react-native/docs/modal.html#transparent + */ + transparent?: ?boolean, + + /** + * The `hardwareAccelerated` prop controls whether to force hardware + * acceleration for the underlying window. + * + * See https://facebook.github.io/react-native/docs/modal.html#hardwareaccelerated + */ + hardwareAccelerated?: ?boolean, + + /** + * The `visible` prop determines whether your modal is visible. + * + * See https://facebook.github.io/react-native/docs/modal.html#visible + */ + visible?: ?boolean, + + /** + * The `onRequestClose` callback is called when the user taps the hardware + * back button on Android or the menu button on Apple TV. + * + * This is required on Apple TV and Android. + * + * See https://facebook.github.io/react-native/docs/modal.html#onrequestclose + */ + onRequestClose?: ?(event?: SyntheticEvent) => mixed, + + /** + * The `onShow` prop allows passing a function that will be called once the + * modal has been shown. + * + * See https://facebook.github.io/react-native/docs/modal.html#onshow + */ + onShow?: ?(event?: SyntheticEvent) => mixed, + + /** + * The `onDismiss` prop allows passing a function that will be called once + * the modal has been dismissed. + * + * See https://facebook.github.io/react-native/docs/modal.html#ondismiss + */ + onDismiss?: ?() => mixed, + + /** + * Deprecated. Use the `animationType` prop instead. + */ + animated?: ?boolean, + + /** + * The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations. + * + * See https://facebook.github.io/react-native/docs/modal.html#supportedorientations + */ + supportedOrientations?: ?$ReadOnlyArray< + | 'portrait' + | 'portrait-upside-down' + | 'landscape' + | 'landscape-left' + | 'landscape-right', + >, + + /** + * The `onOrientationChange` callback is called when the orientation changes while the modal is being displayed. + * + * See https://facebook.github.io/react-native/docs/modal.html#onorientationchange + */ + onOrientationChange?: ?(event: OrientationChangeEvent) => mixed, + + /** + * The `identifier` is the unique number for identifying Modal components. + */ + identifier?: ?number, +|}>; + +type RCTModalHostViewNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTModalHostView', +): any): RCTModalHostViewNativeType); diff --git a/Libraries/Network/RCTHTTPRequestHandler.mm b/Libraries/Network/RCTHTTPRequestHandler.mm index 863fea857ddfd7..2ddbb8a52b7e1d 100644 --- a/Libraries/Network/RCTHTTPRequestHandler.mm +++ b/Libraries/Network/RCTHTTPRequestHandler.mm @@ -23,13 +23,16 @@ @implementation RCTHTTPRequestHandler } @synthesize bridge = _bridge; +@synthesize methodQueue = _methodQueue; RCT_EXPORT_MODULE() - (void)invalidate { - [_session invalidateAndCancel]; - _session = nil; + dispatch_async(self->_methodQueue, ^{ + [self->_session invalidateAndCancel]; + self->_session = nil; + }); } - (BOOL)isValid @@ -73,8 +76,10 @@ - (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request valueOptions:NSPointerFunctionsStrongMemory capacity:0]; } - - NSURLSessionDataTask *task = [_session dataTaskWithRequest:request]; + __block NSURLSessionDataTask *task = nil; + dispatch_sync(self->_methodQueue, ^{ + task = [self->_session dataTaskWithRequest:request]; + }); { std::lock_guard lock(_mutex); [_delegates setObject:delegate forKey:task]; diff --git a/Libraries/Network/RCTNetInfo.m b/Libraries/Network/RCTNetInfo.m index b3ee23c1796da9..f9441f16b7af65 100644 --- a/Libraries/Network/RCTNetInfo.m +++ b/Libraries/Network/RCTNetInfo.m @@ -52,20 +52,25 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC { RCTNetInfo *self = (__bridge id)info; BOOL didSetReachabilityFlags = [self setReachabilityStatus:flags]; + + NSString *connectionType = self->_connectionType ?: RCTConnectionTypeUnknown; + NSString *effectiveConnectionType = self->_effectiveConnectionType ?: RCTEffectiveConnectionTypeUnknown; + NSString *networkInfo = self->_statusDeprecated ?: RCTReachabilityStateUnknown; + if (self->_firstTimeReachability && self->_resolve) { SCNetworkReachabilityUnscheduleFromRunLoop(self->_firstTimeReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); CFRelease(self->_firstTimeReachability); - self->_resolve(@{@"connectionType": self->_connectionType ?: RCTConnectionTypeUnknown, - @"effectiveConnectionType": self->_effectiveConnectionType ?: RCTEffectiveConnectionTypeUnknown, - @"network_info": self->_statusDeprecated ?: RCTReachabilityStateUnknown}); + self->_resolve(@{@"connectionType": connectionType, + @"effectiveConnectionType": effectiveConnectionType, + @"network_info": networkInfo}); self->_firstTimeReachability = nil; self->_resolve = nil; } if (didSetReachabilityFlags && self->_isObserving) { - [self sendEventWithName:@"networkStatusDidChange" body:@{@"connectionType": self->_connectionType, - @"effectiveConnectionType": self->_effectiveConnectionType, - @"network_info": self->_statusDeprecated}]; + [self sendEventWithName:@"networkStatusDidChange" body:@{@"connectionType": connectionType, + @"effectiveConnectionType": effectiveConnectionType, + @"network_info": networkInfo}]; } } diff --git a/Libraries/RCTTest/RCTSnapshotNativeComponent.js b/Libraries/RCTTest/RCTSnapshotNativeComponent.js new file mode 100644 index 00000000000000..b7dcfa3085be77 --- /dev/null +++ b/Libraries/RCTTest/RCTSnapshotNativeComponent.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type SnapshotReadyEvent = SyntheticEvent< + $ReadOnly<{ + testIdentifier: string, + }>, +>; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + onSnapshotReady?: ?(event: SnapshotReadyEvent) => mixed, + testIdentifier?: ?string, +|}>; + +type SnapshotViewNativeType = Class>; + +const requireNativeComponent = require('requireNativeComponent'); + +module.exports = ((requireNativeComponent('RCTSnapshot'):any): SnapshotViewNativeType); diff --git a/Libraries/RCTTest/SnapshotViewIOS.ios.js b/Libraries/RCTTest/SnapshotViewIOS.ios.js index ef2bbdb687017a..9ed3b53d91374c 100644 --- a/Libraries/RCTTest/SnapshotViewIOS.ios.js +++ b/Libraries/RCTTest/SnapshotViewIOS.ios.js @@ -15,8 +15,6 @@ const StyleSheet = require('StyleSheet'); const UIManager = require('UIManager'); const View = require('View'); -const requireNativeComponent = require('requireNativeComponent'); - const {TestModule} = require('NativeModules'); import type {SyntheticEvent} from 'CoreEventTypes'; @@ -26,7 +24,7 @@ import type {ViewProps} from 'ViewPropTypes'; // if you have linked against RCTTest like in tests, otherwise we will have // a warning printed out const RCTSnapshot = UIManager.getViewManagerConfig('RCTSnapshot') - ? requireNativeComponent('RCTSnapshot') + ? require('RCTSnapshotNativeComponent') : View; type SnapshotReadyEvent = SyntheticEvent< diff --git a/Libraries/ReactNative/AppRegistry.js b/Libraries/ReactNative/AppRegistry.js index 1f99c1025086cb..4cae030d574420 100644 --- a/Libraries/ReactNative/AppRegistry.js +++ b/Libraries/ReactNative/AppRegistry.js @@ -21,6 +21,9 @@ const renderApplication = require('renderApplication'); type Task = (taskData: any) => Promise; type TaskProvider = () => Task; +/* $FlowFixMe(>=0.90.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.90 was deployed. To see the error, delete this + * comment and run Flow. */ export type ComponentProvider = () => React$ComponentType; export type ComponentProviderInstrumentationHook = ( component: ComponentProvider, diff --git a/Libraries/StyleSheet/StyleSheet.js b/Libraries/StyleSheet/StyleSheet.js index 6391c095370aba..7d8fe1a465ebf2 100644 --- a/Libraries/StyleSheet/StyleSheet.js +++ b/Libraries/StyleSheet/StyleSheet.js @@ -25,9 +25,6 @@ import type { ____TextStyleProp_Internal, ____ImageStyle_Internal, ____ImageStyleProp_Internal, - ____LayoutStyle_Internal, - ____ShadowStyle_Internal, - ____TransformStyle_Internal, } from 'StyleSheetTypes'; /** @@ -154,21 +151,12 @@ export type ImageStyle = ____ImageStyle_Internal; */ export type DangerouslyImpreciseStyle = ____DangerouslyImpreciseStyle_Internal; -/** - * These types are simlilar to the style types above. They are objects of the - * possible style keys in that group. For example, ShadowStyle contains - * keys like `shadowColor` and `shadowRadius`. - */ -export type LayoutStyle = ____LayoutStyle_Internal; -export type ShadowStyle = ____ShadowStyle_Internal; -export type TransformStyle = ____TransformStyle_Internal; - let hairlineWidth = PixelRatio.roundToNearestPixel(0.4); if (hairlineWidth === 0) { hairlineWidth = 1 / PixelRatio.get(); } -const absoluteFill: LayoutStyle = { +const absoluteFill = { position: 'absolute', left: 0, right: 0, diff --git a/Libraries/StyleSheet/StyleSheetTypes.js b/Libraries/StyleSheet/StyleSheetTypes.js index 5fd2c1a4d958a4..76199ec1a5d264 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.js +++ b/Libraries/StyleSheet/StyleSheetTypes.js @@ -28,7 +28,7 @@ export type DimensionValue = null | number | string | AnimatedNode; * These properties are a subset of our styles that are consumed by the layout * algorithm and affect the positioning and sizing of views. */ -export type ____LayoutStyle_Internal = $ReadOnly<{| +type ____LayoutStyle_Internal = $ReadOnly<{| /** `display` sets the display type of this component. * * It works similarly to `display` in CSS, but only support 'flex' and 'none'. @@ -460,7 +460,7 @@ export type ____LayoutStyle_Internal = $ReadOnly<{| direction?: 'inherit' | 'ltr' | 'rtl', |}>; -export type ____TransformStyle_Internal = $ReadOnly<{| +type ____TransformStyle_Internal = $ReadOnly<{| /** * `transform` accepts an array of transformation objects. Each object specifies * the property that will be transformed as the key, and the value to use in the diff --git a/Libraries/StyleSheet/__flowtests__/StyleSheet-flowtest.js b/Libraries/StyleSheet/__flowtests__/StyleSheet-flowtest.js index de7d6a6521c797..50b8846bb38870 100644 --- a/Libraries/StyleSheet/__flowtests__/StyleSheet-flowtest.js +++ b/Libraries/StyleSheet/__flowtests__/StyleSheet-flowtest.js @@ -13,7 +13,6 @@ const StyleSheet = require('StyleSheet'); import type {ImageStyleProp, TextStyleProp} from 'StyleSheet'; - const imageStyle = {tintColor: 'rgb(0, 0, 0)'}; const textStyle = {color: 'rgb(0, 0, 0)'}; diff --git a/Libraries/Text/TextProps.js b/Libraries/Text/TextProps.js index 878e42bef6fd3f..f70619f4b0b0c8 100644 --- a/Libraries/Text/TextProps.js +++ b/Libraries/Text/TextProps.js @@ -12,7 +12,7 @@ import type {LayoutEvent, PressEvent, TextLayoutEvent} from 'CoreEventTypes'; import type React from 'React'; -import type {DangerouslyImpreciseStyleProp} from 'StyleSheet'; +import type {TextStyleProp} from 'StyleSheet'; import type { AccessibilityRole, AccessibilityStates, @@ -128,7 +128,7 @@ export type TextProps = $ReadOnly<{ * See https://facebook.github.io/react-native/docs/text.html#selectable */ selectable?: ?boolean, - style?: ?DangerouslyImpreciseStyleProp, + style?: ?TextStyleProp, /** * Used to locate this view in end-to-end tests. diff --git a/Libraries/TurboModule/RCTExport.js b/Libraries/TurboModule/RCTExport.js new file mode 100644 index 00000000000000..bf608d6fe8759a --- /dev/null +++ b/Libraries/TurboModule/RCTExport.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. + * + * @flow strict + * @format + */ + +'use strict'; + +/** + * NOTE: This is React Native specific export type. + * + * RCTExport is an interface type that allows native code generation for React + * Native native modules. It exists as a hint to the codegen tool that any + * interface that extends it needs to be codegen'ed. Example usage: + * + * export interface RCTFoobar extends RCTExport {} + * + * Native definition for RCTFoobar will then be generated. + * + * The type param T is a placeholder for future codegen hinting, like versioning + * information, native base classes, etc. For now, simply use `void` type as + * there's nothing to give hint about. + */ + +// eslint-disable-next-line no-unused-vars +export interface RCTExport { + +getConstants?: () => {}; +} + +// eslint-disable-next-line lint/react-native-modules +export interface TurboModule extends RCTExport {} diff --git a/Libraries/TurboModule/TurboModuleRegistry.js b/Libraries/TurboModule/TurboModuleRegistry.js new file mode 100644 index 00000000000000..33d2ce8b09b2ad --- /dev/null +++ b/Libraries/TurboModule/TurboModuleRegistry.js @@ -0,0 +1,37 @@ +/** + * 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 NativeModules = require('NativeModules'); + +import type {TurboModule} from 'RCTExport'; +import invariant from 'invariant'; + +// TODO +function get(name: string): ?T { + // Backward compatibility layer during migration. + const legacyModule = NativeModules[name]; + if (legacyModule != null) { + return ((legacyModule: any): T); + } + + const module: ?T = global.__turboModuleProxy(name); + return module; +} + +function getEnforcing(name: string): T { + const module = get(name); + invariant(module != null, `${name} is not available in this app.`); + return module; +} + +export {get}; +export {getEnforcing}; diff --git a/Libraries/Utilities/PlatformOS.android.js b/Libraries/Utilities/PlatformOS.android.js deleted file mode 100644 index 8a2962d6083951..00000000000000 --- a/Libraries/Utilities/PlatformOS.android.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 - */ - -'use strict'; - -export type PlatformSelectSpec = {| - android: A, - ios: I, -|}; - -const PlatformOS = { - OS: 'android', - select: (spec: PlatformSelectSpec): A | I => spec.android, -}; - -module.exports = PlatformOS; diff --git a/Libraries/Utilities/PlatformOS.ios.js b/Libraries/Utilities/PlatformOS.ios.js deleted file mode 100644 index 2d1b92b71be043..00000000000000 --- a/Libraries/Utilities/PlatformOS.ios.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 - */ - -'use strict'; - -export type PlatformSelectSpec = {| - android: A, - ios: I, -|}; - -const PlatformOS = { - OS: 'ios', - select: (spec: PlatformSelectSpec): A | I => spec.ios, -}; - -module.exports = PlatformOS; diff --git a/Libraries/WebSocket/RCTSRWebSocket.m b/Libraries/WebSocket/RCTSRWebSocket.m index 7febefecfdee64..96de0d396b28bb 100644 --- a/Libraries/WebSocket/RCTSRWebSocket.m +++ b/Libraries/WebSocket/RCTSRWebSocket.m @@ -217,6 +217,8 @@ @implementation RCTSRWebSocket int _closeCode; BOOL _isPumping; + + BOOL _cleanupScheduled; NSMutableSet *_scheduledRunloops; @@ -324,17 +326,11 @@ - (void)dealloc [_inputStream close]; [_outputStream close]; - - _workQueue = NULL; - + if (_receivedHTTPHeaders) { CFRelease(_receivedHTTPHeaders); _receivedHTTPHeaders = NULL; } - - if (_delegateDispatchQueue) { - _delegateDispatchQueue = NULL; - } } #ifndef NDEBUG @@ -626,11 +622,11 @@ - (void)_failWithError:(NSError *)error; }]; self.readyState = RCTSR_CLOSED; - self->_selfRetain = nil; - + RCTSRLog(@"Failing with error %@", error.localizedDescription); [self _disconnect]; + [self _scheduleCleanup]; } }); } @@ -1036,12 +1032,7 @@ - (void)_pumpWriting; !_sentClose) { _sentClose = YES; - [_outputStream close]; - [_inputStream close]; - - for (NSArray *runLoop in [_scheduledRunloops copy]) { - [self unscheduleFromRunLoop:runLoop[0] forMode:runLoop[1]]; - } + [self _scheduleCleanup]; if (!_failed) { [self _performDelegateBlock:^{ @@ -1050,8 +1041,6 @@ - (void)_pumpWriting; } }]; } - - _selfRetain = nil; } } @@ -1345,94 +1334,142 @@ - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; } } } - - assert(_workQueue != NULL); + + // _workQueue cannot be NULL + if (!_workQueue) { + return; + } + __weak typeof(self) weakSelf = self; dispatch_async(_workQueue, ^{ - switch (eventCode) { - case NSStreamEventOpenCompleted: { - RCTSRLog(@"NSStreamEventOpenCompleted %@", aStream); - if (self.readyState >= RCTSR_CLOSING) { - return; - } - assert(self->_readBuffer); - - if (self.readyState == RCTSR_CONNECTING && aStream == self->_inputStream) { - [self didConnect]; - } - [self _pumpWriting]; - [self _pumpScanner]; - break; - } - - case NSStreamEventErrorOccurred: { - RCTSRLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [aStream.streamError copy]); - // TODO: specify error better! - [self _failWithError:aStream.streamError]; - self->_readBufferOffset = 0; - self->_readBuffer.length = 0; - break; + typeof(self) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + [strongSelf safeHandleEvent:eventCode stream:aStream]; + }); +} +- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream +{ + switch (eventCode) { + case NSStreamEventOpenCompleted: { + RCTSRLog(@"NSStreamEventOpenCompleted %@", aStream); + if (self.readyState >= RCTSR_CLOSING) { + return; } - - case NSStreamEventEndEncountered: { - [self _pumpScanner]; - RCTSRLog(@"NSStreamEventEndEncountered %@", aStream); - if (aStream.streamError) { - [self _failWithError:aStream.streamError]; - } else { - dispatch_async(self->_workQueue, ^{ - if (self.readyState != RCTSR_CLOSED) { - self.readyState = RCTSR_CLOSED; - self->_selfRetain = nil; - } - - if (!self->_sentClose && !self->_failed) { - self->_sentClose = YES; - // If we get closed in this state it's probably not clean because we should be sending this when we send messages - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { - [self.delegate webSocket:self didCloseWithCode:RCTSRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; - } - }]; - } - }); - } - - break; + assert(self->_readBuffer); + + if (self.readyState == RCTSR_CONNECTING && aStream == self->_inputStream) { + [self didConnect]; } - - case NSStreamEventHasBytesAvailable: { - RCTSRLog(@"NSStreamEventHasBytesAvailable %@", aStream); - const int bufferSize = 2048; - uint8_t buffer[bufferSize]; - - while (self->_inputStream.hasBytesAvailable) { - NSInteger bytes_read = [self->_inputStream read:buffer maxLength:bufferSize]; - - if (bytes_read > 0) { - [self->_readBuffer appendBytes:buffer length:bytes_read]; - } else if (bytes_read < 0) { - [self _failWithError:self->_inputStream.streamError]; + [self _pumpWriting]; + [self _pumpScanner]; + break; + } + + case NSStreamEventErrorOccurred: { + RCTSRLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [aStream.streamError copy]); + // TODO: specify error better! + [self _failWithError:aStream.streamError]; + self->_readBufferOffset = 0; + self->_readBuffer.length = 0; + break; + + } + + case NSStreamEventEndEncountered: { + [self _pumpScanner]; + RCTSRLog(@"NSStreamEventEndEncountered %@", aStream); + if (aStream.streamError) { + [self _failWithError:aStream.streamError]; + } else { + dispatch_async(self->_workQueue, ^{ + if (self.readyState != RCTSR_CLOSED) { + self.readyState = RCTSR_CLOSED; + [self _scheduleCleanup]; } - - if (bytes_read != bufferSize) { - break; + + if (!self->_sentClose && !self->_failed) { + self->_sentClose = YES; + // If we get closed in this state it's probably not clean because we should be sending this when we send messages + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:RCTSRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; + } + }]; } - }; - [self _pumpScanner]; - break; + }); } + + break; + } + + case NSStreamEventHasBytesAvailable: { + RCTSRLog(@"NSStreamEventHasBytesAvailable %@", aStream); + const int bufferSize = 2048; + uint8_t buffer[bufferSize]; + + while (self->_inputStream.hasBytesAvailable) { + NSInteger bytes_read = [self->_inputStream read:buffer maxLength:bufferSize]; + + if (bytes_read > 0) { + [self->_readBuffer appendBytes:buffer length:bytes_read]; + } else if (bytes_read < 0) { + [self _failWithError:self->_inputStream.streamError]; + } + + if (bytes_read != bufferSize) { + break; + } + }; + [self _pumpScanner]; + break; + } + + case NSStreamEventHasSpaceAvailable: { + RCTSRLog(@"NSStreamEventHasSpaceAvailable %@", aStream); + [self _pumpWriting]; + break; + } + + default: + RCTSRLog(@"(default) %@", aStream); + break; + } +} - case NSStreamEventHasSpaceAvailable: { - RCTSRLog(@"NSStreamEventHasSpaceAvailable %@", aStream); - [self _pumpWriting]; - break; - } +- (void)_scheduleCleanup +{ + if (_cleanupScheduled) { + return; + } + + _cleanupScheduled = YES; + + // Cleanup NSStream's delegate in the same RunLoop used by the streams themselves: + // This way we'll prevent race conditions between handleEvent and SRWebsocket's dealloc + NSTimer *timer = [NSTimer timerWithTimeInterval:(0.0f) target:self selector:@selector(_cleanupSelfReference:) userInfo:nil repeats:NO]; + [[NSRunLoop RCTSR_networkRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; +} - default: - RCTSRLog(@"(default) %@", aStream); - break; - } +- (void)_cleanupSelfReference:(NSTimer *)timer +{ + // Remove the streams, right now, from the networkRunLoop + [_inputStream close]; + [_outputStream close]; + + // Unschedule from RunLoop + for (NSArray *runLoop in [_scheduledRunloops copy]) { + [self unscheduleFromRunLoop:runLoop[0] forMode:runLoop[1]]; + } + + // Nuke NSStream's delegate + _inputStream.delegate = nil; + _outputStream.delegate = nil; + + // Cleanup selfRetain in the same GCD queue as usual + dispatch_async(_workQueue, ^{ + self->_selfRetain = nil; }); } diff --git a/Libraries/polyfills/console.js b/Libraries/polyfills/console.js index 205f332e566656..7954bcfc01f693 100644 --- a/Libraries/polyfills/console.js +++ b/Libraries/polyfills/console.js @@ -509,6 +509,11 @@ function consoleGroupPolyfill(label) { groupStack.push(GROUP_PAD); } +function consoleGroupCollapsedPolyfill(label) { + global.nativeLoggingHook(groupFormat(GROUP_CLOSE, label), LOG_LEVELS.info); + groupStack.push(GROUP_PAD); +} + function consoleGroupEndPolyfill() { groupStack.pop(); global.nativeLoggingHook(groupFormat(GROUP_CLOSE), LOG_LEVELS.info); @@ -516,6 +521,14 @@ function consoleGroupEndPolyfill() { if (global.nativeLoggingHook) { const originalConsole = global.console; + // Preserve the original `console` as `originalConsole` + if (__DEV__ && originalConsole) { + const descriptor = Object.getOwnPropertyDescriptor(global, 'console'); + if (descriptor) { + Object.defineProperty(global, 'originalConsole', descriptor); + } + } + global.console = { error: getNativeLogFunction(LOG_LEVELS.error), info: getNativeLogFunction(LOG_LEVELS.info), @@ -526,18 +539,13 @@ if (global.nativeLoggingHook) { table: consoleTablePolyfill, group: consoleGroupPolyfill, groupEnd: consoleGroupEndPolyfill, + groupCollapsed: consoleGroupCollapsedPolyfill, }; // If available, also call the original `console` method since that is // sometimes useful. Ex: on OS X, this will let you see rich output in // the Safari Web Inspector console. if (__DEV__ && originalConsole) { - // Preserve the original `console` as `originalConsole` - const descriptor = Object.getOwnPropertyDescriptor(global, 'console'); - if (descriptor) { - Object.defineProperty(global, 'originalConsole', descriptor); - } - Object.keys(console).forEach(methodName => { const reactNativeMethod = console[methodName]; if (originalConsole[methodName]) { diff --git a/Libraries/react-native/react-native-implementation.js b/Libraries/react-native/react-native-implementation.js index 98a2f3887b8680..e9b7e56aba1939 100644 --- a/Libraries/react-native/react-native-implementation.js +++ b/Libraries/react-native/react-native-implementation.js @@ -14,6 +14,7 @@ const invariant = require('invariant'); let showedListViewDeprecation = false; let showedSwipeableListViewDeprecation = false; +let showedWebWiewDeprecation = false; // Export React, plus some native additions. module.exports = { @@ -130,9 +131,6 @@ module.exports = { } return require('SwipeableListView'); }, - get TabBarIOS() { - return require('TabBarIOS'); - }, get Text() { return require('Text'); }, @@ -167,6 +165,15 @@ module.exports = { return require('VirtualizedList'); }, get WebView() { + if (!showedWebWiewDeprecation) { + console.warn( + 'WebView has been extracted from react-native core and will be removed in a future release. ' + + "It can now be installed and imported from 'react-native-webview' instead of 'react-native'. " + + 'See https://github.com/react-native-community/react-native-webview for more informations.', + ); + + showedWebWiewDeprecation = true; + } return require('WebView'); }, diff --git a/Libraries/vendor/core/Map.js b/Libraries/vendor/core/Map.js index 4251479a64e1be..8643cebb9e6fce 100644 --- a/Libraries/vendor/core/Map.js +++ b/Libraries/vendor/core/Map.js @@ -26,6 +26,11 @@ module.exports = (function(global, undefined) { return global.Map; } + // In case this module has not already been evaluated, import it now. + require('./_wrapObjectFreezeAndFriends'); + + const hasOwn = Object.prototype.hasOwnProperty; + /** * == ES6 Map Collection == * @@ -38,15 +43,17 @@ module.exports = (function(global, undefined) { * * https://people.mozilla.org/~jorendorff/es6-draft.html#sec-map-objects * - * There only two -- rather small -- diviations from the spec: + * There only two -- rather small -- deviations from the spec: * - * 1. The use of frozen objects as keys. - * We decided not to allow and simply throw an error. The reason being is - * we store a "hash" on the object for fast access to it's place in the - * internal map entries. - * If this turns out to be a popular use case it's possible to implement by - * overiding `Object.freeze` to store a "hash" property on the object - * for later use with the map. + * 1. The use of untagged frozen objects as keys. + * We decided not to allow and simply throw an error, because this + * implementation of Map works by tagging objects used as Map keys + * with a secret hash property for fast access to the object's place + * in the internal _mapData array. However, to limit the impact of + * this spec deviation, Libraries/Core/InitializeCore.js also wraps + * Object.freeze, Object.seal, and Object.preventExtensions so that + * they tag objects before making them non-extensible, by inserting + * each object into a Map and then immediately removing it. * * 2. The `size` property on a map object is a regular property and not a * computed property on the prototype as described by the spec. @@ -445,7 +452,7 @@ module.exports = (function(global, undefined) { // If the `SECRET_SIZE_PROP` property is already defined then we're not // in the first call to `initMap` (e.g. coming from `map.clear()`) so // all we need to do is reset the size without defining the properties. - if (map.hasOwnProperty(SECRET_SIZE_PROP)) { + if (hasOwn.call(map, SECRET_SIZE_PROP)) { map[SECRET_SIZE_PROP] = 0; } else { Object.defineProperty(map, SECRET_SIZE_PROP, { @@ -524,6 +531,9 @@ module.exports = (function(global, undefined) { const hashProperty = '__MAP_POLYFILL_INTERNAL_HASH__'; let hashCounter = 0; + const nonExtensibleObjects = []; + const nonExtensibleHashes = []; + /** * Get the "hash" associated with an object. * @@ -531,29 +541,29 @@ module.exports = (function(global, undefined) { * @return {number} */ return function getHash(o) { - // eslint-disable-line no-shadow - if (o[hashProperty]) { - return o[hashProperty]; - } else if ( - !isES5 && - o.propertyIsEnumerable && - o.propertyIsEnumerable[hashProperty] - ) { - return o.propertyIsEnumerable[hashProperty]; - } else if (!isES5 && o[hashProperty]) { + if (hasOwn.call(o, hashProperty)) { return o[hashProperty]; } + if (!isES5) { + if (hasOwn.call(o, "propertyIsEnumerable") && + hasOwn.call(o.propertyIsEnumerable, hashProperty)) { + return o.propertyIsEnumerable[hashProperty]; + } + } + if (isExtensible(o)) { - hashCounter += 1; if (isES5) { Object.defineProperty(o, hashProperty, { enumerable: false, writable: false, configurable: false, - value: hashCounter, + value: ++hashCounter, }); - } else if (o.propertyIsEnumerable) { + return hashCounter; + } + + if (o.propertyIsEnumerable) { // Since we can't define a non-enumerable property on the object // we'll hijack one of the less-used non-enumerable properties to // save our hash on it. Additionally, since this is a function it @@ -561,14 +571,19 @@ module.exports = (function(global, undefined) { o.propertyIsEnumerable = function() { return propIsEnumerable.apply(this, arguments); }; - o.propertyIsEnumerable[hashProperty] = hashCounter; - } else { - throw new Error('Unable to set a non-enumerable property on object.'); + return o.propertyIsEnumerable[hashProperty] = ++hashCounter; } - return hashCounter; - } else { - throw new Error('Non-extensible objects are not allowed as keys.'); } + + // If the object is not extensible, fall back to storing it in an + // array and using Array.prototype.indexOf to find it. + let index = nonExtensibleObjects.indexOf(o); + if (index < 0) { + index = nonExtensibleObjects.length; + nonExtensibleObjects[index] = o; + nonExtensibleHashes[index] = ++hashCounter; + } + return nonExtensibleHashes[index]; }; })(); diff --git a/Libraries/vendor/core/_wrapObjectFreezeAndFriends.js b/Libraries/vendor/core/_wrapObjectFreezeAndFriends.js new file mode 100644 index 00000000000000..c3ab8a08556fd5 --- /dev/null +++ b/Libraries/vendor/core/_wrapObjectFreezeAndFriends.js @@ -0,0 +1,37 @@ +/** + * 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. + * + * @author Ben Newman (@benjamn) + * @flow + * @format + */ + +'use strict'; + +let testMap; // Initialized lazily. +function getTestMap() { + return testMap || (testMap = new (require('./Map'))()); +} + +// Wrap Object.{freeze,seal,preventExtensions} so each function adds its +// argument to a Map first, which gives our ./Map.js polyfill a chance to +// tag the object before it becomes non-extensible. +["freeze", "seal", "preventExtensions"].forEach(name => { + const method = Object[name]; + if (typeof method === "function") { + (Object: any)[name] = function (obj) { + try { + // If .set succeeds, also call .delete to avoid leaking memory. + getTestMap().set(obj, obj).delete(obj); + } finally { + // If .set fails, the exception will be silently swallowed + // by this return-from-finally statement, and the method will + // behave exactly as it did before it was wrapped. + return method.call(Object, obj); + } + }; + } +}); diff --git a/Libraries/vendor/core/whatwg-fetch.js b/Libraries/vendor/core/whatwg-fetch.js index 37eaebc4c62e2c..3f1767afda813e 100644 --- a/Libraries/vendor/core/whatwg-fetch.js +++ b/Libraries/vendor/core/whatwg-fetch.js @@ -517,7 +517,7 @@ } if ('responseType' in xhr && support.blob) { - xhr.responseType = 'blob' + xhr.responseType = 'blob'; } request.headers.forEach(function(value, name) { diff --git a/Libraries/vendor/emitter/EventEmitter.js b/Libraries/vendor/emitter/EventEmitter.js index eae9ba7fe3461c..28cf41afc889c3 100644 --- a/Libraries/vendor/emitter/EventEmitter.js +++ b/Libraries/vendor/emitter/EventEmitter.js @@ -176,7 +176,7 @@ class EventEmitter { const subscription = subscriptions[i]; // The subscription may have been removed during this event loop. - if (subscription) { + if (subscription && subscription.listener) { this._currentSubscription = subscription; subscription.listener.apply( subscription.context, diff --git a/RNTester/.eslintrc b/RNTester/.eslintrc new file mode 100644 index 00000000000000..9fd33583936042 --- /dev/null +++ b/RNTester/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "react-native/no-inline-styles": 0 + } +} diff --git a/RNTester/RNTester.xcodeproj/project.pbxproj b/RNTester/RNTester.xcodeproj/project.pbxproj index eb5cf560353f18..370d12836e1146 100644 --- a/RNTester/RNTester.xcodeproj/project.pbxproj +++ b/RNTester/RNTester.xcodeproj/project.pbxproj @@ -1880,6 +1880,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUNDLE_CONFIG = "$(SRCROOT)/../rn-cli.config.js"; DEVELOPMENT_TEAM = VYK7DLU38Z; INFOPLIST_FILE = "$(SRCROOT)/RNTester/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -1895,6 +1896,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUNDLE_CONFIG = "$(SRCROOT)/../rn-cli.config.js"; DEVELOPMENT_TEAM = VYK7DLU38Z; INFOPLIST_FILE = "$(SRCROOT)/RNTester/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; diff --git a/RNTester/RNTester/Info.plist b/RNTester/RNTester/Info.plist index 828b7b591e8847..13573d49b86602 100644 --- a/RNTester/RNTester/Info.plist +++ b/RNTester/RNTester/Info.plist @@ -2,6 +2,8 @@ + UIStatusBarStyle + UIStatusBarStyleBlackTranslucent CFBundleDevelopmentRegion en CFBundleExecutable diff --git a/RNTester/RNTesterIntegrationTests/RNTesterSnapshotTests.m b/RNTester/RNTesterIntegrationTests/RNTesterSnapshotTests.m index 81cb0a86472284..55998e0bfb8bdc 100644 --- a/RNTester/RNTesterIntegrationTests/RNTesterSnapshotTests.m +++ b/RNTester/RNTesterIntegrationTests/RNTesterSnapshotTests.m @@ -23,10 +23,8 @@ @implementation RNTesterSnapshotTests - (void)setUp { _runner = RCTInitRunnerForApp(@"RNTester/js/RNTesterApp.ios", nil, nil); - if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) { - _runner.testSuffix = @"-iOS11"; - } else if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10) { - _runner.testSuffix = @"-iOS10"; + if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10) { + _runner.testSuffix = [NSString stringWithFormat:@"-iOS%d", UIDevice.currentDevice.systemVersion.intValue]; } _runner.recordMode = NO; } @@ -46,7 +44,6 @@ - (void)test##name \ // No switch or slider available on tvOS RCT_TEST(SwitchExample) RCT_TEST(SliderExample) -RCT_TEST(TabBarExample) #endif - (void)testZZZNotInRecordMode diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testARTExample_1-iOS12@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testARTExample_1-iOS12@2x.png new file mode 100644 index 00000000000000..c28625a17b86ce Binary files /dev/null and b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testARTExample_1-iOS12@2x.png differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testLayoutExample_1-iOS12@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testLayoutExample_1-iOS12@2x.png new file mode 100644 index 00000000000000..3557bdb6f47801 Binary files /dev/null and b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testLayoutExample_1-iOS12@2x.png differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testScrollViewExample_1-iOS12@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testScrollViewExample_1-iOS12@2x.png new file mode 100644 index 00000000000000..57767fd280bb7d Binary files /dev/null and b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testScrollViewExample_1-iOS12@2x.png differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testSliderExample_1-iOS12@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testSliderExample_1-iOS12@2x.png new file mode 100644 index 00000000000000..2028f0135c66c6 Binary files /dev/null and b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testSliderExample_1-iOS12@2x.png differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testSwitchExample_1-iOS12@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testSwitchExample_1-iOS12@2x.png new file mode 100644 index 00000000000000..01047454a189f7 Binary files /dev/null and b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testSwitchExample_1-iOS12@2x.png differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS10@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS10@2x.png deleted file mode 100644 index 17e31df9f64946..00000000000000 Binary files a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS10@2x.png and /dev/null differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS10_tvOS.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS10_tvOS.png deleted file mode 100644 index ff8b5baceb9ac0..00000000000000 Binary files a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS10_tvOS.png and /dev/null differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS11@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS11@2x.png deleted file mode 100644 index 13693080878dd4..00000000000000 Binary files a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS11@2x.png and /dev/null differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS11@3x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS11@3x.png deleted file mode 100644 index d3eed81254cd31..00000000000000 Binary files a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1-iOS11@3x.png and /dev/null differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1@2x.png deleted file mode 100644 index 94e0a287d297db..00000000000000 Binary files a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1@2x.png and /dev/null differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1_tvOS.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1_tvOS.png deleted file mode 100644 index ae00d3c4786de1..00000000000000 Binary files a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTabBarExample_1_tvOS.png and /dev/null differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTextExample_1-iOS12@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTextExample_1-iOS12@2x.png new file mode 100644 index 00000000000000..d016b823e5a4bb Binary files /dev/null and b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testTextExample_1-iOS12@2x.png differ diff --git a/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testViewExample_1-iOS12@2x.png b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testViewExample_1-iOS12@2x.png new file mode 100644 index 00000000000000..12a4655cc98039 Binary files /dev/null and b/RNTester/RNTesterIntegrationTests/ReferenceImages/RNTester-js-RNTesterApp.ios/testViewExample_1-iOS12@2x.png differ diff --git a/RNTester/android/app/build.gradle b/RNTester/android/app/build.gradle index 6366c9572e003a..a136cea49e531b 100644 --- a/RNTester/android/app/build.gradle +++ b/RNTester/android/app/build.gradle @@ -130,7 +130,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:appcompat-v7:28.0.0' // Build React Native from source implementation project(':ReactAndroid') diff --git a/RNTester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java b/RNTester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java index ccb6aec0b7aca9..e7a6e8219dab46 100644 --- a/RNTester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java +++ b/RNTester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java @@ -20,9 +20,9 @@ public class RNTesterActivity extends ReactActivity { public static class RNTesterActivityDelegate extends ReactActivityDelegate { private static final String PARAM_ROUTE = "route"; private Bundle mInitialProps = null; - private final @Nullable Activity mActivity; + private final @Nullable ReactActivity mActivity; - public RNTesterActivityDelegate(Activity activity, String mainComponentName) { + public RNTesterActivityDelegate(ReactActivity activity, String mainComponentName) { super(activity, mainComponentName); this.mActivity = activity; } diff --git a/RNTester/e2e/__tests__/Button-test.js b/RNTester/e2e/__tests__/Button-test.js index 5a4bf7fd9f4a86..c43fdb05d6b87f 100644 --- a/RNTester/e2e/__tests__/Button-test.js +++ b/RNTester/e2e/__tests__/Button-test.js @@ -9,34 +9,36 @@ */ /* global device, element, by, expect */ +const { + openComponentWithLabel, + openExampleWithTitle, +} = require('../e2e-helpers'); describe('Button', () => { beforeAll(async () => { await device.reloadReactNative(); - await element(by.id('explorer_search')).replaceText('