diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 35667a6..b2eb31d 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -65,6 +65,8 @@ export function AppShell() { ? `${activeChannel.description} Forum channels are listed, but this first pass only wires message streams and DMs.` : activeChannel.description : "Connect to the relay to browse channels and read messages."; + const contentPaneKey = + selectedView === "home" ? "home" : `channel:${activeChannel?.id ?? "none"}`; return ( @@ -108,7 +110,10 @@ export function AppShell() { selectedView={selectedView} /> - + {selectedView === "home" ? ( (null); + const pendingSelectionRef = React.useRef(null); + + const submitMessage = React.useCallback(async () => { + const trimmed = content.trim(); + if (!trimmed || disabled || isSending) { + return; + } + + setContent(""); + + try { + await onSend(trimmed); + } catch { + setContent(trimmed); + } + }, [content, disabled, isSending, onSend]); const handleSubmit = React.useCallback( - async (event: React.FormEvent) => { + (event: React.FormEvent) => { event.preventDefault(); + void submitMessage(); + }, + [submitMessage], + ); - const trimmed = content.trim(); - if (!trimmed || disabled || isSending) { + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key !== "Enter" || event.nativeEvent.isComposing) { return; } - setContent(""); + if (event.ctrlKey) { + const textarea = event.currentTarget; + const { selectionEnd, selectionStart, value } = textarea; + const nextContent = `${value.slice(0, selectionStart)}\n${value.slice(selectionEnd)}`; + + event.preventDefault(); + pendingSelectionRef.current = selectionStart + 1; + setContent(nextContent); + return; + } - try { - await onSend(trimmed); - } catch { - setContent(trimmed); + if (event.metaKey || event.altKey || event.shiftKey) { + return; } + + event.preventDefault(); + void submitMessage(); }, - [content, disabled, isSending, onSend], + [submitMessage], ); + React.useLayoutEffect(() => { + const textarea = textareaRef.current; + if (!textarea) { + return; + } + + const lineHeight = + Number.parseFloat(window.getComputedStyle(textarea).lineHeight) || 24; + const maxHeight = lineHeight * MAX_TEXTAREA_ROWS; + + textarea.style.height = "auto"; + const nextHeight = Math.max( + lineHeight, + Math.min(textarea.scrollHeight, maxHeight), + ); + textarea.style.height = `${nextHeight}px`; + textarea.style.overflowY = + textarea.scrollHeight > maxHeight ? "auto" : "hidden"; + + const pendingSelection = pendingSelectionRef.current; + if (pendingSelection !== null) { + textarea.setSelectionRange(pendingSelection, pendingSelection); + pendingSelectionRef.current = null; + } + }); + return (