diff --git a/src/apps/chat/components/StatusBar.tsx b/src/apps/chat/components/StatusBar.tsx index f03f0470e..1d1785275 100644 --- a/src/apps/chat/components/StatusBar.tsx +++ b/src/apps/chat/components/StatusBar.tsx @@ -114,6 +114,7 @@ function ShortcutItem(props: { shortcut: ShortcutObject }) { {/*{!!props.shortcut.altForNonMac && {_platformAwareModifier('Alt')}}*/} {props.shortcut.key === 'Escape' ? 'Esc' : props.shortcut.key.toUpperCase()}  {props.shortcut.description} + {props.shortcut.endDecoratorIcon && } ); } @@ -128,7 +129,10 @@ export function StatusBar() { // external state const labsShowShortcutBar = useUXLabsStore(state => state.labsShowShortcutBar); const shortcuts = useGlobalShortcutsStore(useShallow(state => { - const visibleShortcuts = !labsShowShortcutBar ? [] : state.getAllShortcuts().filter(shortcut => !!shortcut.description); + let visibleShortcuts = !labsShowShortcutBar ? [] : state.getAllShortcuts().filter(shortcut => !!shortcut.description); + const maxLevel = Math.max(...visibleShortcuts.map(s => s.level ?? 0)); + if (maxLevel > 0) + visibleShortcuts = visibleShortcuts.filter(s => s.level === maxLevel); visibleShortcuts.sort((a, b) => { // if they don't have a 'shift', they are sorted first if (a.shift !== b.shift) diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index 412adaf36..d96cd9102 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -42,7 +42,7 @@ import { supportsScreenCapture } from '~/common/util/screenCaptureUtils'; import { useAppStateStore } from '~/common/state/store-appstate'; import { useChatOverlayStore } from '~/common/chats/store-chat-overlay'; import { useDebouncer } from '~/common/components/useDebouncer'; -import { useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts'; +import { ShortcutKey, ShortcutObject, useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts'; import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; @@ -419,10 +419,28 @@ export function Composer(props: { // useMediaSessionCallbacks({ play: toggleRecognition, pause: toggleRecognition }); - useGlobalShortcuts('Composer', React.useMemo(() => [ - ...(browserSpeechRecognitionCapability().mayWork ? [{ key: 'm', ctrl: true, action: () => toggleRecognition(true), description: recognitionState.isActive ? 'Microphone · Stop & Send' : 'Microphone' }] : []), - ...(supportsClipboardRead ? [{ key: 'v', ctrl: true, shift: true, action: attachAppendClipboardItems, description: 'Attach Clipboard' }] : []), - ], [attachAppendClipboardItems, recognitionState.isActive, toggleRecognition])); + useGlobalShortcuts('Composer', React.useMemo(() => { + const composerShortcuts: ShortcutObject[] = []; + if (supportsClipboardRead) + composerShortcuts.push({ key: 'v', ctrl: true, shift: true, action: attachAppendClipboardItems, description: 'Attach Clipboard' }); + if (recognitionState.isActive) { + composerShortcuts.push({ key: 'm', ctrl: true, action: () => toggleRecognition(true), description: 'Mic · Send', disabled: !recognitionState.hasSpeech, endDecoratorIcon: TelegramIcon as any, level: 1 }); + composerShortcuts.push({ + key: ShortcutKey.Esc, action: () => { + setMicContinuation(false); + toggleRecognition(false); + }, description: 'Mic · Stop', level: 1, + }); + } else if (browserSpeechRecognitionCapability().mayWork) + composerShortcuts.push({ + key: 'm', ctrl: true, action: () => { + // steal focus from the textarea, in case it has - so that enter cannot work against us + (document.activeElement as HTMLElement)?.blur?.(); + toggleRecognition(false); + }, description: 'Microphone', + }); + return composerShortcuts; + }, [attachAppendClipboardItems, recognitionState.hasSpeech, recognitionState.isActive, toggleRecognition])); const micIsRunning = !!speechInterimResult; const micContinuationTrigger = micContinuation && !micIsRunning && !assistantAbortible && !recognitionState.errorMessage; diff --git a/src/common/components/shortcuts/globalShortcutsHandler.ts b/src/common/components/shortcuts/globalShortcutsHandler.ts index 0216d9caa..0552b0124 100644 --- a/src/common/components/shortcuts/globalShortcutsHandler.ts +++ b/src/common/components/shortcuts/globalShortcutsHandler.ts @@ -20,7 +20,7 @@ function _handleGlobalShortcutKeyDown(event: KeyboardEvent) { // Quick-out: either the key is escape/left/right, or we have a modifier key pressed -- otherwise we exit const lcEventKey = event.key.toLowerCase(); if (lcEventKey !== 'escape' && lcEventKey !== 'arrowleft' && lcEventKey !== 'arrowright' && - !event.ctrlKey && !event.shiftKey && !event.altKey) + !event.ctrlKey && !event.shiftKey && !event.altKey && lcEventKey !== 'enter') return; diff --git a/src/common/components/shortcuts/useGlobalShortcuts.ts b/src/common/components/shortcuts/useGlobalShortcuts.ts index d7455ecc4..24c24d146 100644 --- a/src/common/components/shortcuts/useGlobalShortcuts.ts +++ b/src/common/components/shortcuts/useGlobalShortcuts.ts @@ -1,11 +1,14 @@ import * as React from 'react'; +import { SvgIcon } from '@mui/joy'; + import { useGlobalShortcutsStore } from './store-global-shortcuts'; import { ensureGlobalShortcutHandler } from './globalShortcutsHandler'; export const ShortcutKey = { + Enter: 'Enter', Esc: 'Escape', Left: 'ArrowLeft', Right: 'ArrowRight', @@ -19,6 +22,8 @@ export interface ShortcutObject { disabled?: boolean; action: (() => void) | '_specialPrintShortcuts'; description?: string; + endDecoratorIcon?: typeof SvgIcon; + level?: number; // if set, it will exclusively show icons at that level of priority and hide the others }