Allow nested sheets without boilerplate (#5660)

Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
Samuel Newman
2024-10-09 21:30:42 +03:00
committed by GitHub
parent b3ade19bbe
commit cca344a3d1
20 changed files with 596 additions and 555 deletions
+10
View File
@@ -4,9 +4,19 @@ import {
BottomSheetState, BottomSheetState,
BottomSheetViewProps, BottomSheetViewProps,
} from './src/BottomSheet.types' } from './src/BottomSheet.types'
import {BottomSheetNativeComponent} from './src/BottomSheetNativeComponent'
import {
BottomSheetOutlet,
BottomSheetPortalProvider,
BottomSheetProvider,
} from './src/BottomSheetPortal'
export { export {
BottomSheet, BottomSheet,
BottomSheetNativeComponent,
BottomSheetOutlet,
BottomSheetPortalProvider,
BottomSheetProvider,
BottomSheetSnapPoint, BottomSheetSnapPoint,
type BottomSheetState, type BottomSheetState,
type BottomSheetViewProps, type BottomSheetViewProps,
+17 -93
View File
@@ -1,100 +1,24 @@
import * as React from 'react' import React from 'react'
import {
Dimensions,
NativeSyntheticEvent,
Platform,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types' import {BottomSheetViewProps} from './BottomSheet.types'
import {BottomSheetNativeComponent} from './BottomSheetNativeComponent'
import {useBottomSheetPortal_INTERNAL} from './BottomSheetPortal'
const screenHeight = Dimensions.get('screen').height export const BottomSheet = React.forwardRef<
BottomSheetNativeComponent,
BottomSheetViewProps
>(function BottomSheet(props, ref) {
const Portal = useBottomSheetPortal_INTERNAL()
const NativeView: React.ComponentType< if (__DEV__ && !Portal) {
BottomSheetViewProps & { throw new Error(
ref: React.RefObject<any> 'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
style: StyleProp<ViewStyle> )
}
> = requireNativeViewManager('BottomSheet')
const NativeModule = requireNativeModule('BottomSheet')
export class BottomSheet extends React.Component<
BottomSheetViewProps,
{
open: boolean
}
> {
ref = React.createRef<any>()
constructor(props: BottomSheetViewProps) {
super(props)
this.state = {
open: false,
}
}
present() {
this.setState({open: true})
}
dismiss() {
this.ref.current?.dismiss()
}
private onStateChange = (
event: NativeSyntheticEvent<{state: BottomSheetState}>,
) => {
const {state} = event.nativeEvent
const isOpen = state !== 'closed'
this.setState({open: isOpen})
this.props.onStateChange?.(event)
}
private updateLayout = () => {
this.ref.current?.updateLayout()
}
static dismissAll = async () => {
await NativeModule.dismissAll()
}
render() {
const {children, backgroundColor, ...rest} = this.props
const cornerRadius = rest.cornerRadius ?? 0
if (!this.state.open) {
return null
} }
return ( return (
<NativeView <Portal>
{...rest} <BottomSheetNativeComponent {...props} ref={ref} />
onStateChange={this.onStateChange} </Portal>
ref={this.ref}
style={{
position: 'absolute',
height: screenHeight,
width: '100%',
}}
containerBackgroundColor={backgroundColor}>
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
]}>
<View onLayout={this.updateLayout}>{children}</View>
</View>
</NativeView>
) )
} })
}
@@ -0,0 +1,103 @@
import * as React from 'react'
import {
Dimensions,
NativeSyntheticEvent,
Platform,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
import {BottomSheetPortalProvider} from './BottomSheetPortal'
const screenHeight = Dimensions.get('screen').height
const NativeView: React.ComponentType<
BottomSheetViewProps & {
ref: React.RefObject<any>
style: StyleProp<ViewStyle>
}
> = requireNativeViewManager('BottomSheet')
const NativeModule = requireNativeModule('BottomSheet')
export class BottomSheetNativeComponent extends React.Component<
BottomSheetViewProps,
{
open: boolean
}
> {
ref = React.createRef<any>()
constructor(props: BottomSheetViewProps) {
super(props)
this.state = {
open: false,
}
}
present() {
this.setState({open: true})
}
dismiss() {
this.ref.current?.dismiss()
}
private onStateChange = (
event: NativeSyntheticEvent<{state: BottomSheetState}>,
) => {
const {state} = event.nativeEvent
const isOpen = state !== 'closed'
this.setState({open: isOpen})
this.props.onStateChange?.(event)
}
private updateLayout = () => {
this.ref.current?.updateLayout()
}
static dismissAll = async () => {
await NativeModule.dismissAll()
}
render() {
const {children, backgroundColor, ...rest} = this.props
const cornerRadius = rest.cornerRadius ?? 0
if (!this.state.open) {
return null
}
return (
<NativeView
{...rest}
onStateChange={this.onStateChange}
ref={this.ref}
style={{
position: 'absolute',
height: screenHeight,
width: '100%',
}}
containerBackgroundColor={backgroundColor}>
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
]}>
<View onLayout={this.updateLayout}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
</View>
</NativeView>
)
}
}
@@ -0,0 +1,40 @@
import React from 'react'
import {createPortalGroup_INTERNAL} from './lib/Portal'
type PortalContext = React.ElementType<{children: React.ReactNode}>
const Context = React.createContext({} as PortalContext)
export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context)
export function BottomSheetPortalProvider({
children,
}: {
children: React.ReactNode
}) {
const portal = React.useMemo(() => {
return createPortalGroup_INTERNAL()
}, [])
return (
<Context.Provider value={portal.Portal}>
<portal.Provider>
{children}
<portal.Outlet />
</portal.Provider>
</Context.Provider>
)
}
const defaultPortal = createPortalGroup_INTERNAL()
export const BottomSheetOutlet = defaultPortal.Outlet
export function BottomSheetProvider({children}: {children: React.ReactNode}) {
return (
<Context.Provider value={defaultPortal.Portal}>
<defaultPortal.Provider>{children}</defaultPortal.Provider>
</Context.Provider>
)
}
+67
View File
@@ -0,0 +1,67 @@
import React from 'react'
type Component = React.ReactElement
type ContextType = {
outlet: Component | null
append(id: string, component: Component): void
remove(id: string): void
}
type ComponentMap = {
[id: string]: Component
}
export function createPortalGroup_INTERNAL() {
const Context = React.createContext<ContextType>({
outlet: null,
append: () => {},
remove: () => {},
})
function Provider(props: React.PropsWithChildren<{}>) {
const map = React.useRef<ComponentMap>({})
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
const append = React.useCallback<ContextType['append']>((id, component) => {
if (map.current[id]) return
map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const remove = React.useCallback<ContextType['remove']>(id => {
delete map.current[id]
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const contextValue = React.useMemo(
() => ({
outlet,
append,
remove,
}),
[outlet, append, remove],
)
return (
<Context.Provider value={contextValue}>{props.children}</Context.Provider>
)
}
function Outlet() {
const ctx = React.useContext(Context)
return ctx.outlet
}
function Portal({children}: React.PropsWithChildren<{}>) {
const {append, remove} = React.useContext(Context)
const id = React.useId()
React.useEffect(() => {
append(id, children as Component)
return () => remove(id)
}, [id, children, append, remove])
return null
}
return {Provider, Outlet, Portal}
}
+3
View File
@@ -68,6 +68,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@@ -197,6 +198,7 @@ function App() {
<DialogStateProvider> <DialogStateProvider>
<LightboxStateProvider> <LightboxStateProvider>
<PortalProvider> <PortalProvider>
<BottomSheetProvider>
<StarterPackProvider> <StarterPackProvider>
<SafeAreaProvider <SafeAreaProvider
initialMetrics={initialWindowMetrics}> initialMetrics={initialWindowMetrics}>
@@ -205,6 +207,7 @@ function App() {
</IntentDialogProvider> </IntentDialogProvider>
</SafeAreaProvider> </SafeAreaProvider>
</StarterPackProvider> </StarterPackProvider>
</BottomSheetProvider>
</PortalProvider> </PortalProvider>
</LightboxStateProvider> </LightboxStateProvider>
</DialogStateProvider> </DialogStateProvider>
+2 -5
View File
@@ -31,12 +31,12 @@ import {
DialogOuterProps, DialogOuterProps,
} from '#/components/Dialog/types' } from '#/components/Dialog/types'
import {createInput} from '#/components/forms/TextField' import {createInput} from '#/components/forms/TextField'
import {Portal as DefaultPortal} from '#/components/Portal'
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
import { import {
BottomSheetSnapPointChangeEvent, BottomSheetSnapPointChangeEvent,
BottomSheetStateChangeEvent, BottomSheetStateChangeEvent,
} from '../../../modules/bottom-sheet/src/BottomSheet.types' } from '../../../modules/bottom-sheet/src/BottomSheet.types'
import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
@@ -50,10 +50,9 @@ export function Outer({
onClose, onClose,
nativeOptions, nativeOptions,
testID, testID,
Portal = DefaultPortal,
}: React.PropsWithChildren<DialogOuterProps>) { }: React.PropsWithChildren<DialogOuterProps>) {
const t = useTheme() const t = useTheme()
const ref = React.useRef<BottomSheet>(null) const ref = React.useRef<BottomSheetNativeComponent>(null)
const closeCallbacks = React.useRef<(() => void)[]>([]) const closeCallbacks = React.useRef<(() => void)[]>([])
const {setDialogIsOpen, setFullyExpandedCount} = const {setDialogIsOpen, setFullyExpandedCount} =
useDialogStateControlContext() useDialogStateControlContext()
@@ -154,7 +153,6 @@ export function Outer({
) )
return ( return (
<Portal>
<Context.Provider value={context}> <Context.Provider value={context}>
<BottomSheet <BottomSheet
ref={ref} ref={ref}
@@ -167,7 +165,6 @@ export function Outer({
<View testID={testID}>{children}</View> <View testID={testID}>{children}</View>
</BottomSheet> </BottomSheet>
</Context.Provider> </Context.Provider>
</Portal>
) )
} }
-4
View File
@@ -30,18 +30,15 @@ import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {PortalComponent} from '#/components/Portal'
export function GifSelectDialog({ export function GifSelectDialog({
controlRef, controlRef,
onClose, onClose,
onSelectGif: onSelectGifProp, onSelectGif: onSelectGifProp,
Portal,
}: { }: {
controlRef: React.RefObject<{open: () => void}> controlRef: React.RefObject<{open: () => void}>
onClose: () => void onClose: () => void
onSelectGif: (gif: Gif) => void onSelectGif: (gif: Gif) => void
Portal?: PortalComponent
}) { }) {
const control = Dialog.useDialogControl() const control = Dialog.useDialogControl()
@@ -65,7 +62,6 @@ export function GifSelectDialog({
<Dialog.Outer <Dialog.Outer
control={control} control={control}
onClose={onClose} onClose={onClose}
Portal={Portal}
nativeOptions={{ nativeOptions={{
bottomInset: 0, bottomInset: 0,
// use system corner radius on iOS // use system corner radius on iOS
+11 -57
View File
@@ -30,14 +30,11 @@ import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/P
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {createPortalGroup} from '#/components/Portal'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
const ONE_DAY = 24 * 60 * 60 * 1000 const ONE_DAY = 24 * 60 * 60 * 1000
const Portal = createPortalGroup()
export function MutedWordsDialog() { export function MutedWordsDialog() {
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
return ( return (
@@ -108,23 +105,17 @@ function MutedWordsInner() {
}, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]) }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing])
return ( return (
<Portal.Provider>
<Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}> <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
<View> <View>
<Text <Text
style={[ style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}>
a.text_md,
a.font_bold,
a.pb_sm,
t.atoms.text_contrast_high,
]}>
<Trans>Add muted words and tags</Trans> <Trans>Add muted words and tags</Trans>
</Text> </Text>
<Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}> <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans> <Trans>
Posts can be muted based on their text, their tags, or both. We Posts can be muted based on their text, their tags, or both. We
recommend avoiding common words that appear in many posts, since recommend avoiding common words that appear in many posts, since it
it can result in no posts being shown. can result in no posts being shown.
</Trans> </Trans>
</Text> </Text>
@@ -181,12 +172,7 @@ function MutedWordsInner() {
style={[a.flex_1]}> style={[a.flex_1]}>
<TargetToggle> <TargetToggle>
<View <View
style={[ style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
a.flex_1,
a.flex_row,
a.align_center,
a.gap_sm,
]}>
<Toggle.Radio /> <Toggle.Radio />
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
<Trans>Forever</Trans> <Trans>Forever</Trans>
@@ -201,12 +187,7 @@ function MutedWordsInner() {
style={[a.flex_1]}> style={[a.flex_1]}>
<TargetToggle> <TargetToggle>
<View <View
style={[ style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
a.flex_1,
a.flex_row,
a.align_center,
a.gap_sm,
]}>
<Toggle.Radio /> <Toggle.Radio />
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
<Trans>24 hours</Trans> <Trans>24 hours</Trans>
@@ -230,12 +211,7 @@ function MutedWordsInner() {
style={[a.flex_1]}> style={[a.flex_1]}>
<TargetToggle> <TargetToggle>
<View <View
style={[ style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
a.flex_1,
a.flex_row,
a.align_center,
a.gap_sm,
]}>
<Toggle.Radio /> <Toggle.Radio />
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
<Trans>7 days</Trans> <Trans>7 days</Trans>
@@ -250,12 +226,7 @@ function MutedWordsInner() {
style={[a.flex_1]}> style={[a.flex_1]}>
<TargetToggle> <TargetToggle>
<View <View
style={[ style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
a.flex_1,
a.flex_row,
a.align_center,
a.gap_sm,
]}>
<Toggle.Radio /> <Toggle.Radio />
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
<Trans>30 days</Trans> <Trans>30 days</Trans>
@@ -268,9 +239,7 @@ function MutedWordsInner() {
</Toggle.Group> </Toggle.Group>
<Toggle.Group <Toggle.Group
label={_( label={_(msg`Select what content this mute word should apply to.`)}
msg`Select what content this mute word should apply to.`,
)}
type="radio" type="radio"
values={targets} values={targets}
onChange={setTargets}> onChange={setTargets}>
@@ -336,8 +305,7 @@ function MutedWordsInner() {
value={excludeFollowing} value={excludeFollowing}
onChange={setExcludeFollowing}> onChange={setExcludeFollowing}>
<TargetToggle> <TargetToggle>
<View <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
<Trans>Exclude users you follow</Trans> <Trans>Exclude users you follow</Trans>
@@ -405,12 +373,7 @@ function MutedWordsInner() {
<Loader /> <Loader />
) : preferencesError || !preferences ? ( ) : preferencesError || !preferences ? (
<View <View
style={[ style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
a.py_md,
a.px_lg,
a.rounded_md,
t.atoms.bg_contrast_25,
]}>
<Text style={[a.italic, t.atoms.text_contrast_high]}> <Text style={[a.italic, t.atoms.text_contrast_high]}>
<Trans> <Trans>
We're sorry, but we weren't able to load your muted words at We're sorry, but we weren't able to load your muted words at
@@ -430,12 +393,7 @@ function MutedWordsInner() {
)) ))
) : ( ) : (
<View <View
style={[ style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
a.py_md,
a.px_lg,
a.rounded_md,
t.atoms.bg_contrast_25,
]}>
<Text style={[a.italic, t.atoms.text_contrast_high]}> <Text style={[a.italic, t.atoms.text_contrast_high]}>
<Trans>You haven't muted any words or tags yet</Trans> <Trans>You haven't muted any words or tags yet</Trans>
</Text> </Text>
@@ -448,9 +406,6 @@ function MutedWordsInner() {
<Dialog.Close /> <Dialog.Close />
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
<Portal.Outlet />
</Portal.Provider>
) )
} }
@@ -482,7 +437,6 @@ function MutedWordRow({
onConfirm={remove} onConfirm={remove}
confirmButtonCta={_(msg`Remove`)} confirmButtonCta={_(msg`Remove`)}
confirmButtonColor="negative" confirmButtonColor="negative"
Portal={Portal.Portal}
/> />
<View <View
@@ -37,7 +37,6 @@ import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {PortalComponent} from '#/components/Portal'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export type PostInteractionSettingsFormProps = { export type PostInteractionSettingsFormProps = {
@@ -55,15 +54,13 @@ export type PostInteractionSettingsFormProps = {
export function PostInteractionSettingsControlledDialog({ export function PostInteractionSettingsControlledDialog({
control, control,
Portal,
...rest ...rest
}: PostInteractionSettingsFormProps & { }: PostInteractionSettingsFormProps & {
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
Portal?: PortalComponent
}) { }) {
const {_} = useLingui() const {_} = useLingui()
return ( return (
<Dialog.Outer control={control} Portal={Portal}> <Dialog.Outer control={control}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.ScrollableInner <Dialog.ScrollableInner
label={_(msg`Edit post interaction settings`)} label={_(msg`Edit post interaction settings`)}
@@ -207,7 +204,9 @@ export function PostInteractionSettingsDialogControlledInner(
label={_(msg`Edit post interaction settings`)} label={_(msg`Edit post interaction settings`)}
style={[{maxWidth: 500}, a.w_full]}> style={[{maxWidth: 500}, a.w_full]}>
{isLoading ? ( {isLoading ? (
<View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}>
<Loader size="xl" /> <Loader size="xl" />
</View>
) : ( ) : (
<PostInteractionSettingsForm <PostInteractionSettingsForm
replySettingsDisabled={!isThreadgateOwnedByViewer} replySettingsDisabled={!isThreadgateOwnedByViewer}
+2 -2
View File
@@ -3,7 +3,7 @@ import React from 'react'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {DialogControlRefProps} from '#/components/Dialog' import {DialogControlRefProps} from '#/components/Dialog'
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
import {BottomSheet} from '../../../modules/bottom-sheet' import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet'
interface IDialogContext { interface IDialogContext {
/** /**
@@ -61,7 +61,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return openDialogs.current.size > 0 return openDialogs.current.size > 0
} else { } else {
BottomSheet.dismissAll() BottomSheetNativeComponent.dismissAll()
return false return false
} }
}, []) }, [])
+4 -16
View File
@@ -107,9 +107,9 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {createPortalGroup} from '#/components/Portal'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography' import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import { import {
composerReducer, composerReducer,
createComposerState, createComposerState,
@@ -117,8 +117,6 @@ import {
} from './state/composer' } from './state/composer'
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
const Portal = createPortalGroup()
type CancelRef = { type CancelRef = {
onPressCancel: () => void onPressCancel: () => void
} }
@@ -522,7 +520,7 @@ export const ComposePost = ({
const keyboardVerticalOffset = useKeyboardVerticalOffset() const keyboardVerticalOffset = useKeyboardVerticalOffset()
return ( return (
<Portal.Provider> <BottomSheetPortalProvider>
<KeyboardAvoidingView <KeyboardAvoidingView
testID="composePostView" testID="composePostView"
behavior={isIOS ? 'padding' : 'height'} behavior={isIOS ? 'padding' : 'height'}
@@ -666,11 +664,7 @@ export const ComposePost = ({
/> />
</View> </View>
<Gallery <Gallery images={images} dispatch={dispatch} />
images={images}
dispatch={dispatch}
Portal={Portal.Portal}
/>
{extGif && ( {extGif && (
<View style={a.relative} key={extGif.url}> <View style={a.relative} key={extGif.url}>
@@ -684,7 +678,6 @@ export const ComposePost = ({
gif={extGif} gif={extGif}
altText={extGifAlt ?? ''} altText={extGifAlt ?? ''}
onSubmit={handleChangeGifAltText} onSubmit={handleChangeGifAltText}
Portal={Portal.Portal}
/> />
</View> </View>
)} )}
@@ -744,7 +737,6 @@ export const ComposePost = ({
}, },
}) })
}} }}
Portal={Portal.Portal}
/> />
</Animated.View> </Animated.View>
)} )}
@@ -782,7 +774,6 @@ export const ComposePost = ({
}) })
}} }}
style={bottomBarAnimatedStyle} style={bottomBarAnimatedStyle}
Portal={Portal.Portal}
/> />
)} )}
<View <View
@@ -819,7 +810,6 @@ export const ComposePost = ({
onClose={focusTextInput} onClose={focusTextInput}
onSelectGif={onSelectGif} onSelectGif={onSelectGif}
disabled={hasMedia} disabled={hasMedia}
Portal={Portal.Portal}
/> />
{!isMobile ? ( {!isMobile ? (
<Button <Button
@@ -849,11 +839,9 @@ export const ComposePost = ({
onConfirm={onClose} onConfirm={onClose}
confirmButtonCta={_(msg`Discard`)} confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative" confirmButtonColor="negative"
Portal={Portal.Portal}
/> />
</KeyboardAvoidingView> </KeyboardAvoidingView>
<Portal.Outlet /> </BottomSheetPortalProvider>
</Portal.Provider>
) )
} }
+1 -8
View File
@@ -21,7 +21,6 @@ import * as TextField from '#/components/forms/TextField'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {PortalComponent} from '#/components/Portal'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {GifEmbed} from '../util/post-embeds/GifEmbed' import {GifEmbed} from '../util/post-embeds/GifEmbed'
import {AltTextReminder} from './photos/Gallery' import {AltTextReminder} from './photos/Gallery'
@@ -30,12 +29,10 @@ export function GifAltTextDialog({
gif, gif,
altText, altText,
onSubmit, onSubmit,
Portal,
}: { }: {
gif: Gif gif: Gif
altText: string altText: string
onSubmit: (alt: string) => void onSubmit: (alt: string) => void
Portal: PortalComponent
}) { }) {
const {data} = useResolveGifQuery(gif) const {data} = useResolveGifQuery(gif)
const vendorAltText = parseAltFromGIFDescription(data?.description ?? '').alt const vendorAltText = parseAltFromGIFDescription(data?.description ?? '').alt
@@ -50,7 +47,6 @@ export function GifAltTextDialog({
thumb={data.thumb?.source.path} thumb={data.thumb?.source.path}
params={params} params={params}
onSubmit={onSubmit} onSubmit={onSubmit}
Portal={Portal}
/> />
) )
} }
@@ -61,14 +57,12 @@ export function GifAltTextDialogLoaded({
onSubmit, onSubmit,
params, params,
thumb, thumb,
Portal,
}: { }: {
vendorAltText: string vendorAltText: string
altText: string altText: string
onSubmit: (alt: string) => void onSubmit: (alt: string) => void
params: EmbedPlayerParams params: EmbedPlayerParams
thumb: string | undefined thumb: string | undefined
Portal: PortalComponent
}) { }) {
const control = Dialog.useDialogControl() const control = Dialog.useDialogControl()
const {_} = useLingui() const {_} = useLingui()
@@ -113,8 +107,7 @@ export function GifAltTextDialogLoaded({
control={control} control={control}
onClose={() => { onClose={() => {
onSubmit(altTextDraft) onSubmit(altTextDraft)
}} }}>
Portal={Portal}>
<Dialog.Handle /> <Dialog.Handle />
<AltTextInner <AltTextInner
vendorAltText={vendorAltText} vendorAltText={vendorAltText}
+1 -12
View File
@@ -21,7 +21,6 @@ import {ComposerImage, cropImage} from '#/state/gallery'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {PortalComponent} from '#/components/Portal'
import {ComposerAction} from '../state/composer' import {ComposerAction} from '../state/composer'
import {EditImageDialog} from './EditImageDialog' import {EditImageDialog} from './EditImageDialog'
import {ImageAltTextDialog} from './ImageAltTextDialog' import {ImageAltTextDialog} from './ImageAltTextDialog'
@@ -31,7 +30,6 @@ const IMAGE_GAP = 8
interface GalleryProps { interface GalleryProps {
images: ComposerImage[] images: ComposerImage[]
dispatch: (action: ComposerAction) => void dispatch: (action: ComposerAction) => void
Portal: PortalComponent
} }
export let Gallery = (props: GalleryProps): React.ReactNode => { export let Gallery = (props: GalleryProps): React.ReactNode => {
@@ -59,12 +57,7 @@ interface GalleryInnerProps extends GalleryProps {
containerInfo: Dimensions containerInfo: Dimensions
} }
const GalleryInner = ({ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
images,
containerInfo,
dispatch,
Portal,
}: GalleryInnerProps) => {
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {altTextControlStyle, imageControlsStyle, imageStyle} = const {altTextControlStyle, imageControlsStyle, imageStyle} =
@@ -118,7 +111,6 @@ const GalleryInner = ({
onRemove={() => { onRemove={() => {
dispatch({type: 'embed_remove_image', image}) dispatch({type: 'embed_remove_image', image})
}} }}
Portal={Portal}
/> />
) )
})} })}
@@ -135,7 +127,6 @@ type GalleryItemProps = {
imageStyle?: ViewStyle imageStyle?: ViewStyle
onChange: (next: ComposerImage) => void onChange: (next: ComposerImage) => void
onRemove: () => void onRemove: () => void
Portal: PortalComponent
} }
const GalleryItem = ({ const GalleryItem = ({
@@ -145,7 +136,6 @@ const GalleryItem = ({
imageStyle, imageStyle,
onChange, onChange,
onRemove, onRemove,
Portal,
}: GalleryItemProps): React.ReactNode => { }: GalleryItemProps): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@@ -240,7 +230,6 @@ const GalleryItem = ({
control={altTextControl} control={altTextControl}
image={image} image={image}
onChange={onChange} onChange={onChange}
Portal={Portal}
/> />
<EditImageDialog <EditImageDialog
@@ -15,21 +15,18 @@ import * as Dialog from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog' import {DialogControlProps} from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {PortalComponent} from '#/components/Portal'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
type Props = { type Props = {
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
image: ComposerImage image: ComposerImage
onChange: (next: ComposerImage) => void onChange: (next: ComposerImage) => void
Portal: PortalComponent
} }
export const ImageAltTextDialog = ({ export const ImageAltTextDialog = ({
control, control,
image, image,
onChange, onChange,
Portal,
}: Props): React.ReactNode => { }: Props): React.ReactNode => {
const [altText, setAltText] = React.useState(image.alt) const [altText, setAltText] = React.useState(image.alt)
@@ -41,8 +38,7 @@ export const ImageAltTextDialog = ({
...image, ...image,
alt: enforceLen(altText, MAX_ALT_TEXT, true), alt: enforceLen(altText, MAX_ALT_TEXT, true),
}) })
}} }}>
Portal={Portal}>
<Dialog.Handle /> <Dialog.Handle />
<ImageAltTextInner <ImageAltTextInner
control={control} control={control}
@@ -9,16 +9,14 @@ import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {GifSelectDialog} from '#/components/dialogs/GifSelect' import {GifSelectDialog} from '#/components/dialogs/GifSelect'
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif' import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
import {PortalComponent} from '#/components/Portal'
type Props = { type Props = {
onClose: () => void onClose: () => void
onSelectGif: (gif: Gif) => void onSelectGif: (gif: Gif) => void
disabled?: boolean disabled?: boolean
Portal?: PortalComponent
} }
export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) { export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
const {_} = useLingui() const {_} = useLingui()
const ref = useRef<{open: () => void}>(null) const ref = useRef<{open: () => void}>(null)
const t = useTheme() const t = useTheme()
@@ -48,7 +46,6 @@ export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) {
controlRef={ref} controlRef={ref}
onClose={onClose} onClose={onClose}
onSelectGif={onSelectGif} onSelectGif={onSelectGif}
Portal={Portal}
/> />
</> </>
) )
@@ -13,7 +13,6 @@ import * as Dialog from '#/components/Dialog'
import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog' import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {PortalComponent} from '#/components/Portal'
export function ThreadgateBtn({ export function ThreadgateBtn({
postgate, postgate,
@@ -21,7 +20,6 @@ export function ThreadgateBtn({
threadgateAllowUISettings, threadgateAllowUISettings,
onChangeThreadgateAllowUISettings, onChangeThreadgateAllowUISettings,
style, style,
Portal,
}: { }: {
postgate: AppBskyFeedPostgate.Record postgate: AppBskyFeedPostgate.Record
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
@@ -30,8 +28,6 @@ export function ThreadgateBtn({
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
style?: StyleProp<AnimatedStyle<ViewStyle>> style?: StyleProp<AnimatedStyle<ViewStyle>>
Portal: PortalComponent
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@@ -81,7 +77,6 @@ export function ThreadgateBtn({
onChangePostgate={onChangePostgate} onChangePostgate={onChangePostgate}
threadgateAllowUISettings={threadgateAllowUISettings} threadgateAllowUISettings={threadgateAllowUISettings}
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings} onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
Portal={Portal}
/> />
</> </>
) )
@@ -17,7 +17,6 @@ import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC'
import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText' import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {PortalComponent} from '#/components/Portal'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {SubtitleFilePicker} from './SubtitleFilePicker' import {SubtitleFilePicker} from './SubtitleFilePicker'
@@ -30,7 +29,6 @@ interface Props {
captions: CaptionsTrack[] captions: CaptionsTrack[]
saveAltText: (altText: string) => void saveAltText: (altText: string) => void
setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void
Portal: PortalComponent
} }
export function SubtitleDialogBtn(props: Props) { export function SubtitleDialogBtn(props: Props) {
@@ -58,7 +56,7 @@ export function SubtitleDialogBtn(props: Props) {
{isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>} {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
</ButtonText> </ButtonText>
</Button> </Button>
<Dialog.Outer control={control} Portal={props.Portal}> <Dialog.Outer control={control}>
<Dialog.Handle /> <Dialog.Handle />
<SubtitleDialogInner {...props} /> <SubtitleDialogInner {...props} />
</Dialog.Outer> </Dialog.Outer>
+3 -13
View File
@@ -8,13 +8,10 @@ import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
import {createPortalGroup} from '#/components/Portal'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {H3, P, Text} from '#/components/Typography' import {H3, P, Text} from '#/components/Typography'
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
const Portal = createPortalGroup()
export function Dialogs() { export function Dialogs() {
const scrollable = Dialog.useDialogControl() const scrollable = Dialog.useDialogControl()
const basic = Dialog.useDialogControl() const basic = Dialog.useDialogControl()
@@ -201,7 +198,6 @@ export function Dialogs() {
</Dialog.Outer> </Dialog.Outer>
<Dialog.Outer control={withMenu}> <Dialog.Outer control={withMenu}>
<Portal.Provider>
<Dialog.Inner label="test"> <Dialog.Inner label="test">
<H3 nativeID="dialog-title">Dialog with Menu</H3> <H3 nativeID="dialog-title">Dialog with Menu</H3>
<Menu.Root> <Menu.Root>
@@ -218,24 +214,18 @@ export function Dialogs() {
</Button> </Button>
)} )}
</Menu.Trigger> </Menu.Trigger>
<Menu.Outer Portal={Portal.Portal}> <Menu.Outer>
<Menu.Group> <Menu.Group>
<Menu.Item <Menu.Item label="Item 1" onPress={() => console.log('item 1')}>
label="Item 1"
onPress={() => console.log('item 1')}>
<Menu.ItemText>Item 1</Menu.ItemText> <Menu.ItemText>Item 1</Menu.ItemText>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item label="Item 2" onPress={() => console.log('item 2')}>
label="Item 2"
onPress={() => console.log('item 2')}>
<Menu.ItemText>Item 2</Menu.ItemText> <Menu.ItemText>Item 2</Menu.ItemText>
</Menu.Item> </Menu.Item>
</Menu.Group> </Menu.Group>
</Menu.Outer> </Menu.Outer>
</Menu.Root> </Menu.Root>
</Dialog.Inner> </Dialog.Inner>
<Portal.Outlet />
</Portal.Provider>
</Dialog.Outer> </Dialog.Outer>
<Dialog.Outer control={scrollable}> <Dialog.Outer control={scrollable}>
+2
View File
@@ -34,6 +34,7 @@ import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {SigninDialog} from '#/components/dialogs/Signin' import {SigninDialog} from '#/components/dialogs/Signin'
import {Outlet as PortalOutlet} from '#/components/Portal' import {Outlet as PortalOutlet} from '#/components/Portal'
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView' import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
import {RoutesContainer, TabsNavigator} from '../../Navigation' import {RoutesContainer, TabsNavigator} from '../../Navigation'
import {Composer} from './Composer' import {Composer} from './Composer'
@@ -119,6 +120,7 @@ function ShellInner() {
<SigninDialog /> <SigninDialog />
<Lightbox /> <Lightbox />
<PortalOutlet /> <PortalOutlet />
<BottomSheetOutlet />
</> </>
) )
} }