enhance keyboard experience
Now we have network root and stream-view defining their own hints instead of trying to re-use one component. And we also introduce an overlay for additional commands for people.
This commit is contained in:
@@ -10,7 +10,8 @@ import { MediaParticleView, type MediaParticleHandle } from "@/features/particle
|
||||
import { TextParticleView } from "@/features/particles/text-particle-view";
|
||||
import { FallbackParticleView } from "@/features/particles/fallback-particle-view";
|
||||
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
||||
import ControlsIndicator from "@/features/compose/controls-indicator";
|
||||
import { VideoAudioToggle } from "@/components/video-audio-toggle";
|
||||
import { KeybindingsOverlay, type KeybindingGroup } from "@/components/keybindings-overlay";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useNetwork, useNetworks } from "@/hooks/use-networks";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -101,6 +102,34 @@ function useExitCountdown(
|
||||
return remainingMs;
|
||||
}
|
||||
|
||||
// --- Keybindings ---
|
||||
|
||||
const STREAM_VIEW_KEYBINDINGS: KeybindingGroup[] = [
|
||||
{
|
||||
label: "Navigation",
|
||||
bindings: [
|
||||
{ keys: ["←", "→"], description: "Previous / next particle" },
|
||||
{ keys: ["↑", "↓"], description: "Previous / next particle" },
|
||||
{ keys: ["Esc"], description: "Back to network" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Playback",
|
||||
bindings: [
|
||||
{ keys: ["Hold", "Space"], description: "Pause" },
|
||||
{ keys: ["Hold", "Shift"], description: "1.5× speed" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Compose",
|
||||
bindings: [
|
||||
{ keys: ["Hold", "`"], description: "Reply" },
|
||||
{ keys: ["T"], description: "Text compose" },
|
||||
{ keys: ["H"], description: "Join huddle" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// --- StreamView ---
|
||||
|
||||
interface StreamViewProps {
|
||||
@@ -145,6 +174,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
||||
const [composeActive, setComposeActive] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [fastPlayback, setFastPlayback] = useState(false);
|
||||
const [showKeybindings, setShowKeybindings] = useState(false);
|
||||
|
||||
// Show/hide chrome on mouse activity (YouTube-style)
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
@@ -228,6 +258,10 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
||||
navigate(`/${networkId}`);
|
||||
break;
|
||||
}
|
||||
case "?":
|
||||
e.preventDefault();
|
||||
setShowKeybindings((v) => !v);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -252,7 +286,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
},
|
||||
[composeActive, next, prev, pause, resume, navigate, networkId, streamParticle.id],
|
||||
[composeActive, next, prev, pause, resume, navigate, networkId, streamParticle.id, setShowKeybindings],
|
||||
);
|
||||
|
||||
// Click-to-navigate: left 30% = prev, right 70% = next
|
||||
@@ -276,14 +310,10 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
||||
<p className="text-muted-foreground text-sm">
|
||||
No particles in this stream yet
|
||||
</p>
|
||||
<ControlsIndicator type="reply" showEscape={true}>
|
||||
<span>
|
||||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||||
H
|
||||
</kbd>{" "}
|
||||
huddle
|
||||
</span>
|
||||
</ControlsIndicator>
|
||||
<StreamViewControls
|
||||
showEscape
|
||||
onOpenKeybindings={() => setShowKeybindings(true)}
|
||||
/>
|
||||
<ComposeOverlay
|
||||
networkId={networkId}
|
||||
targetPath={path}
|
||||
@@ -373,6 +403,14 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
||||
onGoTo={goTo}
|
||||
presenceBySegment={presenceBySegment}
|
||||
exitRemainingMs={exitRemainingMs}
|
||||
onOpenKeybindings={() => setShowKeybindings(true)}
|
||||
/>
|
||||
|
||||
<KeybindingsOverlay
|
||||
open={showKeybindings}
|
||||
onClose={() => setShowKeybindings(false)}
|
||||
groups={STREAM_VIEW_KEYBINDINGS}
|
||||
title="Stream View"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -386,6 +424,7 @@ function BottomBar({
|
||||
onGoTo,
|
||||
presenceBySegment,
|
||||
exitRemainingMs,
|
||||
onOpenKeybindings,
|
||||
}: {
|
||||
visible: boolean;
|
||||
total: number;
|
||||
@@ -394,6 +433,7 @@ function BottomBar({
|
||||
onGoTo: (index: number) => void;
|
||||
presenceBySegment: Map<number, import("@/hooks/use-presence-positions").HumanPresence[]>;
|
||||
exitRemainingMs: number | null;
|
||||
onOpenKeybindings: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn(
|
||||
@@ -418,7 +458,7 @@ function BottomBar({
|
||||
onGoTo={onGoTo}
|
||||
layer="tracks"
|
||||
/>
|
||||
<div className="flex items-center justify-center px-3 pt-2">
|
||||
<div className="flex items-center justify-center px-3 pt-2 gap-2">
|
||||
{exitRemainingMs !== null && (
|
||||
<div className="flex justify-center">
|
||||
<span className="rounded-full bg-black/30 px-2 text-xs text-white/70 backdrop-blur-sm">
|
||||
@@ -426,20 +466,64 @@ function BottomBar({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<ControlsIndicator type="reply" showEscape={true}>
|
||||
<span>
|
||||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||||
H
|
||||
</kbd>{" "}
|
||||
huddle
|
||||
</span>
|
||||
</ControlsIndicator>
|
||||
<StreamViewControls
|
||||
showEscape
|
||||
onOpenKeybindings={onOpenKeybindings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamViewControls({
|
||||
showEscape,
|
||||
onOpenKeybindings,
|
||||
}: {
|
||||
showEscape?: boolean;
|
||||
onOpenKeybindings: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-4 text-sm text-white/50">
|
||||
{showEscape && (
|
||||
<span>
|
||||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||||
Esc
|
||||
</kbd>{" "}
|
||||
back
|
||||
</span>
|
||||
)}
|
||||
<VideoAudioToggle />
|
||||
<span>
|
||||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||||
Hold `
|
||||
</kbd>{" "}
|
||||
to reply
|
||||
</span>
|
||||
<span>
|
||||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||||
T
|
||||
</kbd>{" "}
|
||||
text
|
||||
</span>
|
||||
<span>
|
||||
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||||
H
|
||||
</kbd>{" "}
|
||||
huddle
|
||||
</span>
|
||||
<kbd
|
||||
role="button"
|
||||
onClick={onOpenKeybindings}
|
||||
className="cursor-pointer rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs transition-colors hover:text-white/80"
|
||||
title="Show all shortcuts"
|
||||
>
|
||||
?
|
||||
</kbd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopBar({ networkId, particle, streamParticle }: { networkId: string; particle: Particle | null; streamParticle: Particle & { type: "stream" } }) {
|
||||
const navigate = useNavigate();
|
||||
const network = useNetwork(networkId);
|
||||
|
||||
Reference in New Issue
Block a user