diff --git a/js/src/features/particles/playback-page-indicator.tsx b/js/src/features/particles/playback-page-indicator.tsx index ac5bb80..10cfa27 100644 --- a/js/src/features/particles/playback-page-indicator.tsx +++ b/js/src/features/particles/playback-page-indicator.tsx @@ -1,4 +1,3 @@ -import { cn } from "@/lib/utils"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Tooltip, @@ -15,6 +14,8 @@ interface PlaybackPageIndicatorProps { progress: number; onGoTo: (index: number) => void; presenceBySegment?: Map; + /** Render only avatars or only tracks. Omit to render both. */ + layer?: "avatars" | "tracks"; } export function PlaybackPageIndicator({ @@ -23,42 +24,47 @@ export function PlaybackPageIndicator({ progress, onGoTo, presenceBySegment, + layer, }: PlaybackPageIndicatorProps) { if (total === 0) return null; + const showAvatars = layer !== "tracks"; + const showTracks = layer !== "avatars"; + return (
{Array.from({ length: total }, (_, i) => { const presence = presenceBySegment?.get(i); return (
- {/* Presence avatars above the track */} - {presence && presence.length > 0 && ( + {showAvatars && presence && presence.length > 0 && ( )} - + className="group relative block h-3 w-full" + > + {/* Dim track */} +
+ {/* Fill */} +
+ + )}
); })} diff --git a/js/src/features/particles/stream-view.tsx b/js/src/features/particles/stream-view.tsx index 7f4751a..1e838a9 100644 --- a/js/src/features/particles/stream-view.tsx +++ b/js/src/features/particles/stream-view.tsx @@ -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>(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 ( -
- {/* Top gradient safe zone — keeps progress bar & breadcrumbs readable */} +
setShowControls(false)} + > + {/* Top gradient safe zone */}
- {/* Bottom gradient safe zone — keeps captions & controls readable */} -
- + {/* TopBar — always visible */}
@@ -301,36 +316,77 @@ export function StreamView({ path, streamParticle }: StreamViewProps) { onParticleCreated={onLocalParticleCreated} /> - {/* Bottom-left: metadata */} -
- {exitRemainingMs !== null && ( - - Closing in {Math.ceil(exitRemainingMs / 1000)}s - - )} -
+ {/* BottomBar */} + +
+ ); +} - {/* Bottom-center: controls */} -
- - - - H - {" "} - huddle - - -
- - {/* Progress indicator — bottom */} -
+function BottomBar({ + visible, + total, + current, + progress, + onGoTo, + presenceBySegment, + exitRemainingMs, +}: { + visible: boolean; + total: number; + current: number; + progress: number; + onGoTo: (index: number) => void; + presenceBySegment: Map; + exitRemainingMs: number | null; +}) { + return ( +
+ {/* Presence avatars — above the blurred background */} + + {/* Blurred background container — tracks + controls */} +
+
+ + + + H + {" "} + huddle + + +
+ {exitRemainingMs !== null && ( +
+ + Closing in {Math.ceil(exitRemainingMs / 1000)}s + +
+ )}
); diff --git a/js/src/features/particles/transcript-overlay.tsx b/js/src/features/particles/transcript-overlay.tsx index ee3a944..e18a1fc 100644 --- a/js/src/features/particles/transcript-overlay.tsx +++ b/js/src/features/particles/transcript-overlay.tsx @@ -75,7 +75,7 @@ export function TranscriptOverlay({ return (

{activeChunk.map((word, i) => {