diff --git a/app/src/pages/conversations/composerSendDecision.test.ts b/app/src/pages/conversations/composerSendDecision.test.ts index abbfa0157..66723ba68 100644 --- a/app/src/pages/conversations/composerSendDecision.test.ts +++ b/app/src/pages/conversations/composerSendDecision.test.ts @@ -4,6 +4,7 @@ import { evaluateComposerSend, getComposerBlockedSendFeedback, handleComposerSlashCommand, + shouldSendComposerKeyDown, } from './composerSendDecision'; describe('evaluateComposerSend', () => { @@ -104,6 +105,30 @@ describe('handleComposerSlashCommand', () => { }); }); +describe('shouldSendComposerKeyDown', () => { + it('allows Enter to send when IME composition is inactive', () => { + expect(shouldSendComposerKeyDown({ key: 'Enter' })).toBe(true); + }); + + it('does not send on Shift+Enter', () => { + expect(shouldSendComposerKeyDown({ key: 'Enter', shiftKey: true })).toBe(false); + }); + + it('does not send while React reports IME composition', () => { + expect(shouldSendComposerKeyDown({ key: 'Enter', nativeEvent: { isComposing: true } })).toBe( + false + ); + }); + + it('does not send while the browser reports legacy IME keyCode 229', () => { + expect(shouldSendComposerKeyDown({ key: 'Enter', nativeEvent: { keyCode: 229 } })).toBe(false); + }); + + it('does not send while textarea composition state is active', () => { + expect(shouldSendComposerKeyDown({ key: 'Enter' }, true)).toBe(false); + }); +}); + describe('getComposerBlockedSendFeedback', () => { it('returns modal and error feedback for usage-limit blocking', () => { expect(getComposerBlockedSendFeedback('usage_limit_reached')).toEqual({ diff --git a/app/src/pages/conversations/composerSendDecision.ts b/app/src/pages/conversations/composerSendDecision.ts index e6068b0d8..2f3b6abe0 100644 --- a/app/src/pages/conversations/composerSendDecision.ts +++ b/app/src/pages/conversations/composerSendDecision.ts @@ -28,6 +28,14 @@ export interface ComposerBlockedSendFeedback { error: { code: 'usage_limit_reached' | 'socket_disconnected'; message: string }; } +export interface ComposerKeyDownEventLike { + key: string; + shiftKey?: boolean; + isComposing?: boolean; + keyCode?: number; + nativeEvent?: { isComposing?: boolean; keyCode?: number }; +} + export const handleComposerSlashCommand = ( command: string, welcomeLocked: boolean @@ -65,6 +73,22 @@ export const evaluateComposerSend = (args: ComposerSendDecisionArgs): ComposerSe return { shouldSend: true, trimmedText }; }; +export const isComposerImeComposing = ( + event: ComposerKeyDownEventLike, + compositionActive = false +): boolean => + compositionActive || + event.isComposing === true || + event.keyCode === 229 || + event.nativeEvent?.isComposing === true || + event.nativeEvent?.keyCode === 229; + +export const shouldSendComposerKeyDown = ( + event: ComposerKeyDownEventLike, + compositionActive = false +): boolean => + event.key === 'Enter' && !event.shiftKey && !isComposerImeComposing(event, compositionActive); + export const getComposerBlockedSendFeedback = ( blockReason: ComposerSendBlockReason | undefined ): ComposerBlockedSendFeedback | null => {