diff --git a/js/desktop/src/features/particles/delete-particle-overlay.tsx b/js/desktop/src/features/particles/delete-particle-overlay.tsx index 64e0081..fef2db1 100644 --- a/js/desktop/src/features/particles/delete-particle-overlay.tsx +++ b/js/desktop/src/features/particles/delete-particle-overlay.tsx @@ -2,21 +2,25 @@ import { useCallback, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmDestructiveOverlay } from '@/components/confirm-destructive-overlay'; 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 { useSuspendPlayback } from '@/hooks/use-suspend-playback'; interface DeleteParticleOverlayProps { - networkId: string; - streamId: string; + /** Path of the stream the particle lives in — may be nested. */ + streamPath: ParticlePath; particle: Particle; userId: string; onClose: () => void; } export function DeleteParticleOverlay({ - networkId, - streamId, + streamPath, particle, userId, onClose, @@ -29,8 +33,9 @@ export function DeleteParticleOverlay({ if (deleting) return; setDeleting(true); try { + const { networkId, segments } = parseParticlePath(streamPath); const docPath = toFirestoreDocPath( - particlePath(networkId, [streamId, particle.id]), + particlePath(networkId, [...segments, particle.id]), ); await softDeleteParticle(docPath, userId); toast.success('Particle deleted'); @@ -41,7 +46,7 @@ export function DeleteParticleOverlay({ toast.error(message); setDeleting(false); } - }, [deleting, networkId, onClose, particle.id, streamId, userId]); + }, [deleting, onClose, particle.id, streamPath, userId]); return ( void; } export function RenameStreamOverlay({ - networkId, + streamPath, streamParticle, onClose, }: RenameStreamOverlayProps) { @@ -32,15 +32,13 @@ export function RenameStreamOverlay({ if (!canSave) return; setSaving(true); try { - const docPath = toFirestoreDocPath( - particlePath(networkId, [streamParticle.id]), - ); + const docPath = toFirestoreDocPath(streamPath); await updateParticleProperties<'stream'>(docPath, { name: trimmed }); onClose(); } finally { setSaving(false); } - }, [canSave, networkId, onClose, streamParticle.id, trimmed]); + }, [canSave, streamPath, onClose, trimmed]); useEffect(() => { const handler = (e: KeyboardEvent) => { diff --git a/js/desktop/src/features/particles/stream-list-sidebar.tsx b/js/desktop/src/features/particles/stream-list-sidebar.tsx index c022276..e8a0d5e 100644 --- a/js/desktop/src/features/particles/stream-list-sidebar.tsx +++ b/js/desktop/src/features/particles/stream-list-sidebar.tsx @@ -103,8 +103,9 @@ function ChatRow({ role="button" tabIndex={0} onClick={onClick} + // Enter only — Space is reserved for play/pause in the stream view. onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') onClick(); + if (e.key === 'Enter') onClick(); }} className={cn( 'flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors', diff --git a/js/desktop/src/features/particles/stream-members-overlay.tsx b/js/desktop/src/features/particles/stream-members-overlay.tsx index 5be51f3..c8a1cbf 100644 --- a/js/desktop/src/features/particles/stream-members-overlay.tsx +++ b/js/desktop/src/features/particles/stream-members-overlay.tsx @@ -10,7 +10,7 @@ import { parseVisibleTo, } from '@/lib/stream-visibility'; 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 { cn, getInitials } from '@/lib/utils'; import { resolveHumanDisplay } from '@/lib/humans'; @@ -19,6 +19,7 @@ import { useSuspendPlayback } from '@/hooks/use-suspend-playback'; interface StreamMembersOverlayProps { networkId: string; + streamPath: ParticlePath; streamParticle: Particle & { type: 'stream' }; isCreator: boolean; onClose: () => void; @@ -26,6 +27,7 @@ interface StreamMembersOverlayProps { export function StreamMembersOverlay({ networkId, + streamPath, streamParticle, isCreator, onClose, @@ -40,10 +42,7 @@ export function StreamMembersOverlay({ [streamParticle.visible_to, networkId], ); - const docPath = useMemo( - () => toFirestoreDocPath(particlePath(networkId, [streamParticle.id])), - [networkId, streamParticle.id], - ); + const docPath = useMemo(() => toFirestoreDocPath(streamPath), [streamPath]); const memberIds = visibility.mode === 'network' diff --git a/js/desktop/src/features/particles/stream-top-bar.tsx b/js/desktop/src/features/particles/stream-top-bar.tsx index 72d9303..a75f7b4 100644 --- a/js/desktop/src/features/particles/stream-top-bar.tsx +++ b/js/desktop/src/features/particles/stream-top-bar.tsx @@ -27,7 +27,7 @@ import { CircleDot, } from 'lucide-react'; 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 { DeleteParticleOverlay } from '@/features/particles/delete-particle-overlay'; import { StreamMembersOverlay } from '@/features/particles/stream-members-overlay'; @@ -51,9 +51,16 @@ interface TopBarProps { networkId: string; particle: Particle | null; 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 userId = useAuthStore((s) => s.user?.id); const isCreator = !!userId && userId === streamParticle.created_by_human_id; @@ -176,9 +183,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) { )} { - const docPath = toFirestoreDocPath( - particlePath(networkId, [streamParticle.id]), - ); + const docPath = toFirestoreDocPath(streamPath); await updateStreamStatus( docPath, isStreamOpen(streamParticle) ? 'closed' : 'open', @@ -211,7 +216,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) { {renameOpen && isCreator && ( setRenameOpen(false)} /> @@ -219,8 +224,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) { {deleteOpen && canDeleteParticle && particle && userId && ( setDeleteOpen(false)} @@ -230,6 +234,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) { {membersOpen && ( setMembersOpen(false)} diff --git a/js/desktop/src/features/particles/stream-view.tsx b/js/desktop/src/features/particles/stream-view.tsx index 39b32b5..85af187 100644 --- a/js/desktop/src/features/particles/stream-view.tsx +++ b/js/desktop/src/features/particles/stream-view.tsx @@ -479,6 +479,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) { networkId={networkId} particle={currentParticle} streamParticle={streamParticle} + streamPath={path} /> diff --git a/js/desktop/src/features/particles/task-particle-view.tsx b/js/desktop/src/features/particles/task-particle-view.tsx index 19753e5..cc73772 100644 --- a/js/desktop/src/features/particles/task-particle-view.tsx +++ b/js/desktop/src/features/particles/task-particle-view.tsx @@ -101,8 +101,12 @@ export function TaskParticleView({ }, [checklist]); const writeChecklist = useCallback( - (items: ChecklistItem[]) => - updateParticleProperties<'task'>(docPath, { checklist: items }), + (items: ChecklistItem[]) => { + // 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], );