From a60f7bdd2bb60fd892aea857dcb1e00014380b93 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 18 Jul 2025 16:22:30 +1000 Subject: [PATCH 1/4] fix: add IME input support to the composition input --- .../composition/CompositionInput.tsx | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/ts/components/conversation/composition/CompositionInput.tsx b/ts/components/conversation/composition/CompositionInput.tsx index ea2e142b1..2ecd69f32 100644 --- a/ts/components/conversation/composition/CompositionInput.tsx +++ b/ts/components/conversation/composition/CompositionInput.tsx @@ -273,6 +273,12 @@ export type ContentEditableProps = Omit, 'o /** If true, disables editing (e.g., remove contentEditable or make read-only). */ disabled?: boolean; innerRef?: React.Ref; + /** Called when IME composition starts */ + onCompositionStart?: (e: React.CompositionEvent) => void; + /** Called when IME composition updates */ + onCompositionUpdate?: (e: React.CompositionEvent) => void; + /** Called when IME composition ends */ + onCompositionEnd?: (e: React.CompositionEvent) => void; }; const UnstyledCompositionInput = forwardRef( @@ -287,6 +293,9 @@ const UnstyledCompositionInput = forwardRef(null); const lastHtmlIndex = useRef(0); + // IME composition state + const isComposing = useRef(false); + const compositionStartIndex = useRef(0); + const compositionData = useRef(''); + useDebouncedSpellcheck({ elementRef: elRef, }); @@ -447,6 +461,11 @@ const UnstyledCompositionInput = forwardRef) => { + // Skip input handling during IME composition + if (isComposing.current) { + return; + } + const hasChanged = handleChange(); if (hasChanged) { emitChangeEvent(e); @@ -769,6 +793,45 @@ const UnstyledCompositionInput = forwardRef) => { + isComposing.current = true; + compositionStartIndex.current = lastHtmlIndex.current; + compositionData.current = ''; + + onCompositionStart?.(e); + }, + [onCompositionStart] + ); + + const handleCompositionUpdate = useCallback( + (e: React.CompositionEvent) => { + if (isComposing.current) { + compositionData.current = e.data; + } + + onCompositionUpdate?.(e); + }, + [onCompositionUpdate] + ); + + const handleCompositionEnd = useCallback( + (e: React.CompositionEvent) => { + isComposing.current = false; + + // Process the final composition result + const hasChanged = handleChange(); + if (hasChanged) { + emitChangeEvent(e as any); + } + + compositionData.current = ''; + onCompositionEnd?.(e); + }, + [handleChange, emitChangeEvent, onCompositionEnd] + ); + useEffect(() => { const el = elRef.current; if (!el) { @@ -802,7 +865,8 @@ const UnstyledCompositionInput = forwardRef {children} From 10d6096aa578d779eb65964f3d483f49d6af13c5 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 18 Jul 2025 17:06:49 +1000 Subject: [PATCH 2/4] feat: update debounced spellcheck hook to toggle decorations instead of the real spellcheck --- ts/hooks/useDebuncedSpellcheck.ts | 81 ++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/ts/hooks/useDebuncedSpellcheck.ts b/ts/hooks/useDebuncedSpellcheck.ts index c61903a7e..0e6114068 100644 --- a/ts/hooks/useDebuncedSpellcheck.ts +++ b/ts/hooks/useDebuncedSpellcheck.ts @@ -6,14 +6,72 @@ interface DebouncedSpellcheckProps { delay?: number; } +// Global reference counter for the hook +let hookUsageCount = 0; +const STYLE_ID = 'debounced-spellcheck-styles'; + +const cssStyles = ` + .spellcheck-hidden::-webkit-spelling-error { + text-decoration: none !important; + } + + .spellcheck-hidden::-webkit-grammar-error { + text-decoration: none !important; + } + + .spellcheck-hidden::-moz-spelling-error { + text-decoration: none !important; + } + + .spellcheck-hidden::-moz-grammar-error { + text-decoration: none !important; + } + + .spellcheck-hidden::spelling-error { + text-decoration: none !important; + } + + .spellcheck-hidden::grammar-error { + text-decoration: none !important; + } +`; + export const useDebouncedSpellcheck = ({ delay = 300, elementRef }: DebouncedSpellcheckProps) => { - const enableSpellcheck = useCallback(() => { - elementRef.current?.setAttribute('spellcheck', 'true'); + // Inject CSS styles if they don't exist + useEffect(() => { + hookUsageCount++; + + // Only inject styles on first usage + if (hookUsageCount === 1 && !document.getElementById(STYLE_ID)) { + const style = document.createElement('style'); + style.id = STYLE_ID; + style.textContent = cssStyles; + document.head.appendChild(style); + } + + // Remove styles only when no components are using the hook + return () => { + hookUsageCount--; + if (hookUsageCount === 0) { + const existingStyle = document.getElementById(STYLE_ID); + if (existingStyle) { + existingStyle.remove(); + } + } + }; + }, []); + + const hideSpellcheckLines = useCallback(() => { + elementRef.current?.classList.add('spellcheck-hidden'); + }, [elementRef]); + + const showSpellcheckLines = useCallback(() => { + elementRef.current?.classList.remove('spellcheck-hidden'); }, [elementRef]); // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: see if we can create our own useDebounce hook - const debouncedSpellcheck = useCallback(debounce(enableSpellcheck, delay), [ - enableSpellcheck, + const debouncedShowSpellcheck = useCallback(debounce(showSpellcheckLines, delay), [ + showSpellcheckLines, delay, ]); @@ -24,8 +82,10 @@ export const useDebouncedSpellcheck = ({ delay = 300, elementRef }: DebouncedSpe } const handleInput = () => { - el.setAttribute('spellcheck', 'false'); - debouncedSpellcheck(); + // Hide spellcheck lines immediately while typing + hideSpellcheckLines(); + // Show them again after user stops typing + debouncedShowSpellcheck(); }; el.addEventListener('input', handleInput); @@ -33,6 +93,13 @@ export const useDebouncedSpellcheck = ({ delay = 300, elementRef }: DebouncedSpe // eslint-disable-next-line consistent-return -- This return is the destructor return () => { el.removeEventListener('input', handleInput); + // Clean up: show spellcheck lines when component unmounts + showSpellcheckLines(); }; - }, [debouncedSpellcheck, elementRef]); + }, [debouncedShowSpellcheck, hideSpellcheckLines, showSpellcheckLines, elementRef]); + + return { + hideSpellcheckLines, + showSpellcheckLines, + }; }; From 43e5d94977f965ecf33fc68174966e800bdcc056 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 21 Jul 2025 15:26:16 +1000 Subject: [PATCH 3/4] chore: increase spellcheck debounce time in composition input --- ts/hooks/useDebuncedSpellcheck.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/hooks/useDebuncedSpellcheck.ts b/ts/hooks/useDebuncedSpellcheck.ts index 0e6114068..94c44882b 100644 --- a/ts/hooks/useDebuncedSpellcheck.ts +++ b/ts/hooks/useDebuncedSpellcheck.ts @@ -36,7 +36,7 @@ const cssStyles = ` } `; -export const useDebouncedSpellcheck = ({ delay = 300, elementRef }: DebouncedSpellcheckProps) => { +export const useDebouncedSpellcheck = ({ delay = 600, elementRef }: DebouncedSpellcheckProps) => { // Inject CSS styles if they don't exist useEffect(() => { hookUsageCount++; From e73c363971df08bb6f39652b84448dc585db10e3 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 22 Jul 2025 10:35:38 +1000 Subject: [PATCH 4/4] chore: bump Session to 1.16.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c77d23cfa..0895d559d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.16.4", + "version": "1.16.5", "license": "GPL-3.0", "author": { "name": "Session Foundation",