From bca6d821a896b57a3640442c245dfa6ba83d5972 Mon Sep 17 00:00:00 2001 From: S3333F <1alitsaaf@gmail.com> Date: Tue, 27 Oct 2026 19:58:11 +0100 Subject: [PATCH] fix(mobile): keep the composer text box off the model button on RTL phones --- .../src/features/threads/ThreadComposer.tsx | 14 +++++++++++--- .../threads/compactEditorInsets.test.ts | 17 +++++++++++++++++ .../src/features/threads/compactEditorInsets.ts | 17 +++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 apps/mobile/src/features/threads/compactEditorInsets.test.ts create mode 100644 apps/mobile/src/features/threads/compactEditorInsets.ts diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 7ac1ad83aa8b5..fadefae059cea 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -32,7 +32,7 @@ import { import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native"; import type { ReactNode } from "react"; import { useEffect, useEffectEvent, useRef, useState, type RefObject } from "react"; -import { Keyboard, Platform, View, type ViewStyle } from "react-native"; +import { I18nManager, Keyboard, Platform, View, type ViewStyle } from "react-native"; import { composerImageGallery } from "../../lib/composerImageGallery"; import { filePreviewGallery } from "../../lib/filePreviewGallery"; import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal"; @@ -95,6 +95,7 @@ import { resolveComposerSendPresentation, type ComposerSendPresentation, } from "./composerSendPresentation"; +import { compactEditorInsets } from "./compactEditorInsets"; import { ComposerCommandPopover } from "./ComposerCommandPopover"; import { ComposerPopoverAnchor } from "./ComposerPopoverAnchor"; import { ComposerQueuedEditAttachments } from "./ComposerQueuedEdit"; @@ -152,12 +153,19 @@ export function useCompactComposerEditor(text: ScaledTextRole, value: string) { (contentHeight > text.lineHeight * 1.5 || (compactWidth > 0 && contentSize.width > compactWidth)); const compactRowHeight = Math.max(44, minimum); + const isRTL = I18nManager.isRTL; const editorStyle = useAnimatedStyle(() => ({ position: "absolute", overflow: "hidden", height, - left: isExpanded ? 14 : editorX.value + 4, - right: isExpanded ? 14 : rowWidth.value - editorX.value - editorWidth.value + 4, + ...(isExpanded + ? { left: 14, right: 14 } + : compactEditorInsets({ + slotX: editorX.value, + slotWidth: editorWidth.value, + rowWidth: rowWidth.value, + isRTL, + })), // The full-width editor must move above the controls in the same frame it widens. bottom: isExpanded ? 52 : (compactRowHeight - minimum) / 2, })); diff --git a/apps/mobile/src/features/threads/compactEditorInsets.test.ts b/apps/mobile/src/features/threads/compactEditorInsets.test.ts new file mode 100644 index 0000000000000..ebb7a39f85a2d --- /dev/null +++ b/apps/mobile/src/features/threads/compactEditorInsets.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { compactEditorInsets } from "./compactEditorInsets"; + +describe("compact editor insets", () => { + it("keeps the measured slot position in left-to-right layouts", () => { + expect(compactEditorInsets({ slotX: 48, slotWidth: 200, rowWidth: 360, isRTL: false })).toEqual( + { left: 52, right: 116 }, + ); + }); + + it("swaps the insets in right-to-left layouts so the editor stays over its slot", () => { + expect(compactEditorInsets({ slotX: 112, slotWidth: 200, rowWidth: 360, isRTL: true })).toEqual( + { left: 52, right: 116 }, + ); + }); +}); diff --git a/apps/mobile/src/features/threads/compactEditorInsets.ts b/apps/mobile/src/features/threads/compactEditorInsets.ts new file mode 100644 index 0000000000000..46cb521b9b308 --- /dev/null +++ b/apps/mobile/src/features/threads/compactEditorInsets.ts @@ -0,0 +1,17 @@ +const COMPACT_EDITOR_SLOT_INSET = 4; + +/** + * Places the floating editor over its slot. Layout x is measured from the left, + * but React Native swaps left and right styles in RTL, so the insets trade places. + */ +export function compactEditorInsets(input: { + readonly slotX: number; + readonly slotWidth: number; + readonly rowWidth: number; + readonly isRTL: boolean; +}): { left: number; right: number } { + "worklet"; + const fromLeft = input.slotX + COMPACT_EDITOR_SLOT_INSET; + const fromRight = input.rowWidth - input.slotX - input.slotWidth + COMPACT_EDITOR_SLOT_INSET; + return input.isRTL ? { left: fromRight, right: fromLeft } : { left: fromLeft, right: fromRight }; +}