From bea723a90ac6e56cc80eaa5457e99cff5ee21fe1 Mon Sep 17 00:00:00 2001 From: talksik Date: Sun, 12 Apr 2026 11:25:35 -0700 Subject: [PATCH] refactor: extract topbar component into separate file --- js/src/features/particles/stream-top-bar.tsx | 278 ++++++++++++++++++ js/src/features/particles/stream-view.tsx | 288 +------------------ 2 files changed, 282 insertions(+), 284 deletions(-) create mode 100644 js/src/features/particles/stream-top-bar.tsx diff --git a/js/src/features/particles/stream-top-bar.tsx b/js/src/features/particles/stream-top-bar.tsx new file mode 100644 index 0000000..3d1c7d2 --- /dev/null +++ b/js/src/features/particles/stream-top-bar.tsx @@ -0,0 +1,278 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuthStore } from "@/stores/auth-store"; +import { apiClient } from "@/api/client"; +import type { Particle } from "@/api/types"; +import { particlePath, toFirestoreDocPath } from "@/lib/particle-path"; +import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { useNetwork } from "@/hooks/use-networks"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Settings, CircleCheckBig, CircleDot, EllipsisVertical, Pencil, Globe } from "lucide-react"; +import { updateStreamStatus } from "@/lib/firestore-particles"; +import { RenameStreamOverlay } from "@/features/particles/rename-stream-overlay"; +import { StreamMembersOverlay } from "@/features/particles/stream-members-overlay"; +import { parseVisibleTo } from "@/lib/stream-visibility"; +import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; +import { WindowControls } from "@/components/window-controls"; +import { RelativeTimestamp } from "@/components/relative-timestamp"; +import { useStreamPresence } from "@/features/particles/stream-presence-context"; +import { getInitials } from "@/lib/utils"; + +function getParticleDisplayName(particle: Particle): string { + switch (particle.type) { + case "stream": + case "folder": + return particle.properties.name; + case "quest": + return particle.properties.title; + case "paper": + return particle.properties.title; + case "file": + return particle.properties.filename; + case "text": + return particle.properties.content.slice(0, 30); + case "media": + return particle.type; + } +} + +interface TopBarProps { + networkId: string; + particle: Particle | null; + streamParticle: Particle & { type: "stream" }; +} + +export function TopBar({ networkId, particle, streamParticle }: TopBarProps) { + const navigate = useNavigate(); + const network = useNetwork(networkId); + const userId = useAuthStore((s) => s.user?.id); + const isCreator = !!userId && userId === streamParticle.created_by_human_id; + const [renameOpen, setRenameOpen] = useState(false); + const [membersOpen, setMembersOpen] = useState(false); + + const huddleParticipants = streamParticle.huddle_active_participants ?? []; + const hasActiveHuddle = huddleParticipants.length > 0; + + const handleJoinHuddle = () => { + apiClient.getLivekitToken(networkId, streamParticle.id).then(({ token, server_url }) => { + window.electronWindow.openHuddle({ token, serverUrl: server_url }); + }); + }; + + return ( +
+ + + + + {streamParticle && ( + <> + + {getParticleDisplayName(streamParticle)} + + + )} + + {particle && ( + <> + + + + + + )} + + + + {hasActiveHuddle && ( + + )} + + {streamParticle.status === "closed" && ( + + + Closed + + )} + + setMembersOpen(true)} + /> + + + + + + + { + const docPath = toFirestoreDocPath(particlePath(networkId, [streamParticle.id])); + await updateStreamStatus(docPath, streamParticle.status === "open" ? "closed" : "open"); + }} + > + {streamParticle.status === "open" ? ( + <> + + Close stream + + ) : ( + <> + + Open stream + + )} + + {isCreator && ( + setRenameOpen(true)}> + + Rename stream + + )} + navigate("/settings")}> + + Settings + + + + + {renameOpen && isCreator && ( + setRenameOpen(false)} + /> + )} + + {membersOpen && ( + setMembersOpen(false)} + /> + )} +
+ ); +} + +function MembersIndicator({ + networkId, + streamParticle, + onClick, +}: { + networkId: string; + streamParticle: Particle & { type: "stream" }; + onClick: () => void; +}) { + const network = useNetwork(networkId); + const visibility = parseVisibleTo(streamParticle.visible_to, networkId); + const humans = network?.humans ?? []; + + const memberIds = + visibility.mode === "network" + ? humans.map((h) => h.id) + : visibility.humanIds; + const shownMembers = memberIds + .slice(0, 3) + .map((id) => humans.find((h) => h.id === id)) + .filter((h): h is NonNullable => !!h); + const overflow = memberIds.length - shownMembers.length; + + return ( + + + + + + {visibility.mode === "network" + ? `Everyone in ${network?.name ?? "network"}` + : `${memberIds.length} ${memberIds.length === 1 ? "member" : "members"}`} + + + ); +} + +function ParticleBreadcrumbContent({ particle, networkId }: { particle: Particle; networkId: string }) { + const network = useNetwork(networkId); + const { onlineHumanIds } = useStreamPresence(); + const creator = network?.humans?.find((h) => h.id === particle.created_by_human_id); + const prefix = creator?.email_prefix ?? particle.created_by_human_id; + const initials = prefix.slice(0, 2).toUpperCase(); + const isOnline = particle.created_by_human_id ? onlineHumanIds.has(particle.created_by_human_id) : false; + + return ( + + + + {initials} + + + {prefix} - + + ); +} diff --git a/js/src/features/particles/stream-view.tsx b/js/src/features/particles/stream-view.tsx index 7c8f3db..e57d545 100644 --- a/js/src/features/particles/stream-view.tsx +++ b/js/src/features/particles/stream-view.tsx @@ -9,34 +9,19 @@ import { PlaybackPageIndicator } from "@/features/particles/playback-page-indica import { MediaParticleView, type MediaParticleHandle } from "@/features/particles/media-particle-view"; 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 { VideoAudioToggle } from "@/components/video-audio-toggle"; import { useMediaSettingsStore } from "@/stores/media-settings-store"; 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"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Settings, CircleCheckBig, CircleDot, EllipsisVertical, Pencil, Globe } from "lucide-react"; -import { updateStreamStatus, toggleParticleReaction } from "@/lib/firestore-particles"; -import { RenameStreamOverlay } from "@/features/particles/rename-stream-overlay"; -import { StreamMembersOverlay } from "@/features/particles/stream-members-overlay"; -import { parseVisibleTo } from "@/lib/stream-visibility"; +import { useNetwork } from "@/hooks/use-networks"; +import { toggleParticleReaction } from "@/lib/firestore-particles"; import { ReactionBar } from "@/features/particles/reaction-bar"; -import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; -import { WindowControls } from "@/components/window-controls"; -import { RelativeTimestamp } from "@/components/relative-timestamp"; +import { TopBar } from "@/features/particles/stream-top-bar"; import { useStreamPlayback } from "@/hooks/use-stream-playback"; import { usePrefetchAdjacentMedia } from "@/hooks/use-prefetch-adjacent-media"; import { usePresencePositions } from "@/hooks/use-presence-positions"; import { StreamPresenceProvider, useStreamPresence, useStreamComposing, useStreamComposingBroadcast, type ComposingMode } from "@/features/particles/stream-presence-context"; import { ComposingIndicator } from "@/components/composing-indicator"; -import { cn, getInitials } from "@/lib/utils"; +import { cn } from "@/lib/utils"; import { useMount } from "react-use"; function getReactions(particle: Particle): Record | undefined { @@ -44,24 +29,6 @@ function getReactions(particle: Particle): Record | undefined return undefined; } -function getParticleDisplayName(particle: Particle): string { - switch (particle.type) { - case "stream": - case "folder": - return particle.properties.name; - case "quest": - return particle.properties.title; - case "paper": - return particle.properties.title; - case "file": - return particle.properties.filename; - case "text": - return particle.properties.content.slice(0, 30); - case "media": - return particle.type; - } -} - // --- Exit countdown hook --- const EXIT_DELAY_MS = 5000; @@ -623,250 +590,3 @@ function StreamViewControls({ ); } - -function TopBar({ networkId, particle, streamParticle }: { networkId: string; particle: Particle | null; streamParticle: Particle & { type: "stream" } }) { - const navigate = useNavigate(); - const network = useNetwork(networkId); - const userId = useAuthStore((s) => s.user?.id); - const isCreator = !!userId && userId === streamParticle.created_by_human_id; - const [renameOpen, setRenameOpen] = useState(false); - const [membersOpen, setMembersOpen] = useState(false); - - const huddleParticipants = streamParticle.huddle_active_participants ?? []; - const hasActiveHuddle = huddleParticipants.length > 0; - - const handleJoinHuddle = () => { - apiClient.getLivekitToken(networkId, streamParticle.id).then(({ token, server_url }) => { - window.electronWindow.openHuddle({ token, serverUrl: server_url }); - }); - }; - - return ( -
- - - - - {streamParticle && ( - <> - - {getParticleDisplayName(streamParticle)} - - - )} - - {particle && ( - <> - - - - - - )} - - - - {hasActiveHuddle && ( - - )} - - {streamParticle.status === "closed" && ( - - - Closed - - )} - - setMembersOpen(true)} - /> - - - - - - - { - const docPath = toFirestoreDocPath(particlePath(networkId, [streamParticle.id])); - await updateStreamStatus(docPath, streamParticle.status === "open" ? "closed" : "open"); - }} - > - {streamParticle.status === "open" ? ( - <> - - Close stream - - ) : ( - <> - - Open stream - - )} - - {isCreator && ( - setRenameOpen(true)}> - - Rename stream - - )} - navigate("/settings")}> - - Settings - - - - - {renameOpen && isCreator && ( - setRenameOpen(false)} - /> - )} - - {membersOpen && ( - setMembersOpen(false)} - /> - )} -
- ); -} - -function MembersIndicator({ - networkId, - streamParticle, - onClick, -}: { - networkId: string; - streamParticle: Particle & { type: "stream" }; - onClick: () => void; -}) { - const network = useNetwork(networkId); - const visibility = parseVisibleTo(streamParticle.visible_to, networkId); - const humans = network?.humans ?? []; - - const memberIds = - visibility.mode === "network" - ? humans.map((h) => h.id) - : visibility.humanIds; - const shownMembers = memberIds - .slice(0, 3) - .map((id) => humans.find((h) => h.id === id)) - .filter((h): h is NonNullable => !!h); - const overflow = memberIds.length - shownMembers.length; - - return ( - - - - - - {visibility.mode === "network" - ? `Everyone in ${network?.name ?? "network"}` - : `${memberIds.length} ${memberIds.length === 1 ? "member" : "members"}`} - - - ); -} - -function NetworkBreadcrumbContent({ networkId }: { networkId: string }) { - const { data: networks } = useNetworks(); - const network = networks?.find((n) => n.id === networkId); - const name = network?.name ?? networkId; - const initials = name.slice(0, 2).toUpperCase(); - - return ( - - - - {initials} - - - {name} - - ); -} - -function ParticleBreadcrumbContent({ particle, networkId }: { particle: Particle; networkId: string }) { - const network = useNetwork(networkId); - const { onlineHumanIds } = useStreamPresence(); - const creator = network?.humans?.find((h) => h.id === particle.created_by_human_id); - const prefix = creator?.email_prefix ?? particle.created_by_human_id; - const initials = prefix.slice(0, 2).toUpperCase(); - const isOnline = particle.created_by_human_id ? onlineHumanIds.has(particle.created_by_human_id) : false; - - return ( - - - - {initials} - - - {prefix} - - - ) -} -