This commit is contained in:
Arjun Patel
2026-06-12 12:20:25 -07:00
parent 224b64f86b
commit 044f1a7457
7 changed files with 43 additions and 30 deletions
@@ -2,21 +2,25 @@ import { useCallback, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { ConfirmDestructiveOverlay } from '@/components/confirm-destructive-overlay'; import { ConfirmDestructiveOverlay } from '@/components/confirm-destructive-overlay';
import { softDeleteParticle } from '@/lib/firestore-particles'; import { softDeleteParticle } from '@/lib/firestore-particles';
import { particlePath, toFirestoreDocPath } from '@/lib/particle-path'; import {
particlePath,
parseParticlePath,
toFirestoreDocPath,
type ParticlePath,
} from '@/lib/particle-path';
import type { Particle } from '@/api/types'; import type { Particle } from '@/api/types';
import { useSuspendPlayback } from '@/hooks/use-suspend-playback'; import { useSuspendPlayback } from '@/hooks/use-suspend-playback';
interface DeleteParticleOverlayProps { interface DeleteParticleOverlayProps {
networkId: string; /** Path of the stream the particle lives in — may be nested. */
streamId: string; streamPath: ParticlePath;
particle: Particle; particle: Particle;
userId: string; userId: string;
onClose: () => void; onClose: () => void;
} }
export function DeleteParticleOverlay({ export function DeleteParticleOverlay({
networkId, streamPath,
streamId,
particle, particle,
userId, userId,
onClose, onClose,
@@ -29,8 +33,9 @@ export function DeleteParticleOverlay({
if (deleting) return; if (deleting) return;
setDeleting(true); setDeleting(true);
try { try {
const { networkId, segments } = parseParticlePath(streamPath);
const docPath = toFirestoreDocPath( const docPath = toFirestoreDocPath(
particlePath(networkId, [streamId, particle.id]), particlePath(networkId, [...segments, particle.id]),
); );
await softDeleteParticle(docPath, userId); await softDeleteParticle(docPath, userId);
toast.success('Particle deleted'); toast.success('Particle deleted');
@@ -41,7 +46,7 @@ export function DeleteParticleOverlay({
toast.error(message); toast.error(message);
setDeleting(false); setDeleting(false);
} }
}, [deleting, networkId, onClose, particle.id, streamId, userId]); }, [deleting, onClose, particle.id, streamPath, userId]);
return ( return (
<ConfirmDestructiveOverlay <ConfirmDestructiveOverlay
@@ -4,18 +4,18 @@ import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { KeyHint } from '@/components/key-hint'; import { KeyHint } from '@/components/key-hint';
import { updateParticleProperties } from '@/lib/firestore-particles'; import { updateParticleProperties } from '@/lib/firestore-particles';
import { particlePath, toFirestoreDocPath } from '@/lib/particle-path'; import { toFirestoreDocPath, type ParticlePath } from '@/lib/particle-path';
import type { Particle } from '@/api/types'; import type { Particle } from '@/api/types';
import { useSuspendPlayback } from '@/hooks/use-suspend-playback'; import { useSuspendPlayback } from '@/hooks/use-suspend-playback';
interface RenameStreamOverlayProps { interface RenameStreamOverlayProps {
networkId: string; streamPath: ParticlePath;
streamParticle: Particle & { type: 'stream' }; streamParticle: Particle & { type: 'stream' };
onClose: () => void; onClose: () => void;
} }
export function RenameStreamOverlay({ export function RenameStreamOverlay({
networkId, streamPath,
streamParticle, streamParticle,
onClose, onClose,
}: RenameStreamOverlayProps) { }: RenameStreamOverlayProps) {
@@ -32,15 +32,13 @@ export function RenameStreamOverlay({
if (!canSave) return; if (!canSave) return;
setSaving(true); setSaving(true);
try { try {
const docPath = toFirestoreDocPath( const docPath = toFirestoreDocPath(streamPath);
particlePath(networkId, [streamParticle.id]),
);
await updateParticleProperties<'stream'>(docPath, { name: trimmed }); await updateParticleProperties<'stream'>(docPath, { name: trimmed });
onClose(); onClose();
} finally { } finally {
setSaving(false); setSaving(false);
} }
}, [canSave, networkId, onClose, streamParticle.id, trimmed]); }, [canSave, streamPath, onClose, trimmed]);
useEffect(() => { useEffect(() => {
const handler = (e: KeyboardEvent) => { const handler = (e: KeyboardEvent) => {
@@ -103,8 +103,9 @@ function ChatRow({
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={onClick} onClick={onClick}
// Enter only — Space is reserved for play/pause in the stream view.
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onClick(); if (e.key === 'Enter') onClick();
}} }}
className={cn( className={cn(
'flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors', 'flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors',
@@ -10,7 +10,7 @@ import {
parseVisibleTo, parseVisibleTo,
} from '@/lib/stream-visibility'; } from '@/lib/stream-visibility';
import { updateParticleVisibleTo } from '@/lib/firestore-particles'; import { updateParticleVisibleTo } from '@/lib/firestore-particles';
import { particlePath, toFirestoreDocPath } from '@/lib/particle-path'; import { toFirestoreDocPath, type ParticlePath } from '@/lib/particle-path';
import { useNetwork } from '@/hooks/use-networks'; import { useNetwork } from '@/hooks/use-networks';
import { cn, getInitials } from '@/lib/utils'; import { cn, getInitials } from '@/lib/utils';
import { resolveHumanDisplay } from '@/lib/humans'; import { resolveHumanDisplay } from '@/lib/humans';
@@ -19,6 +19,7 @@ import { useSuspendPlayback } from '@/hooks/use-suspend-playback';
interface StreamMembersOverlayProps { interface StreamMembersOverlayProps {
networkId: string; networkId: string;
streamPath: ParticlePath;
streamParticle: Particle & { type: 'stream' }; streamParticle: Particle & { type: 'stream' };
isCreator: boolean; isCreator: boolean;
onClose: () => void; onClose: () => void;
@@ -26,6 +27,7 @@ interface StreamMembersOverlayProps {
export function StreamMembersOverlay({ export function StreamMembersOverlay({
networkId, networkId,
streamPath,
streamParticle, streamParticle,
isCreator, isCreator,
onClose, onClose,
@@ -40,10 +42,7 @@ export function StreamMembersOverlay({
[streamParticle.visible_to, networkId], [streamParticle.visible_to, networkId],
); );
const docPath = useMemo( const docPath = useMemo(() => toFirestoreDocPath(streamPath), [streamPath]);
() => toFirestoreDocPath(particlePath(networkId, [streamParticle.id])),
[networkId, streamParticle.id],
);
const memberIds = const memberIds =
visibility.mode === 'network' visibility.mode === 'network'
@@ -27,7 +27,7 @@ import {
CircleDot, CircleDot,
} from 'lucide-react'; } from 'lucide-react';
import { updateStreamStatus } from '@/lib/firestore-particles'; import { updateStreamStatus } from '@/lib/firestore-particles';
import { toFirestoreDocPath, particlePath } from '@/lib/particle-path'; import { toFirestoreDocPath, type ParticlePath } from '@/lib/particle-path';
import { RenameStreamOverlay } from '@/features/particles/rename-stream-overlay'; import { RenameStreamOverlay } from '@/features/particles/rename-stream-overlay';
import { DeleteParticleOverlay } from '@/features/particles/delete-particle-overlay'; import { DeleteParticleOverlay } from '@/features/particles/delete-particle-overlay';
import { StreamMembersOverlay } from '@/features/particles/stream-members-overlay'; import { StreamMembersOverlay } from '@/features/particles/stream-members-overlay';
@@ -51,9 +51,16 @@ interface TopBarProps {
networkId: string; networkId: string;
particle: Particle | null; particle: Particle | null;
streamParticle: Particle & { type: 'stream' }; streamParticle: Particle & { type: 'stream' };
/** Resolved path of the stream — may be nested under a container. */
streamPath: ParticlePath;
} }
export function TopBar({ networkId, particle, streamParticle }: TopBarProps) { export function TopBar({
networkId,
particle,
streamParticle,
streamPath,
}: TopBarProps) {
const network = useNetwork(networkId); const network = useNetwork(networkId);
const userId = useAuthStore((s) => s.user?.id); const userId = useAuthStore((s) => s.user?.id);
const isCreator = !!userId && userId === streamParticle.created_by_human_id; const isCreator = !!userId && userId === streamParticle.created_by_human_id;
@@ -176,9 +183,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
)} )}
<DropdownMenuItem <DropdownMenuItem
onSelect={async () => { onSelect={async () => {
const docPath = toFirestoreDocPath( const docPath = toFirestoreDocPath(streamPath);
particlePath(networkId, [streamParticle.id]),
);
await updateStreamStatus( await updateStreamStatus(
docPath, docPath,
isStreamOpen(streamParticle) ? 'closed' : 'open', isStreamOpen(streamParticle) ? 'closed' : 'open',
@@ -211,7 +216,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
{renameOpen && isCreator && ( {renameOpen && isCreator && (
<RenameStreamOverlay <RenameStreamOverlay
networkId={networkId} streamPath={streamPath}
streamParticle={streamParticle} streamParticle={streamParticle}
onClose={() => setRenameOpen(false)} onClose={() => setRenameOpen(false)}
/> />
@@ -219,8 +224,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
{deleteOpen && canDeleteParticle && particle && userId && ( {deleteOpen && canDeleteParticle && particle && userId && (
<DeleteParticleOverlay <DeleteParticleOverlay
networkId={networkId} streamPath={streamPath}
streamId={streamParticle.id}
particle={particle} particle={particle}
userId={userId} userId={userId}
onClose={() => setDeleteOpen(false)} onClose={() => setDeleteOpen(false)}
@@ -230,6 +234,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
{membersOpen && ( {membersOpen && (
<StreamMembersOverlay <StreamMembersOverlay
networkId={networkId} networkId={networkId}
streamPath={streamPath}
streamParticle={streamParticle} streamParticle={streamParticle}
isCreator={isCreator} isCreator={isCreator}
onClose={() => setMembersOpen(false)} onClose={() => setMembersOpen(false)}
@@ -479,6 +479,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
networkId={networkId} networkId={networkId}
particle={currentParticle} particle={currentParticle}
streamParticle={streamParticle} streamParticle={streamParticle}
streamPath={path}
/> />
</div> </div>
@@ -101,8 +101,12 @@ export function TaskParticleView({
}, [checklist]); }, [checklist]);
const writeChecklist = useCallback( const writeChecklist = useCallback(
(items: ChecklistItem[]) => (items: ChecklistItem[]) => {
updateParticleProperties<'task'>(docPath, { checklist: items }), // Advance the local base before the write so a second edit issued before
// the next snapshot composes on top of this one instead of dropping it.
checklistRef.current = items;
return updateParticleProperties<'task'>(docPath, { checklist: items });
},
[docPath], [docPath],
); );