From 651cc0e5bd235afd3afbfea3b7497455ea7b00de Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 27 Sep 2024 12:42:41 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20introduce=20keyboard=20sh?= =?UTF-8?q?ortcut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inspired by Gmeet for the UX and by Jitsi for the code. Naive implementation of Keyboard shortcuts listener. Will be enhanced in the upcoming commits. --- .../controls/SelectToggleDevice.tsx | 40 ++++++++++++++----- .../src/features/rooms/routes/Room.tsx | 3 ++ .../shortcuts/useKeyboardShortcuts.ts | 31 ++++++++++++++ src/frontend/src/features/shortcuts/utils.ts | 21 ++++++++++ src/frontend/src/stores/keyboardShortcuts.ts | 11 +++++ src/frontend/src/utils/livekit.ts | 4 ++ 6 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 src/frontend/src/features/shortcuts/useKeyboardShortcuts.ts create mode 100644 src/frontend/src/features/shortcuts/utils.ts create mode 100644 src/frontend/src/stores/keyboardShortcuts.ts diff --git a/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx b/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx index 99a1ff71..fbd77d70 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/SelectToggleDevice.tsx @@ -15,7 +15,14 @@ import { RiVideoOnLine, } from '@remixicon/react' import { Track } from 'livekit-client' -import React from 'react' + +import { useEffect, useMemo } from 'react' + +import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts' +import { + formatShortcutKey, + appendShortcutLabel, +} from '@/features/shortcuts/utils' export type ToggleSource = Exclude< Track.Source, @@ -28,6 +35,7 @@ type SelectToggleDeviceConfig = { kind: MediaDeviceKind iconOn: RemixiconComponentType iconOff: RemixiconComponentType + shortcutKey?: string } type SelectToggleDeviceConfigMap = { @@ -39,17 +47,20 @@ const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = { kind: 'audioinput', iconOn: RiMicLine, iconOff: RiMicOffLine, + shortcutKey: 'd', }, [Track.Source.Camera]: { kind: 'videoinput', iconOn: RiVideoOnLine, iconOff: RiVideoOffLine, + shortcutKey: 'e', }, } type SelectToggleDeviceProps = UseTrackToggleProps & { onActiveDeviceChange: (deviceId: string) => void + shortcutKey?: string source: SelectToggleSource } @@ -63,31 +74,40 @@ export const SelectToggleDevice = ({ } const { t } = useTranslation('rooms', { keyPrefix: 'join' }) - const { buttonProps, enabled } = useTrackToggle(props) + const { toggle, enabled } = useTrackToggle(props) const { kind, iconOn, iconOff } = config const { devices, activeDeviceId, setActiveMediaDevice } = useMediaDeviceSelect({ kind }) - const toggleLabel = t(enabled ? 'disable' : 'enable', { - keyPrefix: `join.${kind}`, - }) + const toggleLabel = useMemo(() => { + const label = t(enabled ? 'disable' : 'enable', { + keyPrefix: `join.${kind}`, + }) + return config.shortcutKey + ? appendShortcutLabel(label, config.shortcutKey, true) + : label + }, [enabled, kind, config.shortcutKey, t]) const selectLabel = t('choose', { keyPrefix: `join.${kind}` }) const Icon = enabled ? iconOn : iconOff + useEffect(() => { + if (!config.shortcutKey) return + keyboardShortcutsStore.shortcuts.set( + formatShortcutKey(config.shortcutKey, true), + () => toggle() + ) + }, [toggle, config.shortcutKey]) + return ( - buttonProps.onClick?.( - e as unknown as React.MouseEvent - ) - } + onPress={() => toggle()} aria-label={toggleLabel} tooltip={toggleLabel} groupPosition="left" diff --git a/src/frontend/src/features/rooms/routes/Room.tsx b/src/frontend/src/features/rooms/routes/Room.tsx index 629ba2c1..2424495e 100644 --- a/src/frontend/src/features/rooms/routes/Room.tsx +++ b/src/frontend/src/features/rooms/routes/Room.tsx @@ -8,6 +8,7 @@ import { ErrorScreen } from '@/components/ErrorScreen' import { useUser, UserAware } from '@/features/auth' import { Conference } from '../components/Conference' import { Join } from '../components/Join' +import { useKeyboardShortcuts } from '@/features/shortcuts/useKeyboardShortcuts' export const Room = () => { const { isLoggedIn } = useUser() @@ -19,6 +20,8 @@ export const Room = () => { const mode = isLoggedIn && history.state?.create ? 'create' : 'join' const skipJoinScreen = isLoggedIn && mode === 'create' + useKeyboardShortcuts() + const clearRouterState = () => { if (window?.history?.state) { window.history.replaceState({}, '') diff --git a/src/frontend/src/features/shortcuts/useKeyboardShortcuts.ts b/src/frontend/src/features/shortcuts/useKeyboardShortcuts.ts new file mode 100644 index 00000000..26ec4df6 --- /dev/null +++ b/src/frontend/src/features/shortcuts/useKeyboardShortcuts.ts @@ -0,0 +1,31 @@ +import { useEffect } from 'react' +import { useSnapshot } from 'valtio' +import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts' +import { isMacintosh } from '@/utils/livekit' +import { formatShortcutKey } from './utils' + +export const useKeyboardShortcuts = () => { + const shortcutsSnap = useSnapshot(keyboardShortcutsStore) + + useEffect(() => { + // This approach handles basic shortcuts but isn't comprehensive. + // Issues might occur. First draft. + const onKeyDown = (e: KeyboardEvent) => { + const { key, metaKey, ctrlKey } = e + const shortcutKey = formatShortcutKey( + key, + ctrlKey || (isMacintosh() && metaKey) + ) + const shortcut = shortcutsSnap.shortcuts.get(shortcutKey) + if (!shortcut) return + e.preventDefault() + shortcut() + } + + window.addEventListener('keydown', onKeyDown) + + return () => { + window.removeEventListener('keydown', onKeyDown) + } + }, [shortcutsSnap]) +} diff --git a/src/frontend/src/features/shortcuts/utils.ts b/src/frontend/src/features/shortcuts/utils.ts new file mode 100644 index 00000000..e24e8949 --- /dev/null +++ b/src/frontend/src/features/shortcuts/utils.ts @@ -0,0 +1,21 @@ +import { isMacintosh } from '@/utils/livekit' + +export const CTRL = 'ctrl' + +export const formatShortcutKey = (key: string, ctrlKey?: boolean) => { + if (ctrlKey) return `${CTRL}+${key.toUpperCase()}` + return key.toUpperCase() +} + +export const appendShortcutLabel = ( + label: string, + key: string, + ctrlKey?: boolean +) => { + if (!key) return + let formattedKeyLabel = key.toLowerCase() + if (ctrlKey) { + formattedKeyLabel = `${isMacintosh() ? '⌘' : 'Ctrl'}+${formattedKeyLabel}` + } + return `${label} (${formattedKeyLabel})` +} diff --git a/src/frontend/src/stores/keyboardShortcuts.ts b/src/frontend/src/stores/keyboardShortcuts.ts new file mode 100644 index 00000000..5cf8087a --- /dev/null +++ b/src/frontend/src/stores/keyboardShortcuts.ts @@ -0,0 +1,11 @@ +import { proxy } from 'valtio' + +export type KeyboardShortcutHandler = () => void + +type State = { + shortcuts: Map +} + +export const keyboardShortcutsStore = proxy({ + shortcuts: new Map(), +}) diff --git a/src/frontend/src/utils/livekit.ts b/src/frontend/src/utils/livekit.ts index cd37e731..af933bcb 100644 --- a/src/frontend/src/utils/livekit.ts +++ b/src/frontend/src/utils/livekit.ts @@ -25,3 +25,7 @@ export function isSafari(): boolean { export function isLocal(p: Participant) { return p instanceof LocalParticipant } + +export function isMacintosh() { + return navigator.platform.indexOf('Mac') > -1 +}