From 7c26e225b54d73620526011a8d4702f4b30f6ce1 Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 19 Mar 2026 09:09:12 -0700 Subject: [PATCH] make the stream previews look alive --- .../features/particles/particle-list-view.tsx | 134 ++++++++++++++++-- 1 file changed, 122 insertions(+), 12 deletions(-) diff --git a/js/src/features/particles/particle-list-view.tsx b/js/src/features/particles/particle-list-view.tsx index 0d5f157..13994ff 100644 --- a/js/src/features/particles/particle-list-view.tsx +++ b/js/src/features/particles/particle-list-view.tsx @@ -1,7 +1,19 @@ import { useMemo } from "react"; import { useNavigate } from "react-router-dom"; -import { Radio } from "lucide-react"; +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, @@ -16,12 +28,39 @@ 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": - return "Clip received"; + 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": @@ -44,44 +83,115 @@ function StreamRow({ }) { const streamPath = particlePath(networkId, [particle.id]); const { latestChild } = useLiveLatestChild(streamPath); + const user = useAuthStore((s) => s.user); + const userEmail = user?.email ?? ""; - const initials = latestChild - ? getInitials(latestChild.created_by_email) - : particle.properties.name.slice(0, 2).toUpperCase(); + 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 ( ); }