diff --git a/js/mobile/src/features/compose/ComposeDock.tsx b/js/mobile/src/features/compose/ComposeDock.tsx index 25a6577..001f6b8 100644 --- a/js/mobile/src/features/compose/ComposeDock.tsx +++ b/js/mobile/src/features/compose/ComposeDock.tsx @@ -56,6 +56,13 @@ interface ComposeDockProps { silentPresence?: boolean; submitMedia?: (params: SubmitMediaParams) => Promise; submitText?: (content: string) => Promise; + /** + * Called with the new particle's id right after it's created on the default + * send path. Not fired when `submitMedia`/`submitText` overrides are supplied, + * since those own the created particle themselves. Lets the stream follow a + * just-sent particle when the user was at the end. + */ + onParticleCreated?: (particleId: string) => void; } export function ComposeDock({ @@ -64,6 +71,7 @@ export function ComposeDock({ silentPresence = false, submitMedia, submitText: submitTextOverride, + onParticleCreated, }: ComposeDockProps) { const userId = useAuthStore((s) => s.user?.id); @@ -147,7 +155,7 @@ export function ComposeDock({ source: "camera", }); } else { - await uploadMediaParticle({ + const particleId = await uploadMediaParticle({ networkId, targetPath, fileUri: captured.uri, @@ -156,6 +164,7 @@ export function ComposeDock({ source: "camera", createdByHumanId: userId, }); + onParticleCreated?.(particleId); } void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); setUi({ kind: "idle" }); @@ -174,12 +183,13 @@ export function ComposeDock({ if (submitTextOverride) { await submitTextOverride(content); } else { - await createTextParticle({ + const particleId = await createTextParticle({ networkId, targetPath, content, createdByHumanId: userId, }); + onParticleCreated?.(particleId); } void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); }); diff --git a/js/mobile/src/features/stream-view/StreamView.tsx b/js/mobile/src/features/stream-view/StreamView.tsx index d4cf69d..cc65acf 100644 --- a/js/mobile/src/features/stream-view/StreamView.tsx +++ b/js/mobile/src/features/stream-view/StreamView.tsx @@ -105,8 +105,15 @@ function StreamViewInner({ streamParticle, path, onExit }: StreamViewProps) { const insets = useSafeAreaInsets(); const { composingUsers } = useStreamComposing(); - const { children, currentParticle, currentIndex, status, next, prev } = - useStreamPlayback(streamParticle, path); + const { + children, + currentParticle, + currentIndex, + status, + next, + prev, + goToParticle, + } = useStreamPlayback(streamParticle, path); const paused = usePlaybackPauseStore(selectIsPaused); const composing = usePlaybackPauseStore(selectIsComposing); @@ -257,6 +264,22 @@ function StreamViewInner({ streamParticle, path, onExit }: StreamViewProps) { const openReactions = useCallback(() => setReactionsOpen(true), []); + // When the local user sends a particle while sitting on the last one, follow + // it forward so they stay caught up and the stream isn't left unseen. If they + // were browsing earlier particles, leave their position untouched. `children` + // is still the pre-send snapshot here (the live query hasn't delivered the new + // particle yet), so the last slot is children.length - 1; goToParticle resolves + // once it arrives. Mirrors desktop's StreamView.handleParticleCreated. + const handleParticleCreated = useCallback( + (particleId: string) => { + if (currentIndex === -1) return; + if (currentIndex === children.length - 1) { + goToParticle(particleId); + } + }, + [children.length, currentIndex, goToParticle], + ); + // Reset progress whenever the active particle changes. useEffect(() => { setProgress(0); @@ -642,7 +665,11 @@ function StreamViewInner({ streamParticle, path, onExit }: StreamViewProps) { {/* Compose dock + recording overlays. Sits above the GestureDetector so its hold-FAB pan gesture isn't competed-with by the StreamView tap zones. */} - + {/* Reaction sheet — slides up over everything, suspends playback internally while open. */}