diff --git a/artifacts/tx-os/src/pages/notes.tsx b/artifacts/tx-os/src/pages/notes.tsx index a28b2269..3a98e2c9 100644 --- a/artifacts/tx-os/src/pages/notes.tsx +++ b/artifacts/tx-os/src/pages/notes.tsx @@ -42,6 +42,7 @@ import { ar as dfAr, enUS as dfEn } from "date-fns/locale"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; +import { useVisualViewport } from "@/hooks/use-visual-viewport"; import { Badge } from "@/components/ui/badge"; import { AlertDialog, @@ -2228,6 +2229,16 @@ function ThreadDialog({ const markedRef = useRef(false); const replyInputRef = useRef(null); const focusedRef = useRef(false); + const replyFocused = useRef(false); + const { keyboardInset } = useVisualViewport(); + // When the on-screen keyboard appears while the reply box is focused, + // pull the cursor into the centre of the visible viewport. Gated on a + // non-trivial inset so desktop focus is unaffected. + useEffect(() => { + if (keyboardInset <= 80) return; + if (!replyFocused.current) return; + replyInputRef.current?.scrollIntoView({ block: "center" }); + }, [keyboardInset]); // When opened from the incoming-note popup with reply intent, jump // focus into the reply input as soon as the thread loads. @@ -2410,6 +2421,15 @@ function ThreadDialog({ ref={replyInputRef} value={replyText} onChange={(e) => setReplyText(e.target.value)} + onFocus={(e) => { + replyFocused.current = true; + if (keyboardInset > 80) { + e.currentTarget.scrollIntoView({ block: "center" }); + } + }} + onBlur={() => { + replyFocused.current = false; + }} placeholder={t("notes.replyPlaceholder", "Write a reply…")} className="min-h-[70px] resize-none" data-testid="thread-reply-input" @@ -2886,6 +2906,17 @@ function Composer({ const targetFolderId = folderSelection.kind === "folder" ? folderSelection.id : null; const ref = useRef(null); + const contentRef = useRef(null); + const contentFocused = useRef(false); + const { keyboardInset } = useVisualViewport(); + // Same keyboard-aware scroll-into-view as the thread reply box: only + // fires while a software keyboard is actually open and the composer + // textarea is focused. Desktop focus is unaffected. + useEffect(() => { + if (keyboardInset <= 80) return; + if (!contentFocused.current) return; + contentRef.current?.scrollIntoView({ block: "center" }); + }, [keyboardInset]); useEffect(() => { if (!open) return; @@ -3012,8 +3043,18 @@ function Composer({ ) : (