import { useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { Radio, MessageSquare, Video, Mic, Image, FileText, CircleCheck, StickyNote, Timer, Headphones, type LucideIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useLiveLatestChild } from "@/hooks/use-particle"; import { useAuthStore } from "@/stores/auth-store"; import { particlePath, type ParticlePath, } from "@/lib/particle-path"; import { getInitials } from "@/lib/utils"; import { RelativeTimestamp } from "@/components/relative-timestamp"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Separator } from "@/components/ui/separator"; import { Progress } from "@/components/ui/progress"; import { Small } from "@/components/ui/typography"; import type { Particle, StreamProperties } from "@/api/types"; import { useNetwork } from "@/hooks/use-networks"; import { useExpiringSoon } from "@/hooks/use-expiring-soon"; import { useStreamParticles } from "@/hooks/use-stream-particles"; import { useStreamAutoplay } from "@/hooks/use-stream-autoplay"; function getParticleTypeIcon(particle: Particle): LucideIcon { switch (particle.type) { case "text": return MessageSquare; case "media": { const mime = particle.properties.mime_type; if (mime.startsWith("video/")) return Video; if (mime.startsWith("audio/")) return Mic; if (mime.startsWith("image/")) return Image; return Video; } case "file": return FileText; case "quest": return CircleCheck; case "paper": return StickyNote; default: return Radio; } } function getMessagePreview(particle: Particle): string { switch (particle.type) { case "text": return particle.properties.content; case "media": { const mime = particle.properties.mime_type; if (mime.startsWith("video/")) return "Video clip"; if (mime.startsWith("audio/")) return "Voice note"; if (mime.startsWith("image/")) return "Photo"; return "Media"; } case "file": return particle.properties.filename; case "quest": return particle.properties.title; case "paper": return particle.properties.title; default: return particle.type; } } function StreamRow({ particle, networkId, onClick, isSelected, shortcutKey, }: { particle: Particle & { type: "stream"; properties: StreamProperties }; networkId: string; onClick: () => void; isSelected?: boolean; shortcutKey?: number; }) { const streamPath = particlePath(networkId, [particle.id]); const { latestChild } = useLiveLatestChild(streamPath); const user = useAuthStore((s) => s.user); const userId = user?.id ?? ""; const network = useNetwork(networkId); useStreamAutoplay(latestChild, particle, networkId, network ?? undefined); const expiringSoon = useExpiringSoon( particle.last_child_created_at, network?.message_retention_hours ?? 24, ); const hasActiveHuddle = particle.huddle_active_participants && particle.huddle_active_participants.length > 0; const huddleCount = particle.huddle_active_participants?.length ?? 0; const isDM = particle.visible_to.length === 2 && particle.visible_to.every((v) => v.startsWith("human:")); const initials = useMemo(() => { if (isDM) { const otherEntry = particle.visible_to.find( (v) => v !== `human:${userId}`, ); if (otherEntry) { const otherId = otherEntry.replace("human:", ""); const otherHuman = network?.humans?.find((h) => h.id === otherId); if (otherHuman) return getInitials(otherHuman.email); } } if (latestChild) { const creator = network?.humans?.find((h) => h.id === latestChild.created_by_human_id); if (creator) return getInitials(creator.email); } return particle.properties.name.slice(0, 2).toUpperCase(); }, [isDM, particle.visible_to, particle.properties.name, userId, latestChild, network]); const isUnseen = useMemo(() => { if (!latestChild) return false; const latestChildTimestamp = latestChild.created_at.getTime(); const userPlaybackPosition = particle.playback_markers?.[userId]?.getTime() ?? 0; return latestChildTimestamp > userPlaybackPosition; }, [latestChild, particle.playback_markers, userId]); const senderPrefix = useMemo(() => { if (!latestChild) return null; const isCurrentUser = latestChild.created_by_human_id === userId; if (isDM) { return isCurrentUser ? "You: " : null; } // Group stream if (isCurrentUser) return "You: "; const creator = network?.humans?.find((h) => h.id === latestChild.created_by_human_id); const name = creator?.email_prefix ?? latestChild.created_by_human_id; const capitalized = name.charAt(0).toUpperCase() + name.slice(1); return `${capitalized}: `; }, [latestChild, userId, isDM, network]); const subtitle = latestChild ? getMessagePreview(latestChild) : particle.properties.status; const TypeIcon = latestChild ? getParticleTypeIcon(latestChild) : Radio; return (
{particle.properties.name}
No recent streams yet. Start a conversation using the keyboard shortcuts below.