Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/mobile/src/components/ComposerToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ export function ComposerInlineControl(props: {
accessibilityState={
props.static ? undefined : { disabled: props.disabled, selected: props.selected }
}
className="h-[44px] flex-row items-center gap-2 rounded-xl px-2 active:bg-subtle"
// Icon-only, the control sits right after the composer's text, where
// leading padding would take taps meant for the text field.
className={cn(
"h-[44px] flex-row items-center gap-2 rounded-xl pr-2 active:bg-subtle",
props.iconOnly ? "pl-1" : "pl-2",
)}
disabled={props.disabled || props.static}
onPress={props.onPress}
style={{ opacity: props.disabled ? 0.45 : 1 }}
Expand Down
20 changes: 17 additions & 3 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { I18nManager, Keyboard, Platform, View, type ViewStyle } from "react-native";
import { I18nManager, Keyboard, Platform, Pressable, View, type ViewStyle } from "react-native";
import { composerImageGallery } from "../../lib/composerImageGallery";
import { filePreviewGallery } from "../../lib/filePreviewGallery";
import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal";
Expand Down Expand Up @@ -124,6 +124,9 @@ export const COMPOSER_COLLAPSED_CHROME = 60;

const NO_MACHINES: ReadonlyArray<ComposerEnvironmentCandidate> = [];
const COLLAPSED_EDITOR_DEFAULT_INSET = 6;
// Space between the compact editor and the icon-only model control. It belongs
// to the editor slot, so a tap that lands short of the icon focuses the editor.
const COMPACT_EDITOR_TRAILING_GAP = 12;
// iOS puts line-height slack above glyphs. DM Sans's lowercase midline is
// descender (0.31em) + half x-height (0.504em) above the line bottom.
const LOWERCASE_MIDLINE_EM = 0.562;
Expand Down Expand Up @@ -170,7 +173,9 @@ function useCompactComposerEditor(text: ScaledTextRole, value: string) {
slotWidth: editorWidth.value,
rowWidth: rowWidth.value,
isRTL,
trailingGap: COMPACT_EDITOR_TRAILING_GAP,
})),

// The full-width editor must move above the controls in the same frame it widens.
bottom: isExpanded ? 52 : (compactRowHeight - minimum) / 2,
}));
Expand All @@ -187,7 +192,9 @@ function useCompactComposerEditor(text: ScaledTextRole, value: string) {
if (!isExpanded) {
editorX.set(event.nativeEvent.layout.x);
editorWidth.set(event.nativeEvent.layout.width);
setCompactWidth(Math.max(0, event.nativeEvent.layout.width - 8));
setCompactWidth(
Math.max(0, event.nativeEvent.layout.width - 4 - COMPACT_EDITOR_TRAILING_GAP),
);
}
},
editorProps: {
Expand Down Expand Up @@ -913,7 +920,14 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps
compactEditor.containerStyle,
compactEditor.isExpanded ? { flex: 0, width: 0 } : undefined,
]}
></Animated.View>
>
{/* The editor overlay is inset from this slot; taps on the inset still focus it. */}
<Pressable
accessible={false}
className="flex-1"
onPress={() => inputRef.current?.focus()}
/>
</Animated.View>
{!isExpanded && stripAttachments.length > 0 ? (
<View className="h-[44px] shrink-0 flex-row items-center gap-1 pl-1">
{stripAttachments.slice(0, compactAttachmentLimit).map((attachment) => (
Expand Down
24 changes: 18 additions & 6 deletions apps/mobile/src/features/threads/compactEditorInsets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,26 @@ 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 },
);
expect(
compactEditorInsets({
slotX: 48,
slotWidth: 200,
rowWidth: 360,
isRTL: false,
trailingGap: 12,
}),
).toEqual({ left: 52, right: 124 });
});

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 },
);
expect(
compactEditorInsets({
slotX: 112,
slotWidth: 200,
rowWidth: 360,
isRTL: true,
trailingGap: 12,
}),
).toEqual({ left: 52, right: 124 });
});
});
10 changes: 8 additions & 2 deletions apps/mobile/src/features/threads/compactEditorInsets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ export function compactEditorInsets(input: {
readonly slotWidth: number;
readonly rowWidth: number;
readonly isRTL: boolean;
readonly trailingGap?: number;
}): { 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;
const trailingGap = input.trailingGap ?? COMPACT_EDITOR_SLOT_INSET;
const fromLeft = input.slotX + (input.isRTL ? trailingGap : COMPACT_EDITOR_SLOT_INSET);
const fromRight =
input.rowWidth -
input.slotX -
input.slotWidth +
(input.isRTL ? COMPACT_EDITOR_SLOT_INSET : trailingGap);
return input.isRTL ? { left: fromRight, right: fromLeft } : { left: fromLeft, right: fromRight };
}
Loading