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;
submitMedia?: (params: SubmitMediaParams) => 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({
@@ -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);
});
@@ -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. */}
<ComposeDock networkId={networkId} targetPath={path} />
<ComposeDock
networkId={networkId}
targetPath={path}
onParticleCreated={handleParticleCreated}
/>
{/* Reaction sheet — slides up over everything, suspends playback
internally while open. */}