mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Compare commits
106
Commits
+1
-1
@@ -98,4 +98,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.281.0
|
||||
^0.286.0
|
||||
|
||||
@@ -17,6 +17,9 @@ runs:
|
||||
- name: Install node dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Set React Native Version
|
||||
# We don't want to set the version for stable branches, because this has been
|
||||
# already set from the 'create release' commits on the release branch.
|
||||
if: ${{ !endsWith(github.ref_name, '-stable') }}
|
||||
shell: bash
|
||||
run: node ./scripts/releases/set-rn-artifacts-version.js --build-type ${{ inputs.release-type }}
|
||||
- name: Setup gradle
|
||||
|
||||
@@ -383,8 +383,7 @@ jobs:
|
||||
runs-on: 8-core-ubuntu
|
||||
needs: [set_release_type]
|
||||
container:
|
||||
# Version is pinned to v18.0 to unblock `run_fantom_tests` - see https://github.com/react-native-community/docker-android/pull/242#issuecomment-3280029122
|
||||
image: reactnativecommunity/react-native-android:v18.0
|
||||
image: reactnativecommunity/react-native-android:latest
|
||||
env:
|
||||
TERM: "dumb"
|
||||
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
|
||||
|
||||
@@ -133,6 +133,7 @@ vendor/
|
||||
# Swift Package build folder
|
||||
/packages/react-native/.build
|
||||
/packages/react-native/.swiftpm
|
||||
/packages/react-native/React/includes/
|
||||
|
||||
# @react-native/codegen
|
||||
/packages/react-native/React/FBReactNativeSpec/
|
||||
|
||||
+38
-2
@@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## v0.81.4
|
||||
|
||||
### Fixed
|
||||
|
||||
#### iOS specific
|
||||
|
||||
- Fix regression that skips artifacts code generation ([f6f5ea0b2e](https://github.com/facebook/react-native/commit/f6f5ea0b2eed894725d8f6e8bd045c99c3182026) by [@kitten](https://github.com/kitten))
|
||||
- Reapplied "Use autolinking-generated react-native-config output in second step of cocoapods linking that generates artifacts and generated source" ([bb73315a3f](https://github.com/facebook/react-native/commit/bb73315a3fd8cd203c4ddb3ffa10bfec307c7e92) by [@kitten](https://github.com/kitten))
|
||||
|
||||
## v0.82.0-rc.4
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Hermes V1:** Fixed coordinates for Hermes V1 artifacts ([674fb77d93](https://github.com/facebook/react-native/commit/674fb77d93dfe67a5fe90449dc71b195b1851e13) by [@j-piasecki](https://github.com/j-piasecki))
|
||||
|
||||
#### iOS specific
|
||||
|
||||
- **Switch:** Fix Switch layout to work with iOS26 ([91d427fe52](https://github.com/facebook/react-native/commit/91d427fe52a307f9aa8579912ef956d61808dad2) by [@cipolleschi](https://github.com/cipolleschi))
|
||||
|
||||
## v0.82.0-rc.3
|
||||
|
||||
### Added
|
||||
|
||||
- **Hermes V1:** Added a dependency on hermes-compiler ([0caf8e70d5](https://github.com/facebook/react-native/commit/0caf8e70d53f611a73ad9b55879dfe0a5f036371) by [@j-piasecki](https://github.com/j-piasecki))
|
||||
|
||||
### Fixed
|
||||
|
||||
#### Android specific
|
||||
|
||||
- **Hermes V1:** Changed the coordinates of hermes artifacts when using Hermes V1 ([d0fb33822d](https://github.com/facebook/react-native/commit/d0fb33822de38183895c8b3421f46966909beaf3) by [@j-piasecki](https://github.com/j-piasecki))
|
||||
|
||||
#### iOS specific
|
||||
|
||||
- **Codegen:** Fix regression that skips artifacts code generation ([f6f5ea0b2e](https://github.com/facebook/react-native/commit/f6f5ea0b2eed894725d8f6e8bd045c99c3182026) by [@kitten](https://github.com/kitten))
|
||||
|
||||
## v0.81.3
|
||||
|
||||
### Fixed
|
||||
@@ -66,6 +101,7 @@
|
||||
- **Error Handling:** Unhandled promises are now handled by ExceptionsManager.handleException, instead of being swallowed as Logbox Warnings. ([c4082c9ce2](https://github.com/facebook/react-native/commit/c4082c9ce208a324c2d011823ca2ba432411aafc) by [@krystofwoldrich](https://github.com/krystofwoldrich))
|
||||
- **`shouldEmitW3CPointerEvents`:** Migrate `shouldPressibilityUseW3CPointerEventsForHover` to common private feature flags and remove `shouldEmitW3CPointerEvents` flag. ([fb4587780e](https://github.com/facebook/react-native/commit/fb4587780e8d6111139d73598a9a26ff392dee28) by [@coado](https://github.com/coado))
|
||||
- **TurboModuleUtils:** Remove unused ReactCommon/TurboModuleUtils functions #deepCopyJSIObject and #deepCopyJSIArray ([ead669ade3](https://github.com/facebook/react-native/commit/ead669ade31ee703c407f96c0ce98d8f2991bdc8) by [@christophpurrer](https://github.com/christophpurrer))
|
||||
- **TurboModules:** Delete old location of CallbackWrapper.h / LongLivedObject.h by ([cb94e71](https://github.com/facebook/react-native/commit/cb94e71845259ae7fa823eb4d21cde74fccd7435) [@christophpurrer](https://github.com/christophpurrer))
|
||||
|
||||
#### Android specific
|
||||
|
||||
@@ -80,7 +116,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- **Animated:** `Animated.CompositeAnomation` is now exposed when using `"react-native-strict-api"` ([024d25794a](https://github.com/facebook/react-native/commit/024d25794a51c94c877c1dfa115a82ebbf559614) by [@huntie](https://github.com/huntie))
|
||||
- **Animated:** `Animated.CompositeAnimation` is now exposed when using `"react-native-strict-api"` ([024d25794a](https://github.com/facebook/react-native/commit/024d25794a51c94c877c1dfa115a82ebbf559614) by [@huntie](https://github.com/huntie))
|
||||
- **Animated:** Allow calling createAnimatedNode without batching ([d9d9a49e18](https://github.com/facebook/react-native/commit/d9d9a49e18f3c51caa18cf7da0a1fcd62f1ecf18) by [@zeyap](https://github.com/zeyap))
|
||||
- **Animated:** Allow filter usage with native animated driver. ([138d0eb01d](https://github.com/facebook/react-native/commit/138d0eb01dbe597261459a37d364d1780c3ef228) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
||||
- **API:** Expose NativeComponentRegistry API as JavaScript root export ([f936780cd5](https://github.com/facebook/react-native/commit/f936780cd5c0c17797f9d2bbc8f5cee81c2eefce) by [@zhongwuzw](https://github.com/zhongwuzw))
|
||||
@@ -117,7 +153,7 @@
|
||||
|
||||
- **AGP:** AGP to 8.12.0 ([742ef3d661](https://github.com/facebook/react-native/commit/742ef3d6615c8c1202e9f683e6127ac97d7a9e23) by [@cortinico](https://github.com/cortinico))
|
||||
- **DevSupportManager:** DevSupport `openDebugger()` methods now accept a `panel: String?` param. Frameworks directly implementing `DevSupportManager` will need to adjust call signatures. ([9dba7112cf](https://github.com/facebook/react-native/commit/9dba7112cfd09b02300869a77dba3dca16f49a28) by [@huntie](https://github.com/huntie))
|
||||
- **Kotlin:**Migrated TextAttributeProps to Kotlin. You might need to update your property access to use camelCase instead of Hungarian notation. ([fa921b3c7b](https://github.com/facebook/react-native/commit/fa921b3c7b289800a79196468f993a0eb0bf693f) by [@mateoguzmana](https://github.com/mateoguzmana))
|
||||
- **Kotlin:** Migrated TextAttributeProps to Kotlin. You might need to update your property access to use camelCase instead of Hungarian notation. ([fa921b3c7b](https://github.com/facebook/react-native/commit/fa921b3c7b289800a79196468f993a0eb0bf693f) by [@mateoguzmana](https://github.com/mateoguzmana))
|
||||
- **Kotlin:** Migrated ReactBaseTextShadowNode to Kotlin. You might need to update your property access to use camelCase instead of Hungarian notation. ([8ccfff9a46](https://github.com/facebook/react-native/commit/8ccfff9a46f317fd78f478c8b3f180441535d1ca) by [@mateoguzmana](https://github.com/mateoguzmana))
|
||||
- **Kotlin:** Migrated com.facebook.react.bridge.Arguments to Kotlin. ([2534aeaddb](https://github.com/facebook/react-native/commit/2534aeaddb0490b69dfaba6b8d316616c7e10a9c) by [@mateoguzmana](https://github.com/mateoguzmana))
|
||||
- **Kotlin:** Migrate `YogaConfig` to Kotlin ([4d5caef76b](https://github.com/facebook/react-native/commit/4d5caef76b83eb7e983364ecc81abb6027e5f98e) by [@mateoguzmana](https://github.com/mateoguzmana))
|
||||
|
||||
Vendored
+19
-16
@@ -3057,30 +3057,30 @@ type util$InspectOptions = {
|
||||
};
|
||||
|
||||
declare type util$ParseArgsOption =
|
||||
| {|
|
||||
| $ReadOnly<{|
|
||||
type: 'boolean',
|
||||
multiple?: false,
|
||||
short?: string,
|
||||
default?: boolean,
|
||||
|}
|
||||
| {|
|
||||
|}>
|
||||
| $ReadOnly<{|
|
||||
type: 'boolean',
|
||||
multiple: true,
|
||||
short?: string,
|
||||
default?: Array<boolean>,
|
||||
|}
|
||||
| {|
|
||||
|}>
|
||||
| $ReadOnly<{|
|
||||
type: 'string',
|
||||
multiple?: false,
|
||||
short?: string,
|
||||
default?: string,
|
||||
|}
|
||||
| {|
|
||||
|}>
|
||||
| $ReadOnly<{|
|
||||
type: 'string',
|
||||
multiple: true,
|
||||
short?: string,
|
||||
default?: Array<string>,
|
||||
|};
|
||||
|}>;
|
||||
|
||||
type util$ParseArgsOptionToValue<TOption> = TOption['type'] extends 'boolean'
|
||||
? TOption['multiple'] extends true
|
||||
@@ -3137,7 +3137,7 @@ declare module 'util' {
|
||||
declare function stripVTControlCharacters(str: string): string;
|
||||
|
||||
declare function parseArgs<
|
||||
TOptions: {[string]: util$ParseArgsOption} = {||},
|
||||
TOptions: {+[string]: util$ParseArgsOption} = {||},
|
||||
>(config: {|
|
||||
args?: Array<string>,
|
||||
options?: TOptions,
|
||||
@@ -3995,13 +3995,16 @@ declare class Process extends events$EventEmitter {
|
||||
initgroups?: (user: number | string, extra_group: number | string) => void;
|
||||
kill(pid: number, signal?: string | number): void;
|
||||
mainModule: Object;
|
||||
memoryUsage(): {
|
||||
arrayBuffers: number,
|
||||
rss: number,
|
||||
heapTotal: number,
|
||||
heapUsed: number,
|
||||
external: number,
|
||||
...
|
||||
memoryUsage: {
|
||||
(): {
|
||||
arrayBuffers: number,
|
||||
rss: number,
|
||||
heapTotal: number,
|
||||
heapUsed: number,
|
||||
external: number,
|
||||
...
|
||||
},
|
||||
rss: () => number,
|
||||
};
|
||||
nextTick: <T>(cb: (...T) => mixed, ...T) => void;
|
||||
pid: number;
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and 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
|
||||
*/
|
||||
|
||||
declare module 'electron-store' {
|
||||
declare export type Schema = any;
|
||||
|
||||
declare export type Options = {
|
||||
+name?: string,
|
||||
defaults?: Object,
|
||||
schema?: any,
|
||||
migrations?: any,
|
||||
beforeEachMigration?: any,
|
||||
clearInvalidConfig?: boolean,
|
||||
serialize?: any,
|
||||
deserialize?: any,
|
||||
accessPropertiesByDotNotation?: boolean,
|
||||
watch?: boolean,
|
||||
encryptionKey?: string | Buffer | $ReadOnlyArray<number>,
|
||||
...
|
||||
};
|
||||
|
||||
declare class ElectronStore {
|
||||
constructor(options?: Options): this;
|
||||
get(key: string): any;
|
||||
get(key: string, defaultValue: any): any;
|
||||
set(key: string, value: any): void;
|
||||
set(object: Object): void;
|
||||
has(key: string): boolean;
|
||||
delete(key: string): void;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
declare module.exports: Class<ElectronStore>;
|
||||
}
|
||||
+1
-1
@@ -86,7 +86,7 @@
|
||||
"eslint-plugin-relay": "^1.8.3",
|
||||
"fb-dotslash": "0.5.8",
|
||||
"flow-api-translator": "0.32.0",
|
||||
"flow-bin": "^0.281.0",
|
||||
"flow-bin": "^0.286.0",
|
||||
"glob": "^7.1.1",
|
||||
"hermes-eslint": "0.32.0",
|
||||
"hermes-transform": "0.32.0",
|
||||
|
||||
@@ -119,7 +119,6 @@ async function runServer(
|
||||
watchFolders,
|
||||
});
|
||||
const {middleware, websocketEndpoints} = createDevMiddleware({
|
||||
projectRoot,
|
||||
serverBaseUrl: devServerUrl,
|
||||
logger: createDevMiddlewareLogger(terminalReporter),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@generated SignedSource<<0b54f75686e4893a5444839bf621a317>>
|
||||
Git revision: 5a792db1225adda206313e9bf751198a1ca7851a
|
||||
@generated SignedSource<<d090ed9a9e781c82884f22944254ed65>>
|
||||
Git revision: 8b84a77b2c82c670886ef064dfc92d6bce83e579
|
||||
Built with --nohooks: false
|
||||
Is local checkout: false
|
||||
Remote URL: https://github.com/facebook/react-native-devtools-frontend
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,71 +1,71 @@
|
||||
#!/usr/bin/env dotslash
|
||||
|
||||
// @generated SignedSource<<9df662721aea6e3774a8677e2a6065e4>>
|
||||
// @generated SignedSource<<80974ea6ac4427cf5fd9f14f6779b5c6>>
|
||||
|
||||
|
||||
{
|
||||
"name": "React Native DevTools",
|
||||
"platforms": {
|
||||
"linux-aarch64": {
|
||||
"size": 116056584,
|
||||
"size": 116072551,
|
||||
"hash": "sha256",
|
||||
"digest": "fac3912f10e3c373c874be6c4696f11e05cbaa754c116db6ea72189afae5efe6",
|
||||
"digest": "3fd69eff02f4a41818a7ecb252ab0de449fa1f88fe0dd1de474bd15dd2e63856",
|
||||
"providers": [
|
||||
{
|
||||
"type": "http",
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQPdlYV_w4y_wxd5Rs8c_F-soLMbY1zwXJjD-JM4TkOtOELyxZGyreTb6dTehp_X6e3qVFZUT05ETRs0MbvqYr4mqdb6dkG9DqNQEVv3TBoHwl7TYSygtHJEs3gXTAtMLYEKW9xXODJlVF-9II7fdTdHU7x7Pyf6NR6S3nv7sKUVD-zKpxF50L2TwUQ"
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQOBrLd2GhnOLBe4XfcroGD8Uh8odfhSuE5y5f6WSUCPAD93HJyyUbNxvU8rneUBXRi6SjftdBfU9ggRGlEdLVoTt3bp1bS8C370rWXH5oZIPkX87SovgSPmlCQygx8OQIUmShnoHVMSa3xArpSdXRdOvqNrwiOsmBkmxJXm_NycW502xfhcL6A6d38"
|
||||
}
|
||||
],
|
||||
"format": "tar.gz",
|
||||
"path": "React Native DevTools-linux-arm64/React Native DevTools"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"size": 115929093,
|
||||
"size": 115924495,
|
||||
"hash": "sha256",
|
||||
"digest": "06bbaeb62ae2e0081d184eba42b9f15be0d0b3f4901142b08a8a1430ff83e722",
|
||||
"digest": "d154ffed0d8a66849da78baebf78e1323ecb8bbeda30d27b82a9b507196266c1",
|
||||
"providers": [
|
||||
{
|
||||
"type": "http",
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQPENbrxrg0RF_TiyxSY_YVYqC1UfRKj_bMfKb3qHUsIhBlya8n3FkbfJqTXTNdqL4riFRulXS5ecXZprtvk_9kao-zY59r3kiTRwobzF0jnjM507_9UOnEHWzG5ZJYzQyOtS3kQmT0HwZabVj9qw38OB9mk-MPVCZIPZay2PRnIThBzpKDoOOP9"
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQPp7lTIJZPh1_Bz1ONneTDc_i5ScY8TLzgHe4DzvYz6e7z-QHdBKhUNbNVBInrYrJaPKPum8FOGcvFW9JsvPNpsdMiAbjivdKQPAEkGOZ-unSOqL1dPPjW-yZYSBGucXmcgQiXVaDhqG3Wcu04JLsFKlKb9QvAZZ-tyN72E8ndAQ0mgDyhzEhwB"
|
||||
}
|
||||
],
|
||||
"format": "tar.gz",
|
||||
"path": "React Native DevTools-linux-x64/React Native DevTools"
|
||||
},
|
||||
"macos-aarch64": {
|
||||
"size": 110891603,
|
||||
"size": 110891525,
|
||||
"hash": "sha256",
|
||||
"digest": "eeb9cc1399c0c38c429848dbf622f1b46e88d7d97788dcdc4c30a9fcce37705c",
|
||||
"digest": "b05e1ae5528dcfcff5178d943d90128c9bf28ecd89da4dc303fd4ca748e5fc88",
|
||||
"providers": [
|
||||
{
|
||||
"type": "http",
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQN9bd7nbXi-uF_MooJEiUL2jJVifMqgQxZ236aqbSdiJe5Uzjud5ANf0LDAl8GDVVRShd6x4B-gsZ-qRowH0qJikoGatQIkBgyzp4i8ors52etGOIIwdAxdNIng5Vtp441j2_N__btkJqlHcMMbbqvO8fg9oeYTlhMpOx3MLLWXIlG7ix5IFUDiYChf"
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQMfoVd8CCoqS0ETntAOKhks2FtIDsyoPEeJHLIc-bxPESxTSHf02xDSSM-vjCG9gBitObq3iZECz1LGwbYS9qrjqXdrf7oljIWzK-eszvQwDe0wzCsywqRXBtLlDBb5h8a1WxjA_scRqoGCDbvk7-XovnJNdmTX4vq0CFb8QUUcEz31sVQ_v5IRjDdF"
|
||||
}
|
||||
],
|
||||
"format": "tar.gz",
|
||||
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
|
||||
},
|
||||
"macos-x86_64": {
|
||||
"size": 117770388,
|
||||
"size": 117767014,
|
||||
"hash": "sha256",
|
||||
"digest": "a65e446e526502b267cbe6800ae031e4e1b5b0aca21412152a66ffe1d3a29410",
|
||||
"digest": "6aa15f6dc13bea46503fcad1b9ba93b3007ea659abb63a2f6bf9b34c3abf834a",
|
||||
"providers": [
|
||||
{
|
||||
"type": "http",
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQN29ukKLZS48QcCU-gms06YFnG-fxxDfrWTup-Z6KoIAP-QEu3sFzrpqeZz4g-jFsh3o-IIoHJsdbu4a2U7ROjPfWGguGJrPK8fdc3iV67Qw1_VAPU13dm0dI1DbSY50ah05wh43jdBG3LGCAYYJ-W8_mZMC3HqM2v-8KVd-DoPT4hOh1s9CCFgEQ"
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQM57KwrYx0XeV3M8JrX_StgCFVbMHB5L-ShQfzIdoG4jfz71Z7U7CtK5FZ72Uwzm9wmzx5n0-w_3P8DRd24gvMWUSaN8QNdyxIUxd1n0BqJRYsvsbc8w2YpNeWadcce701zV5dYgQg7PIjPzDSuhYWM_7ih_QtZIBIHUUjCtr0sJBUYZVaqBQag6Q"
|
||||
}
|
||||
],
|
||||
"format": "tar.gz",
|
||||
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"size": 125526370,
|
||||
"size": 125530089,
|
||||
"hash": "sha256",
|
||||
"digest": "26b190c0f85249dee91999e020b8fe7ffd5c007458a2103ed3822558861dbe87",
|
||||
"digest": "da1747672dd222b1389f6ad9e1c67778ea0bc4e8859ddee23222a92bcaaed48f",
|
||||
"providers": [
|
||||
{
|
||||
"type": "http",
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQO0mdIJx1eYiSDJNiAZ2soaeECILayWnvOeiey-fpS4ydDTreAMfwL4QZ5GZo4-AsGwOXCSdF3hBMM2ufnvw6BDFg5UEiTc4DALJu7o6YBBG4wlVZbI-2kkaXd7u8RhBQPjmhsdcX3sDmCGmSr3WK9EkTRKStE2lb6ilOn-969h4dYWekwUUCSX"
|
||||
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQN_2xTT04YUkfSPrA0LaBIHmjUTCCxBkiTgUpb3TUn02lpaFas2x3asL1LeHpZHlUmb_NiLE0PLZYg3T232fdIbmLaquPST7lQpsXDDMG-ykGK7-xxfK5CMU608Pd5Crw5YZBPF3O2PCtiIIcR85tWSqmyWYuoX-PnIhLnBWHjbpNcMcj9RkVqB"
|
||||
}
|
||||
],
|
||||
"format": "tar.gz",
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "37.2.6",
|
||||
"electron-store": "^8.2.0",
|
||||
"semver": "^7.1.3"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -8,13 +8,15 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
// $FlowFixMe[unclear-type] We have no Flow types for the Electron API.
|
||||
const {BrowserWindow, Menu, app, shell, ipcMain} = require('electron') as any;
|
||||
const Store = require('electron-store');
|
||||
import SettingsStore from './SettingsStore.js';
|
||||
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
const appSettings = new Store();
|
||||
// $FlowFixMe[unclear-type] We have no Flow types for the Electron API.
|
||||
const {BrowserWindow, Menu, app, shell, ipcMain} = require('electron') as any;
|
||||
|
||||
const appSettings = new SettingsStore();
|
||||
const windowMetadata = new WeakMap<
|
||||
typeof BrowserWindow,
|
||||
$ReadOnly<{
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and 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
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
// $FlowFixMe[unclear-type] We have no Flow types for the Electron API.
|
||||
const {app} = require('electron') as any;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
type Options = $ReadOnly<{
|
||||
name?: string,
|
||||
defaults?: Object,
|
||||
}>;
|
||||
|
||||
/**
|
||||
* A data persistence layer for storing application settings, modelled after
|
||||
* [`electron-store`](https://www.npmjs.com/package/electron-store).
|
||||
*
|
||||
* Values are saved in a `config.json` file in `app.getPath('userData')`.
|
||||
*
|
||||
* Compatibility:
|
||||
* - Maintains API and file format compatibility with `electron-store@8.2.0`.
|
||||
* - Supports the Electron main process only.
|
||||
*/
|
||||
export default class SettingsStore {
|
||||
#defaultValues: Object = {};
|
||||
path: string;
|
||||
|
||||
constructor(options: Options = {}) {
|
||||
options = {
|
||||
name: 'config',
|
||||
...options,
|
||||
};
|
||||
this.#defaultValues = {
|
||||
...this.#defaultValues,
|
||||
...options.defaults,
|
||||
};
|
||||
this.path = path.resolve(
|
||||
app.getPath('userData'),
|
||||
`${options.name ?? 'config'}.json`,
|
||||
);
|
||||
}
|
||||
|
||||
get(key: string, defaultValue?: any): any {
|
||||
const store = this.store;
|
||||
return store[key] !== undefined ? store[key] : defaultValue;
|
||||
}
|
||||
|
||||
set(key: string, value: any): void {
|
||||
const {store} = this;
|
||||
if (typeof key === 'object') {
|
||||
const object = key;
|
||||
for (const [k, v] of Object.entries(object)) {
|
||||
store[k] = v;
|
||||
}
|
||||
} else {
|
||||
store[key] = value;
|
||||
}
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return key in this.store;
|
||||
}
|
||||
|
||||
reset(...keys: Array<string>): void {
|
||||
for (const key of keys) {
|
||||
if (this.#defaultValues[key] != null) {
|
||||
this.set(key, this.#defaultValues[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete(key: string): void {
|
||||
const {store} = this;
|
||||
delete store[key];
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store = {};
|
||||
for (const key of Object.keys(this.#defaultValues)) {
|
||||
this.reset(key);
|
||||
}
|
||||
}
|
||||
|
||||
get store(): {[string]: mixed} {
|
||||
try {
|
||||
const data = fs.readFileSync(this.path, 'utf8');
|
||||
const deserializedData = this._deserialize(data);
|
||||
return {
|
||||
...((deserializedData: any): {[string]: mixed}),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
this._ensureDirectory();
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
set store(value: mixed): void {
|
||||
this._ensureDirectory();
|
||||
this._write(value);
|
||||
}
|
||||
|
||||
_deserialize = (value: string): mixed => JSON.parse(value);
|
||||
_serialize = (value: mixed): string =>
|
||||
JSON.stringify(value, undefined, '\t') ?? '';
|
||||
|
||||
_ensureDirectory(): void {
|
||||
fs.mkdirSync(path.dirname(this.path), {recursive: true});
|
||||
}
|
||||
|
||||
_write(value: mixed): void {
|
||||
const data = this._serialize(value);
|
||||
|
||||
fs.writeFileSync(this.path, data, {mode: 0o666});
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,9 @@ app.setVersion(pkg.version + '-' + buildInfo.revision);
|
||||
const {
|
||||
values: {version = false},
|
||||
} = util.parseArgs({
|
||||
options: {version: {type: 'boolean'}},
|
||||
options: {
|
||||
version: {type: 'boolean'},
|
||||
},
|
||||
args: process.argv.slice(app.isPackaged ? 1 : 2),
|
||||
strict: false,
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ async function unstable_spawnDebuggerShellWithArgs(
|
||||
{
|
||||
mode = 'detached',
|
||||
flavor = 'prebuilt',
|
||||
prebuiltBinaryPath,
|
||||
}: $ReadOnly<{
|
||||
// In 'syncAndExit' mode, the current process will block until the spawned process exits, and then it will exit
|
||||
// with the same exit code as the spawned process.
|
||||
@@ -37,9 +38,13 @@ async function unstable_spawnDebuggerShellWithArgs(
|
||||
// continue to run normally.
|
||||
mode?: 'syncThenExit' | 'detached',
|
||||
flavor?: DebuggerShellFlavor,
|
||||
prebuiltBinaryPath?: string,
|
||||
}> = {},
|
||||
): Promise<void> {
|
||||
const [binaryPath, baseArgs] = getShellBinaryAndArgs(flavor);
|
||||
const [binaryPath, baseArgs] = getShellBinaryAndArgs(
|
||||
flavor,
|
||||
prebuiltBinaryPath,
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(binaryPath, [...baseArgs, ...args], {
|
||||
@@ -109,14 +114,13 @@ export type DebuggerShellPreparationResult = $ReadOnly<{
|
||||
*/
|
||||
async function unstable_prepareDebuggerShell(
|
||||
flavor: DebuggerShellFlavor,
|
||||
{prebuiltBinaryPath}: {prebuiltBinaryPath?: string} = {},
|
||||
): Promise<DebuggerShellPreparationResult> {
|
||||
const [binaryPath, baseArgs] = getShellBinaryAndArgs(flavor);
|
||||
|
||||
try {
|
||||
switch (flavor) {
|
||||
case 'prebuilt':
|
||||
const prebuiltResult = await prepareDebuggerShellFromDotSlashFile(
|
||||
DEVTOOLS_BINARY_DOTSLASH_FILE,
|
||||
prebuiltBinaryPath ?? DEVTOOLS_BINARY_DOTSLASH_FILE,
|
||||
);
|
||||
if (prebuiltResult.code !== 'success') {
|
||||
return prebuiltResult;
|
||||
@@ -128,6 +132,11 @@ async function unstable_prepareDebuggerShell(
|
||||
flavor as empty;
|
||||
throw new Error(`Unknown flavor: ${flavor}`);
|
||||
}
|
||||
|
||||
const [binaryPath, baseArgs] = getShellBinaryAndArgs(
|
||||
flavor,
|
||||
prebuiltBinaryPath,
|
||||
);
|
||||
const {code, stderr} = await spawnAndGetStderr(binaryPath, [
|
||||
...baseArgs,
|
||||
'--version',
|
||||
@@ -149,10 +158,14 @@ async function unstable_prepareDebuggerShell(
|
||||
|
||||
function getShellBinaryAndArgs(
|
||||
flavor: DebuggerShellFlavor,
|
||||
prebuiltBinaryPath: ?string,
|
||||
): [string, Array<string>] {
|
||||
switch (flavor) {
|
||||
case 'prebuilt':
|
||||
return [require('fb-dotslash'), [DEVTOOLS_BINARY_DOTSLASH_FILE]];
|
||||
return [
|
||||
require('fb-dotslash'),
|
||||
[prebuiltBinaryPath ?? DEVTOOLS_BINARY_DOTSLASH_FILE],
|
||||
];
|
||||
case 'dev':
|
||||
return [
|
||||
// NOTE: Internally at Meta, this is aliased to a workspace that is
|
||||
|
||||
@@ -21,10 +21,6 @@ import {
|
||||
withServerForEachTest,
|
||||
} from './ServerUtils';
|
||||
import {createHash} from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const fsPromise = fs.promises;
|
||||
|
||||
// WebSocket is unreliable when using fake timers.
|
||||
jest.useRealTimers();
|
||||
@@ -47,7 +43,6 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
|
||||
const serverRef = withServerForEachTest({
|
||||
logger: undefined,
|
||||
projectRoot: __dirname,
|
||||
secure: protocol === 'HTTPS',
|
||||
});
|
||||
|
||||
@@ -472,14 +467,7 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
}
|
||||
});
|
||||
|
||||
test('reads source from disk', async () => {
|
||||
// Should be just 'foo\n', but the newline can get mangled by the OS
|
||||
// and/or SCM, so let's read the source of truth from disk.
|
||||
const fileRealContents = await fsPromise.readFile(
|
||||
path.join(__dirname, '__fixtures__', 'mock-source-file.txt'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
test('throws when attempting to pass a filesystem url', async () => {
|
||||
const {device, debugger_} = await createAndConnectTarget(
|
||||
serverRef,
|
||||
autoCleanup.signal,
|
||||
@@ -490,6 +478,7 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
vm: 'bar-vm',
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await sendFromTargetToDebugger(device, debugger_, 'page1', {
|
||||
method: 'Debugger.scriptParsed',
|
||||
@@ -500,7 +489,7 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
endLine: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
hash: createHash('sha256').update(fileRealContents).digest('hex'),
|
||||
hash: createHash('sha256').update('').digest('hex'),
|
||||
},
|
||||
});
|
||||
const response = await debugger_.sendAndGetResponse({
|
||||
@@ -511,8 +500,15 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
},
|
||||
});
|
||||
expect(response.result).toEqual(
|
||||
expect.objectContaining({scriptSource: fileRealContents}),
|
||||
expect.objectContaining({
|
||||
error: {
|
||||
message: expect.stringContaining(
|
||||
'Can\'t parse requested URL "__fixtures__/mock-source-file.txt"',
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// The device does not receive the getScriptSource request, since it
|
||||
// is handled by the proxy.
|
||||
expect(device.wrappedEventParsed).not.toBeCalledWith({
|
||||
|
||||
@@ -33,7 +33,6 @@ describe.each(['HTTP', 'HTTPS'])(
|
||||
protocol => {
|
||||
const serverRef = withServerForEachTest({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
secure: protocol === 'HTTPS',
|
||||
});
|
||||
const autoCleanup = withAbortSignalForEachTest();
|
||||
|
||||
@@ -39,7 +39,6 @@ describe('inspector proxy device message middleware', () => {
|
||||
const createCustomMessageHandler = jest.fn().mockImplementation(() => null);
|
||||
const {server} = await createServer({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
unstable_customInspectorMessageHandler: createCustomMessageHandler,
|
||||
});
|
||||
|
||||
@@ -83,7 +82,6 @@ describe('inspector proxy device message middleware', () => {
|
||||
const createCustomMessageHandler = jest.fn().mockImplementation(() => null);
|
||||
const {server} = await createServer({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
unstable_customInspectorMessageHandler: createCustomMessageHandler,
|
||||
});
|
||||
const {serverBaseUrl, serverBaseWsUrl} = serverRefUrls(server);
|
||||
@@ -169,7 +167,6 @@ describe('inspector proxy device message middleware', () => {
|
||||
|
||||
const {server} = await createServer({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
unstable_customInspectorMessageHandler: createCustomMessageHandler,
|
||||
});
|
||||
|
||||
@@ -231,7 +228,6 @@ describe('inspector proxy device message middleware', () => {
|
||||
const handleDeviceMessage = jest.fn();
|
||||
const {server} = await createServer({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
unstable_customInspectorMessageHandler: () => ({
|
||||
handleDeviceMessage,
|
||||
handleDebuggerMessage() {},
|
||||
@@ -264,7 +260,6 @@ describe('inspector proxy device message middleware', () => {
|
||||
const handleDeviceMessage = jest.fn();
|
||||
const {server} = await createServer({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
unstable_customInspectorMessageHandler: () => ({
|
||||
handleDeviceMessage,
|
||||
handleDebuggerMessage() {},
|
||||
@@ -304,7 +299,6 @@ describe('inspector proxy device message middleware', () => {
|
||||
const handleDebuggerMessage = jest.fn();
|
||||
const {server} = await createServer({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
unstable_customInspectorMessageHandler: () => ({
|
||||
handleDeviceMessage() {},
|
||||
handleDebuggerMessage,
|
||||
@@ -341,7 +335,6 @@ describe('inspector proxy device message middleware', () => {
|
||||
const handleDebuggerMessage = jest.fn();
|
||||
const {server} = await createServer({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
unstable_customInspectorMessageHandler: () => ({
|
||||
handleDeviceMessage() {},
|
||||
handleDebuggerMessage,
|
||||
|
||||
@@ -37,7 +37,6 @@ const PAGE_DEFAULTS = {
|
||||
describe('inspector-proxy device socket handoff', () => {
|
||||
const serverRef = withServerForEachTest({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
});
|
||||
const autoCleanup = withAbortSignalForEachTest();
|
||||
afterEach(() => {
|
||||
|
||||
@@ -27,7 +27,6 @@ jest.useFakeTimers();
|
||||
describe('inspector proxy HTTP API', () => {
|
||||
const serverRef = withServerForEachTest({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
});
|
||||
const autoCleanup = withAbortSignalForEachTest();
|
||||
afterEach(() => {
|
||||
@@ -261,7 +260,6 @@ describe('inspector proxy HTTP API', () => {
|
||||
describe('HTTP vs HTTPS', () => {
|
||||
const secureServerRef = withServerForEachTest({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
secure: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ jest.setTimeout(10000);
|
||||
describe('inspector proxy React Native reloads', () => {
|
||||
const serverRef = withServerForEachTest({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
});
|
||||
const autoCleanup = withAbortSignalForEachTest();
|
||||
afterEach(() => {
|
||||
|
||||
@@ -34,7 +34,6 @@ describe('enableStandaloneFuseboxShell experiment', () => {
|
||||
};
|
||||
const serverRef = withServerForEachTest({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
unstable_browserLauncher: BrowserLauncherWithFuseboxShell,
|
||||
unstable_experiments: {
|
||||
enableStandaloneFuseboxShell: true,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
foo
|
||||
@@ -17,7 +17,6 @@ jest.setTimeout(10000);
|
||||
describe('embedder script', () => {
|
||||
const serverRef = withServerForEachTest({
|
||||
logger: undefined,
|
||||
projectRoot: '',
|
||||
});
|
||||
|
||||
test('is always served', async () => {
|
||||
|
||||
@@ -24,8 +24,6 @@ import path from 'path';
|
||||
import serveStaticMiddleware from 'serve-static';
|
||||
|
||||
type Options = $ReadOnly<{
|
||||
projectRoot: string,
|
||||
|
||||
/**
|
||||
* The base URL to the dev server, as reachable from the machine on which
|
||||
* dev-middleware is hosted. Typically `http://localhost:${metroPort}`.
|
||||
@@ -78,7 +76,6 @@ type DevMiddlewareAPI = $ReadOnly<{
|
||||
}>;
|
||||
|
||||
export default function createDevMiddleware({
|
||||
projectRoot,
|
||||
serverBaseUrl,
|
||||
logger,
|
||||
// $FlowFixMe[incompatible-type]
|
||||
@@ -96,7 +93,6 @@ export default function createDevMiddleware({
|
||||
);
|
||||
|
||||
const inspectorProxy = new InspectorProxy(
|
||||
projectRoot,
|
||||
serverBaseUrl,
|
||||
eventReporter,
|
||||
experiments,
|
||||
|
||||
@@ -28,9 +28,7 @@ import type {
|
||||
|
||||
import CdpDebugLogging from './CdpDebugLogging';
|
||||
import DeviceEventReporter from './DeviceEventReporter';
|
||||
import * as fs from 'fs';
|
||||
import invariant from 'invariant';
|
||||
import * as path from 'path';
|
||||
import WS from 'ws';
|
||||
|
||||
const debug = require('debug')('Metro:InspectorProxy');
|
||||
@@ -75,7 +73,6 @@ export type DeviceOptions = $ReadOnly<{
|
||||
name: string,
|
||||
app: string,
|
||||
socket: WS,
|
||||
projectRoot: string,
|
||||
eventReporter: ?EventReporter,
|
||||
createMessageMiddleware: ?CreateCustomMessageHandlerFn,
|
||||
deviceRelativeBaseUrl: URL,
|
||||
@@ -120,9 +117,6 @@ export default class Device {
|
||||
// Mapping built from scriptParsed events and used to fetch file content in `Debugger.getScriptSource`.
|
||||
#scriptIdToSourcePathMapping: Map<string, string> = new Map();
|
||||
|
||||
// Root of the project used for relative to absolute source path conversion.
|
||||
#projectRoot: string;
|
||||
|
||||
#deviceEventReporter: ?DeviceEventReporter;
|
||||
|
||||
#pagesPollingIntervalId: ReturnType<typeof setInterval>;
|
||||
@@ -151,7 +145,6 @@ export default class Device {
|
||||
name,
|
||||
app,
|
||||
socket,
|
||||
projectRoot,
|
||||
eventReporter,
|
||||
createMessageMiddleware,
|
||||
serverRelativeBaseUrl,
|
||||
@@ -163,7 +156,6 @@ export default class Device {
|
||||
this.#name = name;
|
||||
this.#app = app;
|
||||
this.#deviceSocket = socket;
|
||||
this.#projectRoot = projectRoot;
|
||||
this.#serverRelativeBaseUrl = serverRelativeBaseUrl;
|
||||
this.#deviceRelativeBaseUrl = deviceRelativeBaseUrl;
|
||||
this.#deviceEventReporter = eventReporter
|
||||
@@ -880,7 +872,7 @@ export default class Device {
|
||||
return this.#processDebuggerSetBreakpointByUrl(req, debuggerInfo);
|
||||
case 'Debugger.getScriptSource':
|
||||
// Sends response to debugger via side-effect
|
||||
this.#processDebuggerGetScriptSource(req, socket);
|
||||
void this.#processDebuggerGetScriptSource(req, socket);
|
||||
return null;
|
||||
case 'Network.loadNetworkResource':
|
||||
// If we're rewriting URLs (to frontend-relative), we don't want to
|
||||
@@ -965,10 +957,10 @@ export default class Device {
|
||||
return processedReq;
|
||||
}
|
||||
|
||||
#processDebuggerGetScriptSource(
|
||||
async #processDebuggerGetScriptSource(
|
||||
req: CDPRequest<'Debugger.getScriptSource'>,
|
||||
socket: WS,
|
||||
): void {
|
||||
): Promise<void> {
|
||||
const sendSuccessResponse = (scriptSource: string) => {
|
||||
const result: {
|
||||
scriptSource: string,
|
||||
@@ -1005,33 +997,23 @@ export default class Device {
|
||||
const pathToSource = this.#scriptIdToSourcePathMapping.get(
|
||||
req.params.scriptId,
|
||||
);
|
||||
if (pathToSource != null) {
|
||||
const httpURL = this.#tryParseHTTPURL(pathToSource);
|
||||
if (httpURL) {
|
||||
// URL is server-relatve, so we should be able to fetch it from here.
|
||||
this.#fetchText(httpURL).then(
|
||||
text => sendSuccessResponse(text),
|
||||
err =>
|
||||
sendErrorResponse(
|
||||
`Failed to fetch source url ${pathToSource}: ${err.message}`,
|
||||
),
|
||||
|
||||
try {
|
||||
const httpURL =
|
||||
pathToSource == null ? null : this.#tryParseHTTPURL(pathToSource);
|
||||
if (!httpURL) {
|
||||
throw new Error(
|
||||
`Can't parse requested URL ${pathToSource === undefined ? 'undefined' : JSON.stringify(pathToSource)}`,
|
||||
);
|
||||
} else {
|
||||
let file;
|
||||
try {
|
||||
file = fs.readFileSync(
|
||||
path.resolve(this.#projectRoot, pathToSource),
|
||||
'utf8',
|
||||
);
|
||||
} catch (err) {
|
||||
sendErrorResponse(
|
||||
`Failed to fetch source file ${pathToSource}: ${err.message}`,
|
||||
);
|
||||
}
|
||||
if (file != null) {
|
||||
sendSuccessResponse(file);
|
||||
}
|
||||
}
|
||||
|
||||
const text = await this.#fetchText(httpURL);
|
||||
|
||||
sendSuccessResponse(text);
|
||||
} catch (err) {
|
||||
sendErrorResponse(
|
||||
`Failed to fetch source url ${pathToSource === undefined ? 'undefined' : JSON.stringify(pathToSource)} for scriptId ${req.params.scriptId}: ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,9 +75,6 @@ export interface InspectorProxyQueries {
|
||||
* Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger.
|
||||
*/
|
||||
export default class InspectorProxy implements InspectorProxyQueries {
|
||||
// Root of the project used for relative to absolute source path conversion.
|
||||
#projectRoot: string;
|
||||
|
||||
// The base URL to the dev server from the dev-middleware host.
|
||||
#serverBaseUrl: URL;
|
||||
|
||||
@@ -101,7 +98,6 @@ export default class InspectorProxy implements InspectorProxyQueries {
|
||||
#eventLoopPerfTracker: EventLoopPerfTracker;
|
||||
|
||||
constructor(
|
||||
projectRoot: string,
|
||||
serverBaseUrl: string,
|
||||
eventReporter: ?EventReporter,
|
||||
experiments: Experiments,
|
||||
@@ -109,7 +105,6 @@ export default class InspectorProxy implements InspectorProxyQueries {
|
||||
customMessageHandler: ?CreateCustomMessageHandlerFn,
|
||||
trackEventLoopPerf?: boolean = false,
|
||||
) {
|
||||
this.#projectRoot = projectRoot;
|
||||
this.#serverBaseUrl = new URL(serverBaseUrl);
|
||||
this.#devices = new Map();
|
||||
this.#eventReporter = eventReporter;
|
||||
@@ -355,7 +350,6 @@ export default class InspectorProxy implements InspectorProxyQueries {
|
||||
name: deviceName,
|
||||
app: appName,
|
||||
socket,
|
||||
projectRoot: this.#projectRoot,
|
||||
eventReporter: this.#eventReporter,
|
||||
createMessageMiddleware: this.#customMessageHandler,
|
||||
deviceRelativeBaseUrl,
|
||||
@@ -372,12 +366,6 @@ export default class InspectorProxy implements InspectorProxyQueries {
|
||||
|
||||
this.#devices.set(deviceId, newDevice);
|
||||
|
||||
this.#logger?.info(
|
||||
"Connection established to app='%s' on device='%s'.",
|
||||
appName,
|
||||
deviceName,
|
||||
);
|
||||
|
||||
debug(
|
||||
"Got new device connection: name='%s', app=%s, device=%s, via=%s",
|
||||
deviceName,
|
||||
@@ -450,7 +438,7 @@ export default class InspectorProxy implements InspectorProxyQueries {
|
||||
);
|
||||
|
||||
socket.on('close', (code: number, reason: string) => {
|
||||
this.#logger?.info(
|
||||
debug(
|
||||
"Connection closed to device='%s' for app='%s' with code='%s' and reason='%s'.",
|
||||
deviceName,
|
||||
appName,
|
||||
@@ -526,7 +514,7 @@ export default class InspectorProxy implements InspectorProxyQueries {
|
||||
throw new Error(INTERNAL_ERROR_MESSAGES.UNREGISTERED_DEVICE);
|
||||
}
|
||||
|
||||
this.#logger?.info(
|
||||
debug(
|
||||
"Connection established to DevTools for app='%s' on device='%s'.",
|
||||
device.getApp() || 'unknown',
|
||||
device.getName() || 'unknown',
|
||||
@@ -594,7 +582,7 @@ export default class InspectorProxy implements InspectorProxyQueries {
|
||||
});
|
||||
|
||||
socket.on('close', (code: number, reason: string) => {
|
||||
this.#logger?.info(
|
||||
debug(
|
||||
"Connection closed to DevTools for app='%s' on device='%s' with code='%s' and reason='%s'.",
|
||||
device.getApp() || 'unknown',
|
||||
device.getName() || 'unknown',
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {NextHandleFunction} from 'connect';
|
||||
import type {IncomingMessage, ServerResponse} from 'http';
|
||||
|
||||
import getDevToolsFrontendUrl from '../utils/getDevToolsFrontendUrl';
|
||||
import {createHash} from 'crypto';
|
||||
import url from 'url';
|
||||
|
||||
const LEGACY_SYNTHETIC_PAGE_TITLE =
|
||||
@@ -190,11 +191,15 @@ export default function openDebuggerMiddleware({
|
||||
}
|
||||
}
|
||||
if (shouldUseStandaloneFuseboxShell) {
|
||||
const windowKey = [
|
||||
serverBaseUrl,
|
||||
target.webSocketDebuggerUrl,
|
||||
target.appId,
|
||||
].join('-');
|
||||
const windowKey = createHash('sha256')
|
||||
.update(
|
||||
[
|
||||
serverBaseUrl,
|
||||
target.webSocketDebuggerUrl,
|
||||
target.appId,
|
||||
].join('-'),
|
||||
)
|
||||
.digest('hex');
|
||||
if (!browserLauncher.unstable_showFuseboxShell) {
|
||||
throw new Error(
|
||||
'Fusebox shell is not supported by the current browser launcher',
|
||||
|
||||
+8
@@ -191,6 +191,14 @@ internal object DependencyUtils {
|
||||
"The hermes-android dependency was modified to use the correct Maven group.",
|
||||
)
|
||||
)
|
||||
} else if (hermesV1Enabled) {
|
||||
dependencySubstitution.add(
|
||||
Triple(
|
||||
"com.facebook.react:hermes-android",
|
||||
hermesVersionString,
|
||||
"The hermes-android dependency was modified to use Hermes V1.",
|
||||
)
|
||||
)
|
||||
}
|
||||
return dependencySubstitution
|
||||
}
|
||||
|
||||
+1
-1
@@ -242,7 +242,7 @@ internal fun readPackageJsonFile(
|
||||
return packageJson?.let { JsonUtils.fromPackageJson(it) }
|
||||
}
|
||||
|
||||
private const val HERMES_COMPILER_NPM_DIR = "node_modules/hermes-compiler/%OS-BIN%/"
|
||||
private const val HERMES_COMPILER_NPM_DIR = "node_modules/hermes-compiler/hermesc/%OS-BIN%/"
|
||||
private const val HERMESC_IN_REACT_NATIVE_DIR = "node_modules/react-native/sdks/hermesc/%OS-BIN%/"
|
||||
private const val HERMESC_BUILT_FROM_SOURCE_DIR =
|
||||
"node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/"
|
||||
|
||||
+2
-2
@@ -165,8 +165,8 @@ class PathUtilsTest {
|
||||
@Test
|
||||
@WithOs(OS.MAC)
|
||||
fun detectOSAwareHermesCommand_withHermesV1Enabled() {
|
||||
tempFolder.newFolder("node_modules/hermes-compiler/osx-bin/")
|
||||
val expected = tempFolder.newFile("node_modules/hermes-compiler/osx-bin/hermesc")
|
||||
tempFolder.newFolder("node_modules/hermes-compiler/hermesc/osx-bin/")
|
||||
val expected = tempFolder.newFile("node_modules/hermes-compiler/hermesc/osx-bin/hermesc")
|
||||
|
||||
assertThat(detectOSAwareHermesCommand(tempFolder.root, "", hermesV1Enabled = true))
|
||||
.isEqualTo(expected.toString())
|
||||
|
||||
@@ -47,7 +47,7 @@ function combineSchemas(
|
||||
}
|
||||
return merged;
|
||||
},
|
||||
{modules: {}},
|
||||
{modules: {} /*:: as SchemaType['modules'] */},
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -540,7 +540,7 @@ function buildEventEmitterSchema(
|
||||
|
||||
return {
|
||||
name: eventemitterName,
|
||||
optional: false,
|
||||
optional: Boolean(property.optional),
|
||||
typeAnnotation: {
|
||||
type: 'EventEmitterTypeAnnotation',
|
||||
typeAnnotation: eventTypeAnnotation,
|
||||
|
||||
@@ -873,56 +873,6 @@ describe('Animated', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Interactions', () => {
|
||||
let Animated; // eslint-disable-line no-shadow
|
||||
let InteractionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mock('../../Interaction/InteractionManager');
|
||||
Animated = require('../Animated').default;
|
||||
InteractionManager =
|
||||
require('../../Interaction/InteractionManager').default;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.unmock('../../Interaction/InteractionManager');
|
||||
});
|
||||
|
||||
it('registers an interaction by default', () => {
|
||||
// $FlowFixMe[prop-missing]
|
||||
InteractionManager.createInteractionHandle.mockReturnValue(777);
|
||||
|
||||
const value = new Animated.Value(0);
|
||||
const callback = jest.fn();
|
||||
Animated.timing(value, {
|
||||
toValue: 100,
|
||||
duration: 100,
|
||||
useNativeDriver: false,
|
||||
}).start(callback);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(InteractionManager.createInteractionHandle).toBeCalled();
|
||||
expect(InteractionManager.clearInteractionHandle).toBeCalledWith(777);
|
||||
expect(callback).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('does not register an interaction when specified', () => {
|
||||
const value = new Animated.Value(0);
|
||||
const callback = jest.fn();
|
||||
Animated.timing(value, {
|
||||
toValue: 100,
|
||||
duration: 100,
|
||||
isInteraction: false,
|
||||
useNativeDriver: false,
|
||||
}).start(callback);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(InteractionManager.createInteractionHandle).not.toBeCalled();
|
||||
expect(InteractionManager.clearInteractionHandle).not.toBeCalled();
|
||||
expect(callback).toBeCalledWith({finished: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Tracking', () => {
|
||||
it('should track values', () => {
|
||||
const value1 = new Animated.Value(0);
|
||||
|
||||
@@ -14,8 +14,10 @@ import FlatList, {type FlatListProps} from '../../Lists/FlatList';
|
||||
import createAnimatedComponent from '../createAnimatedComponent';
|
||||
import * as React from 'react';
|
||||
|
||||
// $FlowExpectedError[unclear-type]
|
||||
export default (createAnimatedComponent(FlatList): component<ItemT = any>(
|
||||
export default createAnimatedComponent(FlatList) as $FlowFixMe as component<
|
||||
// $FlowExpectedError[unclear-type]
|
||||
ItemT = any,
|
||||
>(
|
||||
ref?: React.RefSetter<FlatList<ItemT>>,
|
||||
...props: AnimatedProps<FlatListProps<ItemT>>
|
||||
));
|
||||
);
|
||||
|
||||
@@ -14,9 +14,10 @@ import Image from '../../Image/Image';
|
||||
import createAnimatedComponent from '../createAnimatedComponent';
|
||||
import * as React from 'react';
|
||||
|
||||
export default (createAnimatedComponent(
|
||||
(Image: $FlowFixMe),
|
||||
): AnimatedComponentType<
|
||||
export default (createAnimatedComponent<
|
||||
$FlowFixMe,
|
||||
React.ElementRef<typeof Image>,
|
||||
>((Image: $FlowFixMe)): AnimatedComponentType<
|
||||
React.ElementConfig<typeof Image>,
|
||||
React.ElementRef<typeof Image>,
|
||||
>);
|
||||
|
||||
@@ -15,7 +15,7 @@ import createAnimatedComponent from '../createAnimatedComponent';
|
||||
import * as React from 'react';
|
||||
|
||||
// $FlowFixMe[incompatible-type]
|
||||
export default (createAnimatedComponent(SectionList): component<
|
||||
export default createAnimatedComponent(SectionList) as $FlowFixMe as component<
|
||||
// $FlowExpectedError[unclear-type]
|
||||
ItemT = any,
|
||||
// $FlowExpectedError[unclear-type]
|
||||
@@ -23,4 +23,4 @@ export default (createAnimatedComponent(SectionList): component<
|
||||
>(
|
||||
ref?: React.RefSetter<SectionList<ItemT, SectionT>>,
|
||||
...props: AnimatedProps<SectionListProps<ItemT, SectionT>>
|
||||
));
|
||||
);
|
||||
|
||||
@@ -14,6 +14,10 @@ import Text, {type TextProps} from '../../Text/Text';
|
||||
import createAnimatedComponent from '../createAnimatedComponent';
|
||||
import * as React from 'react';
|
||||
|
||||
export default (createAnimatedComponent(
|
||||
(Text: $FlowFixMe),
|
||||
): AnimatedComponentType<TextProps, React.ElementRef<typeof Text>>);
|
||||
export default (createAnimatedComponent<
|
||||
$FlowFixMe,
|
||||
React.ElementRef<typeof Text>,
|
||||
>((Text: $FlowFixMe)): AnimatedComponentType<
|
||||
TextProps,
|
||||
React.ElementRef<typeof Text>,
|
||||
>);
|
||||
|
||||
@@ -15,7 +15,8 @@ import View from '../../Components/View/View';
|
||||
import createAnimatedComponent from '../createAnimatedComponent';
|
||||
import * as React from 'react';
|
||||
|
||||
export default (createAnimatedComponent(View): AnimatedComponentType<
|
||||
// $FlowFixMe[incompatible-type]
|
||||
export default createAnimatedComponent(View) as AnimatedComponentType<
|
||||
ViewProps,
|
||||
React.ElementRef<typeof View>,
|
||||
>);
|
||||
>;
|
||||
|
||||
@@ -63,17 +63,29 @@ type PassThroughProps = $ReadOnly<{
|
||||
passthroughAnimatedPropExplicitValues?: ViewProps | null,
|
||||
}>;
|
||||
|
||||
export type AnimatedProps<Props: {...}> = {
|
||||
[K in keyof Props]: K extends NonAnimatedProps
|
||||
? Props[K]
|
||||
: WithAnimatedValue<Props[K]>,
|
||||
} & PassThroughProps;
|
||||
type LooseOmit<O: interface {}, K: $Keys<$FlowFixMe>> = Pick<
|
||||
O,
|
||||
Exclude<$Keys<O>, K>,
|
||||
>;
|
||||
|
||||
export type AnimatedBaseProps<Props: {...}> = {
|
||||
[K in keyof Props]: K extends NonAnimatedProps
|
||||
? Props[K]
|
||||
: WithAnimatedValue<Props[K]>,
|
||||
};
|
||||
export type AnimatedProps<Props: {...}> = LooseOmit<
|
||||
{
|
||||
[K in keyof Props]: K extends NonAnimatedProps
|
||||
? Props[K]
|
||||
: WithAnimatedValue<Props[K]>,
|
||||
},
|
||||
'ref',
|
||||
> &
|
||||
PassThroughProps;
|
||||
|
||||
export type AnimatedBaseProps<Props: {...}> = LooseOmit<
|
||||
{
|
||||
[K in keyof Props]: K extends NonAnimatedProps
|
||||
? Props[K]
|
||||
: WithAnimatedValue<Props[K]>,
|
||||
},
|
||||
'ref',
|
||||
>;
|
||||
|
||||
export type AnimatedComponentType<Props: {...}, +Instance = mixed> = component(
|
||||
ref?: React.RefSetter<Instance>,
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {AnimatedNodeConfig} from './AnimatedNode';
|
||||
import type AnimatedTracking from './AnimatedTracking';
|
||||
|
||||
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
|
||||
import InteractionManager from '../../Interaction/InteractionManager';
|
||||
import AnimatedInterpolation from './AnimatedInterpolation';
|
||||
import AnimatedWithChildren from './AnimatedWithChildren';
|
||||
|
||||
@@ -312,10 +311,6 @@ export default class AnimatedValue extends AnimatedWithChildren {
|
||||
* See https://reactnative.dev/docs/animatedvalue#animate
|
||||
*/
|
||||
animate(animation: Animation, callback: ?EndCallback): void {
|
||||
let handle = null;
|
||||
if (animation.__isInteraction) {
|
||||
handle = InteractionManager.createInteractionHandle();
|
||||
}
|
||||
const previousAnimation = this._animation;
|
||||
this._animation && this._animation.stop();
|
||||
this._animation = animation;
|
||||
@@ -328,9 +323,6 @@ export default class AnimatedValue extends AnimatedWithChildren {
|
||||
},
|
||||
result => {
|
||||
this._animation = null;
|
||||
if (handle !== null) {
|
||||
InteractionManager.clearInteractionHandle(handle);
|
||||
}
|
||||
callback && callback(result);
|
||||
},
|
||||
previousAnimation,
|
||||
|
||||
@@ -1690,6 +1690,7 @@ class ScrollView extends React.Component<ScrollViewProps, ScrollViewState> {
|
||||
return (
|
||||
<StickyHeaderComponent
|
||||
key={key}
|
||||
/* $FlowFixMe[incompatible-type] */
|
||||
ref={ref => this._setStickyHeaderRef(key, ref)}
|
||||
nextHeaderLayoutY={this._headerLayoutYs.get(
|
||||
this._getKeyForIndex(nextIndex, children),
|
||||
|
||||
+6
-6
@@ -44,7 +44,7 @@ interface Instance extends React.ElementRef<typeof Animated.View> {
|
||||
}
|
||||
|
||||
const ScrollViewStickyHeader: component(
|
||||
ref: React.RefSetter<Instance>,
|
||||
ref?: React.RefSetter<Instance>,
|
||||
...props: ScrollViewStickyHeaderProps
|
||||
) = function ScrollViewStickyHeader({
|
||||
ref: forwardedRef,
|
||||
@@ -282,7 +282,7 @@ const ScrollViewStickyHeader: component(
|
||||
const passthroughAnimatedPropExplicitValues =
|
||||
isFabric && translateY != null
|
||||
? {
|
||||
style: {transform: [{translateY: translateY}]},
|
||||
style: {transform: [{translateY}]},
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -303,20 +303,20 @@ const ScrollViewStickyHeader: component(
|
||||
passthroughAnimatedPropExplicitValues
|
||||
}>
|
||||
{cloneElement(child, {
|
||||
style: styles.fill, // We transfer the child style to the wrapper.
|
||||
onLayout: undefined, // we call this manually through our this._onLayout
|
||||
style: styles.fill, // We transfer the child style to the wrapper.
|
||||
})}
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
zIndex: 10,
|
||||
},
|
||||
fill: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
zIndex: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default ScrollViewStickyHeader;
|
||||
|
||||
@@ -449,6 +449,12 @@ function InternalTextInput(props: TextInputProps): React.Node {
|
||||
before we can get to the long term breaking change.
|
||||
*/
|
||||
if (instance != null) {
|
||||
// Register the input immediately when the ref is set so that focus()
|
||||
// can be called from ref callbacks
|
||||
// Double registering during useLayoutEffect is fine, because the underlying
|
||||
// state is a Set.
|
||||
TextInputState.registerInput(instance);
|
||||
|
||||
// $FlowFixMe[prop-missing] - See the explanation above.
|
||||
// $FlowFixMe[unsafe-object-assign]
|
||||
Object.assign(instance, {
|
||||
|
||||
@@ -47,15 +47,14 @@ class TouchableBounce extends React.Component<
|
||||
|
||||
_createPressabilityConfig(): PressabilityConfig {
|
||||
return {
|
||||
android_disableSound: this.props.touchSoundDisabled,
|
||||
cancelable: !this.props.rejectResponderTermination,
|
||||
disabled: this.props.disabled,
|
||||
hitSlop: this.props.hitSlop,
|
||||
delayLongPress: this.props.delayLongPress,
|
||||
delayPressIn: this.props.delayPressIn,
|
||||
delayPressOut: this.props.delayPressOut,
|
||||
disabled: this.props.disabled,
|
||||
hitSlop: this.props.hitSlop,
|
||||
minPressDuration: 0,
|
||||
pressRectOffset: this.props.pressRetentionOffset,
|
||||
android_disableSound: this.props.touchSoundDisabled,
|
||||
onBlur: event => {
|
||||
if (Platform.isTV) {
|
||||
this._bounceTo(1, 0.4, 0);
|
||||
@@ -113,6 +112,7 @@ class TouchableBounce extends React.Component<
|
||||
this.props.onPressOut(event);
|
||||
}
|
||||
},
|
||||
pressRectOffset: this.props.pressRetentionOffset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,10 +123,10 @@ class TouchableBounce extends React.Component<
|
||||
callback?: ?() => void,
|
||||
) {
|
||||
Animated.spring(this.state.scale, {
|
||||
toValue,
|
||||
velocity,
|
||||
bounciness,
|
||||
toValue,
|
||||
useNativeDriver: true,
|
||||
velocity,
|
||||
}).start(callback);
|
||||
}
|
||||
|
||||
@@ -230,6 +230,6 @@ export default (function TouchableBounceWrapper({
|
||||
}) {
|
||||
return <TouchableBounce {...props} hostRef={hostRef} />;
|
||||
} as component(
|
||||
ref: React.RefSetter<mixed>,
|
||||
ref?: React.RefSetter<mixed>,
|
||||
...props: $ReadOnly<Omit<TouchableBounceProps, 'hostRef'>>
|
||||
));
|
||||
|
||||
@@ -21,12 +21,14 @@ const ViewNativeComponent: HostComponent<Props> =
|
||||
}));
|
||||
|
||||
interface NativeCommands {
|
||||
+focus: (viewRef: HostInstance) => void;
|
||||
+blur: (viewRef: HostInstance) => void;
|
||||
+hotspotUpdate: (viewRef: HostInstance, x: number, y: number) => void;
|
||||
+setPressed: (viewRef: HostInstance, pressed: boolean) => void;
|
||||
}
|
||||
|
||||
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
||||
supportedCommands: ['hotspotUpdate', 'setPressed'],
|
||||
supportedCommands: ['focus', 'blur', 'hotspotUpdate', 'setPressed'],
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+21
-21
@@ -39,6 +39,24 @@ function DebuggingOverlay({
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
clearElementsHighlight() {
|
||||
if (!isNativeComponentReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nativeComponentRef.current != null) {
|
||||
Commands.clearElementsHighlights(nativeComponentRef.current);
|
||||
}
|
||||
},
|
||||
highlightElements(elements) {
|
||||
if (!isNativeComponentReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nativeComponentRef.current != null) {
|
||||
Commands.highlightElements(nativeComponentRef.current, elements);
|
||||
}
|
||||
},
|
||||
highlightTraceUpdates(updates) {
|
||||
if (!isNativeComponentReady) {
|
||||
return;
|
||||
@@ -55,24 +73,6 @@ function DebuggingOverlay({
|
||||
);
|
||||
}
|
||||
},
|
||||
highlightElements(elements) {
|
||||
if (!isNativeComponentReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nativeComponentRef.current != null) {
|
||||
Commands.highlightElements(nativeComponentRef.current, elements);
|
||||
}
|
||||
},
|
||||
clearElementsHighlight() {
|
||||
if (!isNativeComponentReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nativeComponentRef.current != null) {
|
||||
Commands.clearElementsHighlights(nativeComponentRef.current);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@@ -95,14 +95,14 @@ function DebuggingOverlay({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default DebuggingOverlay as component(
|
||||
ref: React.RefSetter<DebuggingOverlayHandle>,
|
||||
ref?: React.RefSetter<DebuggingOverlayHandle>,
|
||||
);
|
||||
|
||||
+3
-6
@@ -8,11 +8,8 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {
|
||||
AbstractImageAndroid,
|
||||
AbstractImageIOS,
|
||||
ImageType as ImageComponent,
|
||||
} from './ImageTypes.flow';
|
||||
import type {HostInstance} from '../..';
|
||||
import type {AbstractImageAndroid, AbstractImageIOS} from './ImageTypes.flow';
|
||||
|
||||
import useMergeRefs from '../Utilities/useMergeRefs';
|
||||
import * as React from 'react';
|
||||
@@ -33,7 +30,7 @@ export function unstable_getImageComponentDecorator(): ?ImageComponentDecorator
|
||||
return injectedImageComponentDecorator;
|
||||
}
|
||||
|
||||
type ImageInstance = React.ElementRef<ImageComponent>;
|
||||
type ImageInstance = HostInstance;
|
||||
|
||||
type ImageAttachedCallback = (
|
||||
imageInstance: ImageInstance,
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {HostInstance} from '../..';
|
||||
import type {RootTag} from '../Types/RootTagTypes';
|
||||
import type {ResolvedAssetSource} from './AssetSourceResolver';
|
||||
import type {ImageProps as ImagePropsType} from './ImageProps';
|
||||
import type {ImageSource} from './ImageSource';
|
||||
import typeof ImageViewNativeComponent from './ImageViewNativeComponent';
|
||||
import typeof TextInlineImageNativeComponent from './TextInlineImageNativeComponent';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
@@ -67,17 +66,14 @@ type ImageComponentStaticsAndroid = $ReadOnly<{
|
||||
}>;
|
||||
|
||||
export type AbstractImageAndroid = component(
|
||||
ref?: React.RefSetter<
|
||||
| React.ElementRef<TextInlineImageNativeComponent>
|
||||
| React.ElementRef<ImageViewNativeComponent>,
|
||||
>,
|
||||
ref?: React.RefSetter<HostInstance>,
|
||||
...props: ImagePropsType
|
||||
);
|
||||
|
||||
export type ImageAndroid = AbstractImageAndroid & ImageComponentStaticsAndroid;
|
||||
|
||||
export type AbstractImageIOS = component(
|
||||
ref?: React.RefSetter<React.ElementRef<ImageViewNativeComponent>>,
|
||||
ref?: React.RefSetter<HostInstance>,
|
||||
...props: ImagePropsType
|
||||
);
|
||||
|
||||
|
||||
+6
-51
@@ -12,7 +12,6 @@
|
||||
|
||||
import type {GestureResponderEvent} from '../Types/CoreEventTypes';
|
||||
|
||||
const InteractionManager = require('./InteractionManager').default;
|
||||
const TouchHistoryMath = require('./TouchHistoryMath').default;
|
||||
|
||||
const currentCentroidXOfTouchesChangedAfter =
|
||||
@@ -31,9 +30,6 @@ const currentCentroidY = TouchHistoryMath.currentCentroidY;
|
||||
* single-touch gestures resilient to extra touches, and can be used to
|
||||
* recognize simple multi-touch gestures.
|
||||
*
|
||||
* By default, `PanResponder` holds an `InteractionManager` handle to block
|
||||
* long-running JS events from interrupting active gestures.
|
||||
*
|
||||
* It provides a predictable wrapper of the responder handlers provided by the
|
||||
* [gesture responder system](docs/gesture-responder-system.html).
|
||||
* For each handler, it provides a new `gestureState` object alongside the
|
||||
@@ -405,9 +401,6 @@ const PanResponder = {
|
||||
getInteractionHandle: () => ?number,
|
||||
panHandlers: GestureResponderHandlerMethods,
|
||||
} {
|
||||
const interactionState = {
|
||||
handle: (null: ?number),
|
||||
};
|
||||
const gestureState: PanResponderGestureState = {
|
||||
// Useful for debugging
|
||||
stateID: Math.random(),
|
||||
@@ -464,10 +457,6 @@ const PanResponder = {
|
||||
},
|
||||
|
||||
onResponderGrant(event: GestureResponderEvent): boolean {
|
||||
if (!interactionState.handle) {
|
||||
interactionState.handle =
|
||||
InteractionManager.createInteractionHandle();
|
||||
}
|
||||
gestureState.x0 = currentCentroidX(event.touchHistory);
|
||||
gestureState.y0 = currentCentroidY(event.touchHistory);
|
||||
gestureState.dx = 0;
|
||||
@@ -482,21 +471,11 @@ const PanResponder = {
|
||||
},
|
||||
|
||||
onResponderReject(event: GestureResponderEvent): void {
|
||||
clearInteractionHandle(
|
||||
interactionState,
|
||||
config.onPanResponderReject,
|
||||
event,
|
||||
gestureState,
|
||||
);
|
||||
config.onPanResponderReject?.call(undefined, event, gestureState);
|
||||
},
|
||||
|
||||
onResponderRelease(event: GestureResponderEvent): void {
|
||||
clearInteractionHandle(
|
||||
interactionState,
|
||||
config.onPanResponderRelease,
|
||||
event,
|
||||
gestureState,
|
||||
);
|
||||
config.onPanResponderRelease?.call(undefined, event, gestureState);
|
||||
PanResponder._initializeGestureState(gestureState);
|
||||
},
|
||||
|
||||
@@ -529,21 +508,11 @@ const PanResponder = {
|
||||
onResponderEnd(event: GestureResponderEvent): void {
|
||||
const touchHistory = event.touchHistory;
|
||||
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
|
||||
clearInteractionHandle(
|
||||
interactionState,
|
||||
config.onPanResponderEnd,
|
||||
event,
|
||||
gestureState,
|
||||
);
|
||||
config.onPanResponderEnd?.call(undefined, event, gestureState);
|
||||
},
|
||||
|
||||
onResponderTerminate(event: GestureResponderEvent): void {
|
||||
clearInteractionHandle(
|
||||
interactionState,
|
||||
config.onPanResponderTerminate,
|
||||
event,
|
||||
gestureState,
|
||||
);
|
||||
config.onPanResponderTerminate?.call(undefined, event, gestureState);
|
||||
PanResponder._initializeGestureState(gestureState);
|
||||
},
|
||||
|
||||
@@ -556,27 +525,13 @@ const PanResponder = {
|
||||
return {
|
||||
panHandlers,
|
||||
getInteractionHandle(): ?number {
|
||||
return interactionState.handle;
|
||||
// TODO: Deprecate and delete this method.
|
||||
return null;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function clearInteractionHandle(
|
||||
interactionState: {handle: ?number, ...},
|
||||
callback: ?(ActiveCallback | PassiveCallback),
|
||||
event: GestureResponderEvent,
|
||||
gestureState: PanResponderGestureState,
|
||||
) {
|
||||
if (interactionState.handle) {
|
||||
InteractionManager.clearInteractionHandle(interactionState.handle);
|
||||
interactionState.handle = null;
|
||||
}
|
||||
if (callback) {
|
||||
callback(event, gestureState);
|
||||
}
|
||||
}
|
||||
|
||||
export type PanResponderInstance = ReturnType<(typeof PanResponder)['create']>;
|
||||
|
||||
export default PanResponder;
|
||||
|
||||
@@ -129,11 +129,13 @@ export function getWithFallback_DEPRECATED<Config: {...}>(
|
||||
// `getRuntimeConfig == null` when static view configs are disabled
|
||||
// If `setRuntimeConfigProvider` is not configured, use native reflection.
|
||||
if (hasNativeViewConfig(name)) {
|
||||
/* $FlowFixMe[incompatible-type] Extra ref prop */
|
||||
return get<Config>(name, viewConfigProvider);
|
||||
}
|
||||
} else {
|
||||
// If there is no runtime config, then the native component is unavailable.
|
||||
if (getRuntimeConfig(name) != null) {
|
||||
/* $FlowFixMe[incompatible-type] Extra ref prop */
|
||||
return get<Config>(name, viewConfigProvider);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -14,6 +14,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
|
||||
import type {HostInstance} from 'react-native';
|
||||
|
||||
import ReactNativeElement from '../../../../src/private/webapis/dom/nodes/ReactNativeElement';
|
||||
import TextInput from '../../../Components/TextInput/TextInput';
|
||||
import TextInputState from '../../../Components/TextInput/TextInputState';
|
||||
import View from '../../../Components/View/View';
|
||||
import ReactFabricHostComponent from '../ReactFabricHostComponent';
|
||||
@@ -48,7 +49,7 @@ describe('ReactFabricPublicInstance', () => {
|
||||
const nodeRef = createRef<HostInstance>();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<View ref={nodeRef} />);
|
||||
root.render(<TextInput ref={nodeRef} />);
|
||||
});
|
||||
|
||||
const node = nullthrows(nodeRef.current);
|
||||
@@ -73,7 +74,7 @@ describe('ReactFabricPublicInstance', () => {
|
||||
const ref = createRef<HostInstance>();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<View ref={ref} />);
|
||||
root.render(<TextInput ref={ref} />);
|
||||
});
|
||||
|
||||
const node = nullthrows(ref.current);
|
||||
|
||||
+15
-7
@@ -14,29 +14,37 @@ import NativeDevLoadingView from './NativeDevLoadingView';
|
||||
|
||||
const COLOR_SCHEME = {
|
||||
dark: {
|
||||
refresh: {
|
||||
backgroundColor: '#2584e8',
|
||||
textColor: '#ffffff',
|
||||
},
|
||||
load: {
|
||||
backgroundColor: '#fafafa',
|
||||
textColor: '#242526',
|
||||
},
|
||||
},
|
||||
default: {
|
||||
refresh: {
|
||||
backgroundColor: '#2584e8',
|
||||
textColor: '#ffffff',
|
||||
},
|
||||
error: {
|
||||
backgroundColor: '#1065AF',
|
||||
textColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
default: {
|
||||
load: {
|
||||
backgroundColor: '#404040',
|
||||
textColor: '#ffffff',
|
||||
},
|
||||
refresh: {
|
||||
backgroundColor: '#2584e8',
|
||||
textColor: '#ffffff',
|
||||
},
|
||||
error: {
|
||||
backgroundColor: '#1065AF',
|
||||
textColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
showMessage(message: string, type: 'load' | 'refresh') {
|
||||
showMessage(message: string, type: 'load' | 'refresh' | 'error') {
|
||||
if (NativeDevLoadingView) {
|
||||
const colorScheme =
|
||||
getColorScheme() === 'dark' ? COLOR_SCHEME.dark : COLOR_SCHEME.default;
|
||||
|
||||
+7
-4
@@ -232,8 +232,6 @@ Error: ${e.message}`;
|
||||
});
|
||||
|
||||
client.on('error', data => {
|
||||
DevLoadingView.hide();
|
||||
|
||||
if (data.type === 'GraphNotFoundError') {
|
||||
client.close();
|
||||
setHMRUnavailableReason(
|
||||
@@ -253,8 +251,6 @@ Error: ${e.message}`;
|
||||
});
|
||||
|
||||
client.on('close', closeEvent => {
|
||||
DevLoadingView.hide();
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
|
||||
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5
|
||||
const isNormalOrUnsetCloseReason =
|
||||
@@ -296,10 +292,17 @@ function setHMRUnavailableReason(reason: string) {
|
||||
}
|
||||
hmrUnavailableReason = reason;
|
||||
|
||||
const DevLoadingView = require('./DevLoadingView').default;
|
||||
DevLoadingView.hide();
|
||||
|
||||
// We only want to show a warning if Fast Refresh is on *and* if we ever
|
||||
// previously managed to connect successfully. We don't want to show
|
||||
// the warning to native engineers who use cached bundles without Metro.
|
||||
if (hmrClient.isEnabled() && didConnect) {
|
||||
DevLoadingView.showMessage(
|
||||
'Fast Refresh disconnected. Reload app to reconnect.',
|
||||
'error',
|
||||
);
|
||||
console.warn(reason);
|
||||
// (Not using the `warning` module to prevent a Buck cycle.)
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ let reactJsInspectorTracing = RNTarget(
|
||||
name: .reactJsInspectorTracing,
|
||||
path: "ReactCommon/jsinspector-modern/tracing",
|
||||
excludedPaths: ["tests"],
|
||||
dependencies: [.reactNativeDependencies, .reactFeatureFlags, .jsi, .reactOSCompat]
|
||||
dependencies: [.reactNativeDependencies, .reactFeatureFlags, .reactJsInspectorNetwork, .jsi, .reactOSCompat]
|
||||
)
|
||||
|
||||
/// React-jsinspectornetwork.podspec
|
||||
|
||||
@@ -380,6 +380,11 @@ static RCTBridge *RCTCurrentBridgeInstance = nil;
|
||||
moduleProvider:(RCTBridgeModuleListProvider)block
|
||||
launchOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
// Only enabld this assertion in OSS
|
||||
#if COCOAPODS
|
||||
[RCTBridge throwIfOnLegacyArch];
|
||||
#endif
|
||||
|
||||
if (self = [super init]) {
|
||||
RCTEnforceNewArchitectureValidation(RCTNotAllowedInBridgeless, self, nil);
|
||||
_delegate = delegate;
|
||||
@@ -393,6 +398,17 @@ static RCTBridge *RCTCurrentBridgeInstance = nil;
|
||||
return self;
|
||||
}
|
||||
|
||||
// Wrap the exception throwing in a static method to avoid the warning: "The code following the exception will never be
|
||||
// executed". This might create build failures internally where we treat warnings as errors.
|
||||
+ (void)throwIfOnLegacyArch
|
||||
{
|
||||
@throw [NSException
|
||||
exceptionWithName:NSInternalInconsistencyException
|
||||
reason:
|
||||
@"You are trying to initialize the legacy architecture. This is not supported anymore and will be removed in the next version of React Native. Please use the New Architecture instead."
|
||||
userInfo:nil];
|
||||
}
|
||||
|
||||
RCT_NOT_IMPLEMENTED(-(instancetype)init)
|
||||
|
||||
- (void)dealloc
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
RCT_EXTERN void RCTEnableAppearancePreference(BOOL enabled);
|
||||
RCT_EXTERN void RCTOverrideAppearancePreference(NSString *const);
|
||||
RCT_EXTERN void RCTOverrideAppearancePreference(NSString *);
|
||||
RCT_EXTERN void RCTUseKeyWindowForSystemStyle(BOOL useMainScreen);
|
||||
RCT_EXTERN NSString *RCTCurrentOverrideAppearancePreference(void);
|
||||
RCT_EXTERN NSString *RCTColorSchemePreference(UITraitCollection *traitCollection);
|
||||
|
||||
@@ -85,14 +85,6 @@ RCT_EXPORT_MODULE()
|
||||
dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), self->_initialMessageBlock);
|
||||
}
|
||||
|
||||
- (void)hideBannerAfter:(CGFloat)delay
|
||||
{
|
||||
// Cancel previous hide call after the delay.
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hide) object:nil];
|
||||
// Set new hide call after a delay.
|
||||
[self performSelector:@selector(hide) withObject:nil afterDelay:delay];
|
||||
}
|
||||
|
||||
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
if (!RCTDevLoadingViewGetEnabled() || _hiding) {
|
||||
@@ -128,6 +120,9 @@ RCT_EXPORT_MODULE()
|
||||
self->_container = [[UIView alloc] init];
|
||||
self->_container.backgroundColor = backgroundColor;
|
||||
self->_container.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)];
|
||||
[self->_container addGestureRecognizer:tapGesture];
|
||||
self->_container.userInteractionEnabled = YES;
|
||||
|
||||
self->_label = [[UILabel alloc] init];
|
||||
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
@@ -158,8 +153,6 @@ RCT_EXPORT_MODULE()
|
||||
[self->_label.centerXAnchor constraintEqualToAnchor:self->_container.centerXAnchor],
|
||||
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5],
|
||||
]];
|
||||
|
||||
[self hideBannerAfter:15.0];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -365,11 +365,14 @@ RCT_EXPORT_MODULE()
|
||||
handler:^(__unused UIAlertAction *action) {
|
||||
[weakSelf setDefaultJSBundle];
|
||||
}]];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(__unused UIAlertAction *action) {
|
||||
return;
|
||||
}]];
|
||||
UIAlertAction *configCancelAction =
|
||||
[UIAlertAction actionWithTitle:@"Cancel"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(__unused UIAlertAction *action) {
|
||||
return;
|
||||
}];
|
||||
[configCancelAction setValue:[UIColor systemRedColor] forKey:@"titleTextColor"];
|
||||
[alertController addAction:configCancelAction];
|
||||
[RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
|
||||
}]];
|
||||
|
||||
@@ -390,6 +393,11 @@ RCT_EXPORT_METHOD(show)
|
||||
|
||||
_actionSheet = [UIAlertController alertControllerWithTitle:@"React Native Dev Menu" message:nil preferredStyle:style];
|
||||
|
||||
NSAttributedString *title =
|
||||
[[NSAttributedString alloc] initWithString:@"React Native Dev Menu"
|
||||
attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:17]}];
|
||||
[_actionSheet setValue:title forKey:@"_attributedTitle"];
|
||||
|
||||
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
|
||||
for (RCTDevMenuItem *item in items) {
|
||||
UIAlertAction *action = [UIAlertAction actionWithTitle:item.title
|
||||
@@ -399,9 +407,11 @@ RCT_EXPORT_METHOD(show)
|
||||
[_actionSheet addAction:action];
|
||||
}
|
||||
|
||||
[_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:[self alertActionHandlerForDevItem:nil]]];
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:[self alertActionHandlerForDevItem:nil]];
|
||||
[cancelAction setValue:[UIColor systemRedColor] forKey:@"titleTextColor"];
|
||||
[_actionSheet addAction:cancelAction];
|
||||
|
||||
_presentedItems = items;
|
||||
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
|
||||
|
||||
+62
-4
@@ -650,7 +650,9 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
|
||||
|
||||
BOOL isPointInside = [self pointInside:point withEvent:event];
|
||||
|
||||
BOOL clipsToBounds = self.currentContainerView.clipsToBounds;
|
||||
UIView *currentContainerView = self.currentContainerView;
|
||||
|
||||
BOOL clipsToBounds = currentContainerView.clipsToBounds;
|
||||
|
||||
clipsToBounds = clipsToBounds || _layoutMetrics.overflowInset == EdgeInsets{};
|
||||
|
||||
@@ -658,8 +660,8 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
|
||||
return nil;
|
||||
}
|
||||
|
||||
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
|
||||
UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
|
||||
for (UIView *subview = nullptr in [currentContainerView.subviews reverseObjectEnumerator]) {
|
||||
UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:currentContainerView] withEvent:event];
|
||||
if (hitView) {
|
||||
return hitView;
|
||||
}
|
||||
@@ -859,7 +861,7 @@ static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle)
|
||||
|
||||
if (_useCustomContainerView) {
|
||||
if (!_containerView) {
|
||||
_containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
|
||||
_containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)];
|
||||
for (UIView *subview = nullptr in effectiveContentView.subviews) {
|
||||
[_containerView addSubview:subview];
|
||||
}
|
||||
@@ -1540,6 +1542,62 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
||||
{
|
||||
if ([commandName isEqualToString:@"focus"]) {
|
||||
[self focus];
|
||||
return;
|
||||
}
|
||||
|
||||
if ([commandName isEqualToString:@"blur"]) {
|
||||
[self blur];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)focus
|
||||
{
|
||||
[self becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (void)blur
|
||||
{
|
||||
[self resignFirstResponder];
|
||||
}
|
||||
|
||||
#pragma mark - Focus Events
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
if (![super becomeFirstResponder]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_eventEmitter && ReactNativeFeatureFlags::enableImperativeFocus()) {
|
||||
_eventEmitter->onFocus();
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)resignFirstResponder
|
||||
{
|
||||
if (![super resignFirstResponder]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_eventEmitter && ReactNativeFeatureFlags::enableImperativeFocus()) {
|
||||
_eventEmitter->onBlur();
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -111,6 +111,12 @@ typedef NS_OPTIONS(NSInteger, RNComponentViewUpdateMask) {
|
||||
*/
|
||||
- (void)prepareForRecycle;
|
||||
|
||||
/*
|
||||
* Called for unmounted components that won't be moved to a recycle pool.
|
||||
* Useful for releasing any associated resources.
|
||||
*/
|
||||
- (void)invalidate;
|
||||
|
||||
/*
|
||||
* Read the last props used to update the view.
|
||||
*/
|
||||
|
||||
@@ -108,6 +108,7 @@ const NSInteger RCTComponentViewRegistryRecyclePoolMaxSize = 1024;
|
||||
auto &recycledViews = _recyclePool[componentHandle];
|
||||
|
||||
if (recycledViews.size() > RCTComponentViewRegistryRecyclePoolMaxSize || !componentViewDescriptor.shouldBeRecycled) {
|
||||
[componentViewDescriptor.view invalidate];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (void)prepareForRecycle;
|
||||
|
||||
- (void)invalidate;
|
||||
|
||||
- (facebook::react::Props::Shared)props;
|
||||
|
||||
- (void)setIsJSResponder:(BOOL)isJSResponder;
|
||||
|
||||
@@ -129,6 +129,11 @@ using namespace facebook::react;
|
||||
// Default implementation does nothing.
|
||||
}
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
// Default implementation does nothing.
|
||||
}
|
||||
|
||||
- (facebook::react::Props::Shared)props
|
||||
{
|
||||
RCTAssert(NO, @"props access should be implemented by RCTViewComponentView.");
|
||||
|
||||
@@ -61,7 +61,7 @@ RCT_EXTERN CGPathRef RCTPathCreateWithRoundedRect(
|
||||
CGRect bounds,
|
||||
RCTCornerInsets cornerInsets,
|
||||
const CGAffineTransform *transform,
|
||||
const BOOL inverted);
|
||||
BOOL inverted);
|
||||
|
||||
/**
|
||||
* Draw a CSS-compliant border as an image. You can determine if it's scalable
|
||||
|
||||
@@ -1892,6 +1892,7 @@ public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementa
|
||||
public fun <init> (Lcom/facebook/react/devsupport/ReactInstanceDevHelper;)V
|
||||
public fun hide ()V
|
||||
public fun showMessage (Ljava/lang/String;)V
|
||||
public fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
|
||||
public fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
|
||||
}
|
||||
|
||||
@@ -2119,6 +2120,7 @@ public abstract interface class com/facebook/react/devsupport/interfaces/DevBund
|
||||
public abstract interface class com/facebook/react/devsupport/interfaces/DevLoadingViewManager {
|
||||
public abstract fun hide ()V
|
||||
public abstract fun showMessage (Ljava/lang/String;)V
|
||||
public abstract fun showMessage (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)V
|
||||
public abstract fun updateProgress (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;)V
|
||||
}
|
||||
|
||||
@@ -5564,7 +5566,7 @@ public final class com/facebook/react/views/scroll/ReactHorizontalScrollContaine
|
||||
public final class com/facebook/react/views/scroll/ReactHorizontalScrollContainerViewManager$Companion {
|
||||
}
|
||||
|
||||
public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android/widget/HorizontalScrollView, android/view/View$OnLayoutChangeListener, android/view/ViewGroup$OnHierarchyChangeListener, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/views/scroll/ReactAccessibleScrollView, com/facebook/react/views/scroll/ReactScrollViewHelper$HasFlingAnimator, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollEventThrottle, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollState, com/facebook/react/views/scroll/ReactScrollViewHelper$HasSmoothScroll, com/facebook/react/views/scroll/ReactScrollViewHelper$HasStateWrapper {
|
||||
public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android/widget/HorizontalScrollView, android/view/View$OnLayoutChangeListener, android/view/ViewGroup$OnHierarchyChangeListener, com/facebook/react/uimanager/ReactClippingViewGroup, com/facebook/react/uimanager/ReactOverflowViewWithInset, com/facebook/react/views/scroll/ReactAccessibleScrollView, com/facebook/react/views/scroll/ReactScrollViewHelper$HasFlingAnimator, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollEventThrottle, com/facebook/react/views/scroll/ReactScrollViewHelper$HasScrollState, com/facebook/react/views/scroll/ReactScrollViewHelper$HasSmoothScroll, com/facebook/react/views/scroll/ReactScrollViewHelper$HasStateWrapper, com/facebook/react/views/scroll/VirtualViewContainer {
|
||||
public fun <init> (Landroid/content/Context;)V
|
||||
public fun <init> (Landroid/content/Context;Lcom/facebook/react/views/scroll/FpsListener;)V
|
||||
public fun abortAnimation ()V
|
||||
@@ -5594,6 +5596,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android
|
||||
public fun getScrollEnabled ()Z
|
||||
public fun getScrollEventThrottle ()I
|
||||
public fun getStateWrapper ()Lcom/facebook/react/uimanager/StateWrapper;
|
||||
public fun getVirtualViewContainerState ()Lcom/facebook/react/views/scroll/VirtualViewContainerState;
|
||||
protected fun handleInterceptedTouchEvent (Landroid/view/MotionEvent;)V
|
||||
public fun isPartiallyScrolledInView (Landroid/view/View;)Z
|
||||
protected fun onAttachedToWindow ()V
|
||||
|
||||
+10
@@ -328,6 +328,15 @@ public class ReactInstanceManager {
|
||||
}
|
||||
|
||||
registerCxxErrorHandlerFunc();
|
||||
|
||||
// Using `if (true)` just to prevent tests / lint errors.
|
||||
if (true) {
|
||||
// Legacy architecture of React Native is deprecated and can't be initialized anymore.
|
||||
// More details on:
|
||||
// https://github.com/react-native-community/discussions-and-proposals/blob/nc/legacy-arch-removal/proposals/0929-legacy-architecture-removal.md
|
||||
throw new UnsupportedOperationException(
|
||||
"ReactInstanceManager.createReactContext is unsupported.");
|
||||
}
|
||||
}
|
||||
|
||||
private ReactInstanceDevHelper createDevHelperInterface() {
|
||||
@@ -1446,6 +1455,7 @@ public class ReactInstanceManager {
|
||||
*/
|
||||
private ReactApplicationContext createReactContext(
|
||||
JavaScriptExecutor jsExecutor, JSBundleLoader jsBundleLoader) {
|
||||
|
||||
FLog.d(ReactConstants.TAG, "ReactInstanceManager.createReactContext()");
|
||||
ReactMarker.logMarker(CREATE_REACT_CONTEXT_START, jsExecutor.getName());
|
||||
|
||||
|
||||
+24
-19
@@ -196,9 +196,11 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext) :
|
||||
|
||||
private val nodesManagerRef = AtomicReference<NativeAnimatedNodesManager?>()
|
||||
|
||||
@Volatile private var currentFrameNumber: Long = 0
|
||||
private var batchingControlledByJS = false // TODO T71377544: delete
|
||||
|
||||
@Volatile private var currentBatchNumber: Long = 0 // frame number at last operations dispatch
|
||||
@Volatile private var currentFrameNumber: Long = 0 // TODO T71377544: delete
|
||||
|
||||
@Volatile private var currentBatchNumber: Long = 0
|
||||
|
||||
private var initializedForFabric = false
|
||||
private var initializedForNonFabric = false
|
||||
@@ -280,19 +282,22 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext) :
|
||||
|
||||
var batchNumber = currentBatchNumber - 1
|
||||
|
||||
// The problem we're trying to solve here: we could be in the middle of queueing
|
||||
// a batch of related animation operations when Fabric flushes a batch of MountItems.
|
||||
// It's visually bad if we execute half of the animation ops and then wait another frame
|
||||
// (or more) to execute the rest.
|
||||
// See mFrameNumber. If the dispatchedFrameNumber drifts too far - that
|
||||
// is, if no MountItems are scheduled for a while, which can happen if a tree
|
||||
// is committed but there are no changes - bring these counts back in sync and
|
||||
// execute any queued operations. This number is arbitrary, but we want it low
|
||||
// enough that the user shouldn't be able to see this delay in most cases.
|
||||
currentFrameNumber++
|
||||
if ((currentFrameNumber - currentBatchNumber) > 2) {
|
||||
currentBatchNumber = currentFrameNumber
|
||||
batchNumber = currentBatchNumber
|
||||
// TODO T71377544: delete this when the JS method is confirmed safe
|
||||
if (!batchingControlledByJS) {
|
||||
// The problem we're trying to solve here: we could be in the middle of queueing
|
||||
// a batch of related animation operations when Fabric flushes a batch of MountItems.
|
||||
// It's visually bad if we execute half of the animation ops and then wait another frame
|
||||
// (or more) to execute the rest.
|
||||
// See mFrameNumber. If the dispatchedFrameNumber drifts too far - that
|
||||
// is, if no MountItems are scheduled for a while, which can happen if a tree
|
||||
// is committed but there are no changes - bring these counts back in sync and
|
||||
// execute any queued operations. This number is arbitrary, but we want it low
|
||||
// enough that the user shouldn't be able to see this delay in most cases.
|
||||
currentFrameNumber++
|
||||
if ((currentFrameNumber - currentBatchNumber) > 2) {
|
||||
currentBatchNumber = currentFrameNumber
|
||||
batchNumber = currentBatchNumber
|
||||
}
|
||||
}
|
||||
|
||||
preOperations.executeBatch(batchNumber, nodesManager)
|
||||
@@ -479,14 +484,14 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext) :
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun startOperationBatch() {
|
||||
// no-op
|
||||
batchingControlledByJS = true
|
||||
currentBatchNumber++
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun finishOperationBatch() {
|
||||
// no-op
|
||||
batchingControlledByJS = false
|
||||
currentBatchNumber++
|
||||
}
|
||||
|
||||
override fun createAnimatedNode(tagDouble: Double, config: ReadableMap) {
|
||||
|
||||
+4
@@ -774,8 +774,12 @@ public class NativeAnimatedNodesManager(
|
||||
("Looks like animated nodes graph has ${reason}, there are $activeNodesCount but toposort visited only $updatedNodesCount")
|
||||
)
|
||||
if (eventListenerInitializedForFabric && cyclesDetected == 0) {
|
||||
// TODO T71377544: investigate these SoftExceptions and see if we can remove entirely
|
||||
// or fix the root cause
|
||||
ReactSoftExceptionLogger.logSoftException(TAG, ReactNoCrashSoftException(ex))
|
||||
} else if (eventListenerInitializedForFabric) {
|
||||
// TODO T71377544: investigate these SoftExceptions and see if we can remove entirely
|
||||
// or fix the root cause
|
||||
ReactSoftExceptionLogger.logSoftException(TAG, ReactNoCrashSoftException(ex))
|
||||
} else {
|
||||
throw ex
|
||||
|
||||
+19
-15
@@ -98,13 +98,7 @@ public object DefaultNewArchitectureEntryPoint {
|
||||
ReactNativeFeatureFlags.override(ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android())
|
||||
}
|
||||
ReleaseLevel.STABLE -> {
|
||||
ReactNativeFeatureFlags.override(
|
||||
ReactNativeFeatureFlagsOverrides_RNOSS_Stable_Android(
|
||||
fabricEnabled,
|
||||
bridgelessEnabled,
|
||||
turboModulesEnabled,
|
||||
)
|
||||
)
|
||||
ReactNativeFeatureFlags.override(ReactNativeFeatureFlagsOverrides_RNOSS_Stable_Android())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +119,16 @@ public object DefaultNewArchitectureEntryPoint {
|
||||
privateConcurrentReactEnabled = featureFlags.enableFabricRenderer()
|
||||
privateBridgelessEnabled = featureFlags.enableBridgelessArchitecture()
|
||||
|
||||
val (isValid, errorMessage) =
|
||||
isConfigurationValid(
|
||||
privateTurboModulesEnabled,
|
||||
privateFabricEnabled,
|
||||
privateBridgelessEnabled,
|
||||
)
|
||||
if (!isValid) {
|
||||
error(errorMessage)
|
||||
}
|
||||
|
||||
DefaultSoLoader.maybeLoadSoLibrary()
|
||||
}
|
||||
|
||||
@@ -158,13 +162,13 @@ public object DefaultNewArchitectureEntryPoint {
|
||||
fabricEnabled: Boolean,
|
||||
bridgelessEnabled: Boolean,
|
||||
): Pair<Boolean, String> =
|
||||
when {
|
||||
fabricEnabled && !turboModulesEnabled ->
|
||||
false to
|
||||
"fabricEnabled=true requires turboModulesEnabled=true (is now false) - Please update your DefaultNewArchitectureEntryPoint.load() parameters."
|
||||
bridgelessEnabled && (!turboModulesEnabled || !fabricEnabled) ->
|
||||
false to
|
||||
"bridgelessEnabled=true requires (turboModulesEnabled=true AND fabricEnabled=true) - Please update your DefaultNewArchitectureEntryPoint.load() parameters."
|
||||
else -> true to ""
|
||||
if (!turboModulesEnabled || !fabricEnabled || !bridgelessEnabled) {
|
||||
false to
|
||||
"You cannot load React Native with the New Architecture disabled. " +
|
||||
"Please use DefaultNewArchitectureEntryPoint.load() instead of " +
|
||||
"DefaultNewArchitectureEntryPoint.load(turboModulesEnabled=$turboModulesEnabled, " +
|
||||
"fabricEnabled=$fabricEnabled, bridgelessEnabled=$bridgelessEnabled)"
|
||||
} else {
|
||||
true to ""
|
||||
}
|
||||
}
|
||||
|
||||
+13
-3
@@ -33,10 +33,14 @@ public class DefaultDevLoadingViewImplementation(
|
||||
private var devLoadingPopup: PopupWindow? = null
|
||||
|
||||
override fun showMessage(message: String) {
|
||||
showMessage(message, color = null, backgroundColor = null)
|
||||
}
|
||||
|
||||
override fun showMessage(message: String, color: Double?, backgroundColor: Double?) {
|
||||
if (!isEnabled) {
|
||||
return
|
||||
}
|
||||
UiThreadUtil.runOnUiThread { showInternal(message) }
|
||||
UiThreadUtil.runOnUiThread { showInternal(message, color, backgroundColor) }
|
||||
}
|
||||
|
||||
override fun updateProgress(status: String?, done: Int?, total: Int?) {
|
||||
@@ -59,7 +63,7 @@ public class DefaultDevLoadingViewImplementation(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showInternal(message: String) {
|
||||
private fun showInternal(message: String, color: Double?, backgroundColor: Double?) {
|
||||
if (devLoadingPopup?.isShowing == true) {
|
||||
// already showing
|
||||
return
|
||||
@@ -84,13 +88,19 @@ public class DefaultDevLoadingViewImplementation(
|
||||
currentActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val view = inflater.inflate(R.layout.dev_loading_view, null) as TextView
|
||||
view.text = message
|
||||
if (color != null) {
|
||||
view.setTextColor(color.toInt())
|
||||
}
|
||||
if (backgroundColor != null) {
|
||||
view.setBackgroundColor(backgroundColor.toInt())
|
||||
}
|
||||
view.setOnClickListener { hideInternal() }
|
||||
val popup =
|
||||
PopupWindow(
|
||||
view,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
popup.isTouchable = false
|
||||
popup.showAtLocation(currentActivity.window.decorView, Gravity.NO_GRAVITY, 0, topOffset)
|
||||
devLoadingView = view
|
||||
devLoadingPopup = popup
|
||||
|
||||
+7
-14
@@ -78,7 +78,6 @@ import java.io.File
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.util.Locale
|
||||
import javax.inject.Provider
|
||||
|
||||
public abstract class DevSupportManagerBase(
|
||||
protected val applicationContext: Context,
|
||||
@@ -185,6 +184,7 @@ public abstract class DevSupportManagerBase(
|
||||
}
|
||||
|
||||
private var perfMonitorOverlayManager: PerfMonitorOverlayManager? = null
|
||||
private var perfMonitorInitialized = false
|
||||
private var tracingStateProvider: TracingStateProvider? = null
|
||||
|
||||
init {
|
||||
@@ -222,13 +222,6 @@ public abstract class DevSupportManagerBase(
|
||||
perfMonitorOverlayManager =
|
||||
PerfMonitorOverlayManager(
|
||||
reactInstanceDevHelper,
|
||||
Provider {
|
||||
val context = reactInstanceDevHelper.currentActivity
|
||||
if (context == null || context.isFinishing) {
|
||||
return@Provider null
|
||||
}
|
||||
context
|
||||
},
|
||||
{ openDebugger(DebuggerFrontendPanelName.PERFORMANCE.toString()) },
|
||||
)
|
||||
}
|
||||
@@ -552,14 +545,16 @@ public abstract class DevSupportManagerBase(
|
||||
}
|
||||
|
||||
override fun onNewReactContextCreated(reactContext: ReactContext) {
|
||||
resetCurrentContext(reactContext)
|
||||
|
||||
if (reactInstanceDevHelper is PerfMonitorDevHelper) {
|
||||
if (!perfMonitorInitialized && reactInstanceDevHelper is PerfMonitorDevHelper) {
|
||||
perfMonitorOverlayManager?.let { manager ->
|
||||
reactInstanceDevHelper.inspectorTarget?.addPerfMonitorListener(manager)
|
||||
}
|
||||
perfMonitorOverlayManager?.enable()
|
||||
perfMonitorOverlayManager?.startBackgroundTrace()
|
||||
perfMonitorInitialized = true
|
||||
}
|
||||
|
||||
resetCurrentContext(reactContext)
|
||||
}
|
||||
|
||||
override fun onReactInstanceDestroyed(reactContext: ReactContext) {
|
||||
@@ -877,8 +872,6 @@ public abstract class DevSupportManagerBase(
|
||||
devLoadingViewManager?.showMessage("Reloading...")
|
||||
}
|
||||
|
||||
perfMonitorOverlayManager?.reset()
|
||||
|
||||
devServerHelper.openPackagerConnection(
|
||||
javaClass.simpleName,
|
||||
object : PackagerCommandListener {
|
||||
@@ -928,7 +921,7 @@ public abstract class DevSupportManagerBase(
|
||||
hideRedboxDialog()
|
||||
hideDevOptionsDialog()
|
||||
devLoadingViewManager?.hide()
|
||||
perfMonitorOverlayManager?.reset()
|
||||
perfMonitorOverlayManager?.disable()
|
||||
|
||||
devServerHelper.closePackagerConnection()
|
||||
}
|
||||
|
||||
+2
@@ -11,6 +11,8 @@ package com.facebook.react.devsupport.interfaces
|
||||
public interface DevLoadingViewManager {
|
||||
public fun showMessage(message: String)
|
||||
|
||||
public fun showMessage(message: String, color: Double?, backgroundColor: Double?)
|
||||
|
||||
public fun updateProgress(status: String?, done: Int?, total: Int?)
|
||||
|
||||
public fun hide()
|
||||
|
||||
+4
@@ -7,11 +7,15 @@
|
||||
|
||||
package com.facebook.react.devsupport.perfmonitor
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
/**
|
||||
* Interface implemented by [com.facebook.react.runtime.ReactHostImplDevHelper] exposing additional
|
||||
* hooks used to implement the V2 Perf Monitor overlay (experimental).
|
||||
*/
|
||||
internal interface PerfMonitorDevHelper {
|
||||
public val currentActivity: Activity?
|
||||
|
||||
/**
|
||||
* The inspector target object. Matches the lifetime of the ReactHost. May be null if modern JS
|
||||
* debugging is disabled.
|
||||
|
||||
+21
-32
@@ -7,42 +7,24 @@
|
||||
|
||||
package com.facebook.react.devsupport.perfmonitor
|
||||
|
||||
import android.content.Context
|
||||
import com.facebook.react.bridge.UiThreadUtil
|
||||
import com.facebook.react.devsupport.interfaces.TracingState
|
||||
import javax.inject.Provider
|
||||
|
||||
internal class PerfMonitorOverlayManager(
|
||||
private val devHelper: PerfMonitorDevHelper,
|
||||
private val contextProvider: Provider<Context?>,
|
||||
private val onRequestOpenDevTools: () -> Unit,
|
||||
) : PerfMonitorUpdateListener {
|
||||
private var enabled: Boolean = false
|
||||
private var initialized: Boolean = false
|
||||
private var view: PerfMonitorOverlayView? = null
|
||||
private var tracingState: TracingState = TracingState.ENABLEDINCDPMODE
|
||||
|
||||
private fun init() {
|
||||
if (initialized || !enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
UiThreadUtil.runOnUiThread {
|
||||
val context = contextProvider.get() ?: return@runOnUiThread
|
||||
view = PerfMonitorOverlayView(context, ::handleRecordingButtonPress)
|
||||
|
||||
// Start background tracing
|
||||
devHelper.inspectorTarget?.resumeBackgroundTrace()
|
||||
|
||||
view?.show()
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
/** Enable the Perf Monitor overlay. Will be shown when updates are received. */
|
||||
/** Enable the Perf Monitor overlay. */
|
||||
fun enable() {
|
||||
enabled = true
|
||||
init()
|
||||
UiThreadUtil.runOnUiThread {
|
||||
val context = devHelper.currentActivity ?: return@runOnUiThread
|
||||
view = PerfMonitorOverlayView(context, ::handleRecordingButtonPress)
|
||||
}
|
||||
}
|
||||
|
||||
/** Disable the Perf Monitor overlay. Will remain hidden when updates are received. */
|
||||
@@ -52,24 +34,31 @@ internal class PerfMonitorOverlayManager(
|
||||
enabled = false
|
||||
}
|
||||
|
||||
/** Reset the Perf Monitor overlay, e.g. after a reload. */
|
||||
fun reset() {
|
||||
// Update with current recording state
|
||||
onRecordingStateChanged(
|
||||
devHelper.inspectorTarget?.getTracingState() ?: TracingState.ENABLEDINCDPMODE
|
||||
)
|
||||
/** Start background trace recording. */
|
||||
fun startBackgroundTrace() {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
devHelper.inspectorTarget?.let { target ->
|
||||
target.resumeBackgroundTrace()
|
||||
onRecordingStateChanged(target.getTracingState())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecordingStateChanged(state: TracingState) {
|
||||
tracingState = state
|
||||
view?.updateRecordingState(state)
|
||||
UiThreadUtil.runOnUiThread {
|
||||
view?.updateRecordingState(state)
|
||||
view?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRecordingButtonPress() {
|
||||
when (tracingState) {
|
||||
TracingState.ENABLEDINBACKGROUNDMODE -> {
|
||||
devHelper.inspectorTarget?.let {
|
||||
if (!it.pauseAndAnalyzeBackgroundTrace()) {
|
||||
devHelper.inspectorTarget?.let { target ->
|
||||
if (!target.pauseAndAnalyzeBackgroundTrace()) {
|
||||
onRequestOpenDevTools()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -55,11 +55,11 @@ internal class PerfMonitorOverlayView(
|
||||
|
||||
if (state == TracingState.ENABLEDINBACKGROUNDMODE) {
|
||||
(statusIndicator.background as GradientDrawable).setColor(Color.RED)
|
||||
statusLabel.text = "Background Profiling Active"
|
||||
statusLabel.text = "Profiling Active"
|
||||
tooltipLabel.text = "Press ☰ to open"
|
||||
} else {
|
||||
(statusIndicator.background as GradientDrawable).setColor(Color.GRAY)
|
||||
statusLabel.text = "Background Profiling Stopped"
|
||||
statusLabel.text = "Profiling Stopped"
|
||||
tooltipLabel.text = "Press ☰ to restart"
|
||||
}
|
||||
dialog.show()
|
||||
@@ -164,7 +164,7 @@ internal class PerfMonitorOverlayView(
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
setColor(Color.BLACK)
|
||||
cornerRadius = dpToPx(14.5f)
|
||||
alpha = (0.8 * 255).toInt()
|
||||
alpha = (0.4 * 255).toInt()
|
||||
setStroke(dpToPx(1f).toInt(), COLOR_OVERLAY_BORDER)
|
||||
}
|
||||
showDividers = LinearLayout.SHOW_DIVIDER_MIDDLE
|
||||
|
||||
+3
@@ -821,6 +821,9 @@ public class SurfaceMountingManager {
|
||||
@UnstableReactNativeAPI
|
||||
public void experimental_prefetchResources(
|
||||
int surfaceId, String componentName, MapBuffer params) {
|
||||
if (isStopped()) {
|
||||
return;
|
||||
}
|
||||
mViewManagerRegistry
|
||||
.get(componentName)
|
||||
.experimental_prefetchResources(
|
||||
|
||||
+13
-1
@@ -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.
|
||||
*
|
||||
* @generated SignedSource<<f089964c958dfcac00f83c3371ccbefc>>
|
||||
* @generated SignedSource<<6b632ac7553ae149fa42a67efde5acfa>>
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -180,6 +180,12 @@ public object ReactNativeFeatureFlags {
|
||||
@JvmStatic
|
||||
public fun enableImagePrefetchingAndroid(): Boolean = accessor.enableImagePrefetchingAndroid()
|
||||
|
||||
/**
|
||||
* When enabled, Android will build and initiate image prefetch requests on ImageShadowNode::layout and batch them together in a single JNI call
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun enableImagePrefetchingJNIBatchingAndroid(): Boolean = accessor.enableImagePrefetchingJNIBatchingAndroid()
|
||||
|
||||
/**
|
||||
* When enabled, Android will initiate image prefetch requested on ImageShadowNode::layout on the UI thread
|
||||
*/
|
||||
@@ -192,6 +198,12 @@ public object ReactNativeFeatureFlags {
|
||||
@JvmStatic
|
||||
public fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean = accessor.enableImmediateUpdateModeForContentOffsetChanges()
|
||||
|
||||
/**
|
||||
* Enable ref.focus() and ref.blur() for all views, not just TextInput.
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun enableImperativeFocus(): Boolean = accessor.enableImperativeFocus()
|
||||
|
||||
/**
|
||||
* This is to fix the issue with interop view manager where component descriptor lookup is causing ViewManager to preload.
|
||||
*/
|
||||
|
||||
+21
-1
@@ -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.
|
||||
*
|
||||
* @generated SignedSource<<912dec895495052328e7e52b94a6738a>>
|
||||
* @generated SignedSource<<c228bdbf6120b4715f49e03c04918a7c>>
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -45,8 +45,10 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
|
||||
private var enableIOSTextBaselineOffsetPerLineCache: Boolean? = null
|
||||
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
|
||||
private var enableImagePrefetchingAndroidCache: Boolean? = null
|
||||
private var enableImagePrefetchingJNIBatchingAndroidCache: Boolean? = null
|
||||
private var enableImagePrefetchingOnUiThreadAndroidCache: Boolean? = null
|
||||
private var enableImmediateUpdateModeForContentOffsetChangesCache: Boolean? = null
|
||||
private var enableImperativeFocusCache: Boolean? = null
|
||||
private var enableInteropViewManagerClassLookUpOptimizationIOSCache: Boolean? = null
|
||||
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
|
||||
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
|
||||
@@ -319,6 +321,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
|
||||
return cached
|
||||
}
|
||||
|
||||
override fun enableImagePrefetchingJNIBatchingAndroid(): Boolean {
|
||||
var cached = enableImagePrefetchingJNIBatchingAndroidCache
|
||||
if (cached == null) {
|
||||
cached = ReactNativeFeatureFlagsCxxInterop.enableImagePrefetchingJNIBatchingAndroid()
|
||||
enableImagePrefetchingJNIBatchingAndroidCache = cached
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
override fun enableImagePrefetchingOnUiThreadAndroid(): Boolean {
|
||||
var cached = enableImagePrefetchingOnUiThreadAndroidCache
|
||||
if (cached == null) {
|
||||
@@ -337,6 +348,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
|
||||
return cached
|
||||
}
|
||||
|
||||
override fun enableImperativeFocus(): Boolean {
|
||||
var cached = enableImperativeFocusCache
|
||||
if (cached == null) {
|
||||
cached = ReactNativeFeatureFlagsCxxInterop.enableImperativeFocus()
|
||||
enableImperativeFocusCache = cached
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
override fun enableInteropViewManagerClassLookUpOptimizationIOS(): Boolean {
|
||||
var cached = enableInteropViewManagerClassLookUpOptimizationIOSCache
|
||||
if (cached == null) {
|
||||
|
||||
+5
-1
@@ -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.
|
||||
*
|
||||
* @generated SignedSource<<ba1e53d93b9fdaf298a034543bc44a57>>
|
||||
* @generated SignedSource<<26dce1945df5641ab84cbca9eaf2b10f>>
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -78,10 +78,14 @@ public object ReactNativeFeatureFlagsCxxInterop {
|
||||
|
||||
@DoNotStrip @JvmStatic public external fun enableImagePrefetchingAndroid(): Boolean
|
||||
|
||||
@DoNotStrip @JvmStatic public external fun enableImagePrefetchingJNIBatchingAndroid(): Boolean
|
||||
|
||||
@DoNotStrip @JvmStatic public external fun enableImagePrefetchingOnUiThreadAndroid(): Boolean
|
||||
|
||||
@DoNotStrip @JvmStatic public external fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean
|
||||
|
||||
@DoNotStrip @JvmStatic public external fun enableImperativeFocus(): Boolean
|
||||
|
||||
@DoNotStrip @JvmStatic public external fun enableInteropViewManagerClassLookUpOptimizationIOS(): Boolean
|
||||
|
||||
@DoNotStrip @JvmStatic public external fun enableLayoutAnimationsOnAndroid(): Boolean
|
||||
|
||||
+5
-1
@@ -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.
|
||||
*
|
||||
* @generated SignedSource<<7601cbcde75ff19ef0fe67f6faef2549>>
|
||||
* @generated SignedSource<<11051ece1b61fd4bf4ca003a3b7fc4f9>>
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -73,10 +73,14 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
|
||||
|
||||
override fun enableImagePrefetchingAndroid(): Boolean = false
|
||||
|
||||
override fun enableImagePrefetchingJNIBatchingAndroid(): Boolean = false
|
||||
|
||||
override fun enableImagePrefetchingOnUiThreadAndroid(): Boolean = false
|
||||
|
||||
override fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean = false
|
||||
|
||||
override fun enableImperativeFocus(): Boolean = false
|
||||
|
||||
override fun enableInteropViewManagerClassLookUpOptimizationIOS(): Boolean = false
|
||||
|
||||
override fun enableLayoutAnimationsOnAndroid(): Boolean = false
|
||||
|
||||
+23
-1
@@ -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.
|
||||
*
|
||||
* @generated SignedSource<<3a13f8e35423b634ed4ef46fbac3c1e9>>
|
||||
* @generated SignedSource<<66fa583c37021750123a483ab0ccb030>>
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -49,8 +49,10 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
|
||||
private var enableIOSTextBaselineOffsetPerLineCache: Boolean? = null
|
||||
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
|
||||
private var enableImagePrefetchingAndroidCache: Boolean? = null
|
||||
private var enableImagePrefetchingJNIBatchingAndroidCache: Boolean? = null
|
||||
private var enableImagePrefetchingOnUiThreadAndroidCache: Boolean? = null
|
||||
private var enableImmediateUpdateModeForContentOffsetChangesCache: Boolean? = null
|
||||
private var enableImperativeFocusCache: Boolean? = null
|
||||
private var enableInteropViewManagerClassLookUpOptimizationIOSCache: Boolean? = null
|
||||
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
|
||||
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
|
||||
@@ -348,6 +350,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
|
||||
return cached
|
||||
}
|
||||
|
||||
override fun enableImagePrefetchingJNIBatchingAndroid(): Boolean {
|
||||
var cached = enableImagePrefetchingJNIBatchingAndroidCache
|
||||
if (cached == null) {
|
||||
cached = currentProvider.enableImagePrefetchingJNIBatchingAndroid()
|
||||
accessedFeatureFlags.add("enableImagePrefetchingJNIBatchingAndroid")
|
||||
enableImagePrefetchingJNIBatchingAndroidCache = cached
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
override fun enableImagePrefetchingOnUiThreadAndroid(): Boolean {
|
||||
var cached = enableImagePrefetchingOnUiThreadAndroidCache
|
||||
if (cached == null) {
|
||||
@@ -368,6 +380,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
|
||||
return cached
|
||||
}
|
||||
|
||||
override fun enableImperativeFocus(): Boolean {
|
||||
var cached = enableImperativeFocusCache
|
||||
if (cached == null) {
|
||||
cached = currentProvider.enableImperativeFocus()
|
||||
accessedFeatureFlags.add("enableImperativeFocus")
|
||||
enableImperativeFocusCache = cached
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
override fun enableInteropViewManagerClassLookUpOptimizationIOS(): Boolean {
|
||||
var cached = enableInteropViewManagerClassLookUpOptimizationIOSCache
|
||||
if (cached == null) {
|
||||
|
||||
+3
-1
@@ -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.
|
||||
*
|
||||
* @generated SignedSource<<4feeb3c1789d8169dd93bee7ae1ff1ac>>
|
||||
* @generated SignedSource<<dffc5db0f80501f6246f9d6087c6fb4a>>
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -25,5 +25,7 @@ public open class ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android :
|
||||
|
||||
override fun enableAccessibilityOrder(): Boolean = true
|
||||
|
||||
override fun enableSwiftUIBasedFilters(): Boolean = true
|
||||
|
||||
override fun preventShadowTreeCommitExhaustion(): Boolean = true
|
||||
}
|
||||
|
||||
+3
-9
@@ -7,16 +7,10 @@
|
||||
|
||||
package com.facebook.react.internal.featureflags
|
||||
|
||||
public class ReactNativeFeatureFlagsOverrides_RNOSS_Stable_Android(
|
||||
private val fabricEnabled: Boolean,
|
||||
private val bridgelessEnabled: Boolean,
|
||||
private val turboModulesEnabled: Boolean,
|
||||
) : ReactNativeNewArchitectureFeatureFlagsDefaults(bridgelessEnabled) {
|
||||
override fun useFabricInterop(): Boolean = bridgelessEnabled || fabricEnabled
|
||||
public class ReactNativeFeatureFlagsOverrides_RNOSS_Stable_Android() :
|
||||
ReactNativeNewArchitectureFeatureFlagsDefaults() {
|
||||
|
||||
override fun enableFabricRenderer(): Boolean = bridgelessEnabled || fabricEnabled
|
||||
|
||||
override fun useTurboModules(): Boolean = bridgelessEnabled || turboModulesEnabled
|
||||
override fun useFabricInterop(): Boolean = true
|
||||
|
||||
override fun useShadowNodeStateOnClone(): Boolean = true
|
||||
}
|
||||
|
||||
+5
-1
@@ -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.
|
||||
*
|
||||
* @generated SignedSource<<143b568248e68033efaefc3f178ff6db>>
|
||||
* @generated SignedSource<<f26998daf87c8b90c2ec822c8316b134>>
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -73,10 +73,14 @@ public interface ReactNativeFeatureFlagsProvider {
|
||||
|
||||
@DoNotStrip public fun enableImagePrefetchingAndroid(): Boolean
|
||||
|
||||
@DoNotStrip public fun enableImagePrefetchingJNIBatchingAndroid(): Boolean
|
||||
|
||||
@DoNotStrip public fun enableImagePrefetchingOnUiThreadAndroid(): Boolean
|
||||
|
||||
@DoNotStrip public fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean
|
||||
|
||||
@DoNotStrip public fun enableImperativeFocus(): Boolean
|
||||
|
||||
@DoNotStrip public fun enableInteropViewManagerClassLookUpOptimizationIOS(): Boolean
|
||||
|
||||
@DoNotStrip public fun enableLayoutAnimationsOnAndroid(): Boolean
|
||||
|
||||
+5
-3
@@ -20,9 +20,11 @@ package com.facebook.react.internal.featureflags
|
||||
* When the new architecture is enabled, we want to set the default values of the flags for Fabric,
|
||||
* TurboModules and Bridgeless as enabled by default.
|
||||
*/
|
||||
public open class ReactNativeNewArchitectureFeatureFlagsDefaults(
|
||||
private val newArchitectureEnabled: Boolean = true
|
||||
) : ReactNativeFeatureFlagsDefaults() {
|
||||
public open class ReactNativeNewArchitectureFeatureFlagsDefaults() :
|
||||
ReactNativeFeatureFlagsDefaults() {
|
||||
|
||||
private val newArchitectureEnabled: Boolean = true
|
||||
|
||||
override fun enableBridgelessArchitecture(): Boolean = newArchitectureEnabled
|
||||
|
||||
override fun enableFabricRenderer(): Boolean = newArchitectureEnabled
|
||||
|
||||
+3
-1
@@ -31,7 +31,9 @@ internal class DevLoadingModule(reactContext: ReactApplicationContext) :
|
||||
}
|
||||
|
||||
override fun showMessage(message: String, color: Double?, backgroundColor: Double?) {
|
||||
UiThreadUtil.runOnUiThread { devLoadingViewManager?.showMessage(message) }
|
||||
UiThreadUtil.runOnUiThread {
|
||||
devLoadingViewManager?.showMessage(message, color, backgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
|
||||
+4
@@ -648,6 +648,10 @@ public class ReactHostImpl(
|
||||
if (currentReactContext != null) {
|
||||
if (ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) {
|
||||
DisplayMetricsHolder.initDisplayMetrics(currentReactContext)
|
||||
|
||||
synchronized(attachedSurfaces) {
|
||||
attachedSurfaces.forEach { surface -> surface.view?.requestLayout() }
|
||||
}
|
||||
}
|
||||
|
||||
val appearanceModule = currentReactContext.getNativeModule(AppearanceModule::class.java)
|
||||
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.BaseInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import android.view.animation.LinearInterpolator
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.common.annotations.VisibleForTesting
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException
|
||||
|
||||
/**
|
||||
* Class responsible for parsing and converting layout animation data into native [Animation] in
|
||||
* order to animate layout when a valid configuration has been supplied by the application.
|
||||
*/
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Deprecated(
|
||||
message = "This class is part of Legacy Architecture and will be removed in a future release",
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
internal abstract class AbstractLayoutAnimation {
|
||||
var interpolator: Interpolator? = null
|
||||
var delayMs: Int = 0
|
||||
var animatedProperty: AnimatedPropertyType? = null
|
||||
var durationMs: Int = 0
|
||||
|
||||
internal abstract fun isValid(): Boolean
|
||||
|
||||
/**
|
||||
* Create an animation object for the current animation type, based on the view and final screen
|
||||
* coordinates. If the application-supplied configuration does not specify an animation definition
|
||||
* for this types, or if the animation definition is invalid, returns null.
|
||||
*/
|
||||
internal abstract fun createAnimationImpl(
|
||||
view: View,
|
||||
x: Int,
|
||||
y: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
): Animation?
|
||||
|
||||
fun reset() {
|
||||
animatedProperty = null
|
||||
durationMs = 0
|
||||
delayMs = 0
|
||||
interpolator = null
|
||||
}
|
||||
|
||||
fun initializeFromConfig(data: ReadableMap, globalDuration: Int) {
|
||||
animatedProperty =
|
||||
if (data.hasKey("property")) {
|
||||
AnimatedPropertyType.fromString(data.getString("property").orEmpty())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
durationMs = if (data.hasKey("duration")) data.getInt("duration") else globalDuration
|
||||
delayMs = if (data.hasKey("delay")) data.getInt("delay") else 0
|
||||
require(data.hasKey("type")) { "Missing interpolation type." }
|
||||
|
||||
interpolator =
|
||||
getInterpolator(InterpolatorType.fromString(data.getString("type").orEmpty()), data)
|
||||
|
||||
if (!isValid()) {
|
||||
throw IllegalViewOperationException("Invalid layout animation : $data")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an animation object to be used to animate the view, based on the animation config
|
||||
* supplied at initialization time and the new view position and size.
|
||||
*
|
||||
* @param view the view to create the animation for
|
||||
* @param x the new X position for the view
|
||||
* @param y the new Y position for the view
|
||||
* @param width the new width value for the view
|
||||
* @param height the new height value for the view
|
||||
*/
|
||||
fun createAnimation(view: View, x: Int, y: Int, width: Int, height: Int): Animation? {
|
||||
if (!isValid()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return createAnimationImpl(view, x, y, width, height)?.apply {
|
||||
val slowdownFactor = if (SLOWDOWN_ANIMATION_MODE) 10 else 1
|
||||
duration = (durationMs * slowdownFactor).toLong()
|
||||
startOffset = (delayMs * slowdownFactor).toLong()
|
||||
interpolator = this@AbstractLayoutAnimation.interpolator
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"AbstractLayoutAnimation",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
|
||||
// Forces animation to be playing 10x slower, used for debug purposes.
|
||||
private const val SLOWDOWN_ANIMATION_MODE = false
|
||||
|
||||
private val INTERPOLATOR: Map<InterpolatorType, BaseInterpolator> =
|
||||
mapOf(
|
||||
InterpolatorType.LINEAR to LinearInterpolator(),
|
||||
InterpolatorType.EASE_IN to AccelerateInterpolator(),
|
||||
InterpolatorType.EASE_OUT to DecelerateInterpolator(),
|
||||
InterpolatorType.EASE_IN_EASE_OUT to AccelerateDecelerateInterpolator(),
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
fun getInterpolator(type: InterpolatorType, params: ReadableMap): Interpolator {
|
||||
val interpolator =
|
||||
if (type == InterpolatorType.SPRING) {
|
||||
SimpleSpringInterpolator(SimpleSpringInterpolator.getSpringDamping(params))
|
||||
} else {
|
||||
INTERPOLATOR[type]
|
||||
}
|
||||
requireNotNull(interpolator) { "Missing interpolator for type : $type" }
|
||||
return interpolator
|
||||
}
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
|
||||
/**
|
||||
* Enum representing the different view properties that can be used when animating layout for view
|
||||
* creation.
|
||||
*/
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Deprecated(
|
||||
message = "This class is part of Legacy Architecture and will be removed in a future release",
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
internal enum class AnimatedPropertyType {
|
||||
OPACITY,
|
||||
SCALE_X,
|
||||
SCALE_Y,
|
||||
SCALE_XY;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromString(name: String): AnimatedPropertyType {
|
||||
return when (name) {
|
||||
"opacity" -> OPACITY
|
||||
"scaleX" -> SCALE_X
|
||||
"scaleY" -> SCALE_Y
|
||||
"scaleXY" -> SCALE_XY
|
||||
else -> throw IllegalArgumentException("Unsupported animated property: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import android.view.View
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.ScaleAnimation
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException
|
||||
|
||||
/** Class responsible for default layout animation, i.e animation of view creation and deletion. */
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Deprecated(
|
||||
message = "This class is part of Legacy Architecture and will be removed in a future release",
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
internal abstract class BaseLayoutAnimation : AbstractLayoutAnimation() {
|
||||
abstract fun isReverse(): Boolean
|
||||
|
||||
override fun isValid(): Boolean = durationMs > 0 && animatedProperty != null
|
||||
|
||||
override fun createAnimationImpl(view: View, x: Int, y: Int, width: Int, height: Int): Animation {
|
||||
animatedProperty?.let {
|
||||
return when (it) {
|
||||
AnimatedPropertyType.OPACITY -> {
|
||||
val fromValue = if (isReverse()) view.alpha else 0.0f
|
||||
val toValue = if (isReverse()) 0.0f else view.alpha
|
||||
OpacityAnimation(view, fromValue, toValue)
|
||||
}
|
||||
|
||||
AnimatedPropertyType.SCALE_XY -> {
|
||||
val fromValue = if (isReverse()) 1.0f else 0.0f
|
||||
val toValue = if (isReverse()) 0.0f else 1.0f
|
||||
ScaleAnimation(
|
||||
fromValue,
|
||||
toValue,
|
||||
fromValue,
|
||||
toValue,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
.5f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
.5f,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedPropertyType.SCALE_X -> {
|
||||
val fromValue = if (isReverse()) 1.0f else 0.0f
|
||||
val toValue = if (isReverse()) 0.0f else 1.0f
|
||||
ScaleAnimation(
|
||||
fromValue,
|
||||
toValue,
|
||||
1f,
|
||||
1f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
.5f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0f,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedPropertyType.SCALE_Y -> {
|
||||
val fromValue = if (isReverse()) 1.0f else 0.0f
|
||||
val toValue = if (isReverse()) 0.0f else 1.0f
|
||||
ScaleAnimation(
|
||||
1f,
|
||||
1f,
|
||||
fromValue,
|
||||
toValue,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
.5f,
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: throw IllegalViewOperationException("Missing animated property from animation config")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
init {
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"BaseLayoutAnimation",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
|
||||
/**
|
||||
* Enum representing the different interpolators that can be used in layout animation configuration.
|
||||
*/
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Deprecated(
|
||||
message = "This class is part of Legacy Architecture and will be removed in a future release",
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
internal enum class InterpolatorType {
|
||||
LINEAR,
|
||||
EASE_IN,
|
||||
EASE_OUT,
|
||||
EASE_IN_EASE_OUT,
|
||||
SPRING;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromString(name: String): InterpolatorType {
|
||||
return when (name.lowercase()) {
|
||||
"linear" -> LINEAR
|
||||
"easein" -> EASE_IN
|
||||
"easeout" -> EASE_OUT
|
||||
"easeineaseout" -> EASE_IN_EASE_OUT
|
||||
"spring" -> SPRING
|
||||
else -> throw IllegalArgumentException("Unsupported interpolation type : $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-174
@@ -9,19 +9,12 @@
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import android.util.SparseArray
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.Animation.AnimationListener
|
||||
import com.facebook.react.bridge.Callback
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.UiThreadUtil.assertOnUiThread
|
||||
import com.facebook.react.bridge.UiThreadUtil.getUiThreadHandler
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger.assertLegacyArchitecture
|
||||
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationType.Companion.toString
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
|
||||
import javax.annotation.concurrent.NotThreadSafe
|
||||
|
||||
/**
|
||||
@@ -36,75 +29,27 @@ import javax.annotation.concurrent.NotThreadSafe
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
public open class LayoutAnimationController {
|
||||
private val layoutCreateAnimation: AbstractLayoutAnimation = LayoutCreateAnimation()
|
||||
private val layoutUpdateAnimation: AbstractLayoutAnimation = LayoutUpdateAnimation()
|
||||
private val layoutDeleteAnimation: AbstractLayoutAnimation = LayoutDeleteAnimation()
|
||||
private val layoutHandlers = SparseArray<LayoutHandlingAnimation?>(0)
|
||||
|
||||
private var shouldAnimateLayout = false
|
||||
private var maxAnimationDuration: Long = -1
|
||||
private var completionRunnable: Runnable? = null
|
||||
|
||||
public fun initializeFromConfig(config: ReadableMap?, completionCallback: Callback?) {
|
||||
if (config == null) {
|
||||
reset()
|
||||
return
|
||||
}
|
||||
|
||||
shouldAnimateLayout = false
|
||||
val globalDuration = if (config.hasKey("duration")) config.getInt("duration") else 0
|
||||
if (config.hasKey(toString(LayoutAnimationType.CREATE))) {
|
||||
layoutCreateAnimation.initializeFromConfig(
|
||||
config.getMap(toString(LayoutAnimationType.CREATE))!!,
|
||||
globalDuration,
|
||||
)
|
||||
shouldAnimateLayout = true
|
||||
}
|
||||
if (config.hasKey(toString(LayoutAnimationType.UPDATE))) {
|
||||
layoutUpdateAnimation.initializeFromConfig(
|
||||
config.getMap(toString(LayoutAnimationType.UPDATE))!!,
|
||||
globalDuration,
|
||||
)
|
||||
shouldAnimateLayout = true
|
||||
}
|
||||
if (config.hasKey(toString(LayoutAnimationType.DELETE))) {
|
||||
layoutDeleteAnimation.initializeFromConfig(
|
||||
config.getMap(toString(LayoutAnimationType.DELETE))!!,
|
||||
globalDuration,
|
||||
)
|
||||
shouldAnimateLayout = true
|
||||
}
|
||||
|
||||
if (shouldAnimateLayout && completionCallback != null) {
|
||||
completionRunnable = Runnable { completionCallback.invoke(java.lang.Boolean.TRUE) }
|
||||
}
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"LayoutAnimationController",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
|
||||
public open fun reset() {
|
||||
layoutCreateAnimation.reset()
|
||||
layoutUpdateAnimation.reset()
|
||||
layoutDeleteAnimation.reset()
|
||||
completionRunnable = null
|
||||
shouldAnimateLayout = false
|
||||
maxAnimationDuration = -1
|
||||
for (i in layoutHandlers.size() - 1 downTo 0) {
|
||||
val animation = layoutHandlers.valueAt(i)
|
||||
if (!animation!!.isValid()) {
|
||||
layoutHandlers.removeAt(i)
|
||||
}
|
||||
}
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"LayoutAnimationController",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
|
||||
public open fun shouldAnimateLayout(viewToAnimate: View?): Boolean {
|
||||
// if view parent is null, skip animation: view have been clipped, we don't want animation to
|
||||
// resume when view is re-attached to parent, which is the standard android animation behavior.
|
||||
// If there's a layout handling animation going on, it should be animated nonetheless since the
|
||||
// ongoing animation needs to be updated.
|
||||
if (viewToAnimate == null) {
|
||||
return false
|
||||
}
|
||||
return ((shouldAnimateLayout && viewToAnimate.parent != null) ||
|
||||
layoutHandlers[viewToAnimate.id] != null)
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"LayoutAnimationController",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,57 +64,10 @@ public open class LayoutAnimationController {
|
||||
* @param height the new height value for the view
|
||||
*/
|
||||
public open fun applyLayoutUpdate(view: View, x: Int, y: Int, width: Int, height: Int) {
|
||||
assertOnUiThread()
|
||||
|
||||
val reactTag = view.id
|
||||
|
||||
// Update an ongoing animation if possible, otherwise the layout update would be ignored as
|
||||
// the existing animation would still animate to the old layout.
|
||||
val existingAnimation = layoutHandlers[reactTag]
|
||||
if (existingAnimation != null) {
|
||||
if (!existingAnimation.isValid()) {
|
||||
layoutHandlers.remove(reactTag)
|
||||
} else {
|
||||
existingAnimation.onLayoutUpdate(x, y, width, height)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which animation to use : if view is initially invisible, use create animation,
|
||||
// otherwise use update animation. This approach is easier than maintaining a list of tags
|
||||
// for recently created views.
|
||||
val layoutAnimation =
|
||||
if ((view.width == 0 || view.height == 0)) layoutCreateAnimation else layoutUpdateAnimation
|
||||
|
||||
val animation = layoutAnimation.createAnimation(view, x, y, width, height)
|
||||
|
||||
if (animation is LayoutHandlingAnimation) {
|
||||
animation.setAnimationListener(
|
||||
object : AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
layoutHandlers.put(reactTag, animation as LayoutHandlingAnimation)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
layoutHandlers.remove(reactTag)
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
||||
}
|
||||
)
|
||||
} else {
|
||||
view.layout(x, y, x + width, y + height)
|
||||
}
|
||||
|
||||
if (animation != null) {
|
||||
val animationDuration = animation.duration
|
||||
if (animationDuration > maxAnimationDuration) {
|
||||
maxAnimationDuration = animationDuration
|
||||
scheduleCompletionCallback(animationDuration)
|
||||
}
|
||||
|
||||
view.startAnimation(animation)
|
||||
}
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"LayoutAnimationController",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,59 +79,9 @@ public open class LayoutAnimationController {
|
||||
* view.
|
||||
*/
|
||||
public open fun deleteView(view: View, listener: LayoutAnimationListener) {
|
||||
assertOnUiThread()
|
||||
|
||||
val animation =
|
||||
layoutDeleteAnimation.createAnimation(view, view.left, view.top, view.width, view.height)
|
||||
|
||||
if (animation != null) {
|
||||
disableUserInteractions(view)
|
||||
|
||||
animation.setAnimationListener(
|
||||
object : AnimationListener {
|
||||
override fun onAnimationStart(anim: Animation) = Unit
|
||||
|
||||
override fun onAnimationRepeat(anim: Animation) = Unit
|
||||
|
||||
override fun onAnimationEnd(anim: Animation) {
|
||||
listener.onAnimationEnd()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val animationDuration = animation.duration
|
||||
if (animationDuration > maxAnimationDuration) {
|
||||
scheduleCompletionCallback(animationDuration)
|
||||
maxAnimationDuration = animationDuration
|
||||
}
|
||||
|
||||
view.startAnimation(animation)
|
||||
} else {
|
||||
listener.onAnimationEnd()
|
||||
}
|
||||
}
|
||||
|
||||
/** Disables user interactions for a view and all it's subviews. */
|
||||
private fun disableUserInteractions(view: View) {
|
||||
view.isClickable = false
|
||||
if (view is ViewGroup) {
|
||||
for (i in 0 until view.childCount) {
|
||||
disableUserInteractions(view.getChildAt(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleCompletionCallback(delayMillis: Long) {
|
||||
if (completionRunnable != null) {
|
||||
val completionHandler = getUiThreadHandler()
|
||||
completionHandler.removeCallbacks(completionRunnable!!)
|
||||
completionHandler.postDelayed(completionRunnable!!, delayMillis)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
init {
|
||||
assertLegacyArchitecture("LayoutAnimationController", LegacyArchitectureLogLevel.ERROR)
|
||||
}
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"LayoutAnimationController",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
|
||||
|
||||
/**
|
||||
* Enum representing the different animation type that can be specified in layout animation config.
|
||||
*/
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Deprecated(
|
||||
message = "This class is part of Legacy Architecture and will be removed in a future release",
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
internal enum class LayoutAnimationType {
|
||||
CREATE,
|
||||
UPDATE,
|
||||
DELETE;
|
||||
|
||||
companion object {
|
||||
init {
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"LayoutAnimationType",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun toString(type: LayoutAnimationType): String {
|
||||
return when (type) {
|
||||
CREATE -> "create"
|
||||
UPDATE -> "update"
|
||||
DELETE -> "delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
|
||||
|
||||
/**
|
||||
* Class responsible for handling layout view creation animation, applied to view whenever a valid
|
||||
* config was supplied for the layout animation of CREATE type.
|
||||
*/
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
internal class LayoutCreateAnimation : BaseLayoutAnimation() {
|
||||
|
||||
override fun isReverse(): Boolean = false
|
||||
|
||||
private companion object {
|
||||
init {
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"LayoutCreateAnimation",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
|
||||
|
||||
/**
|
||||
* Class responsible for handling layout view deletion animation, applied to view whenever a valid
|
||||
* config was supplied for the layout animation of DELETE type.
|
||||
*/
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Deprecated(
|
||||
message = "This class is part of Legacy Architecture and will be removed in a future release",
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
internal class LayoutDeleteAnimation : BaseLayoutAnimation() {
|
||||
|
||||
override fun isReverse(): Boolean = true
|
||||
|
||||
private companion object {
|
||||
init {
|
||||
LegacyArchitectureLogger.assertLegacyArchitecture(
|
||||
"LayoutDeleteAnimation",
|
||||
LegacyArchitectureLogLevel.ERROR,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.uimanager.layoutanimation
|
||||
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitecture
|
||||
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
|
||||
|
||||
/** Interface for an animation type that takes care of updating the view layout. */
|
||||
@LegacyArchitecture(logLevel = LegacyArchitectureLogLevel.ERROR)
|
||||
@Deprecated(
|
||||
message = "This class is part of Legacy Architecture and will be removed in a future release",
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
internal interface LayoutHandlingAnimation {
|
||||
/**
|
||||
* Notifies the animation of a layout update in case one occurs during the animation. This avoids
|
||||
* animating the view to the old layout since it's no longer correct; instead the animation should
|
||||
* update and do whatever it can so that the final layout is correct.
|
||||
*
|
||||
* @param x the new X position for the view
|
||||
* @param y the new Y position for the view
|
||||
* @param width the new width value for the view
|
||||
* @param height the new height value for the view
|
||||
*/
|
||||
fun onLayoutUpdate(x: Int, y: Int, width: Int, height: Int)
|
||||
|
||||
/**
|
||||
* Returns whether the animation is valid and should be used. Because layout animations generally
|
||||
* hold {@link java.lang.ref.WeakReference<android.view.View>} objects, it's possible that the
|
||||
* view has been garbage collected. In this case, the animation should not be used.
|
||||
*
|
||||
* @return true if the animation is valid and can be used, false otherwise
|
||||
*/
|
||||
fun isValid(): Boolean
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user