1import type { ReactNode, RefObject } from "react";
2import { createContext, useContext, useMemo, useRef, useState } from "react";
3import { View, type ViewInstance } from "react-native";
4
5type ComposerPopoverHostValue = {
6 readonly hostRef: RefObject<ViewInstance | null>;
7 readonly setContent: (content: ReactNode) => void;
8 /** Changes when the host resizes, which moves the bottom-anchored composer. */
9 readonly layoutVersion: number;
10};
11
12const ComposerPopoverHostContext = createContext<ComposerPopoverHostValue | null>(null);
13
14export function useComposerPopoverHost() {
15 return useContext(ComposerPopoverHostContext);
16}
17
18/**
19 * Full-size layer around the composer for its popovers. Mount it inside the
20 * composer's KeyboardStickyView: popovers placed here move with the composer
21 * and sit inside a parent that covers them, so Android delivers their scroll
22 * gestures. It wraps the composer because measureLayout needs an ancestor.
23 * Pass `hidden` whenever the composer itself is hidden: popovers live outside
24 * the composer's subtree, so they don't hide with it.
25 */
26export function ComposerPopoverHost(props: {
27 readonly hidden: boolean;
28 readonly children: ReactNode;
29}) {
30 const hostRef = useRef<ViewInstance>(null);
31 const [content, setContent] = useState<ReactNode>(null);
32 const [layoutVersion, setLayoutVersion] = useState(0);
33 const value = useMemo(() => ({ hostRef, setContent, layoutVersion }), [layoutVersion]);
34
35 return (
36 <ComposerPopoverHostContext.Provider value={value}>
37 <View
38 ref={hostRef}
39 collapsable={false}
40 pointerEvents="box-none"
41 className="absolute inset-0"
42 onLayout={() => setLayoutVersion((version) => version + 1)}
43 >
44 {props.children}
45 {props.hidden ? null : content}
46 </View>
47 </ComposerPopoverHostContext.Provider>
48 );
49}