import {
Fragment,
useMemo,
useRef,
useEffect,
memo,
createElement,
} from 'react';
import { Headphones, Radio, FolderIcon } 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 { HumanAvatar } from '@/components/human-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 { useNetwork } from '@/hooks/use-networks';
import { useStreamAutoplay } from '@/hooks/use-stream-autoplay';
import { useDownloadUrl } from '@/hooks/use-download-url';
import { getMessagePreview, getParticleTypeIcon } from '@/lib/particle-display';
import { StreamContextMenu } from '@/features/particles/stream-context-menu';
type StreamParticle = Particle & {
type: 'stream';
properties: StreamProperties;
};
function VideoThumbnail({
objectId,
isUnseen,
}: {
objectId: string;
isUnseen: boolean;
}) {
const { data: url } = useDownloadUrl(objectId);
return (
{url && (
)}
);
}
const StreamRow = memo(function StreamRow({
particle,
networkId,
onOpen,
isSelected,
shortcutKey,
}: {
particle: StreamParticle;
networkId: string;
onOpen: (particleId: string) => 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 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 avatar = 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 {
initials: getInitials(otherHuman.email),
avatarObjectId: otherHuman.avatar_object_id ?? null,
};
}
}
}
if (latestChild) {
const creator = network?.humans?.find(
(h) => h.id === latestChild.created_by_human_id,
);
if (creator) {
return {
initials: getInitials(creator.email),
avatarObjectId: creator.avatar_object_id ?? null,
};
}
}
return {
initials: particle.properties.name.slice(0, 2).toUpperCase(),
avatarObjectId: null,
};
}, [
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 { displayName } = resolveHumanDisplay(
latestChild.created_by_human_id,
network?.humans,
);
const capitalized =
displayName.charAt(0).toUpperCase() + displayName.slice(1);
return `${capitalized}: `;
}, [latestChild, userId, isDM, network]);
const subtitle = latestChild
? getMessagePreview(latestChild)
: particle.properties.name;
// Rendered via createElement below: a call-result used directly as a JSX tag
// is flagged as a dynamically-created component.
const typeIcon = latestChild ? getParticleTypeIcon(latestChild) : Radio;
const videoThumbObjectId =
latestChild &&
!isParticleDeleted(latestChild) &&
latestChild.type === 'media' &&
latestChild.properties.mime_type.startsWith('video/')
? latestChild.properties.object_id
: null;
return (
onOpen(particle.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onOpen(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',
)}
>
{shortcutKey && (
{shortcutKey}
)}
{videoThumbObjectId ? (
) : (
)}
{particle.properties.name}
{hasActiveHuddle && (
{huddleCount}
)}
{latestChild && (
)}
{createElement(typeIcon, {
className: cn(
'size-3.5 shrink-0',
isUnseen ? 'text-foreground' : 'text-muted-foreground',
),
})}
{senderPrefix && (
{senderPrefix}
)}
{subtitle}
{isUnseen &&
}
);
});
const FolderRow = memo(function FolderRow({
particle,
networkId,
onOpen,
isSelected,
shortcutKey,
}: {
particle: Particle & { type: 'folder' };
networkId: string;
onOpen: (particleId: string) => void;
isSelected?: boolean;
shortcutKey?: number;
}) {
const network = useNetwork(networkId);
const creator = resolveHumanDisplay(
particle.created_by_human_id,
network?.humans,
);
return (
onOpen(particle.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onOpen(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',
)}
>
{shortcutKey && (
{shortcutKey}
)}
{particle.properties.name}
Folder · {creator.displayName}
);
});
const LeafRow = memo(function LeafRow({
particle,
networkId,
onOpen,
isSelected,
shortcutKey,
}: {
particle: Particle;
networkId: string;
onOpen: (particleId: string) => void;
isSelected?: boolean;
shortcutKey?: number;
}) {
const network = useNetwork(networkId);
const creator = resolveHumanDisplay(
particle.created_by_human_id,
network?.humans,
);
const typeIcon = getParticleTypeIcon(particle);
const deleted = isParticleDeleted(particle);
const taskDone = particle.type === 'task' && particle.properties.done;
return (
onOpen(particle.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onOpen(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',
)}
>
{shortcutKey && (
{shortcutKey}
)}
{createElement(typeIcon, {
className: cn(
'size-4 shrink-0 text-muted-foreground',
taskDone && 'text-emerald-500',
),
})}
{getMessagePreview(particle)}
{creator.displayName}
);
});
interface ParticleChildrenListProps {
items: Particle[];
networkId: string;
isLoading: boolean;
/** Open (navigate into / select) a particle by id. */
onOpen: (particleId: string) => void;
selectedIndex?: number | null;
/** Render 1–9 shortcut badges next to the first nine rows. */
showShortcuts?: boolean;
emptyMessage?: string;
/** When true, render a footer that invokes onLoadMore. */
canLoadMore?: boolean;
onLoadMore?: () => void;
}
/**
* Browsable list of a container's children, any particle type. Used by the
* network root and folder views.
*/
export function ParticleChildrenList({
items,
networkId,
isLoading,
onOpen,
selectedIndex,
showShortcuts = true,
emptyMessage = 'Nothing here yet. Create something using the keyboard shortcuts below.',
canLoadMore,
onLoadMore,
}: ParticleChildrenListProps) {
const rowRefs = useRef>([]);
useEffect(() => {
if (
selectedIndex !== null &&
selectedIndex !== undefined &&
selectedIndex >= 0
) {
rowRefs.current[selectedIndex]?.scrollIntoView({ block: 'nearest' });
}
}, [selectedIndex]);
if (isLoading) {
return ;
}
if (items.length === 0) {
return (
);
}
return (
{items.map((item, index) => {
const rowProps = {
networkId,
onOpen,
isSelected: index === selectedIndex,
shortcutKey: showShortcuts && index < 9 ? index + 1 : undefined,
};
const row = (
{
rowRefs.current[index] = el;
}}
>
{item.type === 'stream' ? (
) : item.type === 'folder' ? (
) : (
)}
{index < items.length - 1 && }
);
return item.type === 'stream' ? (
{row}
) : (
{row}
);
})}
{canLoadMore && onLoadMore && (
)}
);
}