fix: stream actions not working

Multiple sheets compete with each other and cause silent failure on ios.
This commit is contained in:
talksik
2026-04-30 16:53:48 -07:00
parent 6832c273cd
commit a6a4757a8e
2 changed files with 40 additions and 4 deletions
+22 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { import {
Dimensions, Dimensions,
KeyboardAvoidingView, KeyboardAvoidingView,
@@ -30,6 +30,13 @@ const ANIMATION_MS = 240;
interface BottomSheetProps { interface BottomSheetProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
/**
* Fires after the close animation completes and the underlying Modal has
* unmounted. Use this to chain a follow-up sheet/Alert without stacking
* iOS Modals — presenting a second Modal while another is still mounted
* silently fails on iOS and leaves the app looking frozen.
*/
onClosed?: () => void;
/** When true, wrap content in KeyboardAvoidingView so the sheet floats above the keyboard. */ /** When true, wrap content in KeyboardAvoidingView so the sheet floats above the keyboard. */
avoidKeyboard?: boolean; avoidKeyboard?: boolean;
/** /**
@@ -49,6 +56,7 @@ interface BottomSheetProps {
export function BottomSheet({ export function BottomSheet({
open, open,
onClose, onClose,
onClosed,
avoidKeyboard = false, avoidKeyboard = false,
maxHeight = "85%", maxHeight = "85%",
children, children,
@@ -58,6 +66,18 @@ export function BottomSheet({
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const translateY = useSharedValue(SCREEN_HEIGHT); const translateY = useSharedValue(SCREEN_HEIGHT);
// Latest onClosed in a ref so the worklet→JS bridge always invokes the
// current callback even if the parent re-rendered with a new closure.
const onClosedRef = useRef(onClosed);
useEffect(() => {
onClosedRef.current = onClosed;
}, [onClosed]);
const handleClosed = useCallback(() => {
setMounted(false);
onClosedRef.current?.();
}, []);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
setMounted(true); setMounted(true);
@@ -73,7 +93,7 @@ export function BottomSheet({
SCREEN_HEIGHT, SCREEN_HEIGHT,
{ duration: ANIMATION_MS, easing: Easing.in(Easing.cubic) }, { duration: ANIMATION_MS, easing: Easing.in(Easing.cubic) },
(finished) => { (finished) => {
if (finished) runOnJS(setMounted)(false); if (finished) runOnJS(handleClosed)();
}, },
); );
} }
@@ -1,3 +1,4 @@
import { useState } from "react";
import { Pressable, Text, View } from "react-native"; import { Pressable, Text, View } from "react-native";
import { import {
CircleCheckBig, CircleCheckBig,
@@ -33,13 +34,28 @@ export function StreamActionsSheet({
isCreator, isCreator,
canDeleteParticle, canDeleteParticle,
}: StreamActionsSheetProps) { }: StreamActionsSheetProps) {
// Hold the picked action until the sheet's Modal has fully unmounted, then
// dispatch. Follow-ups like rename/members open another Modal and "delete
// particle" shows an Alert — both are iOS Modals, and iOS will not present
// a second Modal while another is still on screen. Without this defer, the
// tap appears to do nothing and the app feels frozen behind a phantom
// overlay until the close animation finishes.
const [pending, setPending] = useState<StreamActionId | null>(null);
const choose = (id: StreamActionId) => { const choose = (id: StreamActionId) => {
setPending(id);
onClose(); onClose();
onSelect(id); };
const handleClosed = () => {
if (pending) {
onSelect(pending);
setPending(null);
}
}; };
return ( return (
<BottomSheet open={open} onClose={onClose}> <BottomSheet open={open} onClose={onClose} onClosed={handleClosed}>
<View className="py-2"> <View className="py-2">
<ActionRow <ActionRow
icon={ icon={