From ca7349a1a8257d2271d0a44cd414df0cdfd596b1 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Mon, 6 Apr 2026 21:59:46 -0700 Subject: [PATCH] Add autofocus to text boxes --- frontend/src/App.tsx | 18 +- frontend/src/components/MessageInput.tsx | 4 + frontend/src/components/RepeaterLogin.tsx | 3 +- .../settings/SettingsLocalSection.tsx | 228 ++++++++++-------- .../src/test/useConversationActions.test.ts | 2 +- frontend/src/utils/autoFocusInput.ts | 31 +++ 6 files changed, 178 insertions(+), 108 deletions(-) create mode 100644 frontend/src/utils/autoFocusInput.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b33739c..c6a2ab9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,7 +25,8 @@ import { DistanceUnitProvider } from './contexts/DistanceUnitContext'; import { messageContainsMention } from './utils/messageParser'; import { getStateKey } from './utils/conversationState'; import type { BulkCreateHashtagChannelsResult, Conversation, Message, RawPacket } from './types'; -import { CONTACT_TYPE_ROOM } from './types'; +import { CONTACT_TYPE_REPEATER, CONTACT_TYPE_ROOM } from './types'; +import { shouldAutoFocusInput } from './utils/autoFocusInput'; interface ChannelUnreadMarker { channelId: string; @@ -296,6 +297,21 @@ export function App() { } = useConversationMessages(activeConversation, targetMessageId); removeConversationMessagesRef.current = removeConversationMessages; + // Auto-focus the message input on conversation change (desktop only by default) + useEffect(() => { + if (!activeConversation) return; + if (activeConversation.type !== 'channel' && activeConversation.type !== 'contact') return; + // Repeaters show a login form, not a message input + if (activeConversation.type === 'contact') { + const contact = contacts.find((c) => c.public_key === activeConversation.id); + if (contact?.type === CONTACT_TYPE_REPEATER) return; + } + if (!shouldAutoFocusInput()) return; + // Defer to let the input mount/render first + const raf = requestAnimationFrame(() => messageInputRef.current?.focus?.()); + return () => cancelAnimationFrame(raf); + }, [activeConversation?.id]); // eslint-disable-line react-hooks/exhaustive-deps + // Room servers replay stored history as a burst of DMs, all arriving with similar received_at // but spanning a wide range of sender_timestamps. Sort by sender_timestamp for room contacts // so the display reflects the original send order rather than our radio's receipt order. diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index bde9c44..86721e2 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -44,6 +44,7 @@ type LimitState = 'normal' | 'warning' | 'danger' | 'error'; export interface MessageInputHandle { appendText: (text: string) => void; + focus: () => void; } export const MessageInput = forwardRef(function MessageInput( @@ -60,6 +61,9 @@ export const MessageInput = forwardRef(fu // Focus the input after appending inputRef.current?.focus(); }, + focus: () => { + inputRef.current?.focus(); + }, })); // Calculate character limits based on conversation type diff --git a/frontend/src/components/RepeaterLogin.tsx b/frontend/src/components/RepeaterLogin.tsx index bd85462..dc7920a 100644 --- a/frontend/src/components/RepeaterLogin.tsx +++ b/frontend/src/components/RepeaterLogin.tsx @@ -2,6 +2,7 @@ import { useCallback, type FormEvent } from 'react'; import { Input } from './ui/input'; import { Button } from './ui/button'; import { Checkbox } from './ui/checkbox'; +import { shouldAutoFocusInput } from '../utils/autoFocusInput'; interface RepeaterLoginProps { repeaterName: string; @@ -64,7 +65,7 @@ export function RepeaterLogin({ placeholder={passwordPlaceholder} aria-label="Repeater password" disabled={loading} - autoFocus + autoFocus={shouldAutoFocusInput()} />