From 4a4e09ae0b866fc18924c375300f64cdc9477abb Mon Sep 17 00:00:00 2001 From: talksik Date: Tue, 7 Apr 2026 15:31:07 -0700 Subject: [PATCH] show stream state in stream-view --- js/src/features/compose/compose-overlay.tsx | 12 ++++ .../particles/stream-context-menu.tsx | 13 +++- js/src/features/particles/stream-view.tsx | 63 ++++++++++++++++--- 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/js/src/features/compose/compose-overlay.tsx b/js/src/features/compose/compose-overlay.tsx index 460d7b5..dedc065 100644 --- a/js/src/features/compose/compose-overlay.tsx +++ b/js/src/features/compose/compose-overlay.tsx @@ -23,6 +23,8 @@ interface ComposeOverlayProps { targetPath?: ParticlePath; onActiveChange?: (active: boolean) => void; onParticleCreated?: (particleId: string) => void; + /** When true, composing is blocked (e.g. stream is closed). */ + disabled?: boolean; } @@ -37,6 +39,7 @@ export function ComposeOverlay({ targetPath, onActiveChange, onParticleCreated, + disabled, }: ComposeOverlayProps) { const [step, setStep] = useState("idle"); const [error, setError] = useState(null); @@ -56,6 +59,8 @@ export function ComposeOverlay({ // Refs for synchronous reads in keyboard handlers const stepRef = useRef(step); const recordStartRef = useRef(0); + const disabledRef = useRef(disabled); + disabledRef.current = disabled; const setStepSync = useCallback((next: ComposeStep) => { stepRef.current = next; @@ -341,6 +346,13 @@ export function ComposeOverlay({ switch (currentStep) { case "idle": { + if (disabledRef.current) { + if ((e.key === "`" && !e.repeat) || e.key === "t" || e.key === "T") { + e.preventDefault(); + toast.info("This stream is closed"); + } + break; + } if (e.key === "`" && !e.repeat) { e.preventDefault(); recordStartRef.current = Date.now(); diff --git a/js/src/features/particles/stream-context-menu.tsx b/js/src/features/particles/stream-context-menu.tsx index d065f3b..3bb065f 100644 --- a/js/src/features/particles/stream-context-menu.tsx +++ b/js/src/features/particles/stream-context-menu.tsx @@ -4,6 +4,7 @@ import { ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu"; +import { CircleCheckBig, CircleDot } from "lucide-react"; import { updateStreamStatus } from "@/lib/firestore-particles"; import { toFirestoreDocPath, particlePath } from "@/lib/particle-path"; import type { StreamParticle } from "@/hooks/use-stream-particles"; @@ -27,7 +28,17 @@ export function StreamContextMenu({ particle, networkId, children }: StreamConte {children} - {isOpen ? "Close stream" : "Open stream"} + {isOpen ? ( + <> + + Close stream + + ) : ( + <> + + Open stream + + )} diff --git a/js/src/features/particles/stream-view.tsx b/js/src/features/particles/stream-view.tsx index 9f1d19d..3319c1d 100644 --- a/js/src/features/particles/stream-view.tsx +++ b/js/src/features/particles/stream-view.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { useAuthStore } from "@/stores/auth-store"; import { apiClient } from "@/api/client"; import type { Particle } from "@/api/types"; -import { parseParticlePath, type ParticlePath } from "@/lib/particle-path"; +import { parseParticlePath, particlePath, toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path"; import { ComposeOverlay } from "@/features/compose/compose-overlay"; import { PlaybackPageIndicator } from "@/features/particles/playback-page-indicator"; import { MediaParticleView } from "@/features/particles/media-particle-view"; @@ -14,7 +14,14 @@ import ControlsIndicator from "@/features/compose/controls-indicator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useNetwork, useNetworks } from "@/hooks/use-networks"; import { Button } from "@/components/ui/button"; -import { Settings } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Settings, CircleCheckBig, CircleDot, EllipsisVertical } from "lucide-react"; +import { updateStreamStatus } from "@/lib/firestore-particles"; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; import { WindowControls } from "@/components/window-controls"; import { RelativeTimestamp } from "@/components/relative-timestamp"; @@ -248,6 +255,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) { networkId={networkId} targetPath={path} onActiveChange={setComposeActive} + disabled={streamParticle.status === "closed"} /> ); @@ -314,6 +322,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) { targetPath={path} onActiveChange={setComposeActive} onParticleCreated={onLocalParticleCreated} + disabled={streamParticle.status === "closed"} /> {/* BottomBar */} @@ -461,14 +470,48 @@ function TopBar({ networkId, particle, streamParticle }: { networkId: string; pa )} - + {streamParticle.status === "closed" && ( + + + Closed + + )} + + + + + + + { + const docPath = toFirestoreDocPath(particlePath(networkId, [streamParticle.id])); + await updateStreamStatus(docPath, streamParticle.status === "open" ? "closed" : "open"); + }} + > + {streamParticle.status === "open" ? ( + <> + + Close stream + + ) : ( + <> + + Open stream + + )} + + navigate("/settings")}> + + Settings + + + ); }