tidy bottom section of stream-view

This commit is contained in:
talksik
2026-04-01 11:28:54 -07:00
parent 7c77d02ab5
commit e117f58dbe
3 changed files with 121 additions and 59 deletions
+89 -33
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useEffectEvent, useCallback } from "react";
import { useState, useEffect, useEffectEvent, useCallback, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { useAuthStore } from "@/stores/auth-store";
import { apiClient } from "@/api/client";
@@ -21,7 +21,7 @@ import { RelativeTimestamp } from "@/components/relative-timestamp";
import { useStreamPlayback } from "@/hooks/use-stream-playback";
import { usePrefetchAdjacentMedia } from "@/hooks/use-prefetch-adjacent-media";
import { usePresencePositions } from "@/hooks/use-presence-positions";
import { getInitials } from "@/lib/utils";
import { cn, getInitials } from "@/lib/utils";
function getParticleDisplayName(particle: Particle): string {
switch (particle.type) {
@@ -132,6 +132,19 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
const [composeActive, setComposeActive] = useState(false);
const [progress, setProgress] = useState(0);
// Show/hide chrome on mouse activity (YouTube-style)
const [showControls, setShowControls] = useState(true);
const idleTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const handleMouseActivity = useCallback(() => {
setShowControls(true);
clearTimeout(idleTimerRef.current);
idleTimerRef.current = setTimeout(() => setShowControls(false), 3000);
}, []);
useEffect(() => () => clearTimeout(idleTimerRef.current), []);
// Always show controls when compose is active or exit countdown is visible
const controlsVisible = showControls || composeActive || status === "ended";
const handleExitNavigate = useCallback(() => {
navigate(`/${networkId}`);
}, [navigate, networkId]);
@@ -271,13 +284,15 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
}
return (
<div className="relative flex h-screen flex-col bg-black text-white">
{/* Top gradient safe zone — keeps progress bar & breadcrumbs readable */}
<div
className="relative flex h-screen flex-col overflow-hidden bg-black text-white"
onMouseMove={handleMouseActivity}
onMouseLeave={() => setShowControls(false)}
>
{/* Top gradient safe zone */}
<div className="pointer-events-none absolute inset-x-0 top-0 z-[5] h-32 bg-gradient-to-b from-black/60 to-transparent" />
{/* Bottom gradient safe zone — keeps captions & controls readable */}
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-48 bg-gradient-to-t from-black/60 to-transparent" />
{/* TopBar — always visible */}
<div className="z-10 absolute left-0 right-0 mt-3">
<TopBar networkId={networkId} particle={currentParticle} streamParticle={streamParticle} />
</div>
@@ -301,36 +316,77 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
onParticleCreated={onLocalParticleCreated}
/>
{/* Bottom-left: metadata */}
<div className="absolute bottom-0 left-0 z-10 flex items-center gap-2 px-2 pb-2 text-xs text-white/70">
{exitRemainingMs !== null && (
<span className="rounded-full bg-black/30 px-2 py-1 backdrop-blur-sm">
Closing in {Math.ceil(exitRemainingMs / 1000)}s
</span>
)}
</div>
{/* BottomBar */}
<BottomBar
visible={controlsVisible}
total={children.length}
current={currentIndex}
progress={progress}
onGoTo={goTo}
presenceBySegment={presenceBySegment}
exitRemainingMs={exitRemainingMs}
/>
</div>
);
}
{/* Bottom-center: controls */}
<div className="absolute inset-x-0 bottom-0 z-10 flex justify-center pb-3">
<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>
</div>
{/* Progress indicator — bottom */}
<div className="z-10 absolute inset-x-0 bottom-0 mb-12">
function BottomBar({
visible,
total,
current,
progress,
onGoTo,
presenceBySegment,
exitRemainingMs,
}: {
visible: boolean;
total: number;
current: number;
progress: number;
onGoTo: (index: number) => void;
presenceBySegment: Map<number, import("@/hooks/use-presence-positions").HumanPresence[]>;
exitRemainingMs: number | null;
}) {
return (
<div className={cn(
"absolute inset-x-0 bottom-0 z-10 transition-all duration-300",
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-2 pointer-events-none",
)}>
{/* Presence avatars — above the blurred background */}
<PlaybackPageIndicator
total={total}
current={current}
progress={progress}
onGoTo={onGoTo}
presenceBySegment={presenceBySegment}
layer="avatars"
/>
{/* Blurred background container — tracks + controls */}
<div className="pb-3">
<PlaybackPageIndicator
total={children.length}
current={currentIndex}
total={total}
current={current}
progress={progress}
onGoTo={goTo}
presenceBySegment={presenceBySegment}
onGoTo={onGoTo}
layer="tracks"
/>
<div className="flex items-center justify-center px-3 pt-2">
<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>
</div>
{exitRemainingMs !== null && (
<div className="flex justify-center pt-1">
<span className="rounded-full bg-black/30 px-2 py-1 text-xs text-white/70 backdrop-blur-sm">
Closing in {Math.ceil(exitRemainingMs / 1000)}s
</span>
</div>
)}
</div>
</div>
);