Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion examples/SampleApp/src/screens/ThreadScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
useTheme,
useTranslationContext,
useTypingString,
PortalWhileClosingView,
} from 'stream-chat-react-native';
import { useStateStore } from 'stream-chat-react-native';

Expand Down Expand Up @@ -161,7 +162,12 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
onAlsoSentToChannelHeaderPress={onAlsoSentToChannelHeaderPress}
messageId={targetedMessageIdFromParams}
>
<ThreadHeader thread={thread} />
<PortalWhileClosingView
portalHostName='overlay-header'
portalName='channel-header'
>
<ThreadHeader thread={thread} />
</PortalWhileClosingView>
<Thread
onThreadDismount={onThreadDismount}
shouldUseFlashList={messageListImplementation === 'flashlist'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
},
]
}
testID="message-overlay-top"
>
<View
name="top-item"
Expand Down Expand Up @@ -846,6 +847,7 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
},
]
}
testID="message-overlay-message"
>
<View
name="message-overlay"
Expand Down Expand Up @@ -878,6 +880,7 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
},
]
}
testID="message-overlay-bottom"
>
<View
name="bottom-item"
Expand Down Expand Up @@ -1713,6 +1716,7 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
},
]
}
testID="message-overlay-top"
>
<View
name="top-item"
Expand Down Expand Up @@ -1743,6 +1747,7 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
},
]
}
testID="message-overlay-message"
>
<View
name="message-overlay"
Expand Down Expand Up @@ -1775,6 +1780,7 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
},
]
}
testID="message-overlay-bottom"
>
<View
name="bottom-item"
Expand Down Expand Up @@ -2610,6 +2616,7 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
},
]
}
testID="message-overlay-top"
>
<View
name="top-item"
Expand Down Expand Up @@ -2640,6 +2647,7 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
},
]
}
testID="message-overlay-message"
>
<View
name="message-overlay"
Expand Down Expand Up @@ -2672,6 +2680,7 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
},
]
}
testID="message-overlay-bottom"
>
<View
name="bottom-item"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,7 @@ exports[`SendButton should render a SendButton 1`] = `
},
]
}
testID="message-overlay-top"
>
<View
name="top-item"
Expand Down Expand Up @@ -844,6 +845,7 @@ exports[`SendButton should render a SendButton 1`] = `
},
]
}
testID="message-overlay-message"
>
<View
name="message-overlay"
Expand Down Expand Up @@ -876,6 +878,7 @@ exports[`SendButton should render a SendButton 1`] = `
},
]
}
testID="message-overlay-bottom"
>
<View
name="bottom-item"
Expand Down Expand Up @@ -1709,6 +1712,7 @@ exports[`SendButton should render a disabled SendButton 1`] = `
},
]
}
testID="message-overlay-top"
>
<View
name="top-item"
Expand Down Expand Up @@ -1739,6 +1743,7 @@ exports[`SendButton should render a disabled SendButton 1`] = `
},
]
}
testID="message-overlay-message"
>
<View
name="message-overlay"
Expand Down Expand Up @@ -1771,6 +1776,7 @@ exports[`SendButton should render a disabled SendButton 1`] = `
},
]
}
testID="message-overlay-bottom"
>
<View
name="bottom-item"
Expand Down
45 changes: 29 additions & 16 deletions package/src/components/UIComponents/PortalWhileClosingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { Portal } from 'react-native-teleport';
import { useStableCallback } from '../../hooks';
import {
clearClosingPortalLayout,
createClosingPortalLayoutRegistrationId,
setClosingPortalLayout,
useOverlayController,
useShouldTeleportToClosingPortal,
} from '../../state-store';

type PortalWhileClosingViewProps = {
Expand Down Expand Up @@ -39,15 +40,16 @@ type PortalWhileClosingViewProps = {
* This wrapper moves that UI into the overlay host layer for the closing phase, so stacking stays correct.
*
* To use it, simply wrap any view that should remain on top while the overlay is closing, and pass a `portalHostName`
* and a `portalName`. Registration within the host layer will happen automatically, as will calculating layout.
* and a `portalName`. Once the wrapped view has a valid measured layout, it can participate in the closing host layer.
*
* Behavior:
* - renders children in place during normal operation
* - registers absolute layout for `portalHostName`
* - registers absolute layout for `portalHostName` once a valid measurement exists
* - while overlay state is `closing`, teleports children to the matching closing host
* - renders a local placeholder while closing to preserve original layout space
*
* Host registration is done once per key; subsequent layout updates are pushed via shared values.
* Stack presence only starts after first valid measurement. That prevents unmeasured entries from taking over a host
* slot and rendering with incomplete geometry.
*
* Note: As the `PortalWhileClosingView` relies heavily on being able to calculate the layout and positioning
* properties of its children automatically, make sure that you do not wrap absolutely positioned views with
Expand All @@ -65,11 +67,18 @@ export const PortalWhileClosingView = ({
portalHostName,
portalName,
}: PortalWhileClosingViewProps) => {
const { closing } = useOverlayController();
const containerRef = useRef<View | null>(null);
const registrationIdRef = useRef<string | null>(null);
const placeholderLayout = useSharedValue({ h: 0, w: 0 });
const insets = useSafeAreaInsets();

if (!registrationIdRef.current) {
registrationIdRef.current = createClosingPortalLayoutRegistrationId();
}

const registrationId = registrationIdRef.current;
const shouldTeleport = useShouldTeleportToClosingPortal(portalHostName, registrationId);

const syncPortalLayout = useStableCallback(() => {
containerRef.current?.measureInWindow((x, y, width, height) => {
const absolute = {
Expand All @@ -83,14 +92,20 @@ export const PortalWhileClosingView = ({

placeholderLayout.value = { h: height, w: width };

setClosingPortalLayout(portalHostName, {
setClosingPortalLayout(portalHostName, registrationId, {
...absolute,
h: height,
w: width,
});
});
});

useEffect(() => {
return () => {
clearClosingPortalLayout(portalHostName, registrationId);
};
}, [portalHostName, registrationId]);

useEffect(() => {
// Measure once after mount and layout settle.
requestAnimationFrame(() => {
Expand All @@ -100,27 +115,25 @@ export const PortalWhileClosingView = ({
});
}, [insets.top, portalHostName, syncPortalLayout]);

const unregisterPortalHost = useStableCallback(() => clearClosingPortalLayout(portalHostName));

useEffect(() => {
return () => {
unregisterPortalHost();
};
}, [unregisterPortalHost]);

const placeholderStyle = useAnimatedStyle(() => ({
height: placeholderLayout.value.h,
width: placeholderLayout.value.w > 0 ? placeholderLayout.value.w : '100%',
}));

return (
<>
<Portal hostName={closing ? portalHostName : undefined} name={portalName}>
<Portal hostName={shouldTeleport ? portalHostName : undefined} name={portalName}>
<View collapsable={false} ref={containerRef} onLayout={syncPortalLayout}>
{children}
</View>
</Portal>
{closing ? <Animated.View pointerEvents='none' style={placeholderStyle} /> : null}
{shouldTeleport ? (
<Animated.View
pointerEvents='none'
style={placeholderStyle}
testID={`portal-while-closing-placeholder-${portalName}`}
/>
) : null}
</>
);
};
Loading
Loading