✨(frontend) introduce keyboard shortcut
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.
This commit is contained in:
committed by
aleb_the_flash
parent
0dadd472ff
commit
651cc0e5bd
@@ -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<T extends ToggleSource> =
|
||||
UseTrackToggleProps<T> & {
|
||||
onActiveDeviceChange: (deviceId: string) => void
|
||||
shortcutKey?: string
|
||||
source: SelectToggleSource
|
||||
}
|
||||
|
||||
@@ -63,31 +74,40 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
}
|
||||
|
||||
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 (
|
||||
<HStack gap={0}>
|
||||
<ToggleButton
|
||||
isSelected={enabled}
|
||||
variant={enabled ? undefined : 'danger'}
|
||||
toggledStyles={false}
|
||||
onPress={(e) =>
|
||||
buttonProps.onClick?.(
|
||||
e as unknown as React.MouseEvent<HTMLButtonElement>
|
||||
)
|
||||
}
|
||||
onPress={() => toggle()}
|
||||
aria-label={toggleLabel}
|
||||
tooltip={toggleLabel}
|
||||
groupPosition="left"
|
||||
|
||||
@@ -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({}, '')
|
||||
|
||||
31
src/frontend/src/features/shortcuts/useKeyboardShortcuts.ts
Normal file
31
src/frontend/src/features/shortcuts/useKeyboardShortcuts.ts
Normal file
@@ -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])
|
||||
}
|
||||
21
src/frontend/src/features/shortcuts/utils.ts
Normal file
21
src/frontend/src/features/shortcuts/utils.ts
Normal file
@@ -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})`
|
||||
}
|
||||
11
src/frontend/src/stores/keyboardShortcuts.ts
Normal file
11
src/frontend/src/stores/keyboardShortcuts.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
export type KeyboardShortcutHandler = () => void
|
||||
|
||||
type State = {
|
||||
shortcuts: Map<string, KeyboardShortcutHandler>
|
||||
}
|
||||
|
||||
export const keyboardShortcutsStore = proxy<State>({
|
||||
shortcuts: new Map<string, KeyboardShortcutHandler>(),
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user