Introduce a minimal, skippable first-run wizard that teaches the record key. On first authenticated launch it renders as a full-screen takeover (gated above PusherProvider) so the stream/compose keyboard handlers are not mounted to compete for the same keys. Steps: welcome, hold ` to record, tap V to switch audio/video, tap ` to toggle recording, mic/camera permission priming, and a recap. The lessons detect the real gestures and never block — Esc or the Skip control dismisses at any point. Reuses existing pieces: VideoAudioToggle, useMediaDevices().requestLabels for permission priming, KeyHint, and isTypingTarget. Extracts the shared HOLD_THRESHOLD_MS into lib/constants and reuses it in the compose overlay. Resolves #259 Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01A9AHySdLb1zfZAEm6SuaQP
53 lines
1.5 KiB
TypeScript
53 lines
1.5 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { isTypingTarget } from '@/lib/keyboard';
|
|
|
|
interface UseTapGestureOptions {
|
|
/** Detected against KeyboardEvent.key. */
|
|
targetKey: string;
|
|
/** Optional KeyboardEvent.code fallback (e.g. 'Backquote'). */
|
|
targetCode?: string;
|
|
/** Taps required before `satisfied` flips true. Defaults to 1. */
|
|
taps?: number;
|
|
/** Fires on each counted tap, with the new running count. */
|
|
onTap?: (count: number) => void;
|
|
}
|
|
|
|
/**
|
|
* Counts discrete taps of a single key, ignoring auto-repeat. Used both for the
|
|
* one-tap audio/video toggle and the two-tap start/stop demonstration.
|
|
*/
|
|
export function useTapGesture({
|
|
targetKey,
|
|
targetCode,
|
|
taps = 1,
|
|
onTap,
|
|
}: UseTapGestureOptions) {
|
|
const [count, setCount] = useState(0);
|
|
|
|
const onTapRef = useRef(onTap);
|
|
useEffect(() => {
|
|
onTapRef.current = onTap;
|
|
});
|
|
|
|
useEffect(() => {
|
|
const matches = (e: KeyboardEvent) =>
|
|
e.key === targetKey || (targetCode != null && e.code === targetCode);
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.repeat || isTypingTarget(e) || !matches(e)) return;
|
|
e.preventDefault();
|
|
setCount((prev) => {
|
|
if (prev >= taps) return prev;
|
|
const next = prev + 1;
|
|
onTapRef.current?.(next);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [targetKey, targetCode, taps]);
|
|
|
|
return { taps: count, satisfied: count >= taps };
|
|
}
|