From 6f033e2a739b3d3064a89da53135c2e87bebcd56 Mon Sep 17 00:00:00 2001 From: talksik Date: Tue, 24 Mar 2026 10:27:50 -0700 Subject: [PATCH] feat: jump to my newly created particle once created --- js/src/features/compose/compose-overlay.tsx | 10 ++++++++-- js/src/features/particles/stream-view.tsx | 7 +++++++ js/src/hooks/use-stream-playback.ts | 10 ++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/js/src/features/compose/compose-overlay.tsx b/js/src/features/compose/compose-overlay.tsx index a88708c..333e0b7 100644 --- a/js/src/features/compose/compose-overlay.tsx +++ b/js/src/features/compose/compose-overlay.tsx @@ -17,6 +17,7 @@ interface ComposeOverlayProps { // Optional target path for reply mode. If not provided, compose creates a new stream. targetPath?: ParticlePath; onActiveChange?: (active: boolean) => void; + onParticleCreated?: (particleId: string) => void; } @@ -30,6 +31,7 @@ export function ComposeOverlay({ networkId, targetPath, onActiveChange, + onParticleCreated, }: ComposeOverlayProps) { const [step, setStep] = useState("idle"); const [error, setError] = useState(null); @@ -113,8 +115,9 @@ export function ComposeOverlay({ async (path: ParticlePath) => { if (!userEmail) return; + let particleId = ''; if (textContent.trim()) { - await createParticle.mutateAsync({ + particleId = await createParticle.mutateAsync({ path, type: "text", properties: { content: textContent }, @@ -126,7 +129,7 @@ export function ComposeOverlay({ reviewMimeType, ); - await createParticle.mutateAsync({ + particleId = await createParticle.mutateAsync({ path, type: "media", properties: { @@ -138,6 +141,8 @@ export function ComposeOverlay({ createdByEmail: userEmail, }); } + + onParticleCreated?.(particleId); }, [ userEmail, @@ -147,6 +152,7 @@ export function ComposeOverlay({ reviewDurationMs, createParticle, uploadMedia, + onParticleCreated ], ); diff --git a/js/src/features/particles/stream-view.tsx b/js/src/features/particles/stream-view.tsx index 5d2a793..70cb5ee 100644 --- a/js/src/features/particles/stream-view.tsx +++ b/js/src/features/particles/stream-view.tsx @@ -110,6 +110,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) { next, prev, goTo, + goToParticle, pause, resume, } = useStreamPlayback(streamParticle, path); @@ -188,6 +189,11 @@ export function StreamView({ path, streamParticle }: StreamViewProps) { [prev, next], ); + const onLocalParticleCreated = useCallback( + (particleId: string) => { + goToParticle(particleId); + }, [goToParticle]); + if (children.length === 0) { return (
@@ -264,6 +270,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) { networkId={networkId!} targetPath={path} onActiveChange={setComposeActive} + onParticleCreated={onLocalParticleCreated} /> {/* Bottom overlay: stream info + reply */} diff --git a/js/src/hooks/use-stream-playback.ts b/js/src/hooks/use-stream-playback.ts index f0b4edc..1393d79 100644 --- a/js/src/hooks/use-stream-playback.ts +++ b/js/src/hooks/use-stream-playback.ts @@ -86,6 +86,7 @@ interface UseStreamPlaybackResult { next: () => void; prev: () => void; goTo: (index: number) => void; + goToParticle: (particleId: string) => void; pause: () => void; resume: () => void; } @@ -212,6 +213,14 @@ export function useStreamPlayback( [children], ); + // NOTE: if particle doesn't exist in children, this will lead to a brief moment where currentParticle is null + const goToParticle = useCallback( + (particleId: string) => { + // Dispatch directly by ID — if the particle isn't in children yet + // (e.g. just created), it will resolve once the live query delivers it. + dispatch({ type: "SET_PARTICLE", particleId }); + }, []); + const pause = useCallback(() => dispatch({ type: "PAUSE" }), []); const resume = useCallback(() => dispatch({ type: "RESUME" }), []); @@ -225,6 +234,7 @@ export function useStreamPlayback( next, prev, goTo, + goToParticle, pause, resume, };