diff options
20 files changed, 550 insertions, 509 deletions
diff --git a/modules/bottom-sheet/index.ts b/modules/bottom-sheet/index.ts index 1fe3dac0e..4009f2ab2 100644 --- a/modules/bottom-sheet/index.ts +++ b/modules/bottom-sheet/index.ts @@ -4,9 +4,19 @@ import { BottomSheetState, BottomSheetViewProps, } from './src/BottomSheet.types' +import {BottomSheetNativeComponent} from './src/BottomSheetNativeComponent' +import { + BottomSheetOutlet, + BottomSheetPortalProvider, + BottomSheetProvider, +} from './src/BottomSheetPortal' export { BottomSheet, + BottomSheetNativeComponent, + BottomSheetOutlet, + BottomSheetPortalProvider, + BottomSheetProvider, BottomSheetSnapPoint, type BottomSheetState, type BottomSheetViewProps, diff --git a/modules/bottom-sheet/src/BottomSheet.tsx b/modules/bottom-sheet/src/BottomSheet.tsx index 9e7d0c209..bcc2c42ad 100644 --- a/modules/bottom-sheet/src/BottomSheet.tsx +++ b/modules/bottom-sheet/src/BottomSheet.tsx @@ -1,100 +1,24 @@ -import * as React from 'react' -import { - Dimensions, - NativeSyntheticEvent, - Platform, - StyleProp, - View, - ViewStyle, -} from 'react-native' -import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' +import React from 'react' -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< - BottomSheetViewProps & { - ref: React.RefObject<any> - 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 ( - <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}>{children}</View> - </View> - </NativeView> + if (__DEV__ && !Portal) { + throw new Error( + 'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.', ) } -} + + return ( + <Portal> + <BottomSheetNativeComponent {...props} ref={ref} /> + </Portal> + ) +}) diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx new file mode 100644 index 000000000..eadd9b4a1 --- /dev/null +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -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> + ) + } +} diff --git a/modules/bottom-sheet/src/BottomSheetPortal.tsx b/modules/bottom-sheet/src/BottomSheetPortal.tsx new file mode 100644 index 000000000..da14cfa77 --- /dev/null +++ b/modules/bottom-sheet/src/BottomSheetPortal.tsx @@ -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> + ) +} diff --git a/modules/bottom-sheet/src/lib/Portal.tsx b/modules/bottom-sheet/src/lib/Portal.tsx new file mode 100644 index 000000000..dd1bc4c13 --- /dev/null +++ b/modules/bottom-sheet/src/lib/Portal.tsx @@ -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} +} diff --git a/src/App.native.tsx b/src/App.native.tsx index 96b493af4..0b9f112ee 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -68,6 +68,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' +import {BottomSheetProvider} from '../modules/bottom-sheet' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' SplashScreen.preventAutoHideAsync() @@ -197,14 +198,16 @@ function App() { <DialogStateProvider> <LightboxStateProvider> <PortalProvider> - <StarterPackProvider> - <SafeAreaProvider - initialMetrics={initialWindowMetrics}> - <IntentDialogProvider> - <InnerApp /> - </IntentDialogProvider> - </SafeAreaProvider> - </StarterPackProvider> + <BottomSheetProvider> + <StarterPackProvider> + <SafeAreaProvider + initialMetrics={initialWindowMetrics}> + <IntentDialogProvider> + <InnerApp /> + </IntentDialogProvider> + </SafeAreaProvider> + </StarterPackProvider> + </BottomSheetProvider> </PortalProvider> </LightboxStateProvider> </DialogStateProvider> diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 49b5e10b2..6d859aeb0 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -31,12 +31,12 @@ import { DialogOuterProps, } from '#/components/Dialog/types' import {createInput} from '#/components/forms/TextField' -import {Portal as DefaultPortal} from '#/components/Portal' import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' import { BottomSheetSnapPointChangeEvent, BottomSheetStateChangeEvent, } from '../../../modules/bottom-sheet/src/BottomSheet.types' +import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent' export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export * from '#/components/Dialog/types' @@ -50,10 +50,9 @@ export function Outer({ onClose, nativeOptions, testID, - Portal = DefaultPortal, }: React.PropsWithChildren<DialogOuterProps>) { const t = useTheme() - const ref = React.useRef<BottomSheet>(null) + const ref = React.useRef<BottomSheetNativeComponent>(null) const closeCallbacks = React.useRef<(() => void)[]>([]) const {setDialogIsOpen, setFullyExpandedCount} = useDialogStateControlContext() @@ -154,20 +153,18 @@ export function Outer({ ) return ( - <Portal> - <Context.Provider value={context}> - <BottomSheet - ref={ref} - cornerRadius={20} - backgroundColor={t.atoms.bg.backgroundColor} - {...nativeOptions} - onSnapPointChange={onSnapPointChange} - onStateChange={onStateChange} - disableDrag={disableDrag}> - <View testID={testID}>{children}</View> - </BottomSheet> - </Context.Provider> - </Portal> + <Context.Provider value={context}> + <BottomSheet + ref={ref} + cornerRadius={20} + backgroundColor={t.atoms.bg.backgroundColor} + {...nativeOptions} + onSnapPointChange={onSnapPointChange} + onStateChange={onStateChange} + disableDrag={disableDrag}> + <View testID={testID}>{children}</View> + </BottomSheet> + </Context.Provider> ) } diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx index c0ed202da..765e9415d 100644 --- a/src/components/dialogs/GifSelect.tsx +++ b/src/components/dialogs/GifSelect.tsx @@ -30,18 +30,15 @@ import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' -import {PortalComponent} from '#/components/Portal' export function GifSelectDialog({ controlRef, onClose, onSelectGif: onSelectGifProp, - Portal, }: { controlRef: React.RefObject<{open: () => void}> onClose: () => void onSelectGif: (gif: Gif) => void - Portal?: PortalComponent }) { const control = Dialog.useDialogControl() @@ -65,7 +62,6 @@ export function GifSelectDialog({ <Dialog.Outer control={control} onClose={onClose} - Portal={Portal} nativeOptions={{ bottomInset: 0, // use system corner radius on iOS diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index c3aae8f0d..81a614103 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -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 {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Loader} from '#/components/Loader' -import {createPortalGroup} from '#/components/Portal' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' const ONE_DAY = 24 * 60 * 60 * 1000 -const Portal = createPortalGroup() - export function MutedWordsDialog() { const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() return ( @@ -108,349 +105,307 @@ function MutedWordsInner() { }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]) return ( - <Portal.Provider> - <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}> - <View> - <Text - style={[ - a.text_md, - a.font_bold, - a.pb_sm, - t.atoms.text_contrast_high, - ]}> - <Trans>Add muted words and tags</Trans> - </Text> - <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}> - <Trans> - Posts can be muted based on their text, their tags, or both. We - recommend avoiding common words that appear in many posts, since - it can result in no posts being shown. - </Trans> - </Text> - - <View style={[a.pb_sm]}> - <Dialog.Input - autoCorrect={false} - autoCapitalize="none" - autoComplete="off" - label={_(msg`Enter a word or tag`)} - placeholder={_(msg`Enter a word or tag`)} - value={field} - onChangeText={value => { - if (error) { - setError('') - } - setField(value) - }} - onSubmitEditing={submit} - /> - </View> + <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}> + <View> + <Text + style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}> + <Trans>Add muted words and tags</Trans> + </Text> + <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}> + <Trans> + Posts can be muted based on their text, their tags, or both. We + recommend avoiding common words that appear in many posts, since it + can result in no posts being shown. + </Trans> + </Text> + + <View style={[a.pb_sm]}> + <Dialog.Input + autoCorrect={false} + autoCapitalize="none" + autoComplete="off" + label={_(msg`Enter a word or tag`)} + placeholder={_(msg`Enter a word or tag`)} + value={field} + onChangeText={value => { + if (error) { + setError('') + } + setField(value) + }} + onSubmitEditing={submit} + /> + </View> - <View style={[a.pb_xl, a.gap_sm]}> - <Toggle.Group - label={_(msg`Select how long to mute this word for.`)} - type="radio" - values={durations} - onChange={setDurations}> - <Text - style={[ - a.pb_xs, - a.text_sm, - a.font_bold, - t.atoms.text_contrast_medium, - ]}> - <Trans>Duration:</Trans> - </Text> + <View style={[a.pb_xl, a.gap_sm]}> + <Toggle.Group + label={_(msg`Select how long to mute this word for.`)} + type="radio" + values={durations} + onChange={setDurations}> + <Text + style={[ + a.pb_xs, + a.text_sm, + a.font_bold, + t.atoms.text_contrast_medium, + ]}> + <Trans>Duration:</Trans> + </Text> + <View + style={[ + gtMobile && [a.flex_row, a.align_center, a.justify_start], + a.gap_sm, + ]}> <View style={[ - gtMobile && [a.flex_row, a.align_center, a.justify_start], + a.flex_1, + a.flex_row, + a.justify_start, + a.align_center, a.gap_sm, ]}> - <View - style={[ - a.flex_1, - a.flex_row, - a.justify_start, - a.align_center, - a.gap_sm, - ]}> - <Toggle.Item - label={_(msg`Mute this word until you unmute it`)} - name="forever" - style={[a.flex_1]}> - <TargetToggle> - <View - style={[ - a.flex_1, - a.flex_row, - a.align_center, - a.gap_sm, - ]}> - <Toggle.Radio /> - <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>Forever</Trans> - </Toggle.LabelText> - </View> - </TargetToggle> - </Toggle.Item> - - <Toggle.Item - label={_(msg`Mute this word for 24 hours`)} - name="24_hours" - style={[a.flex_1]}> - <TargetToggle> - <View - style={[ - a.flex_1, - a.flex_row, - a.align_center, - a.gap_sm, - ]}> - <Toggle.Radio /> - <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>24 hours</Trans> - </Toggle.LabelText> - </View> - </TargetToggle> - </Toggle.Item> - </View> + <Toggle.Item + label={_(msg`Mute this word until you unmute it`)} + name="forever" + style={[a.flex_1]}> + <TargetToggle> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Radio /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>Forever</Trans> + </Toggle.LabelText> + </View> + </TargetToggle> + </Toggle.Item> - <View - style={[ - a.flex_1, - a.flex_row, - a.justify_start, - a.align_center, - a.gap_sm, - ]}> - <Toggle.Item - label={_(msg`Mute this word for 7 days`)} - name="7_days" - style={[a.flex_1]}> - <TargetToggle> - <View - style={[ - a.flex_1, - a.flex_row, - a.align_center, - a.gap_sm, - ]}> - <Toggle.Radio /> - <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>7 days</Trans> - </Toggle.LabelText> - </View> - </TargetToggle> - </Toggle.Item> - - <Toggle.Item - label={_(msg`Mute this word for 30 days`)} - name="30_days" - style={[a.flex_1]}> - <TargetToggle> - <View - style={[ - a.flex_1, - a.flex_row, - a.align_center, - a.gap_sm, - ]}> - <Toggle.Radio /> - <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>30 days</Trans> - </Toggle.LabelText> - </View> - </TargetToggle> - </Toggle.Item> - </View> + <Toggle.Item + label={_(msg`Mute this word for 24 hours`)} + name="24_hours" + style={[a.flex_1]}> + <TargetToggle> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Radio /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>24 hours</Trans> + </Toggle.LabelText> + </View> + </TargetToggle> + </Toggle.Item> </View> - </Toggle.Group> - <Toggle.Group - label={_( - msg`Select what content this mute word should apply to.`, - )} - type="radio" - values={targets} - onChange={setTargets}> - <Text + <View style={[ - a.pb_xs, - a.text_sm, - a.font_bold, - t.atoms.text_contrast_medium, + a.flex_1, + a.flex_row, + a.justify_start, + a.align_center, + a.gap_sm, ]}> - <Trans>Mute in:</Trans> - </Text> - - <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}> <Toggle.Item - label={_(msg`Mute this word in post text and tags`)} - name="content" + label={_(msg`Mute this word for 7 days`)} + name="7_days" style={[a.flex_1]}> <TargetToggle> <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> <Toggle.Radio /> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>Text & tags</Trans> + <Trans>7 days</Trans> </Toggle.LabelText> </View> - <PageText size="sm" /> </TargetToggle> </Toggle.Item> <Toggle.Item - label={_(msg`Mute this word in tags only`)} - name="tag" + label={_(msg`Mute this word for 30 days`)} + name="30_days" style={[a.flex_1]}> <TargetToggle> <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> <Toggle.Radio /> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>Tags only</Trans> + <Trans>30 days</Trans> </Toggle.LabelText> </View> - <Hashtag size="sm" /> </TargetToggle> </Toggle.Item> </View> - </Toggle.Group> + </View> + </Toggle.Group> - <View> - <Text - style={[ - a.pb_xs, - a.text_sm, - a.font_bold, - t.atoms.text_contrast_medium, - ]}> - <Trans>Options:</Trans> - </Text> + <Toggle.Group + label={_(msg`Select what content this mute word should apply to.`)} + type="radio" + values={targets} + onChange={setTargets}> + <Text + style={[ + a.pb_xs, + a.text_sm, + a.font_bold, + t.atoms.text_contrast_medium, + ]}> + <Trans>Mute in:</Trans> + </Text> + + <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}> <Toggle.Item - label={_(msg`Do not apply this mute word to users you follow`)} - name="exclude_following" - style={[a.flex_row, a.justify_between]} - value={excludeFollowing} - onChange={setExcludeFollowing}> + label={_(msg`Mute this word in post text and tags`)} + name="content" + style={[a.flex_1]}> <TargetToggle> <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> - <Toggle.Checkbox /> + <Toggle.Radio /> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>Exclude users you follow</Trans> + <Trans>Text & tags</Trans> </Toggle.LabelText> </View> + <PageText size="sm" /> </TargetToggle> </Toggle.Item> - </View> - <View style={[a.pt_xs]}> - <Button - disabled={isPending || !field} - label={_(msg`Add mute word for configured settings`)} - size="large" - color="primary" - variant="solid" - style={[]} - onPress={submit}> - <ButtonText> - <Trans>Add</Trans> - </ButtonText> - <ButtonIcon icon={isPending ? Loader : Plus} position="right" /> - </Button> + <Toggle.Item + label={_(msg`Mute this word in tags only`)} + name="tag" + style={[a.flex_1]}> + <TargetToggle> + <View + style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Radio /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>Tags only</Trans> + </Toggle.LabelText> + </View> + <Hashtag size="sm" /> + </TargetToggle> + </Toggle.Item> </View> + </Toggle.Group> - {error && ( - <View - style={[ - a.mb_lg, - a.flex_row, - a.rounded_sm, - a.p_md, - a.mb_xs, - t.atoms.bg_contrast_25, - { - backgroundColor: t.palette.negative_400, - }, - ]}> - <Text - style={[ - a.italic, - {color: t.palette.white}, - native({marginTop: 2}), - ]}> - {error} - </Text> - </View> - )} - </View> - - <Divider /> - - <View style={[a.pt_2xl]}> + <View> <Text style={[ - a.text_md, + a.pb_xs, + a.text_sm, a.font_bold, - a.pb_md, - t.atoms.text_contrast_high, + t.atoms.text_contrast_medium, ]}> - <Trans>Your muted words</Trans> + <Trans>Options:</Trans> </Text> + <Toggle.Item + label={_(msg`Do not apply this mute word to users you follow`)} + name="exclude_following" + style={[a.flex_row, a.justify_between]} + value={excludeFollowing} + onChange={setExcludeFollowing}> + <TargetToggle> + <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> + <Toggle.Checkbox /> + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> + <Trans>Exclude users you follow</Trans> + </Toggle.LabelText> + </View> + </TargetToggle> + </Toggle.Item> + </View> - {isPreferencesLoading ? ( - <Loader /> - ) : preferencesError || !preferences ? ( - <View - style={[ - a.py_md, - a.px_lg, - a.rounded_md, - t.atoms.bg_contrast_25, - ]}> - <Text style={[a.italic, t.atoms.text_contrast_high]}> - <Trans> - We're sorry, but we weren't able to load your muted words at - this time. Please try again. - </Trans> - </Text> - </View> - ) : preferences.moderationPrefs.mutedWords.length ? ( - [...preferences.moderationPrefs.mutedWords] - .reverse() - .map((word, i) => ( - <MutedWordRow - key={word.value + i} - word={word} - style={[i % 2 === 0 && t.atoms.bg_contrast_25]} - /> - )) - ) : ( - <View + <View style={[a.pt_xs]}> + <Button + disabled={isPending || !field} + label={_(msg`Add mute word for configured settings`)} + size="large" + color="primary" + variant="solid" + style={[]} + onPress={submit}> + <ButtonText> + <Trans>Add</Trans> + </ButtonText> + <ButtonIcon icon={isPending ? Loader : Plus} position="right" /> + </Button> + </View> + + {error && ( + <View + style={[ + a.mb_lg, + a.flex_row, + a.rounded_sm, + a.p_md, + a.mb_xs, + t.atoms.bg_contrast_25, + { + backgroundColor: t.palette.negative_400, + }, + ]}> + <Text style={[ - a.py_md, - a.px_lg, - a.rounded_md, - t.atoms.bg_contrast_25, + a.italic, + {color: t.palette.white}, + native({marginTop: 2}), ]}> - <Text style={[a.italic, t.atoms.text_contrast_high]}> - <Trans>You haven't muted any words or tags yet</Trans> - </Text> - </View> - )} - </View> + {error} + </Text> + </View> + )} + </View> + + <Divider /> - {isNative && <View style={{height: 20}} />} + <View style={[a.pt_2xl]}> + <Text + style={[ + a.text_md, + a.font_bold, + a.pb_md, + t.atoms.text_contrast_high, + ]}> + <Trans>Your muted words</Trans> + </Text> + + {isPreferencesLoading ? ( + <Loader /> + ) : preferencesError || !preferences ? ( + <View + style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}> + <Text style={[a.italic, t.atoms.text_contrast_high]}> + <Trans> + We're sorry, but we weren't able to load your muted words at + this time. Please try again. + </Trans> + </Text> + </View> + ) : preferences.moderationPrefs.mutedWords.length ? ( + [...preferences.moderationPrefs.mutedWords] + .reverse() + .map((word, i) => ( + <MutedWordRow + key={word.value + i} + word={word} + style={[i % 2 === 0 && t.atoms.bg_contrast_25]} + /> + )) + ) : ( + <View + style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}> + <Text style={[a.italic, t.atoms.text_contrast_high]}> + <Trans>You haven't muted any words or tags yet</Trans> + </Text> + </View> + )} </View> - <Dialog.Close /> - </Dialog.ScrollableInner> + {isNative && <View style={{height: 20}} />} + </View> - <Portal.Outlet /> - </Portal.Provider> + <Dialog.Close /> + </Dialog.ScrollableInner> ) } @@ -482,7 +437,6 @@ function MutedWordRow({ onConfirm={remove} confirmButtonCta={_(msg`Remove`)} confirmButtonColor="negative" - Portal={Portal.Portal} /> <View diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index bddc49968..0b8b386d3 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -37,7 +37,6 @@ import * as Toggle from '#/components/forms/Toggle' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {Loader} from '#/components/Loader' -import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' export type PostInteractionSettingsFormProps = { @@ -55,15 +54,13 @@ export type PostInteractionSettingsFormProps = { export function PostInteractionSettingsControlledDialog({ control, - Portal, ...rest }: PostInteractionSettingsFormProps & { control: Dialog.DialogControlProps - Portal?: PortalComponent }) { const {_} = useLingui() return ( - <Dialog.Outer control={control} Portal={Portal}> + <Dialog.Outer control={control}> <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`Edit post interaction settings`)} @@ -207,7 +204,9 @@ export function PostInteractionSettingsDialogControlledInner( label={_(msg`Edit post interaction settings`)} style={[{maxWidth: 500}, a.w_full]}> {isLoading ? ( - <Loader size="xl" /> + <View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}> + <Loader size="xl" /> + </View> ) : ( <PostInteractionSettingsForm replySettingsDisabled={!isThreadgateOwnedByViewer} diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx index 80893190f..f770e9c16 100644 --- a/src/state/dialogs/index.tsx +++ b/src/state/dialogs/index.tsx @@ -3,7 +3,7 @@ import React from 'react' import {isWeb} from '#/platform/detection' import {DialogControlRefProps} from '#/components/Dialog' import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' -import {BottomSheet} from '../../../modules/bottom-sheet' +import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet' interface IDialogContext { /** @@ -61,7 +61,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return openDialogs.current.size > 0 } else { - BottomSheet.dismissAll() + BottomSheetNativeComponent.dismissAll() return false } }, []) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 8cc8fba0d..49a498cce 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -107,9 +107,9 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -import {createPortalGroup} from '#/components/Portal' import * as Prompt from '#/components/Prompt' import {Text as NewText} from '#/components/Typography' +import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import { composerReducer, createComposerState, @@ -117,8 +117,6 @@ import { } from './state/composer' import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' -const Portal = createPortalGroup() - type CancelRef = { onPressCancel: () => void } @@ -522,7 +520,7 @@ export const ComposePost = ({ const keyboardVerticalOffset = useKeyboardVerticalOffset() return ( - <Portal.Provider> + <BottomSheetPortalProvider> <KeyboardAvoidingView testID="composePostView" behavior={isIOS ? 'padding' : 'height'} @@ -666,11 +664,7 @@ export const ComposePost = ({ /> </View> - <Gallery - images={images} - dispatch={dispatch} - Portal={Portal.Portal} - /> + <Gallery images={images} dispatch={dispatch} /> {extGif && ( <View style={a.relative} key={extGif.url}> @@ -684,7 +678,6 @@ export const ComposePost = ({ gif={extGif} altText={extGifAlt ?? ''} onSubmit={handleChangeGifAltText} - Portal={Portal.Portal} /> </View> )} @@ -744,7 +737,6 @@ export const ComposePost = ({ }, }) }} - Portal={Portal.Portal} /> </Animated.View> )} @@ -782,7 +774,6 @@ export const ComposePost = ({ }) }} style={bottomBarAnimatedStyle} - Portal={Portal.Portal} /> )} <View @@ -819,7 +810,6 @@ export const ComposePost = ({ onClose={focusTextInput} onSelectGif={onSelectGif} disabled={hasMedia} - Portal={Portal.Portal} /> {!isMobile ? ( <Button @@ -849,11 +839,9 @@ export const ComposePost = ({ onConfirm={onClose} confirmButtonCta={_(msg`Discard`)} confirmButtonColor="negative" - Portal={Portal.Portal} /> </KeyboardAvoidingView> - <Portal.Outlet /> - </Portal.Provider> + </BottomSheetPortalProvider> ) } diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index 01778c381..732bd4bd6 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -21,7 +21,6 @@ import * as TextField from '#/components/forms/TextField' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' import {GifEmbed} from '../util/post-embeds/GifEmbed' import {AltTextReminder} from './photos/Gallery' @@ -30,12 +29,10 @@ export function GifAltTextDialog({ gif, altText, onSubmit, - Portal, }: { gif: Gif altText: string onSubmit: (alt: string) => void - Portal: PortalComponent }) { const {data} = useResolveGifQuery(gif) const vendorAltText = parseAltFromGIFDescription(data?.description ?? '').alt @@ -50,7 +47,6 @@ export function GifAltTextDialog({ thumb={data.thumb?.source.path} params={params} onSubmit={onSubmit} - Portal={Portal} /> ) } @@ -61,14 +57,12 @@ export function GifAltTextDialogLoaded({ onSubmit, params, thumb, - Portal, }: { vendorAltText: string altText: string onSubmit: (alt: string) => void params: EmbedPlayerParams thumb: string | undefined - Portal: PortalComponent }) { const control = Dialog.useDialogControl() const {_} = useLingui() @@ -113,8 +107,7 @@ export function GifAltTextDialogLoaded({ control={control} onClose={() => { onSubmit(altTextDraft) - }} - Portal={Portal}> + }}> <Dialog.Handle /> <AltTextInner vendorAltText={vendorAltText} diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 3958a85c0..5ff7042bc 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -21,7 +21,6 @@ import {ComposerImage, cropImage} from '#/state/gallery' import {Text} from '#/view/com/util/text/Text' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' -import {PortalComponent} from '#/components/Portal' import {ComposerAction} from '../state/composer' import {EditImageDialog} from './EditImageDialog' import {ImageAltTextDialog} from './ImageAltTextDialog' @@ -31,7 +30,6 @@ const IMAGE_GAP = 8 interface GalleryProps { images: ComposerImage[] dispatch: (action: ComposerAction) => void - Portal: PortalComponent } export let Gallery = (props: GalleryProps): React.ReactNode => { @@ -59,12 +57,7 @@ interface GalleryInnerProps extends GalleryProps { containerInfo: Dimensions } -const GalleryInner = ({ - images, - containerInfo, - dispatch, - Portal, -}: GalleryInnerProps) => { +const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => { const {isMobile} = useWebMediaQueries() const {altTextControlStyle, imageControlsStyle, imageStyle} = @@ -118,7 +111,6 @@ const GalleryInner = ({ onRemove={() => { dispatch({type: 'embed_remove_image', image}) }} - Portal={Portal} /> ) })} @@ -135,7 +127,6 @@ type GalleryItemProps = { imageStyle?: ViewStyle onChange: (next: ComposerImage) => void onRemove: () => void - Portal: PortalComponent } const GalleryItem = ({ @@ -145,7 +136,6 @@ const GalleryItem = ({ imageStyle, onChange, onRemove, - Portal, }: GalleryItemProps): React.ReactNode => { const {_} = useLingui() const t = useTheme() @@ -240,7 +230,6 @@ const GalleryItem = ({ control={altTextControl} image={image} onChange={onChange} - Portal={Portal} /> <EditImageDialog diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx index e9e8d4222..aa0b0987a 100644 --- a/src/view/com/composer/photos/ImageAltTextDialog.tsx +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -15,21 +15,18 @@ import * as Dialog from '#/components/Dialog' import {DialogControlProps} from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' type Props = { control: Dialog.DialogOuterProps['control'] image: ComposerImage onChange: (next: ComposerImage) => void - Portal: PortalComponent } export const ImageAltTextDialog = ({ control, image, onChange, - Portal, }: Props): React.ReactNode => { const [altText, setAltText] = React.useState(image.alt) @@ -41,8 +38,7 @@ export const ImageAltTextDialog = ({ ...image, alt: enforceLen(altText, MAX_ALT_TEXT, true), }) - }} - Portal={Portal}> + }}> <Dialog.Handle /> <ImageAltTextInner control={control} diff --git a/src/view/com/composer/photos/SelectGifBtn.tsx b/src/view/com/composer/photos/SelectGifBtn.tsx index d482e0783..d13df0a11 100644 --- a/src/view/com/composer/photos/SelectGifBtn.tsx +++ b/src/view/com/composer/photos/SelectGifBtn.tsx @@ -9,16 +9,14 @@ import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {GifSelectDialog} from '#/components/dialogs/GifSelect' import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif' -import {PortalComponent} from '#/components/Portal' type Props = { onClose: () => void onSelectGif: (gif: Gif) => void disabled?: boolean - Portal?: PortalComponent } -export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) { +export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) { const {_} = useLingui() const ref = useRef<{open: () => void}>(null) const t = useTheme() @@ -48,7 +46,6 @@ export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) { controlRef={ref} onClose={onClose} onSelectGif={onSelectGif} - Portal={Portal} /> </> ) diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx index 7e57a57d4..b0806180c 100644 --- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx +++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx @@ -13,7 +13,6 @@ import * as Dialog from '#/components/Dialog' import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' -import {PortalComponent} from '#/components/Portal' export function ThreadgateBtn({ postgate, @@ -21,7 +20,6 @@ export function ThreadgateBtn({ threadgateAllowUISettings, onChangeThreadgateAllowUISettings, style, - Portal, }: { postgate: AppBskyFeedPostgate.Record onChangePostgate: (v: AppBskyFeedPostgate.Record) => void @@ -30,8 +28,6 @@ export function ThreadgateBtn({ onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void style?: StyleProp<AnimatedStyle<ViewStyle>> - - Portal: PortalComponent }) { const {_} = useLingui() const t = useTheme() @@ -81,7 +77,6 @@ export function ThreadgateBtn({ onChangePostgate={onChangePostgate} threadgateAllowUISettings={threadgateAllowUISettings} onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings} - Portal={Portal} /> </> ) diff --git a/src/view/com/composer/videos/SubtitleDialog.tsx b/src/view/com/composer/videos/SubtitleDialog.tsx index 04522ee1d..27c3de02b 100644 --- a/src/view/com/composer/videos/SubtitleDialog.tsx +++ b/src/view/com/composer/videos/SubtitleDialog.tsx @@ -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 {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' -import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' import {SubtitleFilePicker} from './SubtitleFilePicker' @@ -30,7 +29,6 @@ interface Props { captions: CaptionsTrack[] saveAltText: (altText: string) => void setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void - Portal: PortalComponent } export function SubtitleDialogBtn(props: Props) { @@ -58,7 +56,7 @@ export function SubtitleDialogBtn(props: Props) { {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>} </ButtonText> </Button> - <Dialog.Outer control={control} Portal={props.Portal}> + <Dialog.Outer control={control}> <Dialog.Handle /> <SubtitleDialogInner {...props} /> </Dialog.Outer> diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index e6fcef555..343d7f07b 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -8,13 +8,10 @@ import {atoms as a} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as Menu from '#/components/Menu' -import {createPortalGroup} from '#/components/Portal' import * as Prompt from '#/components/Prompt' import {H3, P, Text} from '#/components/Typography' import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' -const Portal = createPortalGroup() - export function Dialogs() { const scrollable = Dialog.useDialogControl() const basic = Dialog.useDialogControl() @@ -201,41 +198,34 @@ export function Dialogs() { </Dialog.Outer> <Dialog.Outer control={withMenu}> - <Portal.Provider> - <Dialog.Inner label="test"> - <H3 nativeID="dialog-title">Dialog with Menu</H3> - <Menu.Root> - <Menu.Trigger label="Open menu"> - {({props}) => ( - <Button - style={a.mt_2xl} - label="Open menu" - color="primary" - variant="solid" - size="large" - {...props}> - <ButtonText>Open Menu</ButtonText> - </Button> - )} - </Menu.Trigger> - <Menu.Outer Portal={Portal.Portal}> - <Menu.Group> - <Menu.Item - label="Item 1" - onPress={() => console.log('item 1')}> - <Menu.ItemText>Item 1</Menu.ItemText> - </Menu.Item> - <Menu.Item - label="Item 2" - onPress={() => console.log('item 2')}> - <Menu.ItemText>Item 2</Menu.ItemText> - </Menu.Item> - </Menu.Group> - </Menu.Outer> - </Menu.Root> - </Dialog.Inner> - <Portal.Outlet /> - </Portal.Provider> + <Dialog.Inner label="test"> + <H3 nativeID="dialog-title">Dialog with Menu</H3> + <Menu.Root> + <Menu.Trigger label="Open menu"> + {({props}) => ( + <Button + style={a.mt_2xl} + label="Open menu" + color="primary" + variant="solid" + size="large" + {...props}> + <ButtonText>Open Menu</ButtonText> + </Button> + )} + </Menu.Trigger> + <Menu.Outer> + <Menu.Group> + <Menu.Item label="Item 1" onPress={() => console.log('item 1')}> + <Menu.ItemText>Item 1</Menu.ItemText> + </Menu.Item> + <Menu.Item label="Item 2" onPress={() => console.log('item 2')}> + <Menu.ItemText>Item 2</Menu.ItemText> + </Menu.Item> + </Menu.Group> + </Menu.Outer> + </Menu.Root> + </Dialog.Inner> </Dialog.Outer> <Dialog.Outer control={scrollable}> diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 8bc3de24d..9f7569beb 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -34,6 +34,7 @@ import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {SigninDialog} from '#/components/dialogs/Signin' import {Outlet as PortalOutlet} from '#/components/Portal' +import {BottomSheetOutlet} from '../../../modules/bottom-sheet' import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView' import {RoutesContainer, TabsNavigator} from '../../Navigation' import {Composer} from './Composer' @@ -119,6 +120,7 @@ function ShellInner() { <SigninDialog /> <Lightbox /> <PortalOutlet /> + <BottomSheetOutlet /> </> ) } |