make the stream previews look alive
This commit is contained in:
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-accent"
|
||||
>
|
||||
<Avatar>
|
||||
<Avatar
|
||||
className={cn(isUnseen && "ring-2 ring-primary")}
|
||||
>
|
||||
<AvatarFallback className="bg-primary/10 text-primary font-medium">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="truncate text-sm font-medium">
|
||||
<p
|
||||
className={cn(
|
||||
"truncate text-sm",
|
||||
isUnseen
|
||||
? "font-semibold text-foreground"
|
||||
: "font-medium text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{particle.properties.name}
|
||||
</p>
|
||||
{latestChild && (
|
||||
<Small className="text-muted-foreground shrink-0">
|
||||
<Small
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
isUnseen ? "text-primary" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{formatDistanceToNow(latestChild.created_at.toISOString())}
|
||||
</Small>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-1">
|
||||
{!latestChild && <Radio className="size-3" />}
|
||||
<Small className="text-muted-foreground truncate">
|
||||
<div className="flex items-center gap-1">
|
||||
<TypeIcon
|
||||
className={cn(
|
||||
"size-3.5 shrink-0",
|
||||
isUnseen ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
<Small
|
||||
className={cn(
|
||||
"truncate",
|
||||
isUnseen
|
||||
? "text-foreground font-medium"
|
||||
: "text-muted-foreground font-normal",
|
||||
)}
|
||||
>
|
||||
{senderPrefix && (
|
||||
<span className="text-muted-foreground">{senderPrefix}</span>
|
||||
)}
|
||||
{subtitle}
|
||||
</Small>
|
||||
</div>
|
||||
</div>
|
||||
{isUnseen && (
|
||||
<span className="size-2 shrink-0 rounded-full bg-primary" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user