Expo 57 (#11334)
Co-authored-by: Tomek Zawadzki <tomekzawadzki98@gmail.com> Co-authored-by: vineyardbovines <spencerfpope@gmail.com>
This commit is contained in:
@@ -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
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
## Expo Image
|
||||
|
||||
Patches in https://github.com/expo/expo/pull/41442
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
index 7db47bdf190b0790c7bf867fbcfeb594005861be..0f868153edd6ec557730531f61dba7bf26a71742 100644
|
||||
index 18a1c56507a1c0da7eb3b6e1f80a1f25a0a8f171..305e10998b99d0c0256930de669e51b5ba4c98c4 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -42,6 +42,7 @@ dependencies {
|
||||
implementation 'com.google.firebase:firebase-messaging:24.0.1'
|
||||
@@ -43,6 +43,7 @@ dependencies {
|
||||
implementation 'com.google.firebase:firebase-messaging:25.0.1'
|
||||
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
+ implementation project(':expo-background-notification-handler')
|
||||
@@ -124,7 +124,7 @@ index 610d3039cefd589647538ad8ba14587d29fab338..3655fc3121ebc0a97820d9653b61a90b
|
||||
builder.setContentText(content.text)
|
||||
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
|
||||
index 90ca4ff35132b33dcccb80b90d68572506fef603..9d4cb09b35844805d543acff54772380c732c02c 100644
|
||||
index eecdae82e99d2997687e3f3e199c94c3aeffbfe0..216891213fb2eac5a9a382b4f075eadccdcfeb5a 100644
|
||||
--- a/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
|
||||
@@ -137,16 +137,16 @@ index 90ca4ff35132b33dcccb80b90d68572506fef603..9d4cb09b35844805d543acff54772380
|
||||
import expo.modules.interfaces.taskManager.TaskServiceProviderHelper
|
||||
import expo.modules.notifications.notifications.RemoteMessageSerializer
|
||||
import expo.modules.notifications.notifications.background.BackgroundRemoteNotificationTaskConsumer
|
||||
@@ -18,7 +21,7 @@ import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
|
||||
import java.lang.ref.WeakReference
|
||||
@@ -17,7 +20,7 @@ import expo.modules.notifications.service.interfaces.FirebaseMessagingDelegate
|
||||
import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
|
||||
import java.util.*
|
||||
|
||||
-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
|
||||
+open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface{
|
||||
companion object {
|
||||
// Unfortunately we cannot save state between instances of a service other way
|
||||
// than by static properties. Fortunately, using weak references we can
|
||||
@@ -105,8 +108,19 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
|
||||
// than by static properties.
|
||||
@@ -109,8 +112,19 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
|
||||
DebugLogging.logRemoteMessage("FirebaseMessagingDelegate.onMessageReceived: message", remoteMessage)
|
||||
val notification = createNotification(remoteMessage)
|
||||
DebugLogging.logNotification("FirebaseMessagingDelegate.onMessageReceived: notification", notification)
|
||||
@@ -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',
|
||||
@@ -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).
|
||||
+17
-234
@@ -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
|
||||
index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea41a50585 100644
|
||||
index 8603591..20d042b 100644
|
||||
--- a/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 std::shared_ptr<const ContextContainer> &contextContainer,
|
||||
jsi::Runtime &uiRuntime,
|
||||
@@ -168,7 +16,7 @@ index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea
|
||||
const std::shared_ptr<facebook::react::CallInvoker> &jsInvoker
|
||||
#endif
|
||||
)
|
||||
@@ -69,11 +69,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
|
||||
@@ -74,11 +74,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
|
||||
contextContainer_(contextContainer),
|
||||
componentDescriptorRegistry_(componentDescriptorRegistry),
|
||||
uiRuntime_(uiRuntime),
|
||||
@@ -182,7 +30,7 @@ index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea
|
||||
jsInvoker_(jsInvoker)
|
||||
#endif
|
||||
{
|
||||
@@ -93,10 +93,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
|
||||
@@ -98,10 +98,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
|
||||
SharedComponentDescriptorRegistry componentDescriptorRegistry_;
|
||||
jsi::Runtime &uiRuntime_;
|
||||
const std::shared_ptr<UIScheduler> uiScheduler_;
|
||||
@@ -195,10 +43,10 @@ index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea
|
||||
|
||||
void restoreOpacityInCaseOfFlakyEnteringAnimation(SurfaceId surfaceId) const;
|
||||
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
|
||||
+++ 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 std::shared_ptr<const ContextContainer> &contextContainer,
|
||||
jsi::Runtime &uiRuntime,
|
||||
@@ -212,7 +60,7 @@ index eca44e4cf651d16e9741806004ce9119c85d07d6..e39c79a5d7b52659106ed6fe6fbcbbc0
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker
|
||||
#endif
|
||||
)
|
||||
@@ -79,11 +79,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
|
||||
@@ -80,11 +80,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
|
||||
componentDescriptorRegistry,
|
||||
contextContainer,
|
||||
uiRuntime,
|
||||
@@ -227,18 +75,18 @@ index eca44e4cf651d16e9741806004ce9119c85d07d6..e39c79a5d7b52659106ed6fe6fbcbbc0
|
||||
#endif
|
||||
),
|
||||
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
|
||||
+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <reanimated/NativeModules/ReanimatedModuleProxy.h>
|
||||
@@ -1,6 +1,7 @@
|
||||
#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/ShadowViewMutation.h>
|
||||
|
||||
#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);
|
||||
|
||||
@@ -278,7 +126,7 @@ index 83ef7430b923b6b3b99368ee8072168769110cd0..2affd12822ab19bdc90963d3ce8ca1e6
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -318,7 +166,7 @@ index 83ef7430b923b6b3b99368ee8072168769110cd0..2affd12822ab19bdc90963d3ce8ca1e6
|
||||
|
||||
} // namespace reanimated
|
||||
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
|
||||
+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h
|
||||
@@ -3,8 +3,8 @@
|
||||
@@ -376,7 +224,7 @@ index e9a5e9959e89ec33cee179ddb907c17f6dfbd3de..a2c89041518cd71e8ba5ac62ef89c002
|
||||
}
|
||||
|
||||
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,
|
||||
ShadowViewMutationList mutations) const override;
|
||||
|
||||
@@ -404,43 +252,10 @@ index e9a5e9959e89ec33cee179ddb907c17f6dfbd3de..a2c89041518cd71e8ba5ac62ef89c002
|
||||
|
||||
} // namespace reanimated
|
||||
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
|
||||
+++ b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
|
||||
@@ -524,15 +524,13 @@ jsi::Value ReanimatedModuleProxy::getSettledUpdates(jsi::Runtime &rt) {
|
||||
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() {
|
||||
@@ -1235,22 +1235,19 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() {
|
||||
#endif
|
||||
layoutAnimationsProxy_ = std::move(layoutAnimationsProxyExperimental);
|
||||
} 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.
|
||||
+7
-7
@@ -1,28 +1,28 @@
|
||||
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
|
||||
+++ 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.core.view.children
|
||||
import androidx.fragment.app.Fragment
|
||||
+import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.facebook.react.bridge.GuardedRunnable
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
- if (childView is ViewGroup) {
|
||||
+ if (childView is ViewGroup && childView !is RecyclerView) {
|
||||
endTransitionRecursive(childView)
|
||||
}
|
||||
}
|
||||
@@ -666,7 +667,10 @@ class Screen(
|
||||
@@ -491,7 +492,10 @@ class Screen(
|
||||
startTransitionRecursive(child.toolbar)
|
||||
}
|
||||
|
||||
|
||||
- if (child is ViewGroup) {
|
||||
+ // Transition a RecyclerView as one unit. Marking its recyclable children as
|
||||
+ // transitioning keeps their parent set after removal, so RecyclerView crashes
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+8
-6
@@ -1,16 +1,16 @@
|
||||
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
|
||||
+++ 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 { mockedRequestAnimationFrame } from './runLoop/uiRuntime/mockedRequestAnimationFrame';
|
||||
-import { mockedRequestAnimationFrame } from "./runLoop/uiRuntime/mockedRequestAnimationFrame.js";
|
||||
export function scheduleOnUI(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);
|
||||
}
|
||||
export function runOnUIAsync(worklet, ...args) {
|
||||
@@ -73,6 +73,8 @@ index dd3a7f1ab12e5a8030af7f17b6a7a891e0b645d2..7d48ccbf95f00724db6b083a7155f25a
|
||||
});
|
||||
}
|
||||
-const requestAnimationFrameImpl = !globalThis.requestAnimationFrame ? mockedRequestAnimationFrame : globalThis.requestAnimationFrame;
|
||||
-//# sourceMappingURL=threads.js.map
|
||||
\ No newline at end of file
|
||||
+function drainUIQueue(queue) {
|
||||
+ while (queue.length > offset) {
|
||||
+ const [workletFunction, workletArgs, jobResolve] = queue[offset];
|
||||
@@ -83,4 +85,4 @@ index dd3a7f1ab12e5a8030af7f17b6a7a891e0b645d2..7d48ccbf95f00724db6b083a7155f25a
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
//# sourceMappingURL=threads.js.map
|
||||
+//# sourceMappingURL=threads.js.map
|
||||
+1
-1
@@ -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
|
||||
("fix(Worklets): web scheduleOnUI implementation on errors").
|
||||
@@ -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
|
||||
@@ -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
|
||||
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
|
||||
and #53231).
|
||||
Both bugs share one root cause, established by instrumented frame-logging runs on the iOS 26
|
||||
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
|
||||
`_props` as the new props too, so the diff is a no-op and `tintColor`/`progressViewOffset`/`title` are never
|
||||
applied on mount. This hides the pull-to-refresh spinner behind the floating home header (it stays at offset
|
||||
0 instead of `headerOffset`). We diff against the `oldProps` argument instead, null-guarded with default
|
||||
props for the create-mutation path.
|
||||
- `scrollView.refreshControl` assignment inserts the control and creates its content view
|
||||
**synchronously** on iOS 26 (the "UIKit inserts lazily on a later layout pass" folklore is
|
||||
false there).
|
||||
- The content view can also be materialized **earlier** by a pre-attach property write (observed
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
Issue: https://github.com/react/react-native/issues/53450#issuecomment-3298157830
|
||||
Reference in New Issue
Block a user