(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:
lebaudantoine
2024-09-27 12:42:41 +02:00
committed by aleb_the_flash
parent 0dadd472ff
commit 651cc0e5bd
6 changed files with 100 additions and 10 deletions

View File

@@ -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"

View File

@@ -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({}, '')

View 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])
}

View 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})`
}

View 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>(),
})

View File

@@ -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
}