From 41940e7dc4817c430847a0c6f60254c714029fad Mon Sep 17 00:00:00 2001 From: Derek Trimm <275381468+derektrimm@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:14:56 -0500 Subject: [PATCH] fix(mobile): stop the iOS model chip from sitting on the editor Tapping GPT-6-Astra on iPhone selected the composer (Paste/AutoFill) instead of opening Thread Settings. The settings row was position:absolute over the native UITextView, and Reanimated layout morphs on that host can leave the editor's hit box covering the chip. Mount the row in flow only, never as an overlay, and drop the layout animation on the editor wrapper. --- .../src/features/threads/ThreadComposer.tsx | 20 ++++++------- .../threads/composer-draft-expansion.test.ts | 29 +++++++++++++++++++ .../threads/composer-draft-expansion.ts | 15 ++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 apps/mobile/src/features/threads/composer-draft-expansion.test.ts create mode 100644 apps/mobile/src/features/threads/composer-draft-expansion.ts diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 680c9cf0babe..e0157b55f740 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -104,6 +104,7 @@ import { type ExistingThreadSettingsRouteSession, useExistingThreadSettingsRoutePresentation, } from "./ThreadSettingsSheet"; +import { composerSettingsToolbarLayout } from "./composer-draft-expansion"; import { useThreadSettingsSheetPresentation, type NavigationWithFinishTransitioning, @@ -415,6 +416,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const isExpanded = isFocused || settingsSheetPresentation.keepsComposerExpanded; const showsCompactDictation = isVoiceInputPresented && !isExpanded; const isToolbarVisible = isExpanded || isVoiceInputPresented; + const settingsToolbar = composerSettingsToolbarLayout({ + isExpanded, + isVoicePresented: isVoiceInputPresented, + }); const attachmentBlockReason = composerAttachmentUploadBlockReason({ environmentId: props.environmentId, attachments: props.draftAttachments, @@ -726,7 +731,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer ) : null} : null} + {settingsToolbar.mount ? ( + ) : null} diff --git a/apps/mobile/src/features/threads/composer-draft-expansion.test.ts b/apps/mobile/src/features/threads/composer-draft-expansion.test.ts new file mode 100644 index 000000000000..0ad3facee9c1 --- /dev/null +++ b/apps/mobile/src/features/threads/composer-draft-expansion.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { composerSettingsToolbarLayout } from "./composer-draft-expansion"; + +describe("composer settings toolbar layout", () => { + it("never overlays the native editor", () => { + expect( + composerSettingsToolbarLayout({ isExpanded: false, isVoicePresented: false }).overlayEditor, + ).toBe(false); + expect( + composerSettingsToolbarLayout({ isExpanded: true, isVoicePresented: false }).overlayEditor, + ).toBe(false); + expect( + composerSettingsToolbarLayout({ isExpanded: false, isVoicePresented: true }).overlayEditor, + ).toBe(false); + }); + + it("mounts the row only when the draft is expanded or dictation is showing", () => { + expect( + composerSettingsToolbarLayout({ isExpanded: false, isVoicePresented: false }).mount, + ).toBe(false); + expect( + composerSettingsToolbarLayout({ isExpanded: true, isVoicePresented: false }).mount, + ).toBe(true); + expect( + composerSettingsToolbarLayout({ isExpanded: false, isVoicePresented: true }).mount, + ).toBe(true); + }); +}); diff --git a/apps/mobile/src/features/threads/composer-draft-expansion.ts b/apps/mobile/src/features/threads/composer-draft-expansion.ts new file mode 100644 index 000000000000..e2017253abc2 --- /dev/null +++ b/apps/mobile/src/features/threads/composer-draft-expansion.ts @@ -0,0 +1,15 @@ +/** + * The model chip is a RN Pressable. The composer is a UITextView. iOS native + * views win hit testing against overlapping RN siblings, so a chip laid out + * over the editor selects text / shows Paste instead of opening settings. + * Never overlay the settings row on the editor. Mount it in flow, or not at all. + */ +export function composerSettingsToolbarLayout(input: { + readonly isExpanded: boolean; + readonly isVoicePresented: boolean; +}): { readonly overlayEditor: boolean; readonly mount: boolean } { + return { + overlayEditor: false, + mount: input.isExpanded || input.isVoicePresented, + }; +}