Fix uncontrolled multiline TextInput not resizing when children change#56291
Open
janicduplessis wants to merge 1 commit into
Open
Fix uncontrolled multiline TextInput not resizing when children change#56291janicduplessis wants to merge 1 commit into
janicduplessis wants to merge 1 commit into
Conversation
4bbb042 to
f800915
Compare
## Summary When a multiline TextInput uses children (attributed text) instead of the `value` prop (uncontrolled mode), changing the children does not cause the TextInput to resize. For example, clearing children after sending a message leaves the input at its expanded multi-line height. ## Root Cause `measureContent()` is called by Yoga during `YGNodeCalculateLayout`, which runs BEFORE `updateStateIfNeeded()` in the `layout()` callback. In `attributedStringBoxToMeasure()`, when the state is meaningful and the `attributedStringBox` is non-empty, it returns the stale native text for measurement — even though the React tree (children) has already changed. The sequence: 1. User types multiline text → native `_updateState()` updates `attributedStringBox` with the typed text 2. User clears text (e.g. sends message) → React children become empty 3. Yoga calls `measureContent()` → `attributedStringBoxToMeasure()` returns the stale multi-line text → height stays expanded 4. `updateStateIfNeeded()` runs later and updates state, but the layout has already been computed with the wrong size ## Fix In `attributedStringBoxToMeasure()`, check if the React tree attributed string has diverged from what's stored in state. If so, use the React tree version for measurement instead of the stale `attributedStringBox`. This ensures Yoga measures with the correct content when children change between layout passes. ## Changelog [iOS][Fixed] - Fix uncontrolled multiline TextInput not resizing when children change Fixes facebook#54570
f800915 to
adb184c
Compare
6 tasks
janicduplessis
added a commit
to tloncorp/tlon-apps
that referenced
this pull request
Mar 31, 2026
In Fabric, measureContent() is called by Yoga during YGNodeCalculateLayout before updateStateIfNeeded() runs. For uncontrolled TextInputs (no value prop), attributedStringBoxToMeasure() returns the stale native text from attributedStringBox even when React children have changed, causing the TextInput to not resize (e.g. staying expanded after clearing text). The patch checks if the React tree attributed string has diverged from state for uncontrolled inputs and uses it for measurement instead. Upstream PR: react/react-native#56291 Fixes TLON-5492
|
Can confirm the fix is working for us on |
|
Warning JavaScript API change detected This PR commits an update to
This change was flagged as: |
|
LGTM |
dnbrwstr
added a commit
to tloncorp/tlon-apps
that referenced
this pull request
May 6, 2026
* Update Node version to 22.22.0 * Upgrade to Expo 54, React Native 0.81, and React 19 Major version upgrades: - Expo 52 → 54 - React Native 0.73 → 0.81 - React 18 → 19 - React Navigation 6 → 7 - PostHog and other dependencies updated for compatibility * iOS: Migrate AppDelegate from Objective-C to Swift - Delete AppDelegate.h and AppDelegate.mm - Add AppDelegate.swift with equivalent functionality - Remove main.m (Swift uses @main attribute) - Update Xcode project configuration * Android: Migrate MainActivity and MainApplication from Java to Kotlin - Convert MainActivity.java to MainActivity.kt - Convert MainApplication.java to MainApplication.kt - Preserve all functionality including window insets handling * Update build configuration for Expo 54 Mobile: - Update app.config.ts for Expo 54 plugins - Update babel.config.js for new architecture - Update metro.config.js with new resolver config - Update Android gradle and build configuration - Update iOS Info.plist and bridging headers Web: - Update Vite config for React Native Worklets * Mobile: Rename index.js to index.tsx Expo 54 supports TypeScript entry files * Move Cosmos config to mobile app directory - Move cosmos.imports.ts from root to apps/tlon-mobile/ - Add cosmos.config.json to tlon-mobile app - Remove root cosmos.imports.ts * Update code for React 19 and React Navigation v7 compatibility - Refactor navigation logging to work with React Navigation v7 - Remove unnecessary React imports (React 19 auto-imports) - Update TypeScript config for React 19 - Fix navigation context usage * Update PostHog integration for Expo 54 - Refactor PostHog initialization to synchronous pattern - Update telemetry provider for new PostHog API - Fix type compatibility with new PostHog version * Fix type errors and update API usage for Expo 54 - Update Cosmos exports for ES module compatibility - Fix TypeScript errors in signup context and API calls - Update contacts API for new Expo Contacts module - Fix attestation domain types * Remove Expo 52 patch and update .gitignore - Remove expo@52.0.47.patch (no longer needed in Expo 54) - Add .expo directory to .gitignore * Update lockfiles for Expo 54 upgrade - Update pnpm-lock.yaml with new dependency versions - Update iOS Podfile.lock - Update Android gradle.lockfile * Update Tamagui sheet patch to version 1.126.18 - Rename patch from @tamagui__sheet@1.126.12 to @tamagui__sheet@1.126.18 - Update pnpm.patchedDependencies reference - Update lockfiles with new dependency resolutions - Minor dependency version bumps from pnpm install * Mobile: Update iOS bundle generation to use Expo CLI - Replace 'react-native bundle' with 'expo export:embed' - Remove outdated entry-file path and dev flag - Expo CLI automatically handles entry point detection * Migrate from @react-native-clipboard/clipboard to expo-clipboard - Replace all imports with expo-clipboard - Update Clipboard.setString() to Clipboard.setStringAsync() - Update Clipboard.getString() to Clipboard.getStringAsync() - Update Clipboard.hasImage() to Clipboard.hasImageAsync() - Update Clipboard.getImagePNG/JPG() to Clipboard.getImageAsync({ format }) - Update test mocks to use expo-clipboard mock - Make callbacks async where needed for await usage * Revert Tamagui to 1.126.12 with strict version locking - Revert @tamagui/sheet patch from 1.126.18 to 1.126.12 - Lock all Tamagui dependencies to exact 1.126.12 (no range) - Update all package.json files to use strict version (removed ~) - Regenerate lockfiles with clean install - All Tamagui packages now on exact 1.126.12 * Revert Tamagui to ~1.126.12 range and add react-native-picker - Change Tamagui versions from exact 1.126.12 to ~1.126.12 range - @tamagui/react-native-media-driver - @tamagui/babel-plugin - @tamagui/vite-plugin - Add @react-native-picker/picker@^2.11.4 as explicit dependency - Was peer dependency of react-native-phone-input - Now explicitly managed for Expo 54 compatibility - Update Android build scripts: productionDebugOptimized → productionDebug - Regenerate lockfiles * Fix android:preview script to use previewDebug variant * Fix PostHog provider and clean up formatting - TelemetryProvider: Remove null check, use disabled option for tests - AppInfoScreen: Format upload logs button - tsconfig: Remove expo/tsconfig.base extend, add JSDoc comment * Migrate expo-background-task patch from 0.1.4 to 1.0.10 Changes applied: - Android: Set DEFAULT_INTERVAL_MINUTES to 20 minutes (was 24 hours) - iOS: Set intervalSeconds to 15 minutes (was 12 hours) - iOS: Change from BGProcessingTaskRequest to BGAppRefreshTaskRequest - iOS: Change background mode check from 'processing' to 'fetch' - iOS: Set earliestBeginDate to nil for immediate scheduling - iOS: Add debug print statements for task lifecycle - Remove network/power requirements from iOS task requests Removed obsolete patches: - expo-background-task@0.1.4.patch - @react-navigation__drawer@6.7.2.patch - expo-localization@16.0.1.patch - react-native-reanimated@3.8.1.patch - react-native@0.73.4.patch These were automatically removed as those versions are no longer installed. * Update rube Dockerfile Node.js setup to v22 * Upgrade Tamagui to v2.0.0-rc.0 and fix React 19 compatibility - Upgrade @tamagui/* packages from ~1.126.12 to ~2.0.0-rc.0 - Remove @tamagui/sheet patch (no longer needed in v2) Tamagui v2 API changes: - Replace Stack with View/YStack (Stack removed in v2) - Replace animation prop with transition on animated components - Replace tag prop with render (renamed in v2) - Replace editable prop with readOnly on TextArea/Input - Replace onHoverIn/onHoverOut with onMouseEnter/onMouseLeave - Remove textWrap/wordWrap non-existent props - Remove fontWeight from Button (stack-based, not text) - Fix TransitionProp by adding medium/slow animation keys to config React 19 type compatibility: - JSX.Element → ReactNode/ReactElement for children props - RefObject<T> → RefObject<T | null> for ref nullability - Fix generic types for forwardRef components React Native API updates: - BackHandler.addEventListener returns NativeEventSubscription - headerBackTitleVisible → headerBackButtonDisplayMode: 'minimal' - expo-contacts types: Contact → ExistingContact Component fixes: - Update ButtonContext/FloatingActionButton to use useStyledContext() - Fix Pressable navigation props (href/action typing) - Fix OverflowTriggerButton forwardRef generic type - Cast web-only outlineStyle in BareChatInput - Provide defaultTheme fallback in BaseProviderStack - Remove duplicate clipboard image format attempt - Update react-native-country-codes-picker patch for JSX.Element - Add fontFamily to ListItemTitle for consistency - Add position="relative" for absolute positioning contexts * Add customizable hoverStyle prop to ChannelListItem and GroupListItem Allow overriding the default hover background color by accepting a hoverStyle prop, defaulting to the original behavior if not provided. * Fix React 19 and React Native compatibility issues - Fix useRef initialization to explicitly pass undefined - Update BackHandler event listener cleanup to use new API - Fix ts-expect-error placement in PhoneNumberInput - Add type cast for RawBottomSheetTextInput ref * Fix setTimeout type conflict in run-selected-tests.ts Replace NodeJS.Timeout type with ReturnType<typeof setTimeout> to resolve compilation error when DOM types are present in tsconfig. This makes the type declaration work correctly in both DOM and Node.js environments. * Fix production build: add vite preview proxy and expo polyfill - Add preview.proxy to vite config so vite preview proxies API requests to the Urbit ship (the urbit plugin only sets server.proxy for dev) - Add resolveId hook to reactNativeWebPlugin to prefer .web.ts index files for directory imports in node_modules (fixes Rollup resolving expo-modules-core polyfill/index.ts noop instead of index.web.ts) - Add explicit expo-polyfill.ts imported first in main.tsx to ensure globalThis.expo is set up before any expo modules load * Fix Tamagui v2 dialog rendering and env var issues on web - Add envPrefix ['VITE_', 'TAMAGUI_'] to vite config to prevent Tamagui plugin from overriding Vite's default VITE_ prefix - Fix ActionSheet dialog: add disableRemoveScroll to prevent z-index stacking issues, change ScrollView flex to flexShrink to fix 0-height content rendering - Patch @tamagui/dialog to remove render: 'dialog' on DialogPortalFrame which causes stacking context issues with native <dialog> element - Fix GroupTypeCard text overlap by disabling text trimming margins * Fix dialog overlay blocking pointer events in all Dialog usages The previous fix only addressed ActionSheet, but ConfirmDialog (used for delete group confirmation) had the same issue. Add pointerEvents="none" to Dialog.Overlay in both ActionSheet and ConfirmDialog so overlays never intercept clicks, regardless of whether the pnpm patch is applied. Also make e2e test cleanup more robust by pressing Escape to dismiss any lingering dialogs before attempting to interact with background elements. * Bump Node.js version to 22 in EAS build config * Bump Node.js version to 22.22.0 in EAS build config Fixes any-ascii ESM crash during Tamagui static extraction on EAS. Node 22.12.0 had buggy require(esm) interop that caused uncatchable errors with esbuild-register. * Bring back legacy styling for tamagui * Fix React Navigation v7 navigate not popping back to existing screens React Nav v7 changed navigate() to push new screens instead of popping back to existing ones in a stack. This caused duplicate ChannelRoot screens (and 2 MessageInput textareas) when navigating back to a channel from GroupSettings via the sidebar. - Add pop: true to getDesktopChannelRoute nested params so navigate() pops back to existing ChannelRoot instead of pushing a new one - Add pop: true to useNavigateBackFromPost desktop path - Fix navigateToGroupSettings to navigate directly to Channel > GroupSettings in a single call instead of the broken 2-step approach (navigateToGroup + setTimeout with stale navigation ref) * Fix React Navigation v7 nested navigator stale screens and other upgrade issues React Navigation v7 changed how nested navigator state is handled: in v6, navigating with `params: { screen, params }` would reset the nested navigator state. In v7, it dispatches `CommonActions.navigate()` which pushes onto the existing stack, causing stale screens to accumulate. The fix uses `params: { state: { routes: [...], index: 0 } }` which triggers `CommonActions.reset()` to fully replace the nested state, matching v6 behavior. This is applied to all GroupSettings stack navigations. Also adds `pop: true` to navigate calls that should pop back to existing screens (restoring v6 popTo behavior), and fixes several other issues from the Expo 54 / RN v7 upgrade: - Port @tamagui/sheet patch to v2.0.0-rc.0 - Move ForwardPostSheetProvider inside NavigationContainer - Fix ActionSheet Popover z-index behind modals - Add navigation state debug logging - Refactor e2e tests to use navigateBack helper * Fix navigateBack e2e helper for NativeStack on web NativeStack on web renders all stacked screens in the DOM, creating many HeaderBackButton elements. The old helper only checked indices [2, 1, 0] which missed the correct button when 5+ screens were stacked. Now dynamically counts all back buttons and clicks the last visible one (the topmost screen). Also fix roles-management test back-navigation loops to stop once they reach the group channels view instead of blindly navigating back a fixed number of times. * Fix useMarkdownMode editorRef type for React 19 useRef nullability * Cleanups * Clean up navigation logger to match base branch logging style * Restore and update gradle.lockfile for Expo 54 deps * Update Tamagui to v2.0.0-rc.22 and fix breaking changes - Sheet `animation` prop renamed to `transition` - Remove `estimatedItemSize` (FlashList 2.0 auto-calculates) - Remove unused `editorIsFocused` prop from DetailView - Fix expo-sensors version for Expo 54 - Update sheet patch for new version * update podfile * cleanup * Revert some changes * fix ts * fix: replace barrel imports with direct paths to break circular deps Change packages/api and packages/shared db files to import from specific module paths instead of barrel re-exports to avoid circular dependency issues with Metro/Vite. * fix: revert scheme LaunchAction to Debug build configuration * fix: pin expo-audio to ~1.1.1 and remove stale patches Add expo-audio override to prevent pnpm from resolving the `*` peer dep in packages/app to v55, which passes 4 constructor args to the v1.1.1 native module that only expects 3. Also remove the now-unnecessary expo-audio JS patch and update expo-task-manager patch for v14. * fix: update lockfiles and dependencies for Expo 54 - Add expo-audio, expo-speech-transcriber, expo-video deps to mobile - Update gradle and Podfile lockfiles - Remove unused Stack import in MediaViewerScreen - Regenerate cosmos imports * fix: upgrade TypeScript to ~5.9.2 and fix type errors Upgrade TS to match Expo SDK 54 template, fixing expo-file-system Uint8Array generic errors. Also fix: - StackProps -> ViewProps in Pressable (removed in Tamagui v2) - ResultState cast in navigation intent - client -> posthog in posthog.ts - Canvas onLayout wrapped in View in Waveform - OverflowTriggerButton explicit type annotation - NodeJS.Timeout -> ReturnType<typeof setTimeout> for TS 5.9 * fixes * fix: revert unnecessary circular dep import changes in packages/api These direct-path imports are no longer needed and were causing lint issues after rebasing on develop. * Fix preview app splash * fixes * fix tests * fix: use pnpm rebuild in test scripts and update expo-file-system mock Replace npm rebuild with pnpm rebuild to avoid broken symlink traversal in pnpm's virtual store. Update test mock to use expo-file-system/legacy. * fix: target npm rebuild to better-sqlite3 only in test scripts pnpm rebuild skips better-sqlite3 native addon, and blanket npm rebuild chokes on broken esbuild symlinks in pnpm's virtual store. Targeting just better-sqlite3 avoids both issues. * fix: restore VITE_* env vars blocked by Tamagui vite plugin @tamagui/vite-plugin overrides envPrefix to ["TAMAGUI_"], which blocks all VITE_* env vars from import.meta.env. This caused VITE_DISABLE_SPLASH_MODAL to be ignored, leaving the splash modal overlay blocking all e2e test clicks. * fix(e2e): update roles-management back loop for React Navigation v7 The back loop was using a GroupOptionsSheetTrigger testID that was renamed to GroupChannelsHeaderTrigger. Updated to match develop's HeaderBackButton approach, using .last() instead of .first() since v7 renders all stacked screens in the DOM simultaneously. * fix: remove centering from Modal ZStack that broke long-press message actions The justifyContent="center" and alignItems="center" on the Modal's ZStack was interfering with the ChatMessageActions positioning, causing the menu to not reposition correctly, the blur overlay to not display, and touch events to be blocked. Fixes TLON-5490 * fix: bump react-qr-code to 2.0.18 for React 19 compat v2.0.12 used defaultProps which React 19 no longer supports on function components, causing "bad rs block" crash when rendering the QR code on the personal invite sheet. * fix: remove swipe direction guards that broke swipe actions in RNGH 2.28 In RNGH 2.20, onSwipeableWillOpen reported which side was opening (e.g. 'right' when right actions open). In RNGH 2.28, it reports the gesture direction ('left' when swiping left to reveal right actions). This caused the direction guard to hide the very actions being revealed — the right actions returned <View /> because currentSwipeDirection was 'left'. The guards are unnecessary since ReanimatedSwipeable already handles showing/hiding the correct side internally via opacity animations. Fixes TLON-5491 * fix: patch react-native TextInput measurement for uncontrolled inputs In Fabric, measureContent() is called by Yoga during YGNodeCalculateLayout before updateStateIfNeeded() runs. For uncontrolled TextInputs (no value prop), attributedStringBoxToMeasure() returns the stale native text from attributedStringBox even when React children have changed, causing the TextInput to not resize (e.g. staying expanded after clearing text). The patch checks if the React tree attributed string has diverged from state for uncontrolled inputs and uses it for measurement instead. Upstream PR: react/react-native#56291 Fixes TLON-5492 * fix: ignore bogus keyboard events when app is backgrounded When the app is backgrounded, iOS fires keyboardWillChangeFrame with screenY=0 and height=0. The component interpreted this as "keyboard covers the entire screen" and applied massive paddingBottom, which accumulated on each background/foreground cycle. Fixes TLON-5493 * chore: regenerate .prettierignore * fix: use clone-or-copy pnpm import method in e2e Dockerfile The Tamagui v2 RC packages share esbuild as a dependency, causing EEXIST collisions with --package-import-method=copy. Switch to clone-or-copy which handles existing files gracefully. * style: run prettier on files with formatting issues * fix: remove pnpm store cache mount from e2e Dockerfile The cache mount causes EEXIST hardlink collisions when multiple Tamagui v2 packages share esbuild as a dependency. Without the cache mount, pnpm uses its default import strategy which handles this correctly within a single build layer. * fix: use type-only import for db in modelBuilders to break circular dep modelBuilders only uses db namespace for types (db.Post, db.Channel etc), so import type is correct and breaks the import cycle that lint detected. * fix: add @faker-js/faker devDependency to packages/app The EditProfileScreen fixture in packages/app/fixtures/ imports @faker-js/faker but it wasn't listed as a dependency of packages/app. * fix: resolve lint errors from merge (missing Stack import, unused imports) - AppInfoScreen: use View instead of Stack (already imported) - MediaViewerScreen: remove unused Stack import - MessageActions: remove unused api import * Revert "fix: add @faker-js/faker devDependency to packages/app" This reverts commit fde0b90. * fix: correct import paths for StorageCredentials and StorageService These types are exported from @tloncorp/api/urbit/storage, not @tloncorp/api/client/upload. * fix: resolve tsc errors in GestureMediaViewer and GestureTrigger - useRef<string>() requires initial value in React 19 types - pass undefined - GestureTrigger children type needs WithOnPress constraint to match library * fix: patch @tamagui/web to restore ZStack child wrapping In Tamagui v2, createComponent no longer calls spacedChildren() with isZStack, so children are rendered in normal column flow instead of being wrapped in AbsoluteFill containers. This patch restores the wrapping behavior by checking staticConfig.isZStack and wrapping each child in a position:absolute container, matching v1's behavior. This fixes broken layouts in splash sequence, missing buttons in sheets, gallery images not showing, and other ZStack-dependent UIs. Fixes TLON-5564, TLON-5569, TLON-5574 * Revert "fix: patch @tamagui/web to restore ZStack child wrapping" This reverts commit 070a16a. * Reapply "fix: patch @tamagui/web to restore ZStack child wrapping" This reverts commit eb8e676. * Revert "Reapply "fix: patch @tamagui/web to restore ZStack child wrapping"" This reverts commit a1c75da. * fix: resolve tsc errors in packages/app - MediaViewerScreen: animation -> transition (Tamagui v2 prop rename) - MessageInput: useRef<JSONContent>(undefined) for React 19 compat - PostScreenView: useRef<setTimeout>(undefined) for React 19 compat * fix: use ReturnType<typeof setTimeout> for timer ref type NodeJS.Timeout is not assignable from setTimeout's return type in browser environments. ReturnType<typeof setTimeout> works in both. * fix: force copy import method in e2e Dockerfile pnpm defaults to hardlinks in Docker overlayfs, causing EEXIST when multiple Tamagui packages share esbuild. Force copy mode via config flag. * fix: dedupe esbuild via override to prevent EEXIST in Docker @tamagui/build and esbuild-plugin-es5 both depend on esbuild@0.27.3 but pnpm nested them separately, causing hardlink collisions in Docker. Adding an esbuild override forces a single hoisted copy. Also reverts the copy import-method workaround since the root cause (duplicate esbuild) is now fixed. * fix: add .web.* extensions to Vite resolve config The reactNativeWebPlugin only set resolveExtensions for esbuild (dev mode optimizer), not for Rollup production builds. This caused file imports like `../../transcription` to resolve to the native .ts file instead of the .web.ts stub, pulling in expo-speech-recognition which crashes in the browser. Adding the full extension list to resolve.extensions ensures .web.* files are preferred in both dev and production builds. * fix: fix production build for smoke tests - Add resolve.extensions with .web.* entries so Rollup production builds correctly resolve platform-specific modules (e.g. transcription.web.ts) - Move expo-polyfill import to top of main.tsx entry point - Add preview.proxy config so vite preview proxies API requests to the ship (required for production smoke tests) * style: format main.tsx with prettier * fix: correct preview proxy path prefix for auth and API routes The proxy pattern needs a leading slash to match request paths (e.g. /~/login, /~/scry). Also add /spider/ proxy for thread requests. * fix: fix vite preview proxy and disable tamagui config watcher - Fix proxy path to use /~/ prefix (matches /~/login, /~/scry etc) - Add /apps/groups/~/ proxy with path rewrite for base-prefixed requests - Add /spider/ proxy for thread API requests - Disable tamagui config watcher to prevent hanging on reanimated v4 module resolution errors during vite preview * fix: add /apps/landscape proxy for auth redirect in vite preview After login, the ship redirects to /apps/landscape/ which vite preview doesn't serve. Proxy it to the ship so auth setup completes. * revert: remove preview proxy and disableWatchTamaguiConfig These weren't needed before the Tamagui version change. Keep only the resolve.extensions fix which is genuinely needed for .web.ts resolution. * fix expo 54 regressions (#5711) * fix zstack on native * upgrade cosmos to 7.2.0 * fix tamagui unset handling * fix rebase followups * revert main.tsx reorder * chore: fix prettier formatting * chore: pin web import order * fix prod smoke login path * fix: add /apps/landscape proxy for vite preview The production smoke test uses vite preview which doesn't use the dev server proxy. After login via /apps/groups/~/login, the ship redirects to /apps/landscape/ — vite preview can't serve that path (only serves from /apps/groups/ base). Proxy it to the ship so auth setup completes. * fix: proxy /apps/groups/~/ to ship in vite preview Vite preview serves /apps/groups/~/login as an SPA route (returning index.html) instead of the ship's login page, so auth setup fails. Proxy the /apps/groups/~/ prefix to the ship with path rewrite to strip the /apps/groups base. * fix: proxy bare /~/ paths in vite preview The Urbit login form posts to /~/login (no base prefix), and scries use /~/ paths directly. Without a bare /~/ proxy, vite preview serves its "did you mean /apps/groups/~/login" page and auth fails. * fix: proxy bare / to ship in vite preview for login redirect chain After login, the ship returns 303 with Location: /, and the ship's / handler then redirects authenticated users to /apps/landscape/. Without proxying /, vite preview intercepts and redirects to /apps/groups/, breaking the auth flow. The ^/$ regex proxy matches only the exact root path, leaving /apps/groups/ etc. untouched. * fix: simplify vite preview proxy to match urbit plugin dev server Replace the multiple preview.proxy entries with a single regex that matches everything not under /apps/groups/ — mirroring the regex the urbit plugin uses for server.proxy. This ensures all ship-bound paths (/~/login, /~/channel, /, /apps/landscape, etc) are forwarded and only static assets under the app base are served by vite. Also revert the auth.setup.ts USE_PRODUCTION_BUILD branch from PR #5711 back to hitting /~/login directly — which is now proxied correctly. * fix: proxy /apps/groups/desk.js in vite preview The urbit plugin injects <script src="/apps/groups/desk.js"> into index.html, which needs to be proxied to the ship. Vite preview otherwise returns 404 since desk.js doesn't exist in the build. Mirror the two-entry proxy config that urbitPlugin sets for the dev server's server.proxy. * fix: move expo-polyfill import to the very first line in main.tsx The polyfill needs to run before any expo-modules-core consumer. ES module imports execute their full dependency graph in source order, so putting the polyfill import at the very top ensures its side effect (setting globalThis.expo) runs before any subsequent import's transitive dependencies try to access globalThis.expo. * chore: remove redundant preview.proxy from vite config vite preview actually uses server.proxy from the urbit plugin already, so the explicit preview.proxy I added was a no-op. Verified locally that without preview.proxy: /apps/groups/ → vite, /~/login → proxy, /apps/groups/desk.js → proxy, /apps/landscape/ → proxy, / → proxy. The earlier smoke test failures were from PR #5711's auth.setup.ts using /apps/groups/~/login (which is correctly NOT proxied) plus the expo-polyfill ordering issue. Both are fixed independently. * fix: pop to existing tab screens on NavBar taps * fix: tighten ESLint selector to require pop:true in third argument (suggested by copilot review) * fix: use direct navigate instead of getParent() in GroupSettingsStack (Codex review feedback) * cleanup: remove dead navigateToHome prop from GroupMetaScreen * fix: drop ContentContext subscription on ContentImage (TLON-5570) The Expo 54 upgrade pulled in a newer Tamagui rc whose styled-component machinery installs an RNGH-backed press gesture on any styled view that receives a press-shaped prop (onPress, onLongPress, onPressIn, onPressOut). The gesture participates in Tamagui's shared global press-ownership state: on every touch the innermost gesture overwrites the owner, and only the owner's onPress fires. ContentImage subscribed to ContentContext, which carries onLongPress. Tamagui's context merging applied that field to the styled image as a prop — silently making every rendered image a press boundary. The image has no onPress handler, so on release it stole ownership from the surrounding Pressable and then dropped the tap on the floor. Long-press still worked because the image actually had an onLongPress. Net effect: tapping a post image no longer opened the lightbox. ContentImage doesn't need context: the consumers that care (ImageBlock, LineText, BlockWrapper) read ContentContext via useContentContext() or their own variants. Dropping the subscription keeps the image free of press props and restores tap arbitration. * chore(mobile): bump @shopify/flash-list to 2.3.1 * fix(ios): drop lineHeight on single-line TextInput iOS (new architecture) anchors glyphs to the bottom of the line box when lineHeight > fontSize on a single-line UITextField, pushing typed text below the centered placeholder. $label/xl (fontSize 17, lineHeight 24) trips this on every RawTextInput after the Expo 54 / RN 0.81 upgrade. Single-line inputs sit in a fixed-height centered container, so lineHeight has no effect on vertical placement. Strip it from the base style and restore it on TextInputComponent for multiline cases, where line-to-line spacing still matters. * fix(ios): patch expo-image-picker to download iCloud videos Backport expo/expo#40697 onto expo-image-picker 17.0.10 (SDK 54). Adds a shouldDownloadFromNetwork option that sets PHAssetResourceRequestOptions.isNetworkAccessAllowed = true when streaming a video resource via PHAssetResourceManager. Without it, picking a video that has been offloaded to iCloud fails on iOS 26 with "The operation couldn't be completed. (PHPhotosErrorDomain error 3164)". AttachmentSheet opts in by passing shouldDownloadFromNetwork: true to launchImageLibraryAsync. videoExportPreset stays at its native default (Passthrough), which is what the option requires to take effect. The upstream fix ships in expo-image-picker 55.0.0 (SDK 55); no SDK 54 backport was released, so we carry this as a pnpm patch until we upgrade. * fix(mobile): Poor UX report modal buttons overflow card Tamagui TextArea with multiline expands beyond minHeight to fill available flex space, pushing the button row past the card's visible bottom edge. Set an explicit height and flexShrink: 0 to keep the textarea fixed at 120px. * fix(mobile): use numberOfLines instead of minHeight for Poor UX modal Tamagui v1 appears to not propagate minHeight to the native RN TextInput (or it was clamped by the v1 wrapper's default numberOfLines: 4), so the modal stayed compact and the buttons never overflowed. On v2 (via RN 0.81's rows -> numberOfLines mapping) the input can end up larger than the card expects, pushing the button row outside the modal's rounded bottom. Using numberOfLines={5} gives a deterministic intrinsic height (~120px at lineHeight 24) that renders correctly in both versions and doesn't conflict with the variant-computed height. * Expo 54: fix nav issue in group settings * fix(mobile): normalize path to URI in expo-file-system helpers expo-file-system V2 in Expo 54 requires an absolute URI for `new File(...)` and throws `IllegalArgumentException: URI is not absolute` when given a raw path. On Android the audio recorder returns a raw path, so `getMimeType` threw during voice message send — swallowed by an unhandled async rejection and surfacing only as "button reacts but nothing happens". Normalize path-or-URI inputs inside `getMimeType` and `getFileSize`. * fix(mobile): normalize voice memo path at the call site Localize the path→URI normalization to the voice-memo submit path rather than hardening the shared `getMimeType`/`getFileSize` helpers. The audio recorder is the only source that returns a raw path; other callers already pass URIs. * fix(mobile): render notification bell unread dot as circle on Android Tamagui's `Circle` uses `borderRadius: 100_000_000`, which fails to render as a circle on Android with RN 0.81 / new architecture at small sizes. Replace the unread indicator in `NavIcon` with a plain `View` using explicit width, height, and borderRadius so the dot renders correctly on both platforms. * fix(mobile): force remount of nav unread dot on hasUnreads change The previous change replaced Circle with a View but did not address the actual root cause. On a fresh app launch, the dot still rendered as a square because react/react-native#52415 causes an Android new-arch View to lose its border-radius clipping when its backgroundColor transitions from transparent to opaque. Hot reload hid the bug by re-mounting the view. Restore the Circle and key it on `hasUnreads` so each state change remounts the dot with a fresh, already-opaque background, which renders a circle correctly on both platforms. * fix(mobile): work around RN TextInput transparent color bug on Android The OTP input overlays a hidden TextInput on top of the visual digit cells. RN 0.81 (Expo SDK 54) regressed handling of `color: 'transparent'` on Android, causing the underlying glyphs to paint on top of the cell overlay and appear as duplicated/ghosted digits. Using `#ffffff00` instead of the `'transparent'` keyword avoids the regression until the upstream fix ships. Refs: - react/react-native#53343 - react/react-native#55380 * chore(deps): bump tamagui rc.22 -> rc.41 Drops the two tamagui patches that applied to rc.22: - @tamagui/sheet: the patch supplied no-op defaults for setHasScrollView / scrollBridge.onParentDragging so the SheetHandle effect didn't crash when useSheetContext returned the default object. rc.41 guards the call with `if (!context.scrollBridge) return`, so the patch is no longer needed. - @tamagui/web: the patch injected unset.fontFamily from the default font inside createTamagui. The configIn.unset concept has been removed from @tamagui/web in rc.41, so the patch targets code that no longer exists. Also updates pnpm-lock.yaml to drop the now-unreferenced rc.22 tamagui entries; only rc.41 remains for both the direct and transitive dependencies. * fix group avatar recycling * Fix iOS channel sorting with duplicate nav entries * Fix duplicate channel nav memberships via forced sync * e2e: fix group-channel-management to expect new channels at index 0 (backend prepends to zone.order) * Wrap addChannelToNavSection sync handler in a transaction * fix(bottom-sheet): patch gorhom to avoid flex overflow on first open See patches/README.md. * Add consistent POST_HOG_IN_DEV and default to false - `envVars.native.ts` did not have a binding for POST_HOG_IN_DEV - added that and use it instead of directly accessing the string `process.env.POST_HOG_IN_DEV` - `Boolean(process.env.POST_HOG_IN_DEV)` would evaluate to true with `POST_HOG_IN_DEV=false` - changed to check `=== 'true'` * Fix omitting PostHog key in development mode * fix: restore patches * fix(ios): always forward didReceiveRemoteNotification to ExpoAppDelegate The Swift port of AppDelegate guarded the super call with `super.responds(to: #selector(...))`. That check is unreliable when the superclass is a Swift class — `ExpoAppDelegate` (SDK 54) is declared as a pure Swift `open class` and its UIApplicationDelegate methods are not guaranteed to be exposed to the Obj-C runtime via the selector lookup that `responds(to:)` performs. When the check returns false we silently call `completionHandler(.noData)` and drop the notification before expo-notifications, react-native-firebase, etc. can see it. `ExpoAppDelegate` does implement application(_:didReceiveRemoteNotification:fetchCompletionHandler:) (node_modules/expo/ios/AppDelegates/ExpoAppDelegate.swift), so calling super unconditionally is safe and restores the dispatch chain. This mainly affects silent `content-available` pushes — the notify-provider uses these to wake the Notification Service Extension to fetch and render rich content. * fix(reactions): make reaction click work with Tamagui rc.41 Tamagui v2 rc.41 added e.stopPropagation() in createComponent's onPress handler when the View has any onPress/onClick. The reaction button wrapped its emoji content in <Pressable onPress={toggleReaction}> > <Tooltip.Trigger>, where Tooltip.Trigger renders its own View with onPress. The inner Trigger's stopPropagation blocked clicks from reaching the outer Pressable, so clicking a reaction never toggled it on web. Move onPress/onLongPress directly to Tooltip.Trigger (which uses composeEventHandlers, and Tooltip's onOpenToggle is a no-op so click doesn't open the tooltip). Drop the now-redundant outer Pressable wrapper. * style: prettier fix on ReactionsDisplay * Forward hoverStyle to Pressable instead of ListStyle/XStack * test: add tamagui-ignore to ManageChannelsShared Tamagui v2 rc.41 babel plugin emits duplicate testID attributes when the testID is a template literal — and on web the second one is left as raw testID rather than being converted to data-testid, so the rendered DOM has neither. Disabling Tamagui compile-time optimization for this file restores standard testID->data-testid handling via react-native-web at runtime, which the e2e tests rely on. Channel management e2e tests (group-channel-management, group-cross-ship- visibility) currently can't find ChannelItem-* testIDs in the rendered DOM as a result. * fix(reactions): drop onLongPress from Tooltip.Trigger to avoid spurious sheet Tamagui v2's web onPress wrapper unconditionally calls onLongPress on every click (createComponent.mjs:762-768 — `onPress?.(e); onLongPress?.(e);` with no gating). Putting onLongPress on Tooltip.Trigger therefore opened the ViewReactionsSheet on every reaction toggle, causing a stray <span>👍</span> in the sheet's tab/list to live alongside the reaction in the DOM and break strict-mode locator queries (e2e: chat-core-functionality removeReaction). The original code didn't hit this because @tloncorp/ui's Pressable strips onLongPress on web (Pressable.tsx: `const longPressHandler = isWeb ? undefined : onLongPress;`). Tooltip.Trigger renders a tamagui View directly, with no such filtering. Drop onLongPress from Tooltip.Trigger; the outer per-row Pressable still handles long-press on native, and long-press is irrelevant on web anyway. * patch(@tamagui/static): drop spurious style key after createDOMProps Tamagui v2 rc.41's createExtractor passes single non-style props (like `testID`) through `react-native-web-internals.createDOMProps` to get the testID -> data-testid rewrite, but createDOMProps unconditionally sets `domProps.style = stylesAtomic.reduce(...)` (i.e., always emits a `style` key, even an empty {} when no style was provided). The extractor only strips `out.className`, leaving `out.style` behind. Downstream `Object.keys(out).map(...)` then iterates two keys (`style` and `data-testid`/the original prop) instead of one. Both branches return the same original `attr` reference, so the same JSXAttribute ends up duplicated in the emitted JSX. For static-string testIDs the duplicate is harmless (both get rewritten to `data-testid`); for template-literal testIDs the rewrite is skipped and the DOM ends up with `<div testID="..." testID="...">`, which React drops entirely — leaving no `data-testid` and breaking Playwright's getByTestId. Drop the spurious `style` key after `createDOMProps` in both code paths (the single-prop path at line 855 and the variant-expansion path at line 1485). With this patch, the channel-management e2e tests no longer need the `// tamagui-ignore` workaround on ManageChannelsShared. * fix unread dot shape (#5816) * patch(@tamagui/static): rewrite renamed prop name when value is dynamic The previous patch dropped the spurious `style` key from createDOMProps so the same `attr` reference wasn't emitted twice for a single non-style prop, fixing the duplicate-attribute case. But static-string testIDs hid a second bug: when createDOMProps renames a prop (e.g. testID -> data-testid), the extractor only relied on a later `case "attr"` pass that runs `getProps` on the value to discover the new key — and that pass is gated on `attemptEvalSafe(value) !== FAILED_EVAL`, so dynamic values stayed as raw `testID={template}` in the emitted DOM. React then drops the unknown HTML attribute, so `getByTestId` (and any other downstream consumer) never finds it. Now: in the data/aria/HTML-attribute branch of `Object.keys(out).map`, when the createDOMProps output key differs from the original JSX prop name, build a fresh JSXAttribute with the new name and the original expression value. This handles both static and dynamic values uniformly and unblocks template-literal `testID`s without falling back to `// tamagui-ignore`. * patch(@tamagui/static): scope rename to dynamic values only Previous patch revision renamed unconditionally when createDOMProps produced a different output key. That caught testID -> data-testid for both static and dynamic values, but it also rewrote `focusable={true}` to `tabIndex` for static values — which v2 specifically chose NOT to do (see tamagui's webAlignment "focusable is NOT converted to tabIndex in v2" test). Restrict the inline rename to FAILED_EVAL (dynamic) values. Static values continue to flow through the later `case "attr"` rename pass, which uses getProps() and respects v2's hand-picked keep-original-name behavior for props like focusable. * restore preview share extension target (#5818) * fix(posthog): gate side effects on posthogEnabled, not posthog truthiness The merge from develop combined two refactors: the singleton extraction (posthog kept as PostHog | undefined) and the always-defined client with disabled flag. The resulting if (posthog) check is now always truthy, so side effects intended to be skipped when PostHog is disabled - including writing the API key to UserDefaults so iOS native captures fire - were running in test and dev-without-POST_HOG_IN_DEV. Switch the gate to posthogEnabled and drop the unreachable !posthog early-returns now that posthog is always defined. * chore(flash-list): remove dead size measurement helpers FlashList v2's overrideItemLayout type is { span?: number } - the size field present in v1 was dropped. With estimatedItemSize gone too, the sizeRefs / handleHeaderLayout / handleItemLayout / handleOverrideLayout machinery in these four lists no longer affects rendering: the sizes are written but never read by the layout manager. Drop the dead refs, callbacks, onLayout wiring on rendered items, and the overrideItemLayout prop. ForwardChannelSelector keeps ITEM_H since drawDistance still uses it. * chore(vite): drop duplicate resolve.extensions in favor of plugin-supplied reactNativeWebPlugin already declares the same extensions array via its config() hook. A local prod build confirms transcription.web.ts is preferred over transcription.ts (no expo-speech-recognition in the output bundle), so the explicit declaration in vite.config was redundant. Single source of truth in the plugin. If CI smoke tests show platform-specific files no longer resolve in prod, revert to the inverse - extensions only in vite.config and dropped from the plugin. * chore(vite): drop dead resolveId hook from reactNativeWebPlugin A diff of prod bundle hashes with vs without this hook is byte-identical on the current branch. Tracing it with logs showed 8 hits during build (react-native-reanimated's ./ReanimatedModule, expo-modules-core's ./polyfill and ./uuid), but the resolutions don't change the output - Rollup tree-shakes the resolved exports either way because nothing in the app consumes them directly. The polyfill case is handled by the explicit src/expo-polyfill.ts import; file-level platform resolution (transcription.web.ts etc.) is handled by resolve.extensions. If CI smoke tests reveal a path I didn't exercise (dev HMR or other modes), revert is one commit. * Revert "chore(vite): drop dead resolveId hook from reactNativeWebPlugin" This reverts commit 6a07ebd. --------- Co-authored-by: Dan Brewster <dnbrwstr@gmail.com> Co-authored-by: Patrick O'Sullivan <patrick@osullivan.io> Co-authored-by: bᵣᵢₐₙ <90741358+latter-bolden@users.noreply.github.com> Co-authored-by: David <david.isaac.lee@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
When a multiline TextInput is uncontrolled (no
valueprop) and uses children for styled text, changing the children does not cause the TextInput to resize. For example, if the children go from multiline content to empty, the input stays at its expanded height.Root cause
During a layout pass,
YGNodeCalculateLayoutcallsmeasureContent()which usesattributedStringBoxToMeasure()to decide what text to measure. For uncontrolled TextInputs with Text children,attributedStringBoxin state holds the native text from the last_updateState()call — but by the time the React tree children have changed (e.g. cleared),attributedStringBoxstill contains the old native text.updateStateIfNeeded()would normally sync the state, but it runs inlayout()which is called afterYGNodeCalculateLayouthas already computed sizes. So Yoga measures with stale text and the height doesn't update.Fix
In
attributedStringBoxToMeasure(), for uncontrolled TextInputs (props.textis empty), check if the React tree attributed string has diverged from what's stored in state. If so, use the React tree version for measurement instead of the staleattributedStringBox.The check is guarded by
props.text.empty()so controlled TextInputs (which are the common case and don't have this issue) skip it entirely with zero overhead.Changelog:
[IOS][FIXED] - Fix uncontrolled multiline TextInput not resizing when children change
Test Plan: