11import { platform } from "@tauri-apps/plugin-os" ;
22import type { EmojiClickData } from "emoji-picker-react" ;
33import type * as React from "react" ;
4- import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
4+ import {
5+ useCallback ,
6+ useEffect ,
7+ useLayoutEffect ,
8+ useMemo ,
9+ useRef ,
10+ useState ,
11+ } from "react" ;
512import { createPortal } from "react-dom" ;
613import { FaGift , FaList , FaPlus , FaTimes } from "react-icons/fa" ;
714import { v4 as uuidv4 } from "uuid" ;
@@ -166,21 +173,36 @@ export const ChatArea: React.FC<{
166173 // user first scrolled up. Prevents mass DOM insertion at the top when isScrolledUp becomes true,
167174 // which caused WKWebView scroll anchoring to fail and the viewport to jump to the top.
168175 const scrollUpStartRef = useRef < number | null > ( null ) ;
176+ // lineHeight/padding are static CSS values — cache them after first read to avoid
177+ // getComputedStyle on every keystroke, which forces layout flush on mobile WebView.
178+ const textareaMetricsRef = useRef < {
179+ lineHeight : number ;
180+ paddingTop : number ;
181+ paddingBottom : number ;
182+ } | null > ( null ) ;
169183
170184 const resizeTextarea = useCallback ( ( ) => {
171185 const textarea = inputRef . current ;
172186 if ( ! textarea ) return ;
173- textarea . style . height = "auto" ;
174- const computed = window . getComputedStyle ( textarea ) ;
175- const lineHeight = Number . parseFloat ( computed . lineHeight ) || 24 ;
176- const paddingTop = Number . parseFloat ( computed . paddingTop ) || 0 ;
177- const paddingBottom = Number . parseFloat ( computed . paddingBottom ) || 0 ;
187+
188+ if ( ! textareaMetricsRef . current ) {
189+ const computed = window . getComputedStyle ( textarea ) ;
190+ textareaMetricsRef . current = {
191+ lineHeight : Number . parseFloat ( computed . lineHeight ) || 24 ,
192+ paddingTop : Number . parseFloat ( computed . paddingTop ) || 0 ,
193+ paddingBottom : Number . parseFloat ( computed . paddingBottom ) || 0 ,
194+ } ;
195+ }
196+
197+ const { lineHeight, paddingTop, paddingBottom } =
198+ textareaMetricsRef . current ;
178199 const maxHeight = lineHeight * 5 + paddingTop + paddingBottom ;
200+ textarea . style . height = "auto" ;
179201 textarea . style . height = `${ Math . min ( textarea . scrollHeight , maxHeight ) } px` ;
180202 } , [ ] ) ;
181203
182204 // biome-ignore lint/correctness/useExhaustiveDependencies: messageText is intentionally listed so the effect re-runs on every keystroke, even though it isn't referenced inside the body
183- useEffect ( ( ) => {
205+ useLayoutEffect ( ( ) => {
184206 resizeTextarea ( ) ;
185207 } , [ resizeTextarea , messageText ] ) ;
186208
@@ -515,6 +537,17 @@ export const ChatArea: React.FC<{
515537 currentUser,
516538 } ) ;
517539
540+ // Memoize preview styles — selectedColor/selectedFormatting don't change while typing,
541+ // so recomputing this on every keystroke is unnecessary object churn.
542+ const previewStyle = useMemo (
543+ ( ) =>
544+ getPreviewStyles ( {
545+ color : selectedColor || "inherit" ,
546+ formatting : selectedFormatting ,
547+ } ) ,
548+ [ selectedColor , selectedFormatting ] ,
549+ ) ;
550+
518551 // Get messages for current channel or private chat - memoized
519552 const channelKey = useMemo (
520553 ( ) =>
@@ -1335,8 +1368,10 @@ export const ChatArea: React.FC<{
13351368 const handleOpenPM = ( username : string ) => {
13361369 if ( selectedServerId ) {
13371370 openPrivateChat ( selectedServerId , username ) ;
1338- // Find and select the private chat
1339- const server = servers . find ( ( s ) => s . id === selectedServerId ) ;
1371+ // Read fresh store state so newly-created DMs are visible (stale closure fix)
1372+ const server = useStore
1373+ . getState ( )
1374+ . servers . find ( ( s ) => s . id === selectedServerId ) ;
13401375 const privateChat = server ?. privateChats ?. find (
13411376 ( pc ) => pc . username === username ,
13421377 ) ;
@@ -1776,6 +1811,9 @@ export const ChatArea: React.FC<{
17761811 onClick = { handleInputClick }
17771812 onKeyUp = { handleInputKeyUp }
17781813 onKeyDown = { handleKeyDown }
1814+ autoCorrect = "on"
1815+ autoCapitalize = "sentences"
1816+ spellCheck = { true }
17791817 placeholder = {
17801818 selectedChannel
17811819 ? `Message #${ selectedChannel . name . replace ( / ^ # / , "" ) } ${
@@ -1805,10 +1843,7 @@ export const ChatArea: React.FC<{
18051843 : "send"
18061844 }
18071845 className = "bg-transparent border-none outline-none py-3 flex-grow text-discord-text-normal resize-none min-h-[44px] overflow-y-auto placeholder:truncate"
1808- style = { getPreviewStyles ( {
1809- color : selectedColor || "inherit" ,
1810- formatting : selectedFormatting ,
1811- } ) }
1846+ style = { previewStyle }
18121847 rows = { 1 }
18131848 />
18141849 < InputToolbar
@@ -1827,6 +1862,7 @@ export const ChatArea: React.FC<{
18271862 onSendClick = { handleSendMessage }
18281863 showSendButton = { isMobile }
18291864 hideEmoji = { isNativeMobile }
1865+ hasText = { messageText . trim ( ) . length > 0 }
18301866 />
18311867 </ div >
18321868
0 commit comments