This commit is contained in:
Arjun Patel
2026-06-01 14:17:58 -07:00
parent 580703fdf6
commit 52ff92083a
165 changed files with 3736 additions and 2907 deletions
@@ -1,5 +1,12 @@
import { useMemo, useRef, useEffect, useCallback, memo, createElement } from "react";
import { useNavigate } from "react-router-dom";
import {
useMemo,
useRef,
useEffect,
useCallback,
memo,
createElement,
} from 'react';
import { useNavigate } from 'react-router-dom';
import {
Radio,
MessageSquare,
@@ -12,24 +19,28 @@ import {
Headphones,
Trash2,
type LucideIcon,
} from "lucide-react";
import { cn, getInitials } from "@/lib/utils";
import { useLiveLatestChild } from "@/hooks/use-particle";
import { useAuthStore } from "@/stores/auth-store";
import { particlePath } from "@/lib/particle-path";
import { resolveHumanDisplay } from "@/lib/humans";
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 { Button } from "@/components/ui/button";
import { isParticleDeleted, type Particle, type StreamProperties } from "@/api/types";
import type { StreamParticle } from "@/hooks/use-stream-particles";
import { useNetwork } from "@/hooks/use-networks";
import { useStreamAutoplay } from "@/hooks/use-stream-autoplay";
import { useDownloadUrl } from "@/hooks/use-download-url";
import { StreamContextMenu } from "@/features/particles/stream-context-menu";
} from 'lucide-react';
import { cn, getInitials } from '@/lib/utils';
import { useLiveLatestChild } from '@/hooks/use-particle';
import { useAuthStore } from '@/stores/auth-store';
import { particlePath } from '@/lib/particle-path';
import { resolveHumanDisplay } from '@/lib/humans';
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 { Button } from '@/components/ui/button';
import {
isParticleDeleted,
type Particle,
type StreamProperties,
} from '@/api/types';
import type { StreamParticle } from '@/hooks/use-stream-particles';
import { useNetwork } from '@/hooks/use-networks';
import { useStreamAutoplay } from '@/hooks/use-stream-autoplay';
import { useDownloadUrl } from '@/hooks/use-download-url';
import { StreamContextMenu } from '@/features/particles/stream-context-menu';
function VideoThumbnail({
objectId,
@@ -42,8 +53,8 @@ function VideoThumbnail({
return (
<div
className={cn(
"size-8 shrink-0 overflow-hidden rounded-md bg-muted",
isUnseen && "ring-2 ring-primary",
'size-8 shrink-0 overflow-hidden rounded-md bg-muted',
isUnseen && 'ring-2 ring-primary',
)}
>
{url && (
@@ -63,20 +74,20 @@ function VideoThumbnail({
function getParticleTypeIcon(particle: Particle): LucideIcon {
if (isParticleDeleted(particle)) return Trash2;
switch (particle.type) {
case "text":
case 'text':
return MessageSquare;
case "media": {
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;
if (mime.startsWith('video/')) return Video;
if (mime.startsWith('audio/')) return Mic;
if (mime.startsWith('image/')) return Image;
return Video;
}
case "file":
case 'file':
return FileText;
case "quest":
case 'quest':
return CircleCheck;
case "paper":
case 'paper':
return StickyNote;
default:
return Radio;
@@ -84,25 +95,25 @@ function getParticleTypeIcon(particle: Particle): LucideIcon {
}
function getMessagePreview(particle: Particle): string {
if (isParticleDeleted(particle)) return "Deleted particle";
if (isParticleDeleted(particle)) return 'Deleted particle';
switch (particle.type) {
case "text":
case 'text':
return particle.properties.content;
case "media": {
case 'media': {
const mime = particle.properties.mime_type;
if (mime.startsWith("image/")) return "Photo";
if (mime.startsWith("video/") || mime.startsWith("audio/")) {
if (mime.startsWith('image/')) return 'Photo';
if (mime.startsWith('video/') || mime.startsWith('audio/')) {
const transcriptText = particle.properties.transcript?.transcript;
if (transcriptText) return transcriptText;
return mime.startsWith("video/") ? "Video clip" : "Voice note";
return mime.startsWith('video/') ? 'Video clip' : 'Voice note';
}
return "Media";
return 'Media';
}
case "file":
case 'file':
return particle.properties.filename;
case "quest":
case 'quest':
return particle.properties.title;
case "paper":
case 'paper':
return particle.properties.title;
default:
return particle.type;
@@ -116,7 +127,7 @@ const StreamRow = memo(function StreamRow({
isSelected,
shortcutKey,
}: {
particle: Particle & { type: "stream"; properties: StreamProperties };
particle: Particle & { type: 'stream'; properties: StreamProperties };
networkId: string;
onNavigate: (streamId: string) => void;
isSelected?: boolean;
@@ -125,18 +136,19 @@ const StreamRow = memo(function StreamRow({
const streamPath = particlePath(networkId, [particle.id]);
const { latestChild } = useLiveLatestChild(streamPath);
const user = useAuthStore((s) => s.user);
const userId = user?.id ?? "";
const userId = user?.id ?? '';
const network = useNetwork(networkId);
useStreamAutoplay(latestChild, particle, networkId, network ?? undefined);
const hasActiveHuddle =
particle.huddle_active_participants && particle.huddle_active_participants.length > 0;
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:"));
particle.visible_to.every((v) => v.startsWith('human:'));
const initials = useMemo(() => {
if (isDM) {
@@ -144,19 +156,28 @@ const StreamRow = memo(function StreamRow({
(v) => v !== `human:${userId}`,
);
if (otherEntry) {
const otherId = otherEntry.replace("human:", "");
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);
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]);
}, [
isDM,
particle.visible_to,
particle.properties.name,
userId,
latestChild,
network,
]);
const isUnseen = useMemo(() => {
if (!latestChild) return false;
@@ -170,15 +191,16 @@ const StreamRow = memo(function StreamRow({
if (!latestChild) return null;
const isCurrentUser = latestChild.created_by_human_id === userId;
if (isDM) {
return isCurrentUser ? "You: " : null;
return isCurrentUser ? 'You: ' : null;
}
// Group stream
if (isCurrentUser) return "You: ";
if (isCurrentUser) return 'You: ';
const { displayName } = resolveHumanDisplay(
latestChild.created_by_human_id,
network?.humans,
);
const capitalized = displayName.charAt(0).toUpperCase() + displayName.slice(1);
const capitalized =
displayName.charAt(0).toUpperCase() + displayName.slice(1);
return `${capitalized}: `;
}, [latestChild, userId, isDM, network]);
@@ -193,8 +215,8 @@ const StreamRow = memo(function StreamRow({
const videoThumbObjectId =
latestChild &&
!isParticleDeleted(latestChild) &&
latestChild.type === "media" &&
latestChild.properties.mime_type.startsWith("video/")
latestChild.type === 'media' &&
latestChild.properties.mime_type.startsWith('video/')
? latestChild.properties.object_id
: null;
@@ -203,11 +225,13 @@ const StreamRow = memo(function StreamRow({
role="button"
tabIndex={0}
onClick={() => onNavigate(particle.id)}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") onNavigate(particle.id); }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onNavigate(particle.id);
}}
className={cn(
"flex w-full items-center gap-3 px-4 py-3 text-left cursor-pointer transition-colors hover:bg-accent",
isSelected && "bg-accent",
hasActiveHuddle && "bg-gradient-to-r from-red-500/10 to-transparent",
'flex w-full items-center gap-3 px-4 py-3 text-left cursor-pointer transition-colors hover:bg-accent',
isSelected && 'bg-accent',
hasActiveHuddle && 'bg-gradient-to-r from-red-500/10 to-transparent',
)}
>
{shortcutKey && (
@@ -218,7 +242,7 @@ const StreamRow = memo(function StreamRow({
{videoThumbObjectId ? (
<VideoThumbnail objectId={videoThumbObjectId} isUnseen={!!isUnseen} />
) : (
<Avatar className={cn(isUnseen && "ring-2 ring-primary")}>
<Avatar className={cn(isUnseen && 'ring-2 ring-primary')}>
<AvatarFallback className="bg-primary/10 text-primary font-medium">
{initials}
</AvatarFallback>
@@ -228,10 +252,10 @@ const StreamRow = memo(function StreamRow({
<div className="flex items-center justify-between gap-2">
<p
className={cn(
"truncate text-sm",
'truncate text-sm',
isUnseen
? "font-semibold text-foreground"
: "font-medium text-muted-foreground",
? 'font-semibold text-foreground'
: 'font-medium text-muted-foreground',
)}
>
{particle.properties.name}
@@ -240,14 +264,16 @@ const StreamRow = memo(function StreamRow({
{hasActiveHuddle && (
<span className="flex items-center gap-1 rounded-full bg-red-500/15 px-1.5 py-0.5">
<Headphones className="size-3 text-red-400" />
<span className="text-[10px] font-medium text-red-400">{huddleCount}</span>
<span className="text-[10px] font-medium text-red-400">
{huddleCount}
</span>
</span>
)}
{latestChild && (
<Small
className={cn(
"shrink-0",
isUnseen ? "text-primary" : "text-muted-foreground",
'shrink-0',
isUnseen ? 'text-primary' : 'text-muted-foreground',
)}
>
<RelativeTimestamp date={latestChild.created_at} />
@@ -258,16 +284,16 @@ const StreamRow = memo(function StreamRow({
<div className="flex items-center gap-1">
{createElement(typeIcon, {
className: cn(
"size-3.5 shrink-0",
isUnseen ? "text-foreground" : "text-muted-foreground",
'size-3.5 shrink-0',
isUnseen ? 'text-foreground' : 'text-muted-foreground',
),
})}
<Small
className={cn(
"truncate",
'truncate',
isUnseen
? "text-foreground font-medium"
: "text-muted-foreground font-normal",
? 'text-foreground font-medium'
: 'text-muted-foreground font-normal',
)}
>
{senderPrefix && (
@@ -277,9 +303,7 @@ const StreamRow = memo(function StreamRow({
</Small>
</div>
</div>
{isUnseen && (
<span className="size-2 shrink-0 rounded-full bg-primary" />
)}
{isUnseen && <span className="size-2 shrink-0 rounded-full bg-primary" />}
</div>
);
});
@@ -314,8 +338,12 @@ export function ParticleListView({
);
useEffect(() => {
if (selectedIndex !== null && selectedIndex !== undefined && selectedIndex >= 0) {
rowRefs.current[selectedIndex]?.scrollIntoView({ block: "nearest" });
if (
selectedIndex !== null &&
selectedIndex !== undefined &&
selectedIndex >= 0
) {
rowRefs.current[selectedIndex]?.scrollIntoView({ block: 'nearest' });
}
}, [selectedIndex]);
@@ -328,7 +356,8 @@ export function ParticleListView({
<div className="mx-auto flex h-full max-w-sm flex-col items-center justify-center gap-2 px-4 text-center">
<Radio className="text-muted-foreground size-8" />
<p className="text-muted-foreground text-sm">
No streams here. Start a conversation using the keyboard shortcuts below.
No streams here. Start a conversation using the keyboard shortcuts
below.
</p>
</div>
);
@@ -337,9 +366,15 @@ export function ParticleListView({
return (
<div>
{streams.map((stream, index) => (
<StreamContextMenu key={stream.id} particle={stream} networkId={networkId}>
<StreamContextMenu
key={stream.id}
particle={stream}
networkId={networkId}
>
<div
ref={(el) => { rowRefs.current[index] = el; }}
ref={(el) => {
rowRefs.current[index] = el;
}}
>
<StreamRow
particle={stream}