Co-authored-by: Tomek Zawadzki <tomekzawadzki98@gmail.com>
Co-authored-by: vineyardbovines <spencerfpope@gmail.com>
This commit is contained in:
Oleksii Bulenok
2026-08-18 16:46:11 +02:00
committed by GitHub
parent 7f59d8fcfb
commit 45aa1a67b0
75 changed files with 3128 additions and 3584 deletions
+1 -8
View File
@@ -196,14 +196,7 @@ jobs:
with: with:
header: fingerprint-diff header: fingerprint-diff
message: | message: |
The Pull Request introduced fingerprint changes against the base commit: The Pull Request introduced native fingerprint changes against the base commit.
<details><summary>Fingerprint diff</summary>
```json
${{ steps.fingerprint.outputs.diff }}
```
</details>
--- ---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
+1 -5
View File
@@ -184,10 +184,6 @@ module.exports = function (_config) {
androidStatusBar: { androidStatusBar: {
barStyle: 'light-content', barStyle: 'light-content',
}, },
// Dark nav bar in light mode is better than light nav bar in dark mode
androidNavigationBar: {
barStyle: 'light-content',
},
android: { android: {
icon: './assets/app-icons/android_icon_default_next.png', icon: './assets/app-icons/android_icon_default_next.png',
adaptiveIcon: { adaptiveIcon: {
@@ -261,7 +257,7 @@ module.exports = function (_config) {
'expo-build-properties', 'expo-build-properties',
{ {
ios: { ios: {
deploymentTarget: '15.1', deploymentTarget: '16.4',
buildReactNativeFromSource: true, buildReactNativeFromSource: true,
ccacheEnabled: IS_DEV, ccacheEnabled: IS_DEV,
cxxLanguageStandard: 'c++23', cxxLanguageStandard: 'c++23',
+22 -13
View File
@@ -21,9 +21,10 @@ jest.mock('react-native-safe-area-context', () => {
const inset = {top: 0, right: 0, bottom: 0, left: 0} const inset = {top: 0, right: 0, bottom: 0, left: 0}
return { return {
SafeAreaProvider: jest.fn().mockImplementation(({children}) => children), SafeAreaProvider: jest.fn().mockImplementation(({children}) => children),
SafeAreaConsumer: jest SafeAreaConsumer: jest.fn().mockImplementation(
.fn() /** @param {{children: (i: typeof inset) => unknown}} props */
.mockImplementation(({children}) => children(inset)), ({children}) => children(inset),
),
useSafeAreaInsets: jest.fn().mockImplementation(() => inset), useSafeAreaInsets: jest.fn().mockImplementation(() => inset),
} }
}) })
@@ -85,18 +86,26 @@ jest.mock('expo-application', () => ({
})) }))
jest.mock('expo-modules-core', () => ({ jest.mock('expo-modules-core', () => ({
requireNativeModule: jest.fn().mockImplementation(moduleName => { requireNativeModule: jest.fn().mockImplementation(
if (moduleName === 'ExpoPlatformInfo') { /** @param {string} moduleName */
return { moduleName => {
getIsReducedMotionEnabled: () => false, if (moduleName === 'ExpoPlatformInfo') {
return {
getIsReducedMotionEnabled: () => false,
}
} }
} if (moduleName === 'BottomSheet') {
if (moduleName === 'BottomSheet') { return {
return { dismissAll: () => {},
dismissAll: () => {}, }
} }
}
}), const expoModules = /** @type {Record<string, unknown> | undefined} */ (
globalThis.expo?.modules
)
return expoModules?.[moduleName]
},
),
requireNativeViewManager: jest.fn().mockImplementation(_ => { requireNativeViewManager: jest.fn().mockImplementation(_ => {
return () => null return () => null
}), }),
+4 -33
View File
@@ -1,49 +1,20 @@
apply plugin: 'com.android.library' plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'expo.modules.bottomsheet' group = 'expo.modules.bottomsheet'
version = '0.1.0' version = '0.1.0'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
}
}
}
android { android {
namespace "expo.modules.bottomsheet" namespace "expo.modules.bottomsheet"
defaultConfig { defaultConfig {
versionCode 1 versionCode 1
versionName "0.1.0" versionName "0.1.0"
} }
lintOptions {
abortOnError false
}
} }
dependencies { dependencies {
implementation project(':expo-modules-core')
implementation 'com.google.android.material:material:1.13.0' implementation 'com.google.android.material:material:1.13.0'
implementation "com.facebook.react:react-native:+" implementation "com.facebook.react:react-native:+"
} }
@@ -1,93 +1,19 @@
apply plugin: 'com.android.library' plugins {
apply plugin: 'kotlin-android' id 'com.android.library'
apply plugin: 'maven-publish' id 'expo-module-gradle-plugin'
}
group = 'expo.modules.backgroundnotificationhandler' group = 'expo.modules.backgroundnotificationhandler'
version = '0.5.0' version = '0.5.0'
buildscript {
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
if (expoModulesCorePlugin.exists()) {
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
}
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// Ensures backward compatibility
ext.getKotlinVersion = {
if (ext.has("kotlinVersion")) {
ext.kotlinVersion()
} else {
ext.safeExtGet("kotlinVersion", "1.8.10")
}
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
}
}
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
repositories {
maven {
url = mavenLocal().url
}
}
}
}
android { android {
compileSdkVersion safeExtGet("compileSdkVersion", 33)
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.majorVersion
}
}
namespace "expo.modules.backgroundnotificationhandler" namespace "expo.modules.backgroundnotificationhandler"
defaultConfig { defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1 versionCode 1
versionName "0.5.0" versionName "0.5.0"
} }
lintOptions {
abortOnError false
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}
repositories {
mavenCentral()
} }
dependencies { dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation 'com.google.firebase:firebase-messaging-ktx:24.0.0' implementation 'com.google.firebase:firebase-messaging-ktx:24.0.0'
} }
@@ -1,97 +1,21 @@
apply plugin: 'com.android.library' plugins {
apply plugin: 'kotlin-android' id 'com.android.library'
apply plugin: 'maven-publish' id 'expo-module-gradle-plugin'
}
group = 'expo.modules.blueskygifview' group = 'expo.modules.blueskygifview'
version = '0.5.0' version = '0.5.0'
buildscript {
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
if (expoModulesCorePlugin.exists()) {
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
}
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// Ensures backward compatibility
ext.getKotlinVersion = {
if (ext.has("kotlinVersion")) {
ext.kotlinVersion()
} else {
ext.safeExtGet("kotlinVersion", "1.8.10")
}
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
}
}
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
repositories {
maven {
url = mavenLocal().url
}
}
}
}
android { android {
compileSdkVersion safeExtGet("compileSdkVersion", 33)
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.majorVersion
}
}
namespace "expo.modules.blueskygifview" namespace "expo.modules.blueskygifview"
defaultConfig { defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1 versionCode 1
versionName "0.5.0" versionName "0.5.0"
} }
lintOptions {
abortOnError false
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}
repositories {
mavenCentral()
} }
dependencies { dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
def GLIDE_VERSION = "4.16.0"
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
// Keep glide version up to date with expo-image so that we don't have duplicate deps // Keep glide version up to date with expo-image so that we don't have duplicate deps
implementation 'com.github.bumptech.glide:glide:4.13.2' implementation 'com.github.bumptech.glide:glide:4.13.2'
@@ -1,45 +1,17 @@
apply plugin: 'com.android.library' plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'expo.modules.blueskyswissarmy' group = 'expo.modules.blueskyswissarmy'
version = '0.6.0' version = '0.6.0'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
}
}
}
android { android {
namespace "expo.modules.blueskyswissarmy" namespace "expo.modules.blueskyswissarmy"
defaultConfig { defaultConfig {
versionCode 1 versionCode 1
versionName "0.6.0" versionName "0.6.0"
} }
lintOptions {
abortOnError false
}
} }
dependencies { dependencies {
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
s.static_framework = true s.static_framework = true
s.dependency 'ExpoModulesCore' s.dependency 'ExpoModulesCore'
s.dependency 'EXNotifications' s.dependency 'ExpoNotifications'
# Swift/Objective-C compatibility # Swift/Objective-C compatibility
s.pod_target_xcconfig = { s.pod_target_xcconfig = {
@@ -1,4 +1,4 @@
import EXNotifications import ExpoNotifications
import ExpoModulesCore import ExpoModulesCore
import UIKit import UIKit
import UserNotifications import UserNotifications
@@ -1,34 +1,15 @@
apply plugin: 'com.android.library' plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'expo.modules.blueskyvideocompress' group = 'expo.modules.blueskyvideocompress'
version = '1.0.0' version = '1.0.0'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()
buildscript {
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
android { android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
namespace "expo.modules.blueskyvideocompress" namespace "expo.modules.blueskyvideocompress"
defaultConfig { defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1 versionCode 1
versionName "1.0.0" versionName "1.0.0"
} }
lintOptions {
abortOnError false
}
}
dependencies {
} }
+8 -35
View File
@@ -1,46 +1,19 @@
apply plugin: 'com.android.library' plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'expo.community.modules.emojipicker' group = 'expo.community.modules.emojipicker'
version = '0.1.0' version = '0.1.0'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
}
}
}
android { android {
namespace "expo.community.modules.emojipicker" namespace "expo.community.modules.emojipicker"
defaultConfig { defaultConfig {
versionCode 1 versionCode 1
versionName "0.1.0" versionName "0.1.0"
} }
lintOptions { }
abortOnError false
} dependencies {
dependencies { implementation "androidx.emoji2:emoji2-emojipicker:1.5.0"
implementation "androidx.emoji2:emoji2-emojipicker:1.5.0"
}
} }
@@ -1,92 +1,15 @@
apply plugin: 'com.android.library' plugins {
apply plugin: 'kotlin-android' id 'com.android.library'
apply plugin: 'maven-publish' id 'expo-module-gradle-plugin'
}
group = 'xyz.blueskyweb.app.exporeceiveandroidintents' group = 'xyz.blueskyweb.app.exporeceiveandroidintents'
version = '0.4.1' version = '0.4.1'
buildscript {
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
if (expoModulesCorePlugin.exists()) {
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
}
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// Ensures backward compatibility
ext.getKotlinVersion = {
if (ext.has("kotlinVersion")) {
ext.kotlinVersion()
} else {
ext.safeExtGet("kotlinVersion", "1.8.10")
}
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
}
}
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
repositories {
maven {
url = mavenLocal().url
}
}
}
}
android { android {
compileSdkVersion safeExtGet("compileSdkVersion", 33)
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.majorVersion
}
}
namespace "xyz.blueskyweb.app.exporeceiveandroidintents" namespace "xyz.blueskyweb.app.exporeceiveandroidintents"
defaultConfig { defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1 versionCode 1
versionName "0.4.1" versionName "0.4.1"
} }
lintOptions {
abortOnError false
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}
repositories {
mavenCentral()
}
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
} }
+1 -1
View File
@@ -1497,7 +1497,7 @@
}, },
"src/view/com/composer/drafts/state/storage.ts": { "src/view/com/composer/drafts/state/storage.ts": {
"typescript/require-await": { "typescript/require-await": {
"count": 3 "count": 2
} }
}, },
"src/view/com/composer/photos/EditImageDialog.web.tsx": { "src/view/com/composer/photos/EditImageDialog.web.tsx": {
+54 -52
View File
@@ -165,43 +165,44 @@
"emoji-mart": "^5.6.0", "emoji-mart": "^5.6.0",
"emoji-regex": "^10.4.0", "emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"expo": "54.0.35", "expo": "57.0.8",
"expo-age-range": "0.2.18", "expo-age-range": "57.0.2",
"expo-application": "~7.0.8", "expo-application": "~57.0.2",
"expo-asset": "~12.0.13", "expo-asset": "~57.0.7",
"expo-blur": "~15.0.8", "expo-blur": "~57.0.2",
"expo-build-properties": "~1.0.10", "expo-build-properties": "~57.0.7",
"expo-camera": "~17.0.10", "expo-camera": "~57.0.3",
"expo-clipboard": "~8.0.8", "expo-clipboard": "~57.0.1",
"expo-contacts": "^15.0.10", "expo-contacts": "^57.0.2",
"expo-dev-client": "~6.0.20", "expo-dev-client": "~57.0.9",
"expo-device": "~8.0.10", "expo-device": "~57.0.1",
"expo-file-system": "~19.0.21", "expo-file-system": "~57.0.1",
"expo-font": "~14.0.11", "expo-font": "~57.0.1",
"expo-glass-effect": "0.1.10", "expo-glass-effect": "57.0.1",
"expo-haptics": "~15.0.8", "expo-haptics": "~57.0.1",
"expo-image": "~3.0.11", "expo-image": "57.0.1",
"expo-image-manipulator": "~14.0.8", "expo-image-manipulator": "~57.0.6",
"expo-image-picker": "~17.0.10", "expo-image-picker": "~57.0.6",
"expo-intent-launcher": "~13.0.8", "expo-intent-launcher": "~57.0.1",
"expo-keep-awake": "~15.0.8", "expo-keep-awake": "~57.0.1",
"expo-linear-gradient": "~15.0.8", "expo-linear-gradient": "~57.0.1",
"expo-linking": "~8.0.11", "expo-linking": "~57.0.4",
"expo-localization": "~17.0.8", "expo-localization": "~57.0.1",
"expo-location": "~19.0.8", "expo-location": "~57.0.6",
"expo-media-library": "~18.2.1", "expo-media-library": "57.0.3",
"expo-notifications": "~0.32.17", "expo-modules-core": "57.0.8",
"expo-notifications": "57.0.7",
"expo-paste-input": "^0.2.1", "expo-paste-input": "^0.2.1",
"expo-privacy-sensitive": "^0.2.0", "expo-privacy-sensitive": "^0.2.0",
"expo-screen-orientation": "~9.0.8", "expo-screen-orientation": "~57.0.1",
"expo-sharing": "~14.0.8", "expo-sharing": "~57.0.7",
"expo-sms": "^14.0.7", "expo-sms": "^57.0.1",
"expo-splash-screen": "~31.0.13", "expo-splash-screen": "~57.0.5",
"expo-system-ui": "~6.0.9", "expo-system-ui": "~57.0.1",
"expo-updates": "~29.0.17", "expo-updates": "57.0.10",
"expo-video": "~3.0.16", "expo-video": "~57.0.2",
"expo-video-thumbnails": "^10.0.8", "expo-video-thumbnails": "^57.0.1",
"expo-web-browser": "~15.0.10", "expo-web-browser": "~57.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-text-encoding": "^1.0.6", "fast-text-encoding": "^1.0.6",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
@@ -221,36 +222,36 @@
"normalize-url": "^8.0.0", "normalize-url": "^8.0.0",
"psl": "1.9.0", "psl": "1.9.0",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.1.0", "react": "19.2.3",
"react-compiler-runtime": "19.1.0-rc.3", "react-compiler-runtime": "19.1.0-rc.3",
"react-dom": "19.1.0", "react-dom": "19.2.3",
"react-hotkeys-hook": "5.2.4", "react-hotkeys-hook": "5.2.4",
"react-image-crop": "^11.0.7", "react-image-crop": "^11.0.7",
"react-is": "19", "react-is": "19",
"react-keyed-flatten-children": "^5.0.0", "react-keyed-flatten-children": "^5.0.0",
"react-native": "0.81.5", "react-native": "0.86.0",
"react-native-compressor": "1.13.0", "react-native-compressor": "1.13.0",
"react-native-date-picker": "^5.0.13", "react-native-date-picker": "^5.0.13",
"react-native-device-attest": "^0.1.6", "react-native-device-attest": "^0.1.6",
"react-native-drawer-layout": "^4.2.3", "react-native-drawer-layout": "^4.2.3",
"react-native-edge-to-edge": "^1.8.1", "react-native-edge-to-edge": "^1.8.1",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.32.0",
"react-native-keyboard-controller": "^1.21.8", "react-native-keyboard-controller": "1.21.9",
"react-native-mmkv": "^3.3.3", "react-native-mmkv": "^3.3.3",
"react-native-pager-view": "6.8.0", "react-native-pager-view": "6.8.0",
"react-native-progress": "^5.0.1", "react-native-progress": "^5.0.1",
"react-native-qrcode-styled": "^0.3.3", "react-native-qrcode-styled": "^0.3.3",
"react-native-reanimated": "~4.3.2", "react-native-reanimated": "~4.5.3",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.24.0", "react-native-screens": "4.26.2",
"react-native-scroll-forwarder": "link:./modules/react-native-scroll-forwarder", "react-native-scroll-forwarder": "link:./modules/react-native-scroll-forwarder",
"react-native-svg": "15.12.1", "react-native-svg": "15.15.4",
"react-native-uuid": "^2.0.3", "react-native-uuid": "^2.0.3",
"react-native-view-shot": "^4.0.3", "react-native-view-shot": "^5.1.0",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-web-webview": "^1.0.2", "react-native-web-webview": "^1.0.2",
"react-native-webview": "^13.15.0", "react-native-webview": "^13.16.1",
"react-native-worklets": "0.8.3", "react-native-worklets": "0.11.3",
"react-remove-scroll-bar": "^2.3.8", "react-remove-scroll-bar": "^2.3.8",
"react-responsive": "^10.0.1", "react-responsive": "^10.0.1",
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.5.3",
@@ -272,7 +273,8 @@
"@lingui/babel-plugin-lingui-macro": "^5.9.2", "@lingui/babel-plugin-lingui-macro": "^5.9.2",
"@lingui/cli": "^5.9.2", "@lingui/cli": "^5.9.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@react-native/babel-preset": "0.81.5", "@react-native/babel-preset": "0.86.0",
"@react-native/jest-preset": "0.86.0",
"@react-native/typescript-config": "^0.81.5", "@react-native/typescript-config": "^0.81.5",
"@sentry/webpack-plugin": "^3.2.2", "@sentry/webpack-plugin": "^3.2.2",
"@testing-library/react-native": "^13.2.0", "@testing-library/react-native": "^13.2.0",
@@ -281,13 +283,13 @@
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/lodash.shuffle": "^4.2.7", "@types/lodash.shuffle": "^4.2.7",
"@types/psl": "1.1.1", "@types/psl": "1.1.1",
"@types/react": "^19.1.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.1.11", "@types/react-dom": "^19.2.3",
"@typescript/native": "npm:typescript@^7.0.2", "@typescript/native": "npm:typescript@^7.0.2",
"babel-jest": "^29.7.0", "babel-jest": "^29.7.0",
"babel-plugin-module-resolver": "^5.0.2", "babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-react-compiler": "19.1.0-rc.3", "babel-plugin-react-compiler": "19.1.0-rc.3",
"babel-preset-expo": "~54.0.10", "babel-preset-expo": "~57.0.4",
"eslint-plugin-bsky-internal": "link:lint-rules", "eslint-plugin-bsky-internal": "link:lint-rules",
"eslint-plugin-react-native-a11y": "^3.5.1", "eslint-plugin-react-native-a11y": "^3.5.1",
"eslint-plugin-simple-import-sort": "^13.0.0", "eslint-plugin-simple-import-sort": "^13.0.0",
@@ -296,7 +298,7 @@
"husky": "^9.1.7", "husky": "^9.1.7",
"is-ci": "^3.0.1", "is-ci": "^3.0.1",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expo": "~54.0.17", "jest-expo": "~57.0.2",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"lint-staged": "^17.0.8", "lint-staged": "^17.0.8",
"oxlint": "^1.73.0", "oxlint": "^1.73.0",
-29
View File
@@ -1,29 +0,0 @@
diff --git a/ios/MediaHandler.swift b/ios/MediaHandler.swift
index 6e4fbe1b3921173a1cc4e6eff626b0749e8bab14..e334abb680a8965acb1f6bbdfbc1325ad35edf3a 100644
--- a/ios/MediaHandler.swift
+++ b/ios/MediaHandler.swift
@@ -310,10 +310,12 @@ internal struct MediaHandler {
let fileExtension = getFileExtension(from: originalFilename)
let destinationUrl = try generateUrl(withFileExtension: fileExtension)
+ let resourceOptions = PHAssetResourceRequestOptions()
+ resourceOptions.isNetworkAccessAllowed = true
try await PHAssetResourceManager.default().writeData(
for: resource,
toFile: destinationUrl,
- options: nil
+ options: resourceOptions
)
let mimeType = getMimeType(from: destinationUrl.pathExtension)
@@ -389,7 +391,9 @@ internal struct MediaHandler {
// Stream the resource into our cache directory. This API is asynchronous but doesn't require
// a temporary file like `loadFileRepresentation`.
- try await PHAssetResourceManager.default().writeData(for: resource, toFile: destinationUrl, options: nil)
+ let resourceOptions = PHAssetResourceRequestOptions()
+ resourceOptions.isNetworkAccessAllowed = true
+ try await PHAssetResourceManager.default().writeData(for: resource, toFile: destinationUrl, options: resourceOptions)
// Build and return the result using the helper.
let mimeType = getMimeType(from: destinationUrl.pathExtension)
@@ -1,5 +0,0 @@
# `expo-image-picker` patch
Patches this issue: https://github.com/expo/expo/issues/39937
Source: https://github.com/expo/expo/issues/39937#issuecomment-3342082239
-103
View File
@@ -1,103 +0,0 @@
diff --git a/build/Image.types.d.ts b/build/Image.types.d.ts
index 022ae487e65ae9d624f8a4be262b01944928f250..416504fe18ecd00fdc713faca23485fb292caf2c 100644
--- a/build/Image.types.d.ts
+++ b/build/Image.types.d.ts
@@ -152,6 +152,16 @@ export interface ImageProps extends Omit<ViewProps, 'style' | 'children'> {
* @default 'normal'
*/
priority?: 'low' | 'normal' | 'high' | null;
+ /**
+ * The loading behavior for the image. Maps to the native HTML `loading` attribute on web.
+ *
+ * - `'lazy'` - Defers loading until the image is near the viewport.
+ * - `'eager'` - Loads the image immediately.
+ *
+ * @default undefined
+ * @platform web
+ */
+ loading?: 'lazy' | 'eager' | null;
/**
* Determines whether to cache the image and where: on the disk, in the memory or both.
*
diff --git a/src/ExpoImage.web.tsx b/src/ExpoImage.web.tsx
index 2a49ff00649b374b86fa780653a4b0d6f13a4a57..1c3de93ef280bf6f3c27bed34c794b8ff59e3db3 100644
--- a/src/ExpoImage.web.tsx
+++ b/src/ExpoImage.web.tsx
@@ -70,6 +70,7 @@ export default function ExpoImage({
onLoadEnd,
onDisplay,
priority,
+ loading,
blurRadius,
recyclingKey,
style,
@@ -118,6 +119,7 @@ export default function ExpoImage({
accessibilityLabel={accessibilityLabel ?? alt}
cachePolicy={cachePolicy}
priority={priority}
+ loading={loading}
tintColor={tintColor}
/>
),
@@ -149,6 +151,7 @@ export default function ExpoImage({
className={className}
cachePolicy={cachePolicy}
priority={priority}
+ loading={loading}
contentPosition={selectedSource ? contentPosition : { top: '50%', left: '50%' }}
hashPlaceholderContentPosition={contentPosition}
hashPlaceholderStyle={imageHashStyle}
diff --git a/src/Image.types.ts b/src/Image.types.ts
index 9dec0e7aee61dfaa73a3cfa535d08259c9dad209..61c162114977dff35b633ac6b1f3d59e373bbbfe 100644
--- a/src/Image.types.ts
+++ b/src/Image.types.ts
@@ -178,6 +178,17 @@ export interface ImageProps extends Omit<ViewProps, 'style' | 'children'> {
*/
priority?: 'low' | 'normal' | 'high' | null;
+ /**
+ * The loading behavior for the image. Maps to the native HTML `loading` attribute on web.
+ *
+ * - `'lazy'` - Defers loading until the image is near the viewport.
+ * - `'eager'` - Loads the image immediately.
+ *
+ * @default undefined
+ * @platform web
+ */
+ loading?: 'lazy' | 'eager' | null;
+
/**
* Determines whether to cache the image and where: on the disk, in the memory or both.
*
diff --git a/src/web/ImageWrapper.tsx b/src/web/ImageWrapper.tsx
index e8f891d525892f8d3668e34cf96cefccfbfc49f6..89a5cb1e3a8574fc3bd1d0361895a610e3165dfb 100644
--- a/src/web/ImageWrapper.tsx
+++ b/src/web/ImageWrapper.tsx
@@ -30,6 +30,7 @@ const ImageWrapper = React.forwardRef(
contentPosition,
hashPlaceholderContentPosition,
priority,
+ loading,
style,
hashPlaceholderStyle,
tintColor,
@@ -82,6 +83,7 @@ const ImageWrapper = React.forwardRef(
// @ts-ignore
// eslint-disable-next-line react/no-unknown-property
fetchPriority={getFetchPriorityFromImagePriority(priority || 'normal')}
+ loading={loading || undefined}
{...getImageWrapperEventHandler(events, sourceWithHeaders)}
{...getImgPropsFromSource(source)}
{...props}
diff --git a/src/web/ImageWrapper.types.ts b/src/web/ImageWrapper.types.ts
index 19bbe2f15999124cda0ca7c81b3ebcf289f522f8..179837f803a3d315c85075895f4191631b37eda7 100644
--- a/src/web/ImageWrapper.types.ts
+++ b/src/web/ImageWrapper.types.ts
@@ -29,6 +29,7 @@ export type ImageWrapperProps = {
contentPosition?: ImageContentPositionObject;
hashPlaceholderContentPosition?: ImageContentPositionObject;
priority?: string | null;
+ loading?: 'lazy' | 'eager' | null;
style: CSSProperties;
tintColor?: string | null;
hashPlaceholderStyle?: CSSProperties;
-3
View File
@@ -1,3 +0,0 @@
## Expo Image
Patches in https://github.com/expo/expo/pull/41442
-51
View File
@@ -1,51 +0,0 @@
diff --git a/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
index 47c4d15f6b10bbd77858cfff425cda9a618735b9..afe138d22d566244482498a7be0e14b8454eab96 100644
--- a/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
+++ b/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
}
internal fun shouldParseBody(response: Response): Boolean {
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
+ return false
+ }
+
// Check for Content-Type
val skipContentTypes = listOf(
"text/event-stream", // Server Sent Events
diff --git a/ios/Core/ExpoBridgeModule.mm b/ios/Core/ExpoBridgeModule.mm
index 2ed1c00f47406e109750cc27ace7e0d88e42c00e..99d0d140eddf95a8db7beb57c61dfcb12c1424b4 100644
--- a/ios/Core/ExpoBridgeModule.mm
+++ b/ios/Core/ExpoBridgeModule.mm
@@ -7,6 +7,9 @@
// The runtime executor is included as of React Native 0.74 in bridgeless mode.
#if __has_include(<ReactCommon/RCTRuntimeExecutor.h>)
#import <ReactCommon/RCTRuntimeExecutor.h>
+#else // React Native <0.74
+// dispatchBlock:queue: is declared in RCTBridge+Private.h, not the public header.
+#import <React/RCTBridge+Private.h>
#endif // React Native >=0.74
@implementation ExpoBridgeModule
@@ -46,7 +49,20 @@ - (void)setBridge:(RCTBridge *)bridge
_appContext.reactBridge = bridge;
#if !__has_include(<ReactCommon/RCTRuntimeExecutor.h>)
- _appContext._runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:bridge];
+ // Hop the runtime install (and the prepareRuntime() chain it triggers via
+ // _runtime.didSet) onto RCTJSThread. The original line ran synchronously
+ // on whatever thread called setBridge: - typically the main thread - and
+ // raced JSIExecutor::initializeRuntime() on the JS thread, corrupting
+ // Hermes' Hades GC (HadesGC::writeBarrierSlow EXC_BAD_ACCESS).
+ __weak EXAppContext *weakAppContext = _appContext;
+ __weak RCTBridge *weakBridge = bridge;
+ [bridge dispatchBlock:^{
+ EXAppContext *strongAppContext = weakAppContext;
+ RCTBridge *strongBridge = weakBridge;
+ if (strongAppContext != nil && strongBridge != nil && strongAppContext._runtime == nil) {
+ strongAppContext._runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:strongBridge];
+ }
+ } queue:RCTJSThread];
#endif // React Native <0.74
}
-26
View File
@@ -1,26 +0,0 @@
## expo-modules-core Patch
This patch contains two unrelated fixes:
### Android: bitdrift interceptor
Fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools.
### iOS: Hermes startup race in `ExpoBridgeModule.setBridge:`
On the legacy bridge (old architecture, where `RCTRuntimeExecutor.h` is
absent), `setBridge:` installed the Expo runtime synchronously on whatever
thread called it - typically the main thread, since RN's lazy module-load
path ignores `+requiresMainQueueSetup`. The `_runtime.didSet` then ran
`prepareRuntime()` (JSI mutations) on the main thread while the JS thread was
concurrently inside `JSIExecutor::initializeRuntime()`. Two threads mutating
the same Hermes runtime corrupted Hades GC, producing intermittent
`EXC_BAD_ACCESS` launch crashes (e.g. `HadesGC::writeBarrierSlow`,
`prepareRuntime` / `bindNativePerformanceNow`).
The fix hops the runtime install onto `RCTJSThread` so all JSI mutation is
serialized on the JS thread. This backports the upstream fix discussed in
expo/expo#45374; the racy `ExpoBridgeModule` is removed entirely in SDK 55+
(expo/expo#44351), so this patch can be dropped on that upgrade.
Refs: expo/expo#43003, expo/expo#45374, expo/expo#44351
+15
View File
@@ -0,0 +1,15 @@
diff --git a/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
index 47c4d15f6b10bbd77858cfff425cda9a618735b9..afe138d22d566244482498a7be0e14b8454eab96 100644
--- a/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
+++ b/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
}
internal fun shouldParseBody(response: Response): Boolean {
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
+ return false
+ }
+
// Check for Content-Type
val skipContentTypes = listOf(
"text/event-stream", // Server Sent Events
@@ -0,0 +1,5 @@
## expo-modules-core Patch
### Android: bitdrift interceptor
Fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools.
@@ -1,9 +1,9 @@
diff --git a/android/build.gradle b/android/build.gradle diff --git a/android/build.gradle b/android/build.gradle
index 7db47bdf190b0790c7bf867fbcfeb594005861be..0f868153edd6ec557730531f61dba7bf26a71742 100644 index 18a1c56507a1c0da7eb3b6e1f80a1f25a0a8f171..305e10998b99d0c0256930de669e51b5ba4c98c4 100644
--- a/android/build.gradle --- a/android/build.gradle
+++ b/android/build.gradle +++ b/android/build.gradle
@@ -42,6 +42,7 @@ dependencies { @@ -43,6 +43,7 @@ dependencies {
implementation 'com.google.firebase:firebase-messaging:24.0.1' implementation 'com.google.firebase:firebase-messaging:25.0.1'
implementation 'me.leolin:ShortcutBadger:1.1.22@aar' implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
+ implementation project(':expo-background-notification-handler') + implementation project(':expo-background-notification-handler')
@@ -124,7 +124,7 @@ index 610d3039cefd589647538ad8ba14587d29fab338..3655fc3121ebc0a97820d9653b61a90b
builder.setContentText(content.text) builder.setContentText(content.text)
builder.setSubText(content.subText) builder.setSubText(content.subText)
diff --git a/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt diff --git a/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
index 90ca4ff35132b33dcccb80b90d68572506fef603..9d4cb09b35844805d543acff54772380c732c02c 100644 index eecdae82e99d2997687e3f3e199c94c3aeffbfe0..216891213fb2eac5a9a382b4f075eadccdcfeb5a 100644
--- a/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt --- a/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
+++ b/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt +++ b/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
@@ -3,6 +3,9 @@ package expo.modules.notifications.service.delegates @@ -3,6 +3,9 @@ package expo.modules.notifications.service.delegates
@@ -137,16 +137,16 @@ index 90ca4ff35132b33dcccb80b90d68572506fef603..9d4cb09b35844805d543acff54772380
import expo.modules.interfaces.taskManager.TaskServiceProviderHelper import expo.modules.interfaces.taskManager.TaskServiceProviderHelper
import expo.modules.notifications.notifications.RemoteMessageSerializer import expo.modules.notifications.notifications.RemoteMessageSerializer
import expo.modules.notifications.notifications.background.BackgroundRemoteNotificationTaskConsumer import expo.modules.notifications.notifications.background.BackgroundRemoteNotificationTaskConsumer
@@ -18,7 +21,7 @@ import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener @@ -17,7 +20,7 @@ import expo.modules.notifications.service.interfaces.FirebaseMessagingDelegate
import java.lang.ref.WeakReference import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
import java.util.* import java.util.*
-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate { -open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
+open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface{ +open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface{
companion object { companion object {
// Unfortunately we cannot save state between instances of a service other way // Unfortunately we cannot save state between instances of a service other way
// than by static properties. Fortunately, using weak references we can // than by static properties.
@@ -105,8 +108,19 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM @@ -109,8 +112,19 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
DebugLogging.logRemoteMessage("FirebaseMessagingDelegate.onMessageReceived: message", remoteMessage) DebugLogging.logRemoteMessage("FirebaseMessagingDelegate.onMessageReceived: message", remoteMessage)
val notification = createNotification(remoteMessage) val notification = createNotification(remoteMessage)
DebugLogging.logNotification("FirebaseMessagingDelegate.onMessageReceived: notification", notification) DebugLogging.logNotification("FirebaseMessagingDelegate.onMessageReceived: notification", notification)
+97
View File
@@ -0,0 +1,97 @@
diff --git a/build/winter/runtime.native.d.ts b/build/winter/runtime.native.d.ts
index 85c4227e4e37003dd3769a590e73a69874de5fac..e389ca8edd2310df9ce29684991465c275e804c5 100644
--- a/build/winter/runtime.native.d.ts
+++ b/build/winter/runtime.native.d.ts
@@ -1,3 +1,2 @@
import 'react-native/Libraries/Core/InitializeCore';
-import '../../types';
//# sourceMappingURL=runtime.native.d.ts.map
\ No newline at end of file
diff --git a/src/winter/fetch/RequestUtils.ts b/src/winter/fetch/RequestUtils.ts
index a93473fd2beaf2ab0f3880951d292e58e7288d10..c27911bd5cb56ba6051fb8ebf4194cced43bc0fd 100644
--- a/src/winter/fetch/RequestUtils.ts
+++ b/src/winter/fetch/RequestUtils.ts
@@ -50,7 +50,11 @@ function isBlob(obj: any): obj is Blob {
*/
export async function normalizeBodyInitAsync(
body: BodyInit | null | undefined
-): Promise<{ body: Uint8Array | null; overriddenHeaders?: NativeHeadersType }> {
+): Promise<{
+ body: Uint8Array | null;
+ overriddenHeaders?: NativeHeadersType;
+ fallbackHeaders?: NativeHeadersType;
+}> {
if (body == null) {
return { body: null };
}
@@ -71,7 +75,12 @@ export async function normalizeBodyInitAsync(
if (body instanceof Blob || isBlob(body)) {
return {
body: new Uint8Array(await blobToArrayBufferAsync(body)),
- overriddenHeaders: [['Content-Type', body.type]],
+ /*
+ * Per the fetch spec, a blob's type is only a default for Content-Type:
+ * it must not replace a header the caller set explicitly, and an empty
+ * type contributes no header at all.
+ */
+ fallbackHeaders: body.type ? [['Content-Type', body.type]] : undefined,
};
}
@@ -137,6 +146,25 @@ export function overrideHeaders(
return result;
}
+/**
+ * Create a new header array by adding new headers only for keys not already
+ * present (by case-insensitive header key). Used for body-derived defaults
+ * that must not replace caller-provided headers.
+ */
+export function fillMissingHeaders(
+ headers: NativeHeadersType,
+ newHeaders: NativeHeadersType
+): NativeHeadersType {
+ const existingKeySet = new Set(headers.map(([key]) => key.toLocaleLowerCase()));
+ const result: NativeHeadersType = [...headers];
+ for (const [key, value] of newHeaders) {
+ if (!existingKeySet.has(key.toLocaleLowerCase())) {
+ result.push([key, value]);
+ }
+ }
+ return result;
+}
+
/** Normalizes known HTTP methods to uppercase */
export function normalizeMethod(method: string): string {
const normalized = method.toUpperCase();
diff --git a/src/winter/fetch/fetch.ts b/src/winter/fetch/fetch.ts
index ac789c024c96379e239bf44903c47e6a8e540d63..4f953b845c145de02976e8162ba2f63f8bd4dfd5 100644
--- a/src/winter/fetch/fetch.ts
+++ b/src/winter/fetch/fetch.ts
@@ -3,6 +3,7 @@ import { FetchError } from './FetchErrors';
import { FetchResponse, type AbortSubscriptionCleanupFunction } from './FetchResponse';
import type { NativeRequest, NativeRequestInit } from './NativeRequest';
import {
+ fillMissingHeaders,
normalizeBodyInitAsync,
normalizeHeadersInit,
overrideHeaders,
@@ -62,10 +63,17 @@ export async function fetch(
const request = new ExpoFetchModule.NativeRequest(response) as NativeRequest;
- const { body: requestBody, overriddenHeaders } = await normalizeBodyInitAsync(body);
+ const {
+ body: requestBody,
+ overriddenHeaders,
+ fallbackHeaders,
+ } = await normalizeBodyInitAsync(body);
if (overriddenHeaders) {
headers = overrideHeaders(headers, overriddenHeaders);
}
+ if (fallbackHeaders) {
+ headers = fillMissingHeaders(headers, fallbackHeaders);
+ }
const nativeRequestInit: NativeRequestInit = {
credentials: credentials ?? 'include',
+56
View File
@@ -0,0 +1,56 @@
# expo
## build/winter/runtime.native.d.ts
Type-check-only change; no runtime impact (only a `.d.ts` is modified).
Expo 57 added `import '../../types'` to `build/winter/runtime.native.d.ts`
(in Expo 54 the file was an empty `export {}`). That pulls
`expo/types/react-native-web.d.ts` into every native type-check pass via the
chain `expo/build/Expo.fx.d.ts -> ./winter -> runtime.native.d.ts ->
expo/types/index.d.ts`.
`react-native-web.d.ts` augments react-native's `TextStyle` with web-only
props, including `cursor?: string`, which conflicts with react-native 0.86's
own `cursor?: CursorValue`. The merged declaration makes `TextStyle` no
longer assignable to `ViewStyle`, which in turn poisons `StyleSheet.create`
inference (values widen to `ViewStyle | TextStyle | ImageStyle`) and produced
~60 errors in `pnpm typecheck:ios` / `typecheck:android`.
The patch drops the `import '../../types'` line so the web-only augmentation
stays out of the native passes, matching Expo 54 behavior. The web pass is
unaffected: it resolves `runtime.d.ts` (not `.native`), which never had this
import.
Can be removed if Expo stops referencing `./react-native-web` from the types
loaded by the native winter runtime, or guards the augmentation to web.
## src/winter/fetch/RequestUtils.ts + fetch.ts - Blob body must not clobber an explicit Content-Type
Expo 57 installs `expo/fetch` as the global `fetch` on native
(`src/winter/runtime.native.ts`), replacing React Native's whatwg-fetch. When
the request body is a Blob, `normalizeBodyInitAsync` returned
`overriddenHeaders: [['Content-Type', blob.type]]`, which `fetch.ts` applied
*over* the caller's headers (introduced in expo/expo#33405). This is backwards
per the fetch spec: a blob's type is only a default, used when no Content-Type
was provided, and an empty type must contribute no header at all.
In this app it broke publishing posts with any image blob on Android. The
composer uploads via `agent.uploadBlob(blob, {encoding})`; `@atproto/xrpc`
sets `content-type: <mime>` explicitly, but the blob comes from an XHR
`file://` read of a `.bin`-renamed jpeg (the RN#27099 workaround in
`src/lib/api/upload-blob.ts`), for which Android's BlobModule returns an empty
mime. expo/fetch replaced the good header with the empty blob type and the PDS
rejected the upload with "Request encoding (Content-Type) required but not
provided". Also silently rewrote the intended mime on every other blob upload
(avatars, banners, caption files) even when the blob type was non-empty.
The patch splits body-derived headers into two channels: FormData keeps
`overriddenHeaders` (its boundary header must win), while the Blob branch
returns new `fallbackHeaders` (skipped entirely when `blob.type` is empty)
that `fetch.ts` applies via `fillMissingHeaders` only for header keys the
caller did not set. This matches browser behavior.
Upstream: expo/expo#33405 introduced the override; the SDK 58 Request rewrite
(expo/expo#46630) is expected to make this spec-compliant, so re-evaluate on
the next SDK bump. Worth filing an issue against expo/expo referencing this.
@@ -1,31 +0,0 @@
diff --git a/apple/RNGestureHandler.mm b/apple/RNGestureHandler.mm
index c4f760c41a9965245edcfa5e7cb781f8d2c7b66a..bf7d1fb092e22cbcedf787e606876e533879f810 100644
--- a/apple/RNGestureHandler.mm
+++ b/apple/RNGestureHandler.mm
@@ -470,15 +470,19 @@ + (RNGestureHandler *)findGestureHandlerByRecognizer:(UIGestureRecognizer *)reco
// We may try to extract "DummyGestureHandler" in case when "otherGestureRecognizer" belongs to
// a native view being wrapped with "NativeViewGestureHandler"
- RNGHUIView *reactView = recognizer.view;
- while (reactView != nil && reactView.reactTag == nil) {
- reactView = reactView.superview;
- }
+ RNGHUIView *view = recognizer.view;
+ while (view != nil) {
+ for (UIGestureRecognizer *candidateRecognizer in view.gestureRecognizers) {
+ if ([candidateRecognizer isKindOfClass:[RNDummyGestureRecognizer class]]) {
+ return candidateRecognizer.gestureHandler;
+ }
+ }
- for (UIGestureRecognizer *recognizer in reactView.gestureRecognizers) {
- if ([recognizer isKindOfClass:[RNDummyGestureRecognizer class]]) {
- return recognizer.gestureHandler;
+ if ([view isKindOfClass:[RCTViewComponentView class]]) {
+ return nil;
}
+
+ view = view.superview;
}
return nil;
@@ -1,5 +0,0 @@
# react-native-gesture-handler.patch
Updated `findGestureHandlerByRecognizer:` in `apple/RNGestureHandler.mm` to the version from RN GH 2.32.0
This fixes `UIContextMenuInteraction` from `ExpoBlueskyPeekMenuView.swift`. https://github.com/software-mansion/react-native-gesture-handler/commit/fba4dcc06d71dce08b10b2afc738a2af5b01e86a
@@ -1,65 +0,0 @@
# react-native-reanimated@4.3.2.patch
Backports of two merged upstream PRs:
1. PR 9901 (`LayoutAnimation.configureNext` compatibility)
2. PR 9971 (stale `settledProps` on worklet re-animation / after app resume)
## 1. Backport of PR 9901
Backport of https://github.com/software-mansion/react-native-reanimated/pull/9901
("refactor(LayoutAnimations): stop taking over UIManagerAnimationDelegate").
Reanimated's legacy `LayoutAnimationsProxy_Legacy` registered itself as the
`UIManagerAnimationDelegate` only to receive `stopSurface`. Occupying that slot
overwrites the `LayoutAnimationDriver` that React Native installs there, which
silently breaks `LayoutAnimation.configureNext` for the whole app.
The patch makes the proxy detect surface teardown itself via a
`UIManagerCommitHook` (a commit with an empty root marks the surface in
`surfacesToRemove_`), frees the animation-delegate slot, and drops final
keyframe `Update` mutations for views deleted in the same transaction (a
deterministic `configureNext` delete-animation crash found in this app).
`uiManager` moves from Android-only to shared constructor args since the hook
registration needs it on both platforms.
Only the `packages/react-native-reanimated` part of the PR is included (the
`apps/fabric-example` hunk is not part of the published package), and the
include hunk in `LayoutAnimationsProxy_Legacy.cpp` was adjusted to the 4.3.2
release sources.
## 2. Backport of PR 9971 (stale `settledProps`)
Verbatim application of
https://github.com/software-mansion/react-native-reanimated/pull/9971, the
4.3-stable cherry-pick of
https://github.com/software-mansion/react-native-reanimated/pull/9527
("Fix stale settledProps on worklet re-animation"). Fixes the Android DM
composer "phantom jump"
(https://github.com/software-mansion/react-native-reanimated/issues/9574).
Background: with `FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS`, once an
animation settles its final props are handed to JS (polled every 500 ms by
`PropsRegistryGarbageCollector`) and stored in React component state
(`settledProps`), after which the React-side snapshot becomes the sole owner
of the value.
The PR replaces `getUpdatesOlderThanTimestamp` (which evicted registry
entries on a wall-clock 1 s/2 s window) with `collectSettledUpdates`:
- `syncedTags_` / `invalidatedTags_` track which tags React already has a
snapshot for; when a previously-synced view re-animates, its stale snapshot
is refreshed on the next GC tick instead of waiting for the new value to
settle.
- Eviction is no longer time-based. An entry is only evicted on the tick
*after* it was returned to JS (once its `settledProps` commit is
guaranteed), so a missed timer window (app backgrounded, JS thread blocked)
can no longer destroy a settled value before it reaches React. This
replaces the ad-hoc eviction guard an earlier version of this patch added
on top of the pre-merge PR 9527.
- `PropsRegistryGarbageCollector` drops the separate `viewsCount` counter
(which could desync when nested animated components unregister a tag that
was never registered, stopping the GC interval while views remain) in favor
of `viewsMap.size`. Only `src/` is touched, matching the PR; Metro bundles
the app from `src/` via the package's `react-native` field, and the stale
`lib/` copy is unreachable (the feature is native-only).
@@ -1,160 +1,8 @@
diff --git a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp
index 531f0dc7b4eeb9b29cb2255d8444da02a74c35b7..534f419fce55c39a09a7eebfb7ab3c53f8a16637 100644
--- a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp
+++ b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp
@@ -1,8 +1,10 @@
#include <reanimated/Fabric/updates/AnimatedPropsRegistry.h>
#include <reanimated/Tools/FeatureFlags.h>
+#include <functional>
#include <memory>
#include <utility>
+#include <vector>
namespace reanimated {
@@ -25,25 +27,59 @@ void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operation
addUpdatesToBatch(shadowNode, jsi::dynamicFromValue(rt, updates));
if constexpr (StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS")) {
- timestampMap_[shadowNode->getTag()] = timestamp;
+ const auto tag = shadowNode->getTag();
+ timestampMap_[tag] = timestamp;
+ // If JS already has a `settledProps` snapshot for this tag, it is now
+ // stale — schedule a refresh on the next `collectSettledUpdates`.
+ if (syncedTags_.erase(tag) > 0) {
+ invalidatedTags_.insert(tag);
+ }
}
}
}
-jsi::Value AnimatedPropsRegistry::getUpdatesOlderThanTimestamp(
- jsi::Runtime &rt,
- const double timestamp,
- const double cleanupTimestamp) {
+jsi::Value AnimatedPropsRegistry::collectSettledUpdates(jsi::Runtime &rt, const double settledTimestamp) {
std::lock_guard<std::mutex> lock{mutex_};
- removeUpdatesOlderThanTimestamp(cleanupTimestamp);
std::vector<std::pair<Tag, std::reference_wrapper<const folly::dynamic>>> updates;
- for (const auto &[viewTag, pair] : updatesRegistry_) {
- auto it = timestampMap_.find(viewTag);
- if (it != timestampMap_.end() && it->second < timestamp) {
- updates.emplace_back(viewTag, std::cref(pair.second));
+ for (auto it = updatesRegistry_.begin(); it != updatesRegistry_.end();) {
+ const auto viewTag = it->first;
+
+ if (syncedTags_.contains(viewTag)) {
+ // React already has the latest value for this tag (synced on a previous
+ // call, so the `settledProps` state is committed by now) — the registry
+ // entry is redundant. `syncedTags_` is intentionally retained to detect
+ // re-animation staleness. Note that `syncedTags_` and `invalidatedTags_`
+ // are disjoint — `update()` moves tags from the former to the latter.
+ timestampMap_.erase(viewTag);
+ it = updatesRegistry_.erase(it);
+ continue;
+ }
+
+ const auto timestampIt = timestampMap_.find(viewTag);
+ if (timestampIt == timestampMap_.end()) {
+ ++it;
+ continue;
+ }
+ const bool isSettled = timestampIt->second < settledTimestamp;
+ const auto invalidatedIt = invalidatedTags_.find(viewTag);
+ const bool isInvalidated = invalidatedIt != invalidatedTags_.end();
+ if (isSettled || isInvalidated) {
+ updates.emplace_back(viewTag, std::cref(it->second.second));
+ if (isSettled) {
+ // Only settled-path tags are tracked as "synced" so that an ongoing
+ // animation doesn't re-trigger an invalidation/sync on every GC tick.
+ syncedTags_.insert(viewTag);
+ }
+ if (isInvalidated) {
+ // Only erase serviced invalidations; if a tag was invalidated but the
+ // matching update batch hasn't been flushed into updatesRegistry_ yet,
+ // we leave the entry so the next sync picks it up.
+ invalidatedTags_.erase(invalidatedIt);
+ }
}
+ ++it;
}
const jsi::Array array(rt, updates.size());
@@ -58,22 +94,11 @@ jsi::Value AnimatedPropsRegistry::getUpdatesOlderThanTimestamp(
return jsi::Value(rt, array);
}
-void AnimatedPropsRegistry::removeUpdatesOlderThanTimestamp(const double timestamp) {
- for (auto it = timestampMap_.begin(); it != timestampMap_.end();) {
- const auto viewTag = it->first;
- const auto viewTimestamp = it->second;
- if (viewTimestamp < timestamp) {
- it = timestampMap_.erase(it);
- updatesRegistry_.erase(viewTag);
- } else {
- it++;
- }
- }
-}
-
void AnimatedPropsRegistry::removeTag(const Tag tag) {
updatesRegistry_.erase(tag);
timestampMap_.erase(tag);
+ syncedTags_.erase(tag);
+ invalidatedTags_.erase(tag);
}
} // namespace reanimated
diff --git a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h
index 2c6c0e13604c9421e147d7eea7f4a4752288011c..8cd67f118501c2786b94d76541aea29a14ba8c16 100644
--- a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h
+++ b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h
@@ -4,10 +4,8 @@
#include <react/renderer/uimanager/UIManager.h>
-#include <memory>
-#include <string>
#include <unordered_map>
-#include <vector>
+#include <unordered_set>
namespace reanimated {
@@ -15,13 +13,22 @@ class AnimatedPropsRegistry : public UpdatesRegistry {
public:
void update(jsi::Runtime &rt, const jsi::Value &operations, double timestamp);
- /// Also removes updates older than `cleanupTimestamp` from the registry.
- jsi::Value getUpdatesOlderThanTimestamp(jsi::Runtime &rt, double timestamp, double cleanupTimestamp);
+ /// Returns updates that settled (received no update since `settledTimestamp`)
+ /// or whose synced `settledProps` snapshot was invalidated by a fresh update.
+ /// Also evicts entries that have already been synced to React — by the time
+ /// of the next call, the corresponding `settledProps` state is guaranteed to
+ /// be committed, so the registry entries are redundant.
+ jsi::Value collectSettledUpdates(jsi::Runtime &rt, double settledTimestamp);
private:
std::unordered_map<Tag, double> timestampMap_; // viewTag -> timestamp, protected by `mutex_`
+ // Tags whose latest values have already been pushed to React `settledProps`.
+ // Intentionally retained after eviction to detect re-animation staleness.
+ std::unordered_set<Tag> syncedTags_;
+ // Tags that were synced to React but received a fresh worklet update since;
+ // their `settledProps` are stale and need to be refreshed on the next sync.
+ std::unordered_set<Tag> invalidatedTags_;
- void removeUpdatesOlderThanTimestamp(double timestamp);
void removeTag(Tag tag) override;
};
diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h
index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea41a50585 100644 index 8603591..20d042b 100644
--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h
+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h +++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h
@@ -57,11 +57,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele @@ -62,11 +62,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
const SharedComponentDescriptorRegistry &componentDescriptorRegistry, const SharedComponentDescriptorRegistry &componentDescriptorRegistry,
const std::shared_ptr<const ContextContainer> &contextContainer, const std::shared_ptr<const ContextContainer> &contextContainer,
jsi::Runtime &uiRuntime, jsi::Runtime &uiRuntime,
@@ -168,7 +16,7 @@ index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea
const std::shared_ptr<facebook::react::CallInvoker> &jsInvoker const std::shared_ptr<facebook::react::CallInvoker> &jsInvoker
#endif #endif
) )
@@ -69,11 +69,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele @@ -74,11 +74,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
contextContainer_(contextContainer), contextContainer_(contextContainer),
componentDescriptorRegistry_(componentDescriptorRegistry), componentDescriptorRegistry_(componentDescriptorRegistry),
uiRuntime_(uiRuntime), uiRuntime_(uiRuntime),
@@ -182,7 +30,7 @@ index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea
jsInvoker_(jsInvoker) jsInvoker_(jsInvoker)
#endif #endif
{ {
@@ -93,10 +93,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele @@ -98,10 +98,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
SharedComponentDescriptorRegistry componentDescriptorRegistry_; SharedComponentDescriptorRegistry componentDescriptorRegistry_;
jsi::Runtime &uiRuntime_; jsi::Runtime &uiRuntime_;
const std::shared_ptr<UIScheduler> uiScheduler_; const std::shared_ptr<UIScheduler> uiScheduler_;
@@ -195,10 +43,10 @@ index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea
void restoreOpacityInCaseOfFlakyEnteringAnimation(SurfaceId surfaceId) const; void restoreOpacityInCaseOfFlakyEnteringAnimation(SurfaceId surfaceId) const;
diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h
index eca44e4cf651d16e9741806004ce9119c85d07d6..e39c79a5d7b52659106ed6fe6fbcbbc048bf4787 100644 index fcc677f..115971a 100644
--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h
+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h +++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h
@@ -66,11 +66,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon, @@ -67,11 +67,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
const SharedComponentDescriptorRegistry &componentDescriptorRegistry, const SharedComponentDescriptorRegistry &componentDescriptorRegistry,
const std::shared_ptr<const ContextContainer> &contextContainer, const std::shared_ptr<const ContextContainer> &contextContainer,
jsi::Runtime &uiRuntime, jsi::Runtime &uiRuntime,
@@ -212,7 +60,7 @@ index eca44e4cf651d16e9741806004ce9119c85d07d6..e39c79a5d7b52659106ed6fe6fbcbbc0
const std::shared_ptr<CallInvoker> &jsInvoker const std::shared_ptr<CallInvoker> &jsInvoker
#endif #endif
) )
@@ -79,11 +79,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon, @@ -80,11 +80,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
componentDescriptorRegistry, componentDescriptorRegistry,
contextContainer, contextContainer,
uiRuntime, uiRuntime,
@@ -227,18 +75,18 @@ index eca44e4cf651d16e9741806004ce9119c85d07d6..e39c79a5d7b52659106ed6fe6fbcbbc0
#endif #endif
), ),
diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp
index 83ef7430b923b6b3b99368ee8072168769110cd0..2affd12822ab19bdc90963d3ce8ca1e6bb0d43b7 100644 index df53d8d..735f138 100644
--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp
+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp +++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp
@@ -2,6 +2,7 @@ @@ -1,6 +1,7 @@
#include <reanimated/NativeModules/ReanimatedModuleProxy.h> #include <reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h>
#include <react/renderer/animations/utils.h> #include <react/debug/react_native_assert.h>
+#include <react/renderer/mounting/ShadowTree.h> +#include <react/renderer/mounting/ShadowTree.h>
#include <react/renderer/mounting/ShadowViewMutation.h> #include <react/renderer/mounting/ShadowViewMutation.h>
#include <memory> #include <memory>
@@ -53,14 +54,37 @@ std::optional<MountingTransaction> LayoutAnimationsProxy_Legacy::pullTransaction @@ -60,14 +61,37 @@ std::optional<MountingTransaction> LayoutAnimationsProxy_Legacy::pullTransaction
parseRemoveMutations(movedViews, mutations, roots); parseRemoveMutations(movedViews, mutations, roots);
@@ -278,7 +126,7 @@ index 83ef7430b923b6b3b99368ee8072168769110cd0..2affd12822ab19bdc90963d3ce8ca1e6
return MountingTransaction{surfaceId, transactionNumber, std::move(filteredMutations), telemetry}; return MountingTransaction{surfaceId, transactionNumber, std::move(filteredMutations), telemetry};
} }
@@ -947,23 +971,22 @@ inline bool MutationNode::isMutationNode() { @@ -998,23 +1022,22 @@ inline bool MutationNode::isMutationNode() {
return true; return true;
} }
@@ -318,7 +166,7 @@ index 83ef7430b923b6b3b99368ee8072168769110cd0..2affd12822ab19bdc90963d3ce8ca1e6
} // namespace reanimated } // namespace reanimated
diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h
index e9a5e9959e89ec33cee179ddb907c17f6dfbd3de..a2c89041518cd71e8ba5ac62ef89c0022d197c9b 100644 index 57cc134..1a2966c 100644
--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h
+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h +++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h
@@ -3,8 +3,8 @@ @@ -3,8 +3,8 @@
@@ -376,7 +224,7 @@ index e9a5e9959e89ec33cee179ddb907c17f6dfbd3de..a2c89041518cd71e8ba5ac62ef89c002
} }
void startEnteringAnimation(const int tag, ShadowViewMutation &mutation) const; void startEnteringAnimation(const int tag, ShadowViewMutation &mutation) const;
@@ -202,19 +207,15 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, @@ -206,19 +211,15 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon,
const TransactionTelemetry &telemetry, const TransactionTelemetry &telemetry,
ShadowViewMutationList mutations) const override; ShadowViewMutationList mutations) const override;
@@ -404,43 +252,10 @@ index e9a5e9959e89ec33cee179ddb907c17f6dfbd3de..a2c89041518cd71e8ba5ac62ef89c002
} // namespace reanimated } // namespace reanimated
diff --git a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp diff --git a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
index 9ade22bf773005613048a00c47b35767628e86c6..f3415e824da1a8da5c83762415ca54646bd6429f 100644 index 2b68ff7..d08b1ae 100644
--- a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp --- a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
+++ b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +++ b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
@@ -524,15 +524,13 @@ jsi::Value ReanimatedModuleProxy::getSettledUpdates(jsi::Runtime &rt) { @@ -1235,22 +1235,19 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() {
StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS") &&
"getSettledUpdates requires FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS static feature flag to be enabled");
+ constexpr double SETTLED_ANIMATION_THRESHOLD_MS = 1000;
+
// TODO(future): use unified timestamp
const auto currentTimestamp = getAnimationTimestamp_();
- // TODO: fix bug when threshold difference is smaller than 1 second
// TODO(future): flush updates from CSS animations and CSS transitions registries
- // TODO(future): find a better way to obtain timestamp for removing updates
- // TODO(future): move removing old updates to separate method
- return animatedPropsRegistry_->getUpdatesOlderThanTimestamp(
- rt, currentTimestamp - 1000 /* 1 second */, currentTimestamp - 2000 /* 2 seconds */);
+ return animatedPropsRegistry_->collectSettledUpdates(rt, currentTimestamp - SETTLED_ANIMATION_THRESHOLD_MS);
}
bool ReanimatedModuleProxy::handleEvent(
@@ -1306,11 +1304,11 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() {
componentDescriptorRegistry,
scheduler->getContextContainer(),
getJSIRuntimeFromWorkletRuntime(uiRuntime_),
- uiScheduler_
+ uiScheduler_,
+ uiManager_
#ifdef ANDROID
,
filterUnmountedTagsFunction_,
- uiManager_,
jsInvoker_
#endif
);
@@ -1319,22 +1317,19 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() {
#endif #endif
layoutAnimationsProxy_ = std::move(layoutAnimationsProxyExperimental); layoutAnimationsProxy_ = std::move(layoutAnimationsProxyExperimental);
} else { } else {
@@ -466,35 +281,3 @@ index 9ade22bf773005613048a00c47b35767628e86c6..f3415e824da1a8da5c83762415ca5464
} }
} }
} }
diff --git a/src/PropsRegistryGarbageCollector.ts b/src/PropsRegistryGarbageCollector.ts
index f917ce5a8586c02855f1d8d9ae73154592d22510..32148fbac8a9224ffec6edc784b48938da9585fb 100644
--- a/src/PropsRegistryGarbageCollector.ts
+++ b/src/PropsRegistryGarbageCollector.ts
@@ -11,7 +11,6 @@ import { ReanimatedModule } from './ReanimatedModule';
const FLUSH_INTERVAL_MS = 500;
export const PropsRegistryGarbageCollector = {
- viewsCount: 0,
viewsMap: new Map<number, IAnimatedComponentInternal>(),
intervalId: null as NodeJS.Timeout | null,
@@ -25,16 +24,14 @@ export const PropsRegistryGarbageCollector = {
return;
}
this.viewsMap.set(viewTag, component);
- this.viewsCount++;
- if (this.viewsCount === 1) {
+ if (this.viewsMap.size === 1) {
this.registerInterval();
}
},
unregisterView(viewTag: number) {
- this.viewsMap.delete(viewTag);
- this.viewsCount--;
- if (this.viewsCount === 0) {
+ const deleted = this.viewsMap.delete(viewTag);
+ if (deleted && this.viewsMap.size === 0) {
this.unregisterInterval();
}
},
@@ -0,0 +1,27 @@
# react-native-reanimated@4.5.3.patch
Backport of https://github.com/software-mansion/react-native-reanimated/pull/9901
("refactor(LayoutAnimations): stop taking over UIManagerAnimationDelegate").
Reanimated's legacy `LayoutAnimationsProxy_Legacy` registered itself as the
`UIManagerAnimationDelegate` only to receive `stopSurface`. Occupying that slot
overwrites the `LayoutAnimationDriver` that React Native installs there, which
silently breaks `LayoutAnimation.configureNext` for the whole app.
The patch makes the proxy detect surface teardown itself via a
`UIManagerCommitHook` (a commit with an empty root marks the surface in
`surfacesToRemove_`), frees the animation-delegate slot, and drops final
keyframe `Update` mutations for views deleted in the same transaction (a
deterministic `configureNext` delete-animation crash found in this app).
`uiManager` moves from Android-only to shared constructor args since the hook
registration needs it on both platforms.
Only the `packages/react-native-reanimated` part of the PR is included (the
`apps/fabric-example` hunk is not part of the published package), and the hunks
were rebased onto the 4.5.3 release sources.
Note that upstream's own `pullTransaction` rework in 4.5.3 (the new
`reconcileContradictedRemovals`) covers a different case - a `Create`/`Insert`
contradicting a *withheld* exit removal - and does not subsume the deleted-tag
`Update` filter here, which guards against the `LayoutAnimationDriver` final
keyframe. That driver only runs at all once this patch frees the delegate slot.
@@ -1,16 +1,16 @@
diff --git a/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/android/src/main/java/com/swmansion/rnscreens/Screen.kt diff --git a/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/android/src/main/java/com/swmansion/rnscreens/Screen.kt
index c99ca362df5baee81fa48d1c9ef09b40a2c23f07..725c07aa0ed516f9dd09f81e522f2d3191d9b5d0 100644 index 76bb694854b29d7f779f38244cd48113b429ea1f..fd402ac938862e8d39c5467c1b5c86c4e7eb0c83 100644
--- a/android/src/main/java/com/swmansion/rnscreens/Screen.kt --- a/android/src/main/java/com/swmansion/rnscreens/Screen.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/Screen.kt +++ b/android/src/main/java/com/swmansion/rnscreens/Screen.kt
@@ -17,6 +17,7 @@ import androidx.annotation.RequiresApi @@ -16,6 +16,7 @@ import androidx.annotation.RequiresApi
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.children import androidx.core.view.children
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.facebook.react.bridge.GuardedRunnable
import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReactContext
@@ -637,7 +638,7 @@ class Screen( import com.facebook.react.uimanager.PixelUtil
@@ -462,7 +463,7 @@ class Screen(
endTransitionRecursive(childView.toolbar) endTransitionRecursive(childView.toolbar)
} }
@@ -19,7 +19,7 @@ index c99ca362df5baee81fa48d1c9ef09b40a2c23f07..725c07aa0ed516f9dd09f81e522f2d31
endTransitionRecursive(childView) endTransitionRecursive(childView)
} }
} }
@@ -666,7 +667,10 @@ class Screen( @@ -491,7 +492,10 @@ class Screen(
startTransitionRecursive(child.toolbar) startTransitionRecursive(child.toolbar)
} }
@@ -1,4 +1,4 @@
# react-native-screens+4.24.0.patch # react-native-screens+4.26.2.patch
## Android: do not transition RecyclerView children individually ## Android: do not transition RecyclerView children individually
@@ -1,28 +0,0 @@
diff --git a/src/index.js b/src/index.js
index fa76d7e1272e7fbe4bbd153104db127f1f6eecad..018b6860b7fa02d498d73b5fd06028bae99abedb 100644
--- a/src/index.js
+++ b/src/index.js
@@ -125,13 +125,17 @@ export function captureRef<T: React$ElementType>(
}
}
if (typeof view !== "number") {
- const node = findNodeHandle(view);
- if (!node) {
- return Promise.reject(
- new Error("findNodeHandle failed to resolve view=" + String(view))
- );
+ if (Platform.OS == 'web') {
+ view = view;
+ } else {
+ const node = findNodeHandle(view);
+ if (!node) {
+ return Promise.reject(
+ new Error("findNodeHandle failed to resolve view=" + String(view))
+ );
+ }
+ view = node;
}
- view = node;
}
const { options, errors } = validateOptions(optionsObject);
if (__DEV__ && errors.length > 0) {
@@ -1,3 +0,0 @@
## react-native-view-shot patch
Temporary patch for web, where `view`'s type has changed.
@@ -1,16 +1,16 @@
diff --git a/lib/module/threads.js b/lib/module/threads.js diff --git a/lib/module/threads.js b/lib/module/threads.js
index dd3a7f1ab12e5a8030af7f17b6a7a891e0b645d2..7d48ccbf95f00724db6b083a7155f25aadd6b8f5 100644 index c17e314..71f3cf7 100644
--- a/lib/module/threads.js --- a/lib/module/threads.js
+++ b/lib/module/threads.js +++ b/lib/module/threads.js
@@ -2,7 +2,6 @@ @@ -1,7 +1,6 @@
'use strict';
import { WorkletsError } from './debug/WorkletsError';
import { IS_JEST } from './platformChecker'; import { IS_JEST } from './platformChecker';
-import { mockedRequestAnimationFrame } from './runLoop/uiRuntime/mockedRequestAnimationFrame'; -import { mockedRequestAnimationFrame } from "./runLoop/uiRuntime/mockedRequestAnimationFrame.js";
export function scheduleOnUI(worklet, ...args) { export function scheduleOnUI(worklet, ...args) {
enqueueUI(worklet, args); enqueueUI(worklet, args);
} }
@@ -24,38 +23,50 @@ export function scheduleOnRN(fun, ...args) { @@ -23,38 +22,50 @@ export function scheduleOnRN(fun, ...args) {
queueMicrotask(args.length ? () => fun(...args) : fun); queueMicrotask(args.length ? () => fun(...args) : fun);
} }
export function runOnUIAsync(worklet, ...args) { export function runOnUIAsync(worklet, ...args) {
@@ -73,6 +73,8 @@ index dd3a7f1ab12e5a8030af7f17b6a7a891e0b645d2..7d48ccbf95f00724db6b083a7155f25a
}); });
} }
-const requestAnimationFrameImpl = !globalThis.requestAnimationFrame ? mockedRequestAnimationFrame : globalThis.requestAnimationFrame; -const requestAnimationFrameImpl = !globalThis.requestAnimationFrame ? mockedRequestAnimationFrame : globalThis.requestAnimationFrame;
-//# sourceMappingURL=threads.js.map
\ No newline at end of file
+function drainUIQueue(queue) { +function drainUIQueue(queue) {
+ while (queue.length > offset) { + while (queue.length > offset) {
+ const [workletFunction, workletArgs, jobResolve] = queue[offset]; + const [workletFunction, workletArgs, jobResolve] = queue[offset];
@@ -83,4 +85,4 @@ index dd3a7f1ab12e5a8030af7f17b6a7a891e0b645d2..7d48ccbf95f00724db6b083a7155f25a
+ } + }
+ } + }
+} +}
//# sourceMappingURL=threads.js.map +//# sourceMappingURL=threads.js.map
@@ -1,4 +1,4 @@
# react-native-worklets@0.8.3.patch # react-native-worklets@0.11.3.patch
Backport of https://github.com/software-mansion/react-native-reanimated/pull/10167 Backport of https://github.com/software-mansion/react-native-reanimated/pull/10167
("fix(Worklets): web scheduleOnUI implementation on errors"). ("fix(Worklets): web scheduleOnUI implementation on errors").
-239
View File
@@ -1,239 +0,0 @@
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
index c593d9ee2155a826352ebca34845aa5792b2eec3..3c26cd737f21116ff0aa48190e97e6c0649b5fac 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
@@ -101,6 +101,20 @@ - (void)setContentOffset:(CGPoint)contentOffset
RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
}
+- (void)setCenterContent:(BOOL)centerContent
+{
+ if (_centerContent != centerContent) {
+ _centerContent = centerContent;
+ [self centerContentIfNeeded];
+ }
+}
+
+- (void)setContentSize:(CGSize)contentSize
+{
+ [super setContentSize:contentSize];
+ [self centerContentIfNeeded];
+}
+
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
index 0d231bc8aa938da296eb3b981e8ac9595a43b87f..be0a10d9c4de1892fa00bcbf8d63d739b66d8ffe 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
@@ -76,7 +76,17 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
return;
}
- const auto &oldConcreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
+ /*
+ * TODO: Remove after upgrading React Native to 0.82+ (fixed upstream by
+ * facebook/react-native#52615, #52584 and #53231).
+ * Diff against oldProps instead of _props. During the initial-layout replay
+ * from layoutSubviews, _props already holds the new props, so diffing
+ * against it is a no-op and tintColor/progressViewOffset are never applied
+ * on mount (facebook/react-native#56343). oldProps is null-guarded because
+ * the create-mutation path passes nullptr.
+ */
+ const auto &oldConcreteProps = static_cast<const PullToRefreshViewProps &>(
+ oldProps ? *oldProps : *PullToRefreshViewShadowNode::defaultSharedProps());
const auto &newConcreteProps = static_cast<const PullToRefreshViewProps &>(*props);
if (newConcreteProps.tintColor != oldConcreteProps.tintColor) {
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
index 1494fd225aff1fa0429e917404d6b4ca5fc961c5..d0cce700090245444f8ce51e517d5ceca09526f6 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
@@ -380,7 +380,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
MAP_SCROLL_VIEW_PROP(zoomScale);
- if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset) {
+ // When disabling centerContent, reset inset to prop value
+ // (enabling is handled automatically by the setCenterContent: setter)
+ if (oldScrollViewProps.centerContent && !newScrollViewProps.centerContent) {
+ _scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
+ }
+
+ // Only apply contentInset from props if centerContent is disabled
+ // When centerContent is enabled, the inset is calculated by centerContentIfNeeded
+ if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset && !newScrollViewProps.centerContent) {
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
}
@@ -507,7 +515,7 @@ - (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event
}
}
- return isPointInside ? self : nil;
+ return isPointInside ? _scrollView : nil;
}
/*
@@ -1038,6 +1046,11 @@ - (void)_adjustForMaintainVisibleContentPosition
}
}
++ (BOOL)shouldBeRecycled
+{
+ return NO;
+}
+
@end
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
diff --git a/React/Views/RefreshControl/RCTRefreshControl.h b/React/Views/RefreshControl/RCTRefreshControl.h
index e9b330fa7c29c42653a3b0191d0f8a1b13b2d3de..5fbb2e05cadfc06fd7a18bf52b81bc399e92f3ca 100644
--- a/React/Views/RefreshControl/RCTRefreshControl.h
+++ b/React/Views/RefreshControl/RCTRefreshControl.h
@@ -15,5 +15,6 @@
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, weak) UIScrollView *scrollView;
+@property (nonatomic, copy) UIColor *customTintColor;
@end
diff --git a/React/Views/RefreshControl/RCTRefreshControl.m b/React/Views/RefreshControl/RCTRefreshControl.m
index 53bfd04703502d5b8e932c47a528bb03cd79d330..e2e0c9f4e5d1a3a3b178a7ec69aa63e3039b6dec 100644
--- a/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/React/Views/RefreshControl/RCTRefreshControl.m
@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
UIColor *_titleColor;
CGFloat _progressViewOffset;
BOOL _hasMovedToWindow;
+ UIColor *_customTintColor;
}
- (instancetype)init
@@ -58,6 +59,12 @@ - (void)layoutSubviews
_isInitialRender = false;
}
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
+ [self setTintColor:_customTintColor];
+}
+
- (void)didMoveToWindow
{
[super didMoveToWindow];
@@ -221,4 +228,16 @@ - (void)refreshControlValueChanged
}
}
+// Fix for https://github.com/facebook/react-native/issues/43388
+// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor
+// is set before the refresh control gets added to the scrollview. We'll call this
+// function whenever the superview changes. We'll also call it if the value of customTintColor
+// changes.
+- (void)setTintColor:(UIColor *)tintColor
+{
+ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
+ [super setTintColor:tintColor];
+ }
+}
+
@end
diff --git a/React/Views/RefreshControl/RCTRefreshControlManager.m b/React/Views/RefreshControl/RCTRefreshControlManager.m
index 40aaf9c51ebda9fedb1d1db2e9aacec84b4c39c8..1c60164b69762997b3369b46609a07768a06bad3 100644
--- a/React/Views/RefreshControl/RCTRefreshControlManager.m
+++ b/React/Views/RefreshControl/RCTRefreshControlManager.m
@@ -22,11 +22,12 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
+
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt
index 8b6571698fc5dd091a0d8980a33bb40295faf305..27c97bfeb6f13907c89f1d85f2bb8b8af7bdfb43 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt
+++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt
@@ -313,8 +313,9 @@ public open class JavaTimerManager(
// We also capture the idleCallbackRunnable to tentatively fix:
// https://github.com/facebook/react-native/issues/44842
currentIdleCallbackRunnable?.cancel()
- currentIdleCallbackRunnable = IdleCallbackRunnable(frameTimeNanos)
- reactApplicationContext.runOnJSQueueThread(currentIdleCallbackRunnable)
+ val idleCallbackRunnable = IdleCallbackRunnable(frameTimeNanos)
+ currentIdleCallbackRunnable = idleCallbackRunnable
+ reactApplicationContext.runOnJSQueueThread(idleCallbackRunnable)
reactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.IDLE_EVENT, this)
}
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
index 89b666dcf0258df0702c812600b685463128294c..2b1c3971f0c31a0d7a592b90170e4cc53a8a69dd 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
@@ -431,6 +431,13 @@ public open class ReactViewGroup public constructor(context: Context?) :
inSubviewClippingLoop = true
var clippedSoFar = 0
for (i in 0..<allChildrenCount) {
+ // Reentrant child removal during this loop can compact allChildren and leave a null at
+ // an index below allChildrenCount. A null entry means the view is already detached, so
+ // treat it as clipped instead of crashing.
+ if (childArray[i] == null) {
+ clippedSoFar++
+ continue
+ }
try {
updateSubviewClipStatus(clippingRect, i, clippedSoFar, excludedViewsSet)
} catch (ex: IndexOutOfBoundsException) {
@@ -466,7 +473,9 @@ public open class ReactViewGroup public constructor(context: Context?) :
) {
assertOnUiThread()
- val child = checkNotNull(allChildren?.get(idx))
+ // allChildren can be mutated reentrantly while a clipping pass is running, so a stale
+ // index can point at a null slot. Skip it instead of crashing.
+ val child = allChildren?.get(idx) ?: return
val intersects = clippingRect.intersects(child.left, child.top, child.right, child.bottom)
var needUpdateClippingRecursive = false
diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
index 216bb23beb023ef6c3ae814c17e05bccbda7fc91..6ad5cc1d9ed5b8cd2df08ad77adca56c6bb58ff4 100644
--- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
+++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
@@ -386,9 +386,10 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage
size.height = enumeratedLinesHeight;
}
+ CGFloat epsilon = 0.001;
size = (CGSize){
- ceil(size.width * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
- ceil(size.height * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
+ ceil((size.width + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
+ ceil((size.height + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
__block auto attachments = TextMeasurement::Attachments{};
diff --git a/third-party-podspecs/fmt.podspec b/third-party-podspecs/fmt.podspec
index 2f38990e226c13f483aaf1b986302d4094243814..9b02e481e290299be20a6f09c42056ff51695e9b 100644
--- a/third-party-podspecs/fmt.podspec
+++ b/third-party-podspecs/fmt.podspec
@@ -26,4 +26,11 @@ Pod::Spec.new do |spec|
spec.public_header_files = "include/fmt/*.h"
spec.header_mappings_dir = "include"
spec.source_files = ["include/fmt/*.h", "src/format.cc"]
+
+ # TODO: Remove after upgrading React Native past 0.83.x
+ # Fix fmt 11.0.2 consteval build error with Xcode 26.4 (facebook/react-native#55601)
+ # Fixed in RN 0.84+ which bumps fmt to a compatible version.
+ spec.prepare_command = <<~SCRIPT
+ perl -i -pe 's/^# define FMT_USE_CONSTEVAL 1$/# define FMT_USE_CONSTEVAL 0/' include/fmt/base.h
+ SCRIPT
end
+373
View File
@@ -0,0 +1,373 @@
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
index 1b02e8b2d39672063551411d5c403a69b671a869..b3481c1b98b45dea769035140dc2fd8d9b088b24 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
@@ -102,6 +102,20 @@ - (void)setContentOffset:(CGPoint)contentOffset
RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
}
+- (void)setCenterContent:(BOOL)centerContent
+{
+ if (_centerContent != centerContent) {
+ _centerContent = centerContent;
+ [self centerContentIfNeeded];
+ }
+}
+
+- (void)setContentSize:(CGSize)contentSize
+{
+ [super setContentSize:contentSize];
+ [self centerContentIfNeeded];
+}
+
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
index a087536f3af0d33b13fe38d8abd1bc6d7935def2..01f5c884ea4772350c0ebe6263723d97632f2b74 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
@@ -396,7 +396,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
MAP_SCROLL_VIEW_PROP(zoomScale);
- if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset) {
+ // When disabling centerContent, reset inset to prop value
+ // (enabling is handled automatically by the setCenterContent: setter)
+ if (oldScrollViewProps.centerContent && !newScrollViewProps.centerContent) {
+ _scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
+ }
+
+ // Only apply contentInset from props if centerContent is disabled
+ // When centerContent is enabled, the inset is calculated by centerContentIfNeeded
+ if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset && !newScrollViewProps.centerContent) {
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
}
@@ -523,7 +531,7 @@ - (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event
}
}
- return isPointInside ? self : nil;
+ return isPointInside ? _scrollView : nil;
}
/*
@@ -1133,6 +1141,11 @@ - (RCTVirtualViewContainerState *)virtualViewContainerState
return _virtualViewContainerState;
}
++ (BOOL)shouldBeRecycled
+{
+ return NO;
+}
+
@end
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
diff --git a/React/Views/RefreshControl/RCTRefreshControl.h b/React/Views/RefreshControl/RCTRefreshControl.h
index ed306d7cadbf36a2fed79be8bd9d68b5dca135bd..d447dad534fefa9fcbdbbde6dcbbdcddadd5a824 100644
--- a/React/Views/RefreshControl/RCTRefreshControl.h
+++ b/React/Views/RefreshControl/RCTRefreshControl.h
@@ -18,6 +18,7 @@ __attribute__((deprecated("This API will be removed along with the legacy archit
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, weak) UIScrollView *scrollView;
+@property (nonatomic, copy) UIColor *customTintColor;
@end
diff --git a/React/Views/RefreshControl/RCTRefreshControl.m b/React/Views/RefreshControl/RCTRefreshControl.m
index 2dc86e464264c9450eef18d7b153d35bf6a5cc55..6661dc69a04766afa0284d6e83839b219e98cf57 100644
--- a/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/React/Views/RefreshControl/RCTRefreshControl.m
@@ -25,6 +25,7 @@ @implementation RCTRefreshControl {
UIColor *_titleColor;
CGFloat _progressViewOffset;
BOOL _hasMovedToWindow;
+ UIColor *_customTintColor;
}
- (instancetype)init
@@ -60,6 +61,12 @@ - (void)layoutSubviews
_isInitialRender = false;
}
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
+ [self setTintColor:_customTintColor];
+}
+
- (void)didMoveToWindow
{
[super didMoveToWindow];
@@ -225,6 +232,18 @@ - (void)refreshControlValueChanged
}
}
+// Fix for https://github.com/facebook/react-native/issues/43388
+// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor
+// is set before the refresh control gets added to the scrollview. We'll call this
+// function whenever the superview changes. We'll also call it if the value of customTintColor
+// changes.
+- (void)setTintColor:(UIColor *)tintColor
+{
+ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
+ [super setTintColor:tintColor];
+ }
+}
+
@end
#endif // RCT_REMOVE_LEGACY_ARCH
diff --git a/React/Views/RefreshControl/RCTRefreshControlManager.m b/React/Views/RefreshControl/RCTRefreshControlManager.m
index 1e9ff527f4e6691d716da624031113a397876981..44329c5422c6f24d8a437fa35c6f2bad6bf8622b 100644
--- a/React/Views/RefreshControl/RCTRefreshControlManager.m
+++ b/React/Views/RefreshControl/RCTRefreshControlManager.m
@@ -24,11 +24,12 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
+
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
index 59775241c80bec99ad3ec080f2425aacc8900c24..426de3aa77cda2032d7b0991e2ca3f8482a438d3 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
@@ -459,6 +459,13 @@ public open class ReactViewGroup public constructor(context: Context?) :
inSubviewClippingLoop = true
var clippedSoFar = 0
for (i in 0..<allChildrenCount) {
+ // Reentrant child removal during this loop can compact allChildren and leave a null at
+ // an index below allChildrenCount. A null entry means the view is already detached, so
+ // treat it as clipped instead of crashing.
+ if (childArray[i] == null) {
+ clippedSoFar++
+ continue
+ }
try {
updateSubviewClipStatus(clippingRect, i, clippedSoFar, excludedViewsSet)
} catch (ex: IndexOutOfBoundsException) {
@@ -496,7 +503,9 @@ public open class ReactViewGroup public constructor(context: Context?) :
) {
assertOnUiThread()
- val child = checkNotNull(allChildren?.get(idx))
+ // allChildren can be mutated reentrantly while a clipping pass is running, so a stale
+ // index can point at a null slot. Skip it instead of crashing.
+ val child = allChildren?.get(idx) ?: return
val intersects = clippingRect.intersects(child.left, child.top, child.right, child.bottom)
var needUpdateClippingRecursive = false
diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm
index 9b04cadc22f5ae7b105f9f9875a242b53188cf03..b2b27626edc46625ac2372a13977d700948835b6 100644
--- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm
+++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm
@@ -361,7 +361,7 @@ static UIFontDescriptorSystemDesign RCTGetFontDescriptorSystemDesign(NSString *f
font = [UIFont fontWithName:fontProperties.family size:effectiveFontSize];
if (font != nullptr) {
fontNames = [UIFont fontNamesForFamilyName:font.familyName];
- fontWeight = (fontWeight != 0.0) ?: RCTGetFontWeight(font);
+ fontWeight = (fontWeight != 0.0) ? fontWeight : RCTGetFontWeight(font);
} else {
// Failback to system font.
font = RCTDefaultFontWithFontProperties(fontProperties);
diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
index ac553045a9c0ce77e288277912538d9e131ebc01..d99c8f4db5a07f1e4ffe7e03ff23adce9c63137b 100644
--- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
+++ b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
@@ -389,8 +389,9 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage
size.height = enumeratedLinesHeight;
}
- size = (CGSize){ceil(size.width * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
- ceil(size.height * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
+ CGFloat epsilon = 0.001;
+ size = (CGSize){ceil((size.width + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
+ ceil((size.height + epsilon) * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
NSRange visibleGlyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
index 60160efb163d91813fa2ca7ca758b51afcf261e1..fb646fe945ffe4aa4a386f80a1e42a90180691f1 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
@@ -42,6 +42,32 @@ - (void)setRefreshing:(BOOL)refreshing
@implementation RCTPullToRefreshViewComponentView {
UIRefreshControl *_refreshControl;
RCTScrollViewComponentView *__weak _scrollViewComponentView;
+ /*
+ * Deferred props: updateProps runs during the Create mount mutation, before
+ * _attach puts the control on the scroll view, and writes to a detached
+ * UIRefreshControl are hazardous:
+ *
+ * - tintColor: writing it to a detached control permanently suppresses the
+ * pull-to-refresh trigger haptic on iOS 17.4+
+ * (https://github.com/facebook/react-native/issues/43388).
+ *
+ * - progressViewOffset (the bounds.origin shift): on iOS 26 the control's
+ * _UIRefreshControlModernContentView positions itself at whatever
+ * bounds.origin it observes when it is CREATED - at insertion into the
+ * scroll view, or earlier if a pre-attach property write materializes it -
+ * and keeps that y forever (width tracks, y never re-pins; verified via
+ * on-device frame logging, Aug 2026). A pre-attach shift is therefore
+ * baked into the content view's own frame and cancelled exactly, hiding
+ * the spinner. Applied post-attach, the content view has already been
+ * created at origin 0 and the same bounds shift works as intended.
+ *
+ * Both props are parked here and applied only once the control is inside
+ * the scroll view.
+ */
+ UIColor *_pendingTintColor;
+ BOOL _hasPendingTintColor;
+ CGFloat _pendingProgressViewOffset;
+ BOOL _hasPendingProgressViewOffset;
// This variable keeps track of whether the view is recycled or not. Once the view is recycled, the component
// creates a new instance of UIRefreshControl, resetting the native props to the default values.
// However, when recycling, we are keeping around the old _props. The flag is used to force the application
@@ -79,10 +105,25 @@ + (ComponentDescriptorProvider)componentDescriptorProvider
return concreteComponentDescriptorProvider<PullToRefreshViewComponentDescriptor>();
}
+// Recycled instances get all props force-applied in updateProps, which runs
+// before the new UIRefreshControl is inserted into the scroll view hierarchy;
+// touching the control that early suppresses the pull-to-refresh haptic on
+// iOS 17.4+ (react-native#43388). Opting out of recycling keeps every mount on
+// the untouched-before-attach path. Refresh controls are rare and cheap, so
+// losing recycling for them is negligible.
++ (BOOL)shouldBeRecycled
+{
+ return NO;
+}
+
- (void)prepareForRecycle
{
[super prepareForRecycle];
_scrollViewComponentView = nil;
+ _pendingTintColor = nil;
+ _hasPendingTintColor = NO;
+ _pendingProgressViewOffset = 0;
+ _hasPendingProgressViewOffset = NO;
[self _initializeUIRefreshControl];
_recycled = YES;
}
@@ -93,7 +134,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
const auto &newConcreteProps = static_cast<const PullToRefreshViewProps &>(*props);
if (_recycled || newConcreteProps.tintColor != oldConcreteProps.tintColor) {
- _refreshControl.tintColor = RCTUIColorFromSharedColor(newConcreteProps.tintColor);
+ // Deferred until the control is inside the scroll view (#43388).
+ [self _updateTintColor:RCTUIColorFromSharedColor(newConcreteProps.tintColor)];
}
if (_recycled || newConcreteProps.progressViewOffset != oldConcreteProps.progressViewOffset) {
@@ -141,11 +183,50 @@ - (void)handleUIControlEventValueChanged
- (void)_updateProgressViewOffset:(Float)progressViewOffset
{
+ _pendingProgressViewOffset = progressViewOffset;
+ _hasPendingProgressViewOffset = YES;
+ // Applies immediately for runtime changes while the control is attached;
+ // pre-attach sets wait until the control is inside the scroll view (see the
+ // _pendingProgressViewOffset declaration).
+ [self _applyPendingProgressViewOffsetIfPossible];
+ if (_hasPendingProgressViewOffset) {
+ [self setNeedsLayout];
+ }
+}
+
+- (void)_applyPendingProgressViewOffsetIfPossible
+{
+ if (!_hasPendingProgressViewOffset || ![_refreshControl.superview isKindOfClass:[UIScrollView class]]) {
+ return;
+ }
_refreshControl.bounds = CGRectMake(
_refreshControl.bounds.origin.x,
- -progressViewOffset,
+ -_pendingProgressViewOffset,
_refreshControl.bounds.size.width,
_refreshControl.bounds.size.height);
+ _hasPendingProgressViewOffset = NO;
+}
+
+- (void)_updateTintColor:(UIColor *)tintColor
+{
+ _pendingTintColor = tintColor;
+ _hasPendingTintColor = YES;
+ // Applies immediately for runtime changes while the control is attached;
+ // pre-attach sets wait until the control is inside the scroll view.
+ [self _applyPendingTintColorIfPossible];
+ if (_hasPendingTintColor) {
+ [self setNeedsLayout];
+ }
+}
+
+- (void)_applyPendingTintColorIfPossible
+{
+ if (!_hasPendingTintColor || ![_refreshControl.superview isKindOfClass:[UIScrollView class]]) {
+ return;
+ }
+ _refreshControl.tintColor = _pendingTintColor;
+ _pendingTintColor = nil;
+ _hasPendingTintColor = NO;
}
- (void)_updateTitle
@@ -153,7 +234,12 @@ - (void)_updateTitle
const auto &concreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
if (concreteProps.title.empty()) {
- _refreshControl.attributedTitle = nil;
+ // Avoid touching the control when there is nothing to clear - writing
+ // attributedTitle (even nil) before the control is in the scroll view
+ // hierarchy can suppress the pull-to-refresh haptic (#43388).
+ if (_refreshControl.attributedTitle != nil) {
+ _refreshControl.attributedTitle = nil;
+ }
return;
}
@@ -172,6 +258,18 @@ - (void)layoutSubviews
{
[super layoutSubviews];
+ /*
+ * Fallback for the pending props: _attach applies them right after the
+ * refreshControl assignment (insertion is synchronous there on current iOS),
+ * but should UIKit ever defer the insertion to a later layout pass, re-arm
+ * and retry until the control is actually inside the scroll view.
+ */
+ [self _applyPendingTintColorIfPossible];
+ [self _applyPendingProgressViewOffsetIfPossible];
+ if ((_hasPendingTintColor || _hasPendingProgressViewOffset) && _scrollViewComponentView != nil) {
+ [self setNeedsLayout];
+ }
+
// Attempts to begin refreshing before the initial layout are ignored by _refreshControl. So if the control is
// refreshing when mounted, we need to call beginRefreshing in layoutSubviews or it won't work.
if (self.window) {
@@ -209,6 +307,15 @@ - (void)_attach
// This ensures that layoutSubviews is called. Without this, recycled instances won't refresh on mount
[self setNeedsLayout];
+
+ /*
+ * The assignment above inserts the control (and creates its content view)
+ * synchronously on current iOS - verified via frame logging - so the
+ * pending props can be applied immediately. layoutSubviews is the fallback
+ * if insertion is ever deferred.
+ */
+ [self _applyPendingTintColorIfPossible];
+ [self _applyPendingProgressViewOffsetIfPossible];
}
}
@@ -6,18 +6,62 @@ Patching `RCTRefreshControl.mm` temporarily to play an impact haptic on refresh
17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update 17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update
in the RN repo: https://github.com/facebook/react-native/issues/43388 in the RN repo: https://github.com/facebook/react-native/issues/43388
## RCTPullToRefreshViewComponentView.mm Patch - RefreshControl initial props dropped on New Arch ## RCTPullToRefreshViewComponentView.mm Patch - iOS 17.4+ haptic regression and iOS 26 progressViewOffset cancellation on New Arch
**TODO: Remove after bumping React Native to 0.82+** (fixed upstream by facebook/react-native#52615, #52584 Both bugs share one root cause, established by instrumented frame-logging runs on the iOS 26
and #53231). simulator (Aug 2026): **writes to a detached `UIRefreshControl` are hazardous, because the
control's `_UIRefreshControlModernContentView` bakes in the state it observes at its own
creation.** Facts proven by the logs:
On Fabric, `updateProps` diffs against `_props`, but the initial-layout replay in `layoutSubviews` passes - `scrollView.refreshControl` assignment inserts the control and creates its content view
`_props` as the new props too, so the diff is a no-op and `tintColor`/`progressViewOffset`/`title` are never **synchronously** on iOS 26 (the "UIKit inserts lazily on a later layout pass" folklore is
applied on mount. This hides the pull-to-refresh spinner behind the floating home header (it stays at offset false there).
0 instead of `headerOffset`). We diff against the `oldProps` argument instead, null-guarded with default - The content view can also be materialized **earlier** by a pre-attach property write (observed
props for the create-mutation path. with `tintColor`) while the control is still detached.
- The content view positions itself at whatever `bounds.origin` exists at its creation and keeps
that y forever - width tracks on later layouts, y never re-pins.
Issue: https://github.com/facebook/react-native/issues/56343 Consequences:
**1. progressViewOffset.** Stock Fabric writes the offset as a `bounds.origin` shift in
`updateProps`, pre-attach. The content view is then created (at insertion) already inside the
shifted bounds, pins to it, and cancels the shift exactly - spinner hidden behind the floating
home header (home is the only screen passing a non-zero offset). Stock RN appeared to work only
by accident: its own pre-attach `tintColor` write materialized the content view at origin 0
*before* the offset write. Possibly related upstream: react-native#54183.
**2. Haptic (react-native#43388).** The Paper fix above does not cover Fabric: `updateProps`
writes `tintColor` pre-attach, and a tint write on a detached control materializes the content
view outside the scroll view, permanently suppressing the trigger haptic on iOS 17.4+ (the
creation-time-state story likely explains this too, though the haptic wiring itself is not
observable in logs).
**The fix**: both `tintColor` and `progressViewOffset` are parked in the component view
(`_pendingTintColor` / `_pendingProgressViewOffset`, no `UIRefreshControl` subclass) and applied
only once `_refreshControl.superview` is the scroll view - by then the content view exists,
was created at origin 0, and a bounds shift lands visibly. Application points: immediately in
`_updateX` for runtime changes while attached; in `_attach` right after the assignment (insertion
is synchronous); and from `layoutSubviews` with a `setNeedsLayout` re-arm as a fallback should
insertion ever be deferred.
Supporting changes:
- `shouldBeRecycled = NO`: recycled instances get all props force-applied in `updateProps` before
the new control is attached, which would re-trigger the pre-attach hazards; opting out keeps
every mount on the untouched-before-attach path.
- `_updateTitle` no longer writes `attributedTitle = nil` when there is nothing to clear - even a
nil write before attach suppresses the haptic.
History: an earlier iteration fixed the offset by porting Paper's frame-offset trick into an
`RCTHapticCompatibleRefreshControl` subclass (worked, verified on device) - replaced by the
deferral once the root cause was understood. The control's `didMoveToSuperview` appeared broken
as a tint application point in early non-rigorous testing; unproven, not disproven.
Upstream issue #43388 still open as of Aug 2026. Haptics cannot be verified on the simulator -
physical device only. Spinner position verified via frame logs; haptic on this variant NOT yet
device-verified.
Opened issue in RN repo: https://github.com/react/react-native/issues/57843
## RCTEnhancedScrollView.mm / RCTScrollViewComponentView.mm Patch - centerContent insets stale after content resize on New Arch ## RCTEnhancedScrollView.mm / RCTScrollViewComponentView.mm Patch - centerContent insets stale after content resize on New Arch
@@ -86,6 +130,16 @@ the prebuilt AAR from Maven Central instead, where this hunk (like any ReactAndr
source change) has no effect - do not expect to see the fix in a local debug build unless source change) has no effect - do not expect to see the fix in a local debug build unless
you prebuild with EXPO_PUBLIC_ENV=production or add the substitution block manually. you prebuild with EXPO_PUBLIC_ENV=production or add the substitution block manually.
## RCTFontUtils.mm Patch - Custom font weights render as the heaviest face on New Arch
**TODO: Remove after bumping React Native to a release that contains facebook/react-native#57483**
(commit 918fb15bfe5f, on `main`; not in 0.86 and not yet released).
Backport of the upstream one-liner: use a real ternary so the numeric weight is returned instead of
the boolean. For a double, `(A != 0.0) ? A : B` is exactly equivalent to the original `A ?: B`.
PR: https://github.com/facebook/react-native/pull/57483
## RCTTextLayoutManager.mm Patch - Text overflows instead of wrapping on the last line ## RCTTextLayoutManager.mm Patch - Text overflows instead of wrapping on the last line
Issue: https://github.com/react/react-native/issues/53450#issuecomment-3298157830 Issue: https://github.com/react/react-native/issues/53450#issuecomment-3298157830
@@ -56,7 +56,7 @@ const withXcodeTarget = (config, {targetName}) => {
buildSettingsObj.SWIFT_VERSION = '5.0' buildSettingsObj.SWIFT_VERSION = '5.0'
buildSettingsObj.TARGETED_DEVICE_FAMILY = `"1"` buildSettingsObj.TARGETED_DEVICE_FAMILY = `"1"`
buildSettingsObj.DEVELOPMENT_TEAM = 'B3LX46C5HS' buildSettingsObj.DEVELOPMENT_TEAM = 'B3LX46C5HS'
buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '15.1' buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '16.4'
buildSettingsObj.ASSETCATALOG_COMPILER_APPICON_NAME = 'AppIcon' buildSettingsObj.ASSETCATALOG_COMPILER_APPICON_NAME = 'AppIcon'
} }
} }
+2157 -2212
View File
File diff suppressed because it is too large Load Diff
+18 -21
View File
@@ -13,16 +13,16 @@ packageExtensions:
trustPolicy: 'no-downgrade' # default: off trustPolicy: 'no-downgrade' # default: off
trustPolicyIgnoreAfter: 10080 # 7 days, default: undefined trustPolicyIgnoreAfter: 10080 # 7 days, default: undefined
overrides: overrides:
'@react-native/babel-preset': '0.81.5' '@react-native/babel-preset': '0.86.0'
'@react-native/normalize-colors': '0.81.5' '@react-native/normalize-colors': '0.86.0'
'@expo/image-utils': '0.8.12' '@expo/image-utils': '0.8.12'
'@types/estree': '1.0.6' '@types/estree': '1.0.6'
'react-native-compressor': '1.13.0' 'react-native-compressor': '1.13.0'
'react-native-reanimated': '4.3.2' 'react-native-reanimated': '4.5.3'
'react-native-worklets': '0.8.3' 'react-native-worklets': '0.11.3'
'psl': '1.9.0' 'psl': '1.9.0'
'@types/psl': '1.1.1' '@types/psl': '1.1.1'
'react-native-screens': '4.24.0' 'react-native-screens': '4.26.2'
allowBuilds: allowBuilds:
'@sentry/cli': true '@sentry/cli': true
'core-js': false 'core-js': false
@@ -31,26 +31,23 @@ allowBuilds:
'unrs-resolver': true 'unrs-resolver': true
patchedDependencies: patchedDependencies:
'@sentry/expo-upload-sourcemaps@8.18.0': patches/@sentry__expo-upload-sourcemaps@8.18.0.patch '@sentry/expo-upload-sourcemaps@8.18.0': patches/@sentry__expo-upload-sourcemaps@8.18.0.patch
'expo-age-range@0.2.18': patches/expo-age-range@0.2.18.patch 'expo-age-range@57.0.2': patches/expo-age-range@57.0.2.patch
'expo-haptics@15.0.8': patches/expo-haptics@15.0.8.patch 'expo-haptics@57.0.1': patches/expo-haptics@57.0.1.patch
'expo-image-picker@17.0.11': patches/expo-image-picker@17.0.11.patch 'expo-media-library@57.0.3': patches/expo-media-library@57.0.3.patch
'expo-image@3.0.11': patches/expo-image@3.0.11.patch 'expo-modules-core@57.0.8': patches/expo-modules-core@57.0.8.patch
'expo-media-library@18.2.1': patches/expo-media-library@18.2.1.patch 'expo-notifications@57.0.7': patches/expo-notifications@57.0.7.patch
'expo-modules-core@3.0.30': patches/expo-modules-core@3.0.30.patch 'expo-updates@57.0.10': patches/expo-updates@57.0.10.patch
'expo-notifications@0.32.17': patches/expo-notifications@0.32.17.patch expo@57.0.8: patches/expo@57.0.8.patch
'expo-updates@29.0.17': patches/expo-updates@29.0.17.patch
'react-native-compressor@1.13.0': patches/react-native-compressor@1.13.0.patch 'react-native-compressor@1.13.0': patches/react-native-compressor@1.13.0.patch
'react-native-date-picker@5.0.13': patches/react-native-date-picker@5.0.13.patch 'react-native-date-picker@5.0.13': patches/react-native-date-picker@5.0.13.patch
'react-native-drawer-layout@4.2.3': patches/react-native-drawer-layout@4.2.3.patch 'react-native-drawer-layout@4.2.3': patches/react-native-drawer-layout@4.2.3.patch
'react-native-gesture-handler': patches/react-native-gesture-handler.patch 'react-native-keyboard-controller@1.21.9': patches/react-native-keyboard-controller@1.21.9.patch
'react-native-keyboard-controller@1.21.8': patches/react-native-keyboard-controller@1.21.8.patch
'react-native-pager-view@6.8.0': patches/react-native-pager-view@6.8.0.patch 'react-native-pager-view@6.8.0': patches/react-native-pager-view@6.8.0.patch
'react-native-reanimated@4.3.2': patches/react-native-reanimated@4.3.2.patch 'react-native-reanimated@4.5.3': patches/react-native-reanimated@4.5.3.patch
react-native-screens@4.24.0: patches/react-native-screens@4.24.0.patch 'react-native-screens@4.26.2': patches/react-native-screens@4.26.2.patch
'react-native-svg@15.12.1': patches/react-native-svg@15.12.1.patch 'react-native-svg@15.15.4': patches/react-native-svg@15.15.4.patch
'react-native-view-shot@4.0.3': patches/react-native-view-shot@4.0.3.patch react-native-worklets@0.11.3: patches/react-native-worklets@0.11.3.patch
react-native-worklets@0.8.3: patches/react-native-worklets@0.8.3.patch 'react-native@0.86.0': patches/react-native@0.86.0.patch
'react-native@0.81.5': patches/react-native@0.81.5.patch
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- '@atproto/*' - '@atproto/*'
- '@atproto-labs/*' - '@atproto-labs/*'
+1 -1
View File
@@ -14,7 +14,7 @@ SETTINGS_GRADLE="$ANDROID_DIR/settings.gradle"
# Guard against building with the wrong app identity. The New Arch build must # Guard against building with the wrong app identity. The New Arch build must
# use a distinct rootProject.name so it installs alongside the store app rather # use a distinct rootProject.name so it installs alongside the store app rather
# than overwriting it. # than overwriting it.
EXPECTED_APP_NAME="rootProject.name = 'Bluesky (New Arch)'" EXPECTED_APP_NAME="rootProject.name = 'Bluesky'"
if ! grep -qF "$EXPECTED_APP_NAME" "$SETTINGS_GRADLE"; then if ! grep -qF "$EXPECTED_APP_NAME" "$SETTINGS_GRADLE"; then
echo "Error: expected \"$EXPECTED_APP_NAME\" in $SETTINGS_GRADLE" >&2 echo "Error: expected \"$EXPECTED_APP_NAME\" in $SETTINGS_GRADLE" >&2
echo "(Set the app name in settings.gradle before building the New Arch release.)" >&2 echo "(Set the app name in settings.gradle before building the New Arch release.)" >&2
+5 -4
View File
@@ -2,7 +2,6 @@ import {forwardRef, useCallback, useEffect, useState} from 'react'
import { import {
AccessibilityInfo, AccessibilityInfo,
Image as RNImage, Image as RNImage,
StyleSheet,
useColorScheme, useColorScheme,
View, View,
} from 'react-native' } from 'react-native'
@@ -20,6 +19,7 @@ import {Image} from 'expo-image'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
import {Logotype} from '#/view/icons/Logotype' import {Logotype} from '#/view/icons/Logotype'
import {atoms as a} from '#/alf'
// @ts-ignore // @ts-ignore
import splashImagePointer from '../assets/splash/splash.png' import splashImagePointer from '../assets/splash/splash.png'
// @ts-ignore // @ts-ignore
@@ -170,12 +170,12 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return ( return (
<View style={{flex: 1}} onLayout={onLayout}> <View style={{flex: 1}} onLayout={onLayout}>
{!isAnimationComplete && ( {!isAnimationComplete && (
<View style={StyleSheet.absoluteFillObject}> <View style={[a.absolute, a.inset_0]}>
<Image <Image
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
onLoadEnd={onLoadEnd} onLoadEnd={onLoadEnd}
source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}} source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
style={StyleSheet.absoluteFillObject} style={[a.absolute, a.inset_0]}
/> />
<Animated.View <Animated.View
@@ -205,7 +205,8 @@ export function Splash(props: React.PropsWithChildren<Props>) {
{!isAnimationComplete && ( {!isAnimationComplete && (
<Animated.View <Animated.View
style={[ style={[
StyleSheet.absoluteFillObject, a.absolute,
a.inset_0,
logoAnimation, logoAnimation,
{ {
flex: 1, flex: 1,
+3 -6
View File
@@ -2,7 +2,7 @@ import {lazy, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
// @ts-expect-error missing types // @ts-expect-error missing types
import QRCode from 'react-native-qrcode-styled' import QRCode from 'react-native-qrcode-styled'
import type ViewShot from 'react-native-view-shot' import {type ViewShotRef} from 'react-native-view-shot'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
@@ -14,10 +14,7 @@ import {IS_WEB} from '#/env'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
const LazyViewShot = lazy( const LazyViewShot = lazy(() => import('react-native-view-shot'))
// @ts-expect-error dynamic import
() => import('react-native-view-shot/src/index'),
)
export function QrCode({ export function QrCode({
starterPack, starterPack,
@@ -26,7 +23,7 @@ export function QrCode({
}: { }: {
starterPack: app.bsky.graph.defs.StarterPackView starterPack: app.bsky.graph.defs.StarterPackView
link: string link: string
ref: React.Ref<ViewShot> ref: React.Ref<ViewShotRef>
}) { }) {
const {record} = starterPack const {record} = starterPack
+6 -3
View File
@@ -1,7 +1,10 @@
import {Suspense, useRef, useState} from 'react' import {Suspense, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import type ViewShot from 'react-native-view-shot' import {type ViewShotRef} from 'react-native-view-shot'
import {requestPermissionsAsync, saveToLibraryAsync} from 'expo-media-library' import {
requestPermissionsAsync,
saveToLibraryAsync,
} from 'expo-media-library/legacy'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -38,7 +41,7 @@ export function QrCodeDialog({
const [isSaveProcessing, setIsSaveProcessing] = useState(false) const [isSaveProcessing, setIsSaveProcessing] = useState(false)
const [isCopyProcessing, setIsCopyProcessing] = useState(false) const [isCopyProcessing, setIsCopyProcessing] = useState(false)
const ref = useRef<ViewShot>(null) const ref = useRef<ViewShotRef>(null)
const getCanvas = (base64: string): Promise<HTMLCanvasElement> => { const getCanvas = (base64: string): Promise<HTMLCanvasElement> => {
return new Promise(resolve => { return new Promise(resolve => {
@@ -1,8 +1,11 @@
import {Suspense, useRef} from 'react' import {Suspense, useRef} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import type ViewShot from 'react-native-view-shot' import {type ViewShotRef} from 'react-native-view-shot'
import {setStringAsync} from 'expo-clipboard' import {setStringAsync} from 'expo-clipboard'
import {requestPermissionsAsync, saveToLibraryAsync} from 'expo-media-library' import {
requestPermissionsAsync,
saveToLibraryAsync,
} from 'expo-media-library/legacy'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -41,7 +44,7 @@ export function InviteFriendsDialogInner({
const {currentAccount} = useSession() const {currentAccount} = useSession()
const profileQuery = useProfileQuery({did: currentAccount?.did}) const profileQuery = useProfileQuery({did: currentAccount?.did})
const [themeKey, setThemeKey] = useInviteThemeKey() const [themeKey, setThemeKey] = useInviteThemeKey()
const captureRef = useRef<ViewShot>(null) const captureRef = useRef<ViewShotRef>(null)
const theme = getInviteTheme(themeKey) const theme = getInviteTheme(themeKey)
const variant = t.name === 'light' ? theme.light : theme.dark const variant = t.name === 'light' ? theme.light : theme.dark
@@ -1,5 +1,5 @@
import {useCallback, useState} from 'react' import {useCallback, useState} from 'react'
import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native' import {Pressable, useWindowDimensions, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Svg, {Path} from 'react-native-svg' import Svg, {Path} from 'react-native-svg'
import { import {
@@ -125,7 +125,7 @@ export function InviteScannerScreen() {
noInsetTop noInsetTop
style={{backgroundColor: t.palette.black}}> style={{backgroundColor: t.palette.black}}>
<CameraView <CameraView
style={StyleSheet.absoluteFill} style={[a.absolute, a.inset_0]}
facing="back" facing="back"
barcodeScannerSettings={{barcodeTypes: ['qr']}} barcodeScannerSettings={{barcodeTypes: ['qr']}}
onBarcodeScanned={scannerEnabled ? onBarcodeScanned : undefined} onBarcodeScanned={scannerEnabled ? onBarcodeScanned : undefined}
@@ -254,7 +254,7 @@ function ScannerScrim() {
`V${y + r} A${r} ${r} 0 0 1 ${x + r} ${y} Z` `V${y + r} A${r} ${r} 0 0 1 ${x + r} ${y} Z`
return ( return (
<Svg <Svg
style={StyleSheet.absoluteFill} style={[a.absolute, a.inset_0]}
width={width} width={width}
height={height} height={height}
pointerEvents="none"> pointerEvents="none">
@@ -2,7 +2,7 @@ import {lazy} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
// @ts-expect-error missing types // @ts-expect-error missing types
import QRCode from 'react-native-qrcode-styled' import QRCode from 'react-native-qrcode-styled'
import type ViewShot from 'react-native-view-shot' import {type ViewShotRef} from 'react-native-view-shot'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
@@ -12,10 +12,7 @@ import {hexToRgb, rgbToHex} from '#/alf/util/colorGeneration'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {type InviteThemeVariant} from '../themes' import {type InviteThemeVariant} from '../themes'
const LazyViewShot = lazy( const LazyViewShot = lazy(() => import('react-native-view-shot'))
// @ts-expect-error dynamic import
() => import('react-native-view-shot/src/index'),
)
const CARD_WIDTH = 278 const CARD_WIDTH = 278
const CARD_GRADIENT_PADDING = 12 const CARD_GRADIENT_PADDING = 12
@@ -37,7 +34,7 @@ export function ThemedQrCard({
shareUrl: string shareUrl: string
handle: string handle: string
avatarUri?: string avatarUri?: string
captureRef: React.Ref<ViewShot> captureRef: React.Ref<ViewShotRef>
}) { }) {
const t = useTheme() const t = useTheme()
return ( return (
@@ -1,5 +1,5 @@
import {useMemo, useState} from 'react' import {useMemo, useState} from 'react'
import {type ColorValue, Dimensions, StyleSheet, View} from 'react-native' import {type ColorValue, Dimensions, View} from 'react-native'
import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import Animated, { import Animated, {
clamp, clamp,
@@ -16,6 +16,7 @@ import Animated, {
import {scheduleOnRN} from 'react-native-worklets' import {scheduleOnRN} from 'react-native-worklets'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {atoms as a} from '#/alf'
import {type GestureActions} from './GestureActionView.shared' import {type GestureActions} from './GestureActionView.shared'
const MAX_WIDTH = Dimensions.get('screen').width const MAX_WIDTH = Dimensions.get('screen').width
@@ -287,8 +288,7 @@ export function GestureActionView({
return ( return (
<GestureDetector gesture={composedGesture}> <GestureDetector gesture={composedGesture}>
<View> <View>
<Animated.View <Animated.View style={[a.absolute, a.inset_0, animatedBackgroundStyle]}>
style={[StyleSheet.absoluteFill, animatedBackgroundStyle]}>
<View <View
style={{ style={{
flex: 1, flex: 1,
+1 -1
View File
@@ -1,6 +1,6 @@
import {Linking} from 'react-native' import {Linking} from 'react-native'
import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera' import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library/legacy'
import {Alert} from '#/view/com/util/Alert' import {Alert} from '#/view/com/util/Alert'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
+1 -1
View File
@@ -13,7 +13,7 @@ import {
writeAsStringAsync, writeAsStringAsync,
} from 'expo-file-system/legacy' } from 'expo-file-system/legacy'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library/legacy'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import {logger} from '#/logger' import {logger} from '#/logger'
+1 -1
View File
@@ -1,5 +1,5 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library/legacy'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
+40 -8
View File
@@ -18,27 +18,42 @@ declare module '*.css'
/* /*
* expo-file-system's declarations build File/Directory on top of * expo-file-system's declarations build File/Directory on top of
* `./ExpoFileSystem`, which remaps to a web shim whose classes are empty. * `./ExpoFileSystem`, which remaps to a web shim whose classes are empty.
* The fully-typed base classes live in ExpoFileSystem.types (no .web * As of SDK 57 the fully-typed base classes live in
* sibling), so mirror the FileSystem.d.ts wrapper classes on top of those. * internal/NativeFileSystem.types (no .web sibling), so mirror the File.d.ts
* and Directory.d.ts wrapper classes on top of those.
*/ */
declare module 'expo-file-system' { declare module 'expo-file-system' {
import { import {
Directory as ExpoFileSystemDirectory, NativeFileSystemDirectory as ExpoFileSystemDirectory,
File as ExpoFileSystemFile, NativeFileSystemFile as ExpoFileSystemFile,
} from 'expo-file-system/build/ExpoFileSystem.types' } from 'expo-file-system/build/internal/NativeFileSystem.types'
export { export {
type DirectoryCreateOptions, type DirectoryCreateOptions,
type DirectoryInfo, type DirectoryInfo,
type DownloadOptions, type DownloadOptions,
EncodingType, EncodingType,
type FileCreateOptions, type FileCreateOptions,
FileHandle, type FileHandle,
type FileInfo, type FileInfo,
type FileWriteOptions, type FileWriteOptions,
type InfoOptions, type InfoOptions,
type PathInfo, type PathInfo,
} from 'expo-file-system/build/ExpoFileSystem.types' } from 'expo-file-system/build/FileSystem.types'
import {type PathInfo as ExpoPathInfo} from 'expo-file-system/build/ExpoFileSystem.types' import {type PathInfo as ExpoPathInfo} from 'expo-file-system/build/FileSystem.types'
import {
type WatchEvent,
type WatchOptions,
type WatchSubscription,
} from 'expo-file-system/build/FileSystemWatcher.types'
import {
type DownloadTask,
type UploadTask,
} from 'expo-file-system/build/NetworkTasks'
import {
type DownloadTaskOptions,
type UploadOptions,
type UploadResult,
} from 'expo-file-system/build/NetworkTasks.types'
import {PathUtilities} from 'expo-file-system/build/pathUtilities' import {PathUtilities} from 'expo-file-system/build/pathUtilities'
export class Paths extends PathUtilities { export class Paths extends PathUtilities {
@@ -59,8 +74,21 @@ declare module 'expo-file-system' {
readableStream(): ReadableStream<Uint8Array<ArrayBuffer>> readableStream(): ReadableStream<Uint8Array<ArrayBuffer>>
writableStream(): WritableStream<Uint8Array<ArrayBufferLike>> writableStream(): WritableStream<Uint8Array<ArrayBufferLike>>
arrayBuffer(): Promise<ArrayBuffer> arrayBuffer(): Promise<ArrayBuffer>
json(): Promise<unknown>
formData(): ReturnType<Response['formData']>
stream(): ReadableStream<Uint8Array<ArrayBuffer>> stream(): ReadableStream<Uint8Array<ArrayBuffer>>
slice(start?: number, end?: number, contentType?: string): Blob slice(start?: number, end?: number, contentType?: string): Blob
upload(url: string, options?: UploadOptions): Promise<UploadResult>
createUploadTask(url: string, options?: UploadOptions): UploadTask
static createDownloadTask(
url: string,
destination: File | Directory,
options?: DownloadTaskOptions,
): DownloadTask
watch(
callback: (event: WatchEvent<File>) => void,
options?: WatchOptions,
): WatchSubscription
} }
export class Directory extends ExpoFileSystemDirectory { export class Directory extends ExpoFileSystemDirectory {
@@ -70,6 +98,10 @@ declare module 'expo-file-system' {
get name(): string get name(): string
createFile(name: string, mimeType: string | null): File createFile(name: string, mimeType: string | null): File
createDirectory(name: string): Directory createDirectory(name: string): Directory
watch(
callback: (event: WatchEvent<File | Directory>) => void,
options?: WatchOptions,
): WatchSubscription
} }
} }
@@ -7,15 +7,12 @@ import {
useRef, useRef,
} from 'react' } from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import type ViewShot from 'react-native-view-shot' import {type ViewShotRef} from 'react-native-view-shot'
import {useAvatar} from '#/screens/Onboarding/StepProfile/index' import {useAvatar} from '#/screens/Onboarding/StepProfile/index'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
const LazyViewShot = lazy( const LazyViewShot = lazy(() => import('react-native-view-shot'))
// @ts-expect-error dynamic import
() => import('react-native-view-shot/src/index'),
)
const SIZE_MULTIPLIER = 5 const SIZE_MULTIPLIER = 5
@@ -28,7 +25,7 @@ export interface PlaceholderCanvasRef {
export const PlaceholderCanvas = forwardRef<PlaceholderCanvasRef, {}>( export const PlaceholderCanvas = forwardRef<PlaceholderCanvasRef, {}>(
function PlaceholderCanvas({}, ref) { function PlaceholderCanvas({}, ref) {
const {avatar} = useAvatar() const {avatar} = useAvatar()
const viewshotRef = useRef<ViewShot>(null) const viewshotRef = useRef<ViewShotRef>(null)
const Icon = avatar.placeholder.component const Icon = avatar.placeholder.component
const styles = useMemo( const styles = useMemo(
@@ -107,6 +107,7 @@ export function StepProfile() {
const response = await sheetWrapper( const response = await sheetWrapper(
launchImageLibraryAsync({ launchImageLibraryAsync({
exif: false, exif: false,
shouldDownloadFromNetwork: true,
mediaTypes: ['images'], mediaTypes: ['images'],
quality: 1, quality: 1,
...opts, ...opts,
+1 -2
View File
@@ -95,8 +95,7 @@ export function StepInfo({
tldtsRef.current = tldts tldtsRef.current = tldts
}) })
// This will get used in the avatar creator a few steps later, so lets preload it now // This will get used in the avatar creator a few steps later, so lets preload it now
// @ts-expect-error - valid path void import('react-native-view-shot')
void import('react-native-view-shot/src/index')
}, []) }, [])
const onNextPress = () => { const onNextPress = () => {
+1 -1
View File
@@ -567,7 +567,7 @@ export const ComposePost = ({
FileSystem.Paths.cache, FileSystem.Paths.cache,
tempFileName, tempFileName,
) )
sourceFile.copy(tempFile) await sourceFile.copy(tempFile)
logger.debug('restoreVideo: copied to temp file', { logger.debug('restoreVideo: copied to temp file', {
source: videoInfo.uri, source: videoInfo.uri,
temp: tempFile.uri, temp: tempFile.uri,
@@ -50,7 +50,7 @@ export async function saveMediaToLocal(
try { try {
const sourceFile = new File(normalizedSource) const sourceFile = new File(normalizedSource)
sourceFile.copy(destFile) await sourceFile.copy(destFile)
// Update cache after successful save // Update cache after successful save
mediaExistsCache.set(localRefPath, true) mediaExistsCache.set(localRefPath, true)
} catch (error) { } catch (error) {
@@ -1,5 +1,5 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library/legacy'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
+2 -2
View File
@@ -369,7 +369,7 @@ export function TabBar({
</Animated.View> </Animated.View>
</ScrollView> </ScrollView>
</BlockDrawerGesture> </BlockDrawerGesture>
<View style={[t.atoms.border_contrast_low, styles.outerBottomBorder]} /> <View style={[t.atoms.bg_contrast_100, styles.outerBottomBorder]} />
</View> </View>
) )
} }
@@ -470,6 +470,6 @@ const styles = StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
top: '100%', top: '100%',
borderBottomWidth: StyleSheet.hairlineWidth, height: StyleSheet.hairlineWidth,
}, },
}) })
+15
View File
@@ -12,6 +12,21 @@
*/ */
"expo-file-system/legacy": [ "expo-file-system/legacy": [
"./node_modules/expo-file-system/build/legacy/index.d.ts" "./node_modules/expo-file-system/build/legacy/index.d.ts"
],
/*
* Mirrors the root tsconfig mapping (paths does not merge across
* extends): expo-file-system 57's `exports` map blocks the deep type
* imports in src/platform/misc.web-check.d.ts.
*/
"expo-file-system/build/*": ["./node_modules/expo-file-system/build/*"],
/*
* The react-native exports condition resolves to the package's raw
* TypeScript source, whose findNodeHandle usage breaks under the web
* pass. Point it at the compiled declarations, where skipLibCheck
* applies.
*/
"react-native-view-shot": [
"./node_modules/react-native-view-shot/lib/index.d.ts"
] ]
} }
}, },
+6
View File
@@ -9,6 +9,12 @@
"paths": { "paths": {
"#/*": ["./src/*"], "#/*": ["./src/*"],
"crypto": ["./src/platform/crypto.ts"], "crypto": ["./src/platform/crypto.ts"],
/*
* expo-file-system 57 ships an `exports` map without `./build/*`, which
* blocks the deep type imports in src/platform/misc.web-check.d.ts
* under `moduleResolution: "bundler"`. Type-check-only escape hatch.
*/
"expo-file-system/build/*": ["./node_modules/expo-file-system/build/*"],
}, },
"plugins": [ "plugins": [
{ {
+20
View File
@@ -0,0 +1,20 @@
/*
* Replacement for react-native-reanimated's js-reanimated/webUtils.web.js,
* wired up via a webpack alias in webpack.config.js.
*
* The original file declares ES module exports but populates them with bare
* CommonJS `require()` calls wrapped in try/catch. Webpack parses the file as
* ESM and therefore leaves those `require`s untouched, so in the browser they
* throw ReferenceError, the try/catch swallows it, and createReactDOMStyle &
* co. stay undefined. That silently disables reanimated's DOM update path and
* _updatePropsJS later crashes with "Cannot convert undefined or null to
* object" on Object.keys(component.props). Importing the same react-native-web
* internals statically fixes the resolution.
*/
import createReactDOMStyle from 'react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle'
import {
createTextShadowValue,
createTransformValue,
} from 'react-native-web/dist/exports/StyleSheet/preprocess'
export {createReactDOMStyle, createTextShadowValue, createTransformValue}
+37
View File
@@ -1,6 +1,7 @@
const path = require('path') const path = require('path')
const createExpoWebpackConfigAsync = require('@expo/webpack-config') const createExpoWebpackConfigAsync = require('@expo/webpack-config')
const webpack = require('webpack')
const {withAlias} = require('@expo/webpack-config/addons') const {withAlias} = require('@expo/webpack-config/addons')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer') const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
@@ -65,6 +66,13 @@ module.exports = async function (env, argv) {
'react-native-webview': 'react-native-web-webview', 'react-native-webview': 'react-native-web-webview',
'react-native-gesture-handler': false, // RNGH should not be used on web, so let's cause a build error if it sneaks in 'react-native-gesture-handler': false, // RNGH should not be used on web, so let's cause a build error if it sneaks in
'@sentry-internal/replay': false, // not used, ~300kb of dead weight '@sentry-internal/replay': false, // not used, ~300kb of dead weight
/*
* @sentry/react-native's tracing integration probes for expo-router via a
* try/catch require(). We don't use expo-router, so the module can't
* resolve and webpack warns on every build. Stubbing it to an empty
* module makes the probe return null (`mod?.store ?? null`) silently.
*/
'expo-router/build/global-state/router-store': false,
/* /*
* react-native-svg's fetchData util imports the ~55KB `buffer` polyfill, * react-native-svg's fetchData util imports the ~55KB `buffer` polyfill,
* but is only needed by SvgUri/SvgXml remote loading, which we don't use. * but is only needed by SvgUri/SvgXml remote loading, which we don't use.
@@ -76,8 +84,37 @@ module.exports = async function (env, argv) {
__dirname, __dirname,
'node_modules/react-native-svg/lib/module/utils/fetchData', 'node_modules/react-native-svg/lib/module/utils/fetchData',
)]: false, )]: false,
/*
* reanimated's webUtils.web.js mixes ESM exports with bare CommonJS
* require() calls in try/catch, which webpack leaves untranspiled - they
* throw at runtime and createReactDOMStyle & co. silently stay undefined,
* making _updatePropsJS crash on every animated style update. The shim
* imports the same react-native-web internals statically. See the shim
* file for details.
*/
[path.join(
__dirname,
'node_modules/react-native-reanimated/lib/module/ReanimatedModule/js-reanimated/webUtils',
)]: path.join(__dirname, 'web/reanimatedWebUtilsShim.js'),
}) })
/*
* expo-font's serverContext.web.js imports `node:async_hooks` for SSR-only
* font collection, but webpack can't resolve `node:` URIs for web targets.
* Every call site is guarded by `typeof window === 'undefined'`, so in the
* browser bundle the module is dead code - strip the scheme prefix and stub
* the builtin out with an empty module.
*/
config.plugins.push(
new webpack.NormalModuleReplacementPlugin(
/^node:async_hooks$/,
resource => {
resource.request = 'async_hooks'
},
),
)
config.resolve.fallback = {...config.resolve.fallback, async_hooks: false}
// react-native-uuid ships sourceMappingURL comments but no .map files. // react-native-uuid ships sourceMappingURL comments but no .map files.
patchSourceMapFilter(config.module.rules, /react-native-uuid/) patchSourceMapFilter(config.module.rules, /react-native-uuid/)
config.module.rules = [ config.module.rules = [