fix(mobile): auto-follow newly sent particles in stream view #226

Merged
talksik merged 1 commits from claude/gracious-brown-0f98m into main 2026-05-30 18:09:44 +00:00
2 changed files with 42 additions and 5 deletions
+12 -2
View File
@@ -56,6 +56,13 @@ interface ComposeDockProps {
silentPresence?: boolean; silentPresence?: boolean;
submitMedia?: (params: SubmitMediaParams) => Promise<void>; submitMedia?: (params: SubmitMediaParams) => Promise<void>;
submitText?: (content: string) => Promise<void>; submitText?: (content: string) => Promise<void>;
/**
* 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({ export function ComposeDock({
@@ -64,6 +71,7 @@ export function ComposeDock({
silentPresence = false, silentPresence = false,
submitMedia, submitMedia,
submitText: submitTextOverride, submitText: submitTextOverride,
onParticleCreated,
}: ComposeDockProps) { }: ComposeDockProps) {
const userId = useAuthStore((s) => s.user?.id); const userId = useAuthStore((s) => s.user?.id);
@@ -147,7 +155,7 @@ export function ComposeDock({
source: "camera", source: "camera",
}); });
} else { } else {
await uploadMediaParticle({ const particleId = await uploadMediaParticle({
networkId, networkId,
targetPath, targetPath,
fileUri: captured.uri, fileUri: captured.uri,
@@ -156,6 +164,7 @@ export function ComposeDock({
source: "camera", source: "camera",
createdByHumanId: userId, createdByHumanId: userId,
}); });
onParticleCreated?.(particleId);
} }
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setUi({ kind: "idle" }); setUi({ kind: "idle" });
@@ -174,12 +183,13 @@ export function ComposeDock({
if (submitTextOverride) { if (submitTextOverride) {
await submitTextOverride(content); await submitTextOverride(content);
} else { } else {
await createTextParticle({ const particleId = await createTextParticle({
networkId, networkId,
targetPath, targetPath,
content, content,
createdByHumanId: userId, createdByHumanId: userId,
}); });
onParticleCreated?.(particleId);
} }
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}); });
@@ -105,8 +105,15 @@ function StreamViewInner({ streamParticle, path, onExit }: StreamViewProps) {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { composingUsers } = useStreamComposing(); const { composingUsers } = useStreamComposing();
const { children, currentParticle, currentIndex, status, next, prev } = const {
useStreamPlayback(streamParticle, path); children,
currentParticle,
currentIndex,
status,
next,
prev,
goToParticle,
} = useStreamPlayback(streamParticle, path);
const paused = usePlaybackPauseStore(selectIsPaused); const paused = usePlaybackPauseStore(selectIsPaused);
const composing = usePlaybackPauseStore(selectIsComposing); const composing = usePlaybackPauseStore(selectIsComposing);
@@ -257,6 +264,22 @@ function StreamViewInner({ streamParticle, path, onExit }: StreamViewProps) {
const openReactions = useCallback(() => setReactionsOpen(true), []); 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. // Reset progress whenever the active particle changes.
useEffect(() => { useEffect(() => {
setProgress(0); setProgress(0);
@@ -642,7 +665,11 @@ function StreamViewInner({ streamParticle, path, onExit }: StreamViewProps) {
{/* Compose dock + recording overlays. Sits above the GestureDetector {/* Compose dock + recording overlays. Sits above the GestureDetector
so its hold-FAB pan gesture isn't competed-with by the StreamView so its hold-FAB pan gesture isn't competed-with by the StreamView
tap zones. */} tap zones. */}
<ComposeDock networkId={networkId} targetPath={path} /> <ComposeDock
networkId={networkId}
targetPath={path}
onParticleCreated={handleParticleCreated}
/>
{/* Reaction sheet — slides up over everything, suspends playback {/* Reaction sheet — slides up over everything, suspends playback
internally while open. */} internally while open. */}