import { useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { Radio, MessageSquare, Video, Mic, Image, FileText, CircleCheck, StickyNote, type LucideIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useLiveParticleChildren, useLiveLatestChild } from "@/hooks/use-particle"; import { useAuthStore } from "@/stores/auth-store"; import { parseParticlePath, particlePath, type ParticlePath, } from "@/lib/particle-path"; import { getInitials } from "@/lib/utils"; import { formatDistanceToNow } from "@/lib/time-utils"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { ScrollArea } from "@/components/ui/scroll-area"; 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"; 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, }: { particle: Particle & { type: "stream"; properties: StreamProperties }; networkId: string; onClick: () => void; }) { const streamPath = particlePath(networkId, [particle.id]); const { latestChild } = useLiveLatestChild(streamPath); const user = useAuthStore((s) => s.user); const userEmail = user?.email ?? ""; 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:${userEmail}`, ); if (otherEntry) { const otherEmail = otherEntry.replace("human:", ""); return getInitials(otherEmail); } } return particle.properties.name.slice(0, 2).toUpperCase(); }, [isDM, particle.visible_to, particle.properties.name, userEmail]); const isUnseen = useMemo(() => { if (!latestChild) return false; const latestChildTimestamp = latestChild.created_at.getTime(); const userPlaybackPosition = particle.markers?.[userEmail]?.getTime() ?? 0; return latestChildTimestamp > userPlaybackPosition; }, [latestChild, particle.markers, userEmail]); const senderPrefix = useMemo(() => { if (!latestChild) return null; const isCurrentUser = latestChild.created_by_email === userEmail; if (isDM) { return isCurrentUser ? "You: " : null; } // Group stream if (isCurrentUser) return "You: "; const emailPrefix = latestChild.created_by_email.split("@")[0]; const capitalized = emailPrefix.charAt(0).toUpperCase() + emailPrefix.slice(1); return `${capitalized}: `; }, [latestChild, userEmail, isDM]); const subtitle = latestChild ? getMessagePreview(latestChild) : particle.properties.status; const TypeIcon = latestChild ? getParticleTypeIcon(latestChild) : Radio; return ( {initials} {particle.properties.name} {latestChild && ( {formatDistanceToNow(latestChild.created_at.toISOString())} )} {senderPrefix && ( {senderPrefix} )} {subtitle} {isUnseen && ( )} ); } interface ParticleListViewProps { path: ParticlePath; } /** * List of stream particles for a container (network root, folder, etc.). */ export function ParticleListView({ path }: ParticleListViewProps) { const { children, isLoading } = useLiveParticleChildren(path, "last_child_created_at", "desc"); const { networkId } = parseParticlePath(path); const navigate = useNavigate(); const streams = useMemo( () => children.filter((c) => c.type === "stream"), [children], ); if (isLoading) { return ; } return ( {streams.map((stream, index) => ( navigate(`/${networkId}/${stream.id}`)} /> {index < streams.length - 1 && } ))} ); }
{particle.properties.name}