diff options
Diffstat (limited to 'src/components')
39 files changed, 744 insertions, 1025 deletions
diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 1c14b48c7..4acb4f1dc 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -87,6 +87,7 @@ export type ButtonProps = Pick< style?: StyleProp<ViewStyle> hoverStyle?: StyleProp<ViewStyle> children: NonTextElements | ((context: ButtonContext) => NonTextElements) + PressableComponent?: React.ComponentType<PressableProps> } export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} @@ -114,6 +115,7 @@ export const Button = React.forwardRef<View, ButtonProps>( disabled = false, style, hoverStyle: hoverStyleProp, + PressableComponent = Pressable, ...rest }, ref, @@ -449,10 +451,11 @@ export const Button = React.forwardRef<View, ButtonProps>( const flattenedBaseStyles = flatten([baseStyles, style]) return ( - <Pressable + <PressableComponent role="button" accessibilityHint={undefined} // optional {...rest} + // @ts-ignore - this will always be a pressable ref={ref} aria-label={label} aria-pressed={state.pressed} @@ -500,7 +503,7 @@ export const Button = React.forwardRef<View, ButtonProps>( <Context.Provider value={context}> {typeof children === 'function' ? children(context) : children} </Context.Provider> - </Pressable> + </PressableComponent> ) }, ) diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 859f8edd7..b479bc7f0 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -6,9 +6,14 @@ import { DialogControlRefProps, DialogOuterProps, } from '#/components/Dialog/types' +import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types' export const Context = React.createContext<DialogContextProps>({ close: () => {}, + isNativeDialog: false, + nativeSnapPoint: BottomSheetSnapPoint.Hidden, + disableDrag: false, + setDisableDrag: () => {}, }) export function useDialogContext() { diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index d5d92048a..49b5e10b2 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -1,86 +1,48 @@ import React, {useImperativeHandle} from 'react' import { - Dimensions, - Keyboard, + NativeScrollEvent, + NativeSyntheticEvent, Pressable, + ScrollView, StyleProp, + TextInput, View, ViewStyle, } from 'react-native' -import Animated, {useAnimatedStyle} from 'react-native-reanimated' +import { + KeyboardAwareScrollView, + useKeyboardHandler, +} from 'react-native-keyboard-controller' +import {runOnJS} from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import BottomSheet, { - BottomSheetBackdropProps, - BottomSheetFlatList, - BottomSheetFlatListMethods, - BottomSheetScrollView, - BottomSheetScrollViewMethods, - BottomSheetTextInput, - BottomSheetView, - useBottomSheet, - WINDOW_HEIGHT, -} from '@discord/bottom-sheet/src' -import {BottomSheetFlatListProps} from '@discord/bottom-sheet/src/components/bottomSheetScrollable/types' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {logger} from '#/logger' +import {isAndroid, isIOS} from '#/platform/detection' +import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' -import {atoms as a, flatten, useTheme} from '#/alf' -import {Context} from '#/components/Dialog/context' +import {List, ListMethods, ListProps} from '#/view/com/util/List' +import {atoms as a, useTheme} from '#/alf' +import {Context, useDialogContext} from '#/components/Dialog/context' import { DialogControlProps, DialogInnerProps, DialogOuterProps, } from '#/components/Dialog/types' import {createInput} from '#/components/forms/TextField' -import {FullWindowOverlay} from '#/components/FullWindowOverlay' -import {Portal} from '#/components/Portal' +import {Portal as DefaultPortal} from '#/components/Portal' +import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' +import { + BottomSheetSnapPointChangeEvent, + BottomSheetStateChangeEvent, +} from '../../../modules/bottom-sheet/src/BottomSheet.types' export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export * from '#/components/Dialog/types' export * from '#/components/Dialog/utils' // @ts-ignore -export const Input = createInput(BottomSheetTextInput) - -function Backdrop(props: BottomSheetBackdropProps) { - const t = useTheme() - const bottomSheet = useBottomSheet() - - const animatedStyle = useAnimatedStyle(() => { - const opacity = - (Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000 - - return { - opacity: Math.min(Math.max(opacity, 0), 0.55), - } - }) - - const onPress = React.useCallback(() => { - bottomSheet.close() - }, [bottomSheet]) - - return ( - <Animated.View - style={[ - t.atoms.bg_contrast_300, - { - top: 0, - left: 0, - right: 0, - bottom: 0, - position: 'absolute', - }, - animatedStyle, - ]}> - <Pressable - accessibilityRole="button" - accessibilityLabel="Dialog backdrop" - accessibilityHint="Press the backdrop to close the dialog" - style={{flex: 1}} - onPress={onPress} - /> - </Animated.View> - ) -} +export const Input = createInput(TextInput) export function Outer({ children, @@ -88,24 +50,22 @@ export function Outer({ onClose, nativeOptions, testID, + Portal = DefaultPortal, }: React.PropsWithChildren<DialogOuterProps>) { const t = useTheme() - const sheet = React.useRef<BottomSheet>(null) - const sheetOptions = nativeOptions?.sheet || {} - const hasSnapPoints = !!sheetOptions.snapPoints - const insets = useSafeAreaInsets() + const ref = React.useRef<BottomSheet>(null) const closeCallbacks = React.useRef<(() => void)[]>([]) - const {setDialogIsOpen} = useDialogStateControlContext() + const {setDialogIsOpen, setFullyExpandedCount} = + useDialogStateControlContext() - /* - * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet` - */ - const [openIndex, setOpenIndex] = React.useState(-1) + const prevSnapPoint = React.useRef<BottomSheetSnapPoint>( + BottomSheetSnapPoint.Hidden, + ) - /* - * `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open. - */ - const isOpen = openIndex > -1 + const [disableDrag, setDisableDrag] = React.useState(false) + const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>( + BottomSheetSnapPoint.Partial, + ) const callQueuedCallbacks = React.useCallback(() => { for (const cb of closeCallbacks.current) { @@ -119,25 +79,19 @@ export function Outer({ closeCallbacks.current = [] }, []) - const open = React.useCallback<DialogControlProps['open']>( - ({index} = {}) => { - // Run any leftover callbacks that might have been queued up before calling `.open()` - callQueuedCallbacks() - - setDialogIsOpen(control.id, true) - // can be set to any index of `snapPoints`, but `0` is the first i.e. "open" - setOpenIndex(index || 0) - sheet.current?.snapToIndex(index || 0) - }, - [setDialogIsOpen, control.id, callQueuedCallbacks], - ) + const open = React.useCallback<DialogControlProps['open']>(() => { + // Run any leftover callbacks that might have been queued up before calling `.open()` + callQueuedCallbacks() + setDialogIsOpen(control.id, true) + ref.current?.present() + }, [setDialogIsOpen, control.id, callQueuedCallbacks]) // This is the function that we call when we want to dismiss the dialog. const close = React.useCallback<DialogControlProps['close']>(cb => { if (typeof cb === 'function') { closeCallbacks.current.push(cb) } - sheet.current?.close() + ref.current?.dismiss() }, []) // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to @@ -146,12 +100,39 @@ export function Outer({ // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this // tells us that we need to toggle the accessibility overlay setting setDialogIsOpen(control.id, false) - setOpenIndex(-1) - callQueuedCallbacks() onClose?.() }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) + const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => { + const {snapPoint} = e.nativeEvent + setSnapPoint(snapPoint) + + if ( + snapPoint === BottomSheetSnapPoint.Full && + prevSnapPoint.current !== BottomSheetSnapPoint.Full + ) { + setFullyExpandedCount(c => c + 1) + } else if ( + snapPoint !== BottomSheetSnapPoint.Full && + prevSnapPoint.current === BottomSheetSnapPoint.Full + ) { + setFullyExpandedCount(c => c - 1) + } + prevSnapPoint.current = snapPoint + } + + const onStateChange = (e: BottomSheetStateChangeEvent) => { + if (e.nativeEvent.state === 'closed') { + onCloseAnimationComplete() + + if (prevSnapPoint.current === BottomSheetSnapPoint.Full) { + setFullyExpandedCount(c => c - 1) + } + prevSnapPoint.current = BottomSheetSnapPoint.Hidden + } + } + useImperativeHandle( control.ref, () => ({ @@ -161,159 +142,144 @@ export function Outer({ [open, close], ) - React.useEffect(() => { - return () => { - setDialogIsOpen(control.id, false) - } - }, [control.id, setDialogIsOpen]) - - const context = React.useMemo(() => ({close}), [close]) + const context = React.useMemo( + () => ({ + close, + isNativeDialog: true, + nativeSnapPoint: snapPoint, + disableDrag, + setDisableDrag, + }), + [close, snapPoint, disableDrag, setDisableDrag], + ) return ( - isOpen && ( - <Portal> - <FullWindowOverlay> - <View - // iOS - accessibilityViewIsModal - // Android - importantForAccessibility="yes" - style={[a.absolute, a.inset_0]} - testID={testID} - onTouchMove={() => Keyboard.dismiss()}> - <BottomSheet - enableDynamicSizing={!hasSnapPoints} - enablePanDownToClose - keyboardBehavior="interactive" - android_keyboardInputMode="adjustResize" - keyboardBlurBehavior="restore" - topInset={insets.top} - {...sheetOptions} - snapPoints={sheetOptions.snapPoints || ['100%']} - ref={sheet} - index={openIndex} - backgroundStyle={{backgroundColor: 'transparent'}} - backdropComponent={Backdrop} - handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} - handleStyle={{display: 'none'}} - onClose={onCloseAnimationComplete}> - <Context.Provider value={context}> - <View - style={[ - a.absolute, - a.inset_0, - t.atoms.bg, - { - borderTopLeftRadius: 40, - borderTopRightRadius: 40, - height: Dimensions.get('window').height * 2, - }, - ]} - /> - {children} - </Context.Provider> - </BottomSheet> - </View> - </FullWindowOverlay> - </Portal> - ) + <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> ) } export function Inner({children, style}: DialogInnerProps) { const insets = useSafeAreaInsets() return ( - <BottomSheetView + <View style={[ - a.py_xl, + a.pt_2xl, a.px_xl, { - paddingTop: 40, - borderTopLeftRadius: 40, - borderTopRightRadius: 40, - paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, + paddingBottom: insets.bottom + insets.top, }, - flatten(style), + style, ]}> {children} - </BottomSheetView> + </View> ) } -export const ScrollableInner = React.forwardRef< - BottomSheetScrollViewMethods, - DialogInnerProps ->(function ScrollableInner({children, style}, ref) { - const insets = useSafeAreaInsets() - return ( - <BottomSheetScrollView - keyboardShouldPersistTaps="handled" - style={[ - a.flex_1, // main diff is this - a.p_xl, - a.h_full, - { - paddingTop: 40, - borderTopLeftRadius: 40, - borderTopRightRadius: 40, - }, - style, - ]} - contentContainerStyle={a.pb_4xl} - ref={ref}> - {children} - <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> - </BottomSheetScrollView> - ) -}) +export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( + function ScrollableInner({children, style, ...props}, ref) { + const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() + const insets = useSafeAreaInsets() + const [keyboardHeight, setKeyboardHeight] = React.useState(0) + useKeyboardHandler({ + onEnd: e => { + 'worklet' + runOnJS(setKeyboardHeight)(e.height) + }, + }) + + const basePading = + (isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight) + const fullPaddingBase = insets.bottom + insets.top + basePading + const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50 + + const paddingBottom = + nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading + + const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { + const {contentOffset} = e.nativeEvent + if (contentOffset.y > 0 && !disableDrag) { + setDisableDrag(true) + } else if (contentOffset.y <= 1 && disableDrag) { + setDisableDrag(false) + } + } + + return ( + <KeyboardAwareScrollView + style={[style]} + contentContainerStyle={[a.pt_2xl, a.px_xl, {paddingBottom}]} + ref={ref} + {...props} + bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} + bottomOffset={30} + scrollEventThrottle={50} + onScroll={isAndroid ? onScroll : undefined}> + {children} + </KeyboardAwareScrollView> + ) + }, +) export const InnerFlatList = React.forwardRef< - BottomSheetFlatListMethods, - BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>} ->(function InnerFlatList({style, contentContainerStyle, ...props}, ref) { + ListMethods, + ListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>} +>(function InnerFlatList({style, ...props}, ref) { const insets = useSafeAreaInsets() - + const {nativeSnapPoint} = useDialogContext() return ( - <BottomSheetFlatList + <List keyboardShouldPersistTaps="handled" - contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]} + bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} ListFooterComponent={ <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> } ref={ref} {...props} - style={[ - a.flex_1, - a.p_xl, - a.pt_0, - a.h_full, - { - marginTop: 40, - }, - flatten(style), - ]} + style={[style]} /> ) }) export function Handle() { const t = useTheme() + const {_} = useLingui() + const {screenReaderEnabled} = useA11y() + const {close} = useDialogContext() return ( - <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}> - <View - style={[ - a.rounded_sm, - { - top: a.pt_lg.paddingTop, - width: 35, - height: 4, - alignSelf: 'center', - backgroundColor: t.palette.contrast_900, - opacity: 0.5, - }, - ]} - /> + <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}> + <Pressable + accessible={screenReaderEnabled} + onPress={() => close()} + accessibilityLabel={_(msg`Dismiss`)} + accessibilityHint={_(msg`Double tap to close the dialog`)}> + <View + style={[ + a.rounded_sm, + { + top: 10, + width: 35, + height: 5, + alignSelf: 'center', + backgroundColor: t.palette.contrast_975, + opacity: 0.5, + }, + ]} + /> + </Pressable> </View> ) } diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index bf20bd295..7b9cfb693 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -103,6 +103,10 @@ export function Outer({ const context = React.useMemo( () => ({ close, + isNativeDialog: false, + nativeSnapPoint: 0, + disableDrag: false, + setDisableDrag: () => {}, }), [close], ) @@ -229,10 +233,6 @@ export const InnerFlatList = React.forwardRef< ) }) -export function Handle() { - return null -} - export function Close() { const {_} = useLingui() const {close} = React.useContext(Context) @@ -258,3 +258,7 @@ export function Close() { </View> ) } + +export function Handle() { + return null +} diff --git a/src/components/Dialog/sheet-wrapper.ts b/src/components/Dialog/sheet-wrapper.ts new file mode 100644 index 000000000..37c663383 --- /dev/null +++ b/src/components/Dialog/sheet-wrapper.ts @@ -0,0 +1,20 @@ +import {useCallback} from 'react' + +import {useDialogStateControlContext} from '#/state/dialogs' + +/** + * If we're calling a system API like the image picker that opens a sheet + * wrap it in this function to make sure the status bar is the correct color. + */ +export function useSheetWrapper() { + const {setFullyExpandedCount} = useDialogStateControlContext() + return useCallback( + async <T>(promise: Promise<T>): Promise<T> => { + setFullyExpandedCount(c => c + 1) + const res = await promise + setFullyExpandedCount(c => c - 1) + return res + }, + [setFullyExpandedCount], + ) +} diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index 1ddab02ee..caa787535 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -4,9 +4,11 @@ import type { GestureResponderEvent, ScrollViewProps, } from 'react-native' -import {BottomSheetProps} from '@discord/bottom-sheet/src' import {ViewStyleProp} from '#/alf' +import {PortalComponent} from '#/components/Portal' +import {BottomSheetViewProps} from '../../../modules/bottom-sheet' +import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types' type A11yProps = Required<AccessibilityProps> @@ -37,6 +39,10 @@ export type DialogControlProps = DialogControlRefProps & { export type DialogContextProps = { close: DialogControlProps['close'] + isNativeDialog: boolean + nativeSnapPoint: BottomSheetSnapPoint + disableDrag: boolean + setDisableDrag: React.Dispatch<React.SetStateAction<boolean>> } export type DialogControlOpenOptions = { @@ -52,11 +58,10 @@ export type DialogControlOpenOptions = { export type DialogOuterProps = { control: DialogControlProps onClose?: () => void - nativeOptions?: { - sheet?: Omit<BottomSheetProps, 'children'> - } + nativeOptions?: Omit<BottomSheetViewProps, 'children'> webOptions?: {} testID?: string + Portal?: PortalComponent } type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T diff --git a/src/components/KeyboardControllerPadding.android.tsx b/src/components/KeyboardControllerPadding.android.tsx deleted file mode 100644 index 92ef1b0b0..000000000 --- a/src/components/KeyboardControllerPadding.android.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react' -import {useKeyboardHandler} from 'react-native-keyboard-controller' -import Animated, { - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated' - -export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) { - const keyboardHeight = useSharedValue(0) - - useKeyboardHandler( - { - onMove: e => { - 'worklet' - - if (maxHeight && e.height > maxHeight) { - keyboardHeight.value = maxHeight - } else { - keyboardHeight.value = e.height - } - }, - }, - [maxHeight], - ) - - const animatedStyle = useAnimatedStyle(() => ({ - height: keyboardHeight.value, - })) - - return <Animated.View style={animatedStyle} /> -} diff --git a/src/components/KeyboardControllerPadding.tsx b/src/components/KeyboardControllerPadding.tsx deleted file mode 100644 index f3163d87c..000000000 --- a/src/components/KeyboardControllerPadding.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export function KeyboardControllerPadding({ - maxHeight: _, -}: { - maxHeight?: number -}) { - return null -} diff --git a/src/components/LikesDialog.tsx b/src/components/LikesDialog.tsx index 94a3f27e2..4c68596f7 100644 --- a/src/components/LikesDialog.tsx +++ b/src/components/LikesDialog.tsx @@ -1,20 +1,19 @@ -import React, {useMemo, useCallback} from 'react' +import React, {useCallback, useMemo} from 'react' import {ActivityIndicator, FlatList, View} from 'react-native' +import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' -import {useResolveUriQuery} from '#/state/queries/resolve-uri' -import {useLikedByQuery} from '#/state/queries/post-liked-by' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' - +import {useLikedByQuery} from '#/state/queries/post-liked-by' +import {useResolveUriQuery} from '#/state/queries/resolve-uri' +import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {atoms as a, useTheme} from '#/alf' -import {Text} from '#/components/Typography' import * as Dialog from '#/components/Dialog' -import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' interface LikesDialogProps { control: Dialog.DialogOuterProps['control'] @@ -25,7 +24,6 @@ export function LikesDialog(props: LikesDialogProps) { return ( <Dialog.Outer control={props.control}> <Dialog.Handle /> - <LikesDialogInner {...props} /> </Dialog.Outer> ) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index c80b9f370..447833a23 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -103,17 +103,17 @@ export function useLink({ linkRequiresWarning(href, displayText), ) - if (requiresWarning) { + if (isWeb) { e.preventDefault() + } + if (requiresWarning) { openModal({ name: 'link-warning', text: displayText, href: href, }) } else { - e.preventDefault() - if (isExternal) { openLink(href) } else { diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index a0a21a50f..a22f43cf8 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -4,7 +4,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import flattenReactChildren from 'react-keyed-flatten-children' -import {isNative} from 'platform/detection' +import {isNative} from '#/platform/detection' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -82,19 +82,21 @@ export function Outer({ style?: StyleProp<ViewStyle> }>) { const context = React.useContext(Context) + const {_} = useLingui() return ( - <Dialog.Outer control={context.control}> + <Dialog.Outer + control={context.control} + nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> - {/* Re-wrap with context since Dialogs are portal-ed to root */} <Context.Provider value={context}> - <Dialog.ScrollableInner label="Menu TODO"> + <Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.pt_sm]}> <View style={[a.gap_lg]}> {children} {isNative && showCancel && <Cancel />} + <View style={[{height: a.pb_lg.paddingBottom}]} /> </View> - <View style={{height: a.gap_lg.gap}} /> </Dialog.ScrollableInner> </Context.Provider> </Dialog.Outer> @@ -116,15 +118,14 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) { {...rest} accessibilityHint="" accessibilityLabel={label} - onPress={e => { - onPress(e) - + onFocus={onFocus} + onBlur={onBlur} + onPress={async e => { + await onPress(e) if (!e.defaultPrevented) { control?.close() } }} - onFocus={onFocus} - onBlur={onBlur} onPressIn={e => { onPressIn() rest.onPressIn?.(e) diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx index 1a523a839..0e3520658 100644 --- a/src/components/NewskieDialog.tsx +++ b/src/components/NewskieDialog.tsx @@ -5,12 +5,12 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {differenceInSeconds} from 'date-fns' +import {HITSLOP_10} from '#/lib/constants' import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' +import {sanitizeDisplayName} from '#/lib/strings/display-names' import {isNative} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {HITSLOP_10} from 'lib/constants' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {useSession} from 'state/session' +import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx index 03b397b2b..7441df005 100644 --- a/src/components/Portal.tsx +++ b/src/components/Portal.tsx @@ -12,6 +12,8 @@ type ComponentMap = { [id: string]: Component } +export type PortalComponent = ({children}: {children?: React.ReactNode}) => null + export function createPortalGroup() { const Context = React.createContext<ContextType>({ outlet: null, diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 8765cdee3..fc6919af8 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -4,8 +4,9 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button' +import {Button, ButtonColor, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' +import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' export { @@ -25,9 +26,11 @@ export function Outer({ children, control, testID, + Portal, }: React.PropsWithChildren<{ control: Dialog.DialogControlProps testID?: string + Portal?: PortalComponent }>) { const {gtMobile} = useBreakpoints() const titleId = React.useId() @@ -39,10 +42,9 @@ export function Outer({ ) return ( - <Dialog.Outer control={control} testID={testID}> + <Dialog.Outer control={control} testID={testID} Portal={Portal}> + <Dialog.Handle /> <Context.Provider value={context}> - <Dialog.Handle /> - <Dialog.ScrollableInner accessibilityLabelledBy={titleId} accessibilityDescribedBy={descriptionId} @@ -141,7 +143,7 @@ export function Action({ * Note: The dialog will close automatically when the action is pressed, you * should NOT close the dialog as a side effect of this method. */ - onPress: ButtonProps['onPress'] + onPress: (e: GestureResponderEvent) => void color?: ButtonColor /** * Optional i18n string. If undefined, it will default to "Confirm". @@ -181,6 +183,7 @@ export function Basic({ onConfirm, confirmButtonColor, showCancel = true, + Portal, }: React.PropsWithChildren<{ control: Dialog.DialogOuterProps['control'] title: string @@ -194,12 +197,13 @@ export function Basic({ * Note: The dialog will close automatically when the action is pressed, you * should NOT close the dialog as a side effect of this method. */ - onConfirm: ButtonProps['onPress'] + onConfirm: (e: GestureResponderEvent) => void confirmButtonColor?: ButtonColor showCancel?: boolean + Portal?: PortalComponent }>) { return ( - <Outer control={control} testID="confirmModal"> + <Outer control={control} testID="confirmModal" Portal={Portal}> <TitleText>{title}</TitleText> <DescriptionText>{description}</DescriptionText> <Actions> diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx index f7a8139ea..039bbf123 100644 --- a/src/components/ReportDialog/SelectLabelerView.tsx +++ b/src/components/ReportDialog/SelectLabelerView.tsx @@ -4,7 +4,6 @@ import {AppBskyLabelerDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -export {useDialogControl as useReportDialogControl} from '#/components/Dialog' import {getLabelingServiceTitle} from '#/lib/moderation' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, useButtonContext} from '#/components/Button' diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx index e323d1504..ef4a9b7fb 100644 --- a/src/components/ReportDialog/SubmitView.tsx +++ b/src/components/ReportDialog/SubmitView.tsx @@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react' import {getLabelingServiceTitle} from '#/lib/moderation' import {ReportOption} from '#/lib/moderation/useReportOptions' +import {isAndroid} from '#/platform/detection' import {useAgent} from '#/state/session' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import * as Toast from '#/view/com/util/Toast' @@ -225,6 +226,8 @@ export function SubmitView({ {submitting && <ButtonIcon icon={Loader} />} </Button> </View> + {/* Maybe fix this later -h */} + {isAndroid ? <View style={{height: 300}} /> : null} </View> ) } diff --git a/src/components/ReportDialog/index.tsx b/src/components/ReportDialog/index.tsx index c87d32f9e..5bf8aa5b4 100644 --- a/src/components/ReportDialog/index.tsx +++ b/src/components/ReportDialog/index.tsx @@ -1,5 +1,6 @@ import React from 'react' import {Pressable, View} from 'react-native' +import {ScrollView} from 'react-native-gesture-handler' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -8,12 +9,10 @@ import {useMyLabelersQuery} from '#/state/queries/preferences' export {useDialogControl as useReportDialogControl} from '#/components/Dialog' import {AppBskyLabelerDefs} from '@atproto/api' -import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src' import {atoms as a} from '#/alf' import * as Dialog from '#/components/Dialog' import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' -import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {SelectLabelerView} from './SelectLabelerView' @@ -25,7 +24,6 @@ export function ReportDialog(props: ReportDialogProps) { return ( <Dialog.Outer control={props.control}> <Dialog.Handle /> - <ReportDialogInner {...props} /> </Dialog.Outer> ) @@ -40,10 +38,7 @@ function ReportDialogInner(props: ReportDialogProps) { } = useMyLabelersQuery() const isLoading = useDelayedLoading(500, isLabelerLoading) - const ref = React.useRef<BottomSheetScrollViewMethods>(null) - useOnKeyboardDidShow(() => { - ref.current?.scrollToEnd({animated: true}) - }) + const ref = React.useRef<ScrollView>(null) return ( <Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}> diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx index b2af8ff73..2feea0973 100644 --- a/src/components/StarterPack/QrCodeDialog.tsx +++ b/src/components/StarterPack/QrCodeDialog.tsx @@ -149,7 +149,6 @@ export function QrCodeDialog({ return ( <Dialog.Outer control={control}> - <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`Create a QR code for a starter pack`)}> <View style={[a.flex_1, a.align_center, a.gap_5xl]}> diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx index 9851b0856..997c6479c 100644 --- a/src/components/StarterPack/ShareDialog.tsx +++ b/src/components/StarterPack/ShareDialog.tsx @@ -6,14 +6,14 @@ import {AppBskyGraphDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {saveImageToMediaLibrary} from '#/lib/media/manip' +import {shareUrl} from '#/lib/sharing' +import {logEvent} from '#/lib/statsig/statsig' +import {getStarterPackOgCard} from '#/lib/strings/starter-pack' import {logger} from '#/logger' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {saveImageToMediaLibrary} from 'lib/media/manip' -import {shareUrl} from 'lib/sharing' -import {logEvent} from 'lib/statsig/statsig' -import {getStarterPackOgCard} from 'lib/strings/starter-pack' -import {isNative, isWeb} from 'platform/detection' -import * as Toast from 'view/com/util/Toast' +import {isNative, isWeb} from '#/platform/detection' +import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {DialogControlProps} from '#/components/Dialog' @@ -32,6 +32,7 @@ interface Props { export function ShareDialog(props: Props) { return ( <Dialog.Outer control={props.control}> + <Dialog.Handle /> <ShareDialogInner {...props} /> </Dialog.Outer> ) @@ -84,7 +85,6 @@ function ShareDialogInner({ return ( <> - <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`Share link dialog`)}> {!imageLoaded || !link ? ( <View style={[a.p_xl, a.align_center]}> diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx index f7b0aba34..1e9f1c52d 100644 --- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx +++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx @@ -3,13 +3,13 @@ import type {ListRenderItemInfo} from 'react-native' import {View} from 'react-native' import {AppBskyActorDefs, ModerationOpts} from '@atproto/api' import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' -import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' -import {isWeb} from 'platform/detection' -import {useSession} from 'state/session' +import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {isWeb} from '#/platform/detection' +import {useSession} from '#/state/session' +import {ListMethods} from '#/view/com/util/List' import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' import {atoms as a, native, useTheme, web} from '#/alf' import {Button, ButtonText} from '#/components/Button' @@ -45,7 +45,7 @@ export function WizardEditListDialog({ const {currentAccount} = useSession() const initialNumToRender = useInitialNumToRender() - const listRef = useRef<BottomSheetFlatListMethods>(null) + const listRef = useRef<ListMethods>(null) const getData = () => { if (state.currentStep === 'Feeds') return state.feeds @@ -76,10 +76,7 @@ export function WizardEditListDialog({ ) return ( - <Dialog.Outer - control={control} - testID="newChatDialog" - nativeOptions={{sheet: {snapPoints: ['95%']}}}> + <Dialog.Outer control={control} testID="newChatDialog"> <Dialog.Handle /> <Dialog.InnerFlatList ref={listRef} @@ -89,6 +86,7 @@ export function WizardEditListDialog({ ListHeaderComponent={ <View style={[ + native(a.pt_4xl), a.flex_row, a.justify_between, a.border_b, @@ -103,13 +101,7 @@ export function WizardEditListDialog({ height: 48, }, ] - : [ - a.pb_sm, - a.align_end, - { - height: 68, - }, - ], + : [a.pb_sm, a.align_end], ]}> <View style={{width: 60}} /> <Text style={[a.font_bold, a.text_xl]}> @@ -143,8 +135,6 @@ export function WizardEditListDialog({ paddingHorizontal: 0, marginTop: 0, paddingTop: 0, - borderTopLeftRadius: 40, - borderTopRightRadius: 40, }), ]} webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx index 2c6a0b674..917624a03 100644 --- a/src/components/TagMenu/index.tsx +++ b/src/components/TagMenu/index.tsx @@ -85,7 +85,6 @@ export function TagMenu({ <Dialog.Outer control={control}> <Dialog.Handle /> - <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}> {isPreferencesLoading ? ( <View style={[a.w_full, a.align_center]}> diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx index 08608f9d8..81d0c6740 100644 --- a/src/components/dialogs/BirthDateSettings.tsx +++ b/src/components/dialogs/BirthDateSettings.tsx @@ -31,7 +31,6 @@ export function BirthDateSettingsDialog({ return ( <Dialog.Outer control={control}> <Dialog.Handle /> - <Dialog.ScrollableInner label={_(msg`My Birthday`)}> <View style={[a.gap_sm, a.pb_lg]}> <Text style={[a.text_2xl, a.font_bold]}> diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx index 765b8adc7..824155d8b 100644 --- a/src/components/dialogs/EmbedConsent.tsx +++ b/src/components/dialogs/EmbedConsent.tsx @@ -50,7 +50,6 @@ export function EmbedConsentDialog({ return ( <Dialog.Outer control={control}> <Dialog.Handle /> - <Dialog.ScrollableInner label={_(msg`External Media`)} style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}> diff --git a/src/components/dialogs/GifSelect.ios.tsx b/src/components/dialogs/GifSelect.ios.tsx deleted file mode 100644 index 2f867e865..000000000 --- a/src/components/dialogs/GifSelect.ios.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import React, { - useCallback, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react' -import {Modal, ScrollView, TextInput, View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {cleanError} from '#/lib/strings/errors' -import { - Gif, - useFeaturedGifsQuery, - useGifSearchQuery, -} from '#/state/queries/tenor' -import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' -import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' -import {FlatList_INTERNAL} from '#/view/com/util/Views' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import * as TextField from '#/components/forms/TextField' -import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' -import {Button, ButtonText} from '../Button' -import {Handle} from '../Dialog' -import {useThrottledValue} from '../hooks/useThrottledValue' -import {ListFooter, ListMaybePlaceholder} from '../Lists' -import {GifPreview} from './GifSelect.shared' - -export function GifSelectDialog({ - controlRef, - onClose, - onSelectGif: onSelectGifProp, -}: { - controlRef: React.RefObject<{open: () => void}> - onClose: () => void - onSelectGif: (gif: Gif) => void -}) { - const t = useTheme() - const [open, setOpen] = useState(false) - - useImperativeHandle(controlRef, () => ({ - open: () => setOpen(true), - })) - - const close = useCallback(() => { - setOpen(false) - onClose() - }, [onClose]) - - const onSelectGif = useCallback( - (gif: Gif) => { - onSelectGifProp(gif) - close() - }, - [onSelectGifProp, close], - ) - - const renderErrorBoundary = useCallback( - (error: any) => <ModalError details={String(error)} close={close} />, - [close], - ) - - return ( - <Modal - visible={open} - animationType="slide" - presentationStyle="formSheet" - onRequestClose={close} - aria-modal - accessibilityViewIsModal> - <View style={[a.flex_1, t.atoms.bg]}> - <Handle /> - <ErrorBoundary renderError={renderErrorBoundary}> - <GifList onSelectGif={onSelectGif} close={close} /> - </ErrorBoundary> - </View> - </Modal> - ) -} - -function GifList({ - onSelectGif, -}: { - close: () => void - onSelectGif: (gif: Gif) => void -}) { - const {_} = useLingui() - const t = useTheme() - const {gtMobile} = useBreakpoints() - const textInputRef = useRef<TextInput>(null) - const listRef = useRef<FlatList_INTERNAL>(null) - const [undeferredSearch, setSearch] = useState('') - const search = useThrottledValue(undeferredSearch, 500) - - const isSearching = search.length > 0 - - const trendingQuery = useFeaturedGifsQuery() - const searchQuery = useGifSearchQuery(search) - - const { - data, - fetchNextPage, - isFetchingNextPage, - hasNextPage, - error, - isLoading, - isError, - refetch, - } = isSearching ? searchQuery : trendingQuery - - const flattenedData = useMemo(() => { - return data?.pages.flatMap(page => page.results) || [] - }, [data]) - - const renderItem = useCallback( - ({item}: {item: Gif}) => { - return <GifPreview gif={item} onSelectGif={onSelectGif} /> - }, - [onSelectGif], - ) - - const onEndReached = React.useCallback(() => { - if (isFetchingNextPage || !hasNextPage || error) return - fetchNextPage() - }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) - - const hasData = flattenedData.length > 0 - - const onGoBack = useCallback(() => { - if (isSearching) { - // clear the input and reset the state - textInputRef.current?.clear() - setSearch('') - } else { - close() - } - }, [isSearching]) - - const listHeader = useMemo(() => { - return ( - <View style={[a.relative, a.mb_lg, a.pt_4xl, a.flex_row, a.align_center]}> - {/* cover top corners */} - <View - style={[ - a.absolute, - a.inset_0, - { - borderBottomLeftRadius: 8, - borderBottomRightRadius: 8, - }, - t.atoms.bg, - ]} - /> - - <TextField.Root> - <TextField.Icon icon={Search} /> - <TextField.Input - label={_(msg`Search GIFs`)} - placeholder={_(msg`Search Tenor`)} - onChangeText={text => { - setSearch(text) - listRef.current?.scrollToOffset({offset: 0, animated: false}) - }} - returnKeyType="search" - clearButtonMode="while-editing" - inputRef={textInputRef} - maxLength={50} - /> - </TextField.Root> - </View> - ) - }, [t.atoms.bg, _]) - - return ( - <FlatList_INTERNAL - ref={listRef} - key={gtMobile ? '3 cols' : '2 cols'} - data={flattenedData} - renderItem={renderItem} - numColumns={gtMobile ? 3 : 2} - columnWrapperStyle={a.gap_sm} - contentContainerStyle={a.px_lg} - ListHeaderComponent={ - <> - {listHeader} - {!hasData && ( - <ListMaybePlaceholder - isLoading={isLoading} - isError={isError} - onRetry={refetch} - onGoBack={onGoBack} - emptyType="results" - sideBorders={false} - topBorder={false} - errorTitle={_(msg`Failed to load GIFs`)} - errorMessage={_(msg`There was an issue connecting to Tenor.`)} - emptyMessage={ - isSearching - ? _(msg`No search results found for "${search}".`) - : _( - msg`No featured GIFs found. There may be an issue with Tenor.`, - ) - } - /> - )} - </> - } - stickyHeaderIndices={[0]} - onEndReached={onEndReached} - onEndReachedThreshold={4} - keyExtractor={(item: Gif) => item.id} - keyboardDismissMode="on-drag" - ListFooterComponent={ - hasData ? ( - <ListFooter - isFetchingNextPage={isFetchingNextPage} - error={cleanError(error)} - onRetry={fetchNextPage} - style={{borderTopWidth: 0}} - /> - ) : null - } - /> - ) -} - -function ModalError({details, close}: {details?: string; close: () => void}) { - const {_} = useLingui() - - return ( - <ScrollView - style={[a.flex_1, a.gap_md]} - centerContent - contentContainerStyle={a.px_lg}> - <ErrorScreen - title={_(msg`Oh no!`)} - message={_( - msg`There was an unexpected issue in the application. Please let us know if this happened to you!`, - )} - details={details} - /> - <Button - label={_(msg`Close dialog`)} - onPress={close} - color="primary" - size="large" - variant="solid"> - <ButtonText> - <Trans>Close</Trans> - </ButtonText> - </Button> - </ScrollView> - ) -} diff --git a/src/components/dialogs/GifSelect.shared.tsx b/src/components/dialogs/GifSelect.shared.tsx deleted file mode 100644 index 90b2abaa8..000000000 --- a/src/components/dialogs/GifSelect.shared.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, {useCallback} from 'react' -import {Image} from 'expo-image' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {logEvent} from '#/lib/statsig/statsig' -import {Gif} from '#/state/queries/tenor' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button} from '../Button' - -export function GifPreview({ - gif, - onSelectGif, -}: { - gif: Gif - onSelectGif: (gif: Gif) => void -}) { - const {gtTablet} = useBreakpoints() - const {_} = useLingui() - const t = useTheme() - - const onPress = useCallback(() => { - logEvent('composer:gif:select', {}) - onSelectGif(gif) - }, [onSelectGif, gif]) - - return ( - <Button - label={_(msg`Select GIF "${gif.title}"`)} - style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]} - onPress={onPress}> - {({pressed}) => ( - <Image - style={[ - a.flex_1, - a.mb_sm, - a.rounded_sm, - {aspectRatio: 1, opacity: pressed ? 0.8 : 1}, - t.atoms.bg_contrast_25, - ]} - source={{ - uri: gif.media_formats.tinygif.url, - }} - contentFit="cover" - accessibilityLabel={gif.title} - accessibilityHint="" - cachePolicy="none" - accessibilityIgnoresInvertColors - /> - )} - </Button> - ) -} diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx index 1afc588da..6023b5808 100644 --- a/src/components/dialogs/GifSelect.tsx +++ b/src/components/dialogs/GifSelect.tsx @@ -6,10 +6,12 @@ import React, { useState, } from 'react' import {TextInput, View} from 'react-native' -import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' +import {useWindowDimensions} from 'react-native' +import {Image} from 'expo-image' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {logEvent} from '#/lib/statsig/statsig' import {cleanError} from '#/lib/strings/errors' import {isWeb} from '#/platform/detection' import { @@ -19,7 +21,8 @@ import { } from '#/state/queries/tenor' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {ListMethods} from '#/view/com/util/List' +import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {useThrottledValue} from '#/components/hooks/useThrottledValue' @@ -27,16 +30,18 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arr import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' import {Button, ButtonIcon, ButtonText} from '../Button' import {ListFooter, ListMaybePlaceholder} from '../Lists' -import {GifPreview} from './GifSelect.shared' +import {PortalComponent} from '../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() @@ -59,8 +64,13 @@ export function GifSelectDialog({ return ( <Dialog.Outer control={control} - nativeOptions={{sheet: {snapPoints: ['100%']}}} - onClose={onClose}> + onClose={onClose} + Portal={Portal} + nativeOptions={{ + bottomInset: 0, + // use system corner radius on iOS + ...ios({cornerRadius: undefined}), + }}> <Dialog.Handle /> <ErrorBoundary renderError={renderErrorBoundary}> <GifList control={control} onSelectGif={onSelectGif} /> @@ -80,9 +90,10 @@ function GifList({ const t = useTheme() const {gtMobile} = useBreakpoints() const textInputRef = useRef<TextInput>(null) - const listRef = useRef<BottomSheetFlatListMethods>(null) + const listRef = useRef<ListMethods>(null) const [undeferredSearch, setSearch] = useState('') const search = useThrottledValue(undeferredSearch, 500) + const {height} = useWindowDimensions() const isSearching = search.length > 0 @@ -95,7 +106,7 @@ function GifList({ isFetchingNextPage, hasNextPage, error, - isLoading, + isPending, isError, refetch, } = isSearching ? searchQuery : trendingQuery @@ -132,6 +143,7 @@ function GifList({ return ( <View style={[ + native(a.pt_4xl), a.relative, a.mb_lg, a.flex_row, @@ -196,13 +208,14 @@ function GifList({ data={flattenedData} renderItem={renderItem} numColumns={gtMobile ? 3 : 2} - columnWrapperStyle={a.gap_sm} + columnWrapperStyle={[a.gap_sm]} + contentContainerStyle={[native([a.px_xl, {minHeight: height}])]} ListHeaderComponent={ <> {listHeader} {!hasData && ( <ListMaybePlaceholder - isLoading={isLoading} + isLoading={isPending} isError={isError} onRetry={refetch} onGoBack={onGoBack} @@ -273,3 +286,47 @@ function DialogError({details}: {details?: string}) { </Dialog.ScrollableInner> ) } + +export function GifPreview({ + gif, + onSelectGif, +}: { + gif: Gif + onSelectGif: (gif: Gif) => void +}) { + const {gtTablet} = useBreakpoints() + const {_} = useLingui() + const t = useTheme() + + const onPress = useCallback(() => { + logEvent('composer:gif:select', {}) + onSelectGif(gif) + }, [onSelectGif, gif]) + + return ( + <Button + label={_(msg`Select GIF "${gif.title}"`)} + style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]} + onPress={onPress}> + {({pressed}) => ( + <Image + style={[ + a.flex_1, + a.mb_sm, + a.rounded_sm, + {aspectRatio: 1, opacity: pressed ? 0.8 : 1}, + t.atoms.bg_contrast_25, + ]} + source={{ + uri: gif.media_formats.tinygif.url, + }} + contentFit="cover" + accessibilityLabel={gif.title} + accessibilityHint="" + cachePolicy="none" + accessibilityIgnoresInvertColors + /> + )} + </Button> + ) +} diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 81a614103..c3aae8f0d 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -30,11 +30,14 @@ 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 ( @@ -105,307 +108,349 @@ function MutedWordsInner() { }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]) return ( - <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> + <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_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_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={[ - gtMobile && [a.flex_row, a.align_center, a.justify_start], - a.gap_sm, - ]}> <View style={[ - a.flex_1, - a.flex_row, - a.justify_start, - a.align_center, + gtMobile && [a.flex_row, a.align_center, a.justify_start], 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> + <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 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 + 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> </View> + </Toggle.Group> - <View + <Toggle.Group + label={_( + msg`Select what content this mute word should apply to.`, + )} + type="radio" + values={targets} + onChange={setTargets}> + <Text style={[ - a.flex_1, - a.flex_row, - a.justify_start, - a.align_center, - a.gap_sm, + 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`Mute this word for 7 days`)} - name="7_days" + 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.Radio /> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>7 days</Trans> + <Trans>Text & tags</Trans> </Toggle.LabelText> </View> + <PageText size="sm" /> </TargetToggle> </Toggle.Item> <Toggle.Item - label={_(msg`Mute this word for 30 days`)} - name="30_days" + 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>30 days</Trans> + <Trans>Tags only</Trans> </Toggle.LabelText> </View> + <Hashtag size="sm" /> </TargetToggle> </Toggle.Item> </View> - </View> - </Toggle.Group> + </Toggle.Group> - <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]}> + <View> + <Text + style={[ + a.pb_xs, + a.text_sm, + a.font_bold, + t.atoms.text_contrast_medium, + ]}> + <Trans>Options:</Trans> + </Text> <Toggle.Item - label={_(msg`Mute this word in post text and tags`)} - name="content" - style={[a.flex_1]}> + 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.Radio /> + <Toggle.Checkbox /> <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> - <Trans>Text & tags</Trans> + <Trans>Exclude users you follow</Trans> </Toggle.LabelText> </View> - <PageText size="sm" /> </TargetToggle> </Toggle.Item> + </View> - <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 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> - </Toggle.Group> - <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.italic, + {color: t.palette.white}, + native({marginTop: 2}), + ]}> + {error} + </Text> + </View> + )} + </View> + + <Divider /> + + <View style={[a.pt_2xl]}> <Text style={[ - a.pb_xs, - a.text_sm, + a.text_md, a.font_bold, - t.atoms.text_contrast_medium, + a.pb_md, + t.atoms.text_contrast_high, ]}> - <Trans>Options:</Trans> + <Trans>Your muted words</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> - - <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 + {isPreferencesLoading ? ( + <Loader /> + ) : preferencesError || !preferences ? ( + <View style={[ - a.italic, - {color: t.palette.white}, - native({marginTop: 2}), + a.py_md, + a.px_lg, + a.rounded_md, + t.atoms.bg_contrast_25, ]}> - {error} - </Text> - </View> - )} - </View> - - <Divider /> - - <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> + <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> - {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> - )} + {isNative && <View style={{height: 20}} />} </View> - {isNative && <View style={{height: 20}} />} - </View> + <Dialog.Close /> + </Dialog.ScrollableInner> - <Dialog.Close /> - </Dialog.ScrollableInner> + <Portal.Outlet /> + </Portal.Provider> ) } @@ -437,6 +482,7 @@ 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 47eefae6f..bddc49968 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -37,6 +37,7 @@ 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 = { @@ -54,13 +55,15 @@ export type PostInteractionSettingsFormProps = { export function PostInteractionSettingsControlledDialog({ control, + Portal, ...rest }: PostInteractionSettingsFormProps & { control: Dialog.DialogControlProps + Portal?: PortalComponent }) { const {_} = useLingui() return ( - <Dialog.Outer control={control}> + <Dialog.Outer control={control} Portal={Portal}> <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`Edit post interaction settings`)} @@ -231,7 +234,6 @@ export function PostInteractionSettingsForm({ }: PostInteractionSettingsFormProps) { const t = useTheme() const {_} = useLingui() - const control = Dialog.useDialogContext() const {data: lists} = useMyListsQuery('curate') const [quotesEnabled, setQuotesEnabled] = React.useState( !( @@ -437,7 +439,6 @@ export function PostInteractionSettingsForm({ <Button label={_(msg`Save`)} onPress={onSave} - onAccessibilityEscape={control.close} color="primary" size="large" variant="solid" diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx index 0bd4bcb8c..ea870e2da 100644 --- a/src/components/dialogs/SwitchAccount.tsx +++ b/src/components/dialogs/SwitchAccount.tsx @@ -43,7 +43,6 @@ export function SwitchAccountDialog({ return ( <Dialog.Outer control={control}> <Dialog.Handle /> - <Dialog.ScrollableInner label={_(msg`Switch Account`)}> <View style={[a.gap_lg]}> <Text style={[a.text_2xl, a.font_bold]}> diff --git a/src/components/dialogs/nuxs/NeueTypography.tsx b/src/components/dialogs/nuxs/NeueTypography.tsx index f160c8774..f29dc356d 100644 --- a/src/components/dialogs/nuxs/NeueTypography.tsx +++ b/src/components/dialogs/nuxs/NeueTypography.tsx @@ -44,7 +44,6 @@ export function NeueTypography() { return ( <Dialog.Outer control={control} onClose={onClose}> <Dialog.Handle /> - <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}> <View style={[a.gap_xl]}> <View style={[a.gap_md]}> diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index a4fa625fa..affc292c1 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -136,7 +136,7 @@ let ConvoMenu = ({ <Menu.Outer> <Menu.Item label={_(msg`Leave conversation`)} - onPress={leaveConvoControl.open}> + onPress={() => leaveConvoControl.open()}> <Menu.ItemText> <Trans>Leave conversation</Trans> </Menu.ItemText> @@ -195,7 +195,7 @@ let ConvoMenu = ({ </Menu.Item> <Menu.Item label={_(msg`Report conversation`)} - onPress={reportControl.open}> + onPress={() => reportControl.open()}> <Menu.ItemText> <Trans>Report conversation</Trans> </Menu.ItemText> @@ -206,7 +206,7 @@ let ConvoMenu = ({ <Menu.Group> <Menu.Item label={_(msg`Leave conversation`)} - onPress={leaveConvoControl.open}> + onPress={() => leaveConvoControl.open()}> <Menu.ItemText> <Trans>Leave conversation</Trans> </Menu.ItemText> diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx index 2978d2b22..8680a68bf 100644 --- a/src/components/dms/MessageMenu.tsx +++ b/src/components/dms/MessageMenu.tsx @@ -7,11 +7,11 @@ import {useLingui} from '@lingui/react' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {getTranslatorLink} from '#/locale/helpers' +import {isWeb} from '#/platform/detection' +import {useConvoActive} from '#/state/messages/convo' import {useLanguagePrefs} from '#/state/preferences' import {useOpenLink} from '#/state/preferences/in-app-browser' -import {isWeb} from 'platform/detection' -import {useConvoActive} from 'state/messages/convo' -import {useSession} from 'state/session' +import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {ReportDialog} from '#/components/dms/ReportDialog' @@ -120,7 +120,7 @@ export let MessageMenu = ({ <Menu.Item testID="messageDropdownDeleteBtn" label={_(msg`Delete message for me`)} - onPress={deleteControl.open}> + onPress={() => deleteControl.open()}> <Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText> <Menu.ItemIcon icon={Trash} position="right" /> </Menu.Item> @@ -128,7 +128,7 @@ export let MessageMenu = ({ <Menu.Item testID="messageDropdownReportBtn" label={_(msg`Report message`)} - onPress={reportControl.open}> + onPress={() => reportControl.open()}> <Menu.ItemText>{_(msg`Report`)}</Menu.ItemText> <Menu.ItemIcon icon={Warning} position="right" /> </Menu.Item> diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx index 2dcd77854..06d69ff4b 100644 --- a/src/components/dms/ReportDialog.tsx +++ b/src/components/dms/ReportDialog.tsx @@ -10,13 +10,11 @@ import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' import {ReportOption} from '#/lib/moderation/useReportOptions' -import {isAndroid} from '#/platform/detection' import {useAgent} from '#/state/session' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' -import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding' import {Button, ButtonIcon, ButtonText} from '../Button' import {Divider} from '../Divider' import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron' @@ -41,14 +39,11 @@ let ReportDialog = ({ }): React.ReactNode => { const {_} = useLingui() return ( - <Dialog.Outer - control={control} - nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}> + <Dialog.Outer control={control}> <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`Report this message`)}> <DialogInner params={params} /> <Dialog.Close /> - <KeyboardControllerPadding /> </Dialog.ScrollableInner> </Dialog.Outer> ) diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx index 19f6eb6df..e80fef2d7 100644 --- a/src/components/dms/dialogs/NewChatDialog.tsx +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -2,9 +2,9 @@ import React, {useCallback} from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' -import {logEvent} from 'lib/statsig/statsig' import {FAB} from '#/view/com/util/fab/FAB' import * as Toast from '#/view/com/util/Toast' import {useTheme} from '#/alf' @@ -55,10 +55,8 @@ export function NewChat({ accessibilityHint="" /> - <Dialog.Outer - control={control} - testID="newChatDialog" - nativeOptions={{sheet: {snapPoints: ['100%']}}}> + <Dialog.Outer control={control} testID="newChatDialog"> + <Dialog.Handle /> <SearchablePeopleList title={_(msg`Start a new chat`)} onSelectChat={onCreateChat} diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx index a13dfe509..a5687a096 100644 --- a/src/components/dms/dialogs/SearchablePeopleList.tsx +++ b/src/components/dms/dialogs/SearchablePeopleList.tsx @@ -5,10 +5,8 @@ import React, { useRef, useState, } from 'react' -import type {TextInput as TextInputType} from 'react-native' -import {View} from 'react-native' +import {TextInput, View} from 'react-native' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' -import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -16,18 +14,17 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useListConvosQuery} from '#/state/queries/messages/list-converations' import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useSession} from '#/state/session' -import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' +import {ListMethods} from '#/view/com/util/List' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, useTheme, web} from '#/alf' -import {Button} from '#/components/Button' +import {Button, ButtonIcon} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {TextInput} from '#/components/dms/dialogs/TextInput' import {canBeMessaged} from '#/components/dms/util' import {useInteractionState} from '#/components/hooks/useInteractionState' -import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Text} from '#/components/Typography' @@ -66,9 +63,9 @@ export function SearchablePeopleList({ const {_} = useLingui() const moderationOpts = useModerationOpts() const control = Dialog.useDialogContext() - const listRef = useRef<BottomSheetFlatListMethods>(null) + const listRef = useRef<ListMethods>(null) const {currentAccount} = useSession() - const inputRef = useRef<TextInputType>(null) + const inputRef = useRef<TextInput>(null) const [searchText, setSearchText] = useState('') @@ -101,15 +98,15 @@ export function SearchablePeopleList({ }) } - _items = _items.sort(a => { + _items = _items.sort(item => { // @ts-ignore - return a.enabled ? -1 : 1 + return item.enabled ? -1 : 1 }) } } else { const placeholders: Item[] = Array(10) .fill(0) - .map((_, i) => ({ + .map((__, i) => ({ type: 'placeholder', key: i + '', })) @@ -155,9 +152,9 @@ export function SearchablePeopleList({ } // only sort follows - followsItems = followsItems.sort(a => { + followsItems = followsItems.sort(item => { // @ts-ignore - return a.enabled ? -1 : 1 + return item.enabled ? -1 : 1 }) // then append @@ -177,9 +174,9 @@ export function SearchablePeopleList({ } } - _items = _items.sort(a => { + _items = _items.sort(item => { // @ts-ignore - return a.enabled ? -1 : 1 + return item.enabled ? -1 : 1 }) } else { _items.push(...placeholders) @@ -242,57 +239,46 @@ export function SearchablePeopleList({ <View style={[ a.relative, - a.pt_md, + web(a.pt_lg), + native(a.pt_4xl), a.pb_xs, a.px_lg, a.border_b, t.atoms.border_contrast_low, t.atoms.bg, - native([a.pt_lg]), ]}> - <View - style={[ - a.relative, - native(a.align_center), - a.justify_center, - {height: 32}, - ]}> - <Button - label={_(msg`Close`)} - size="small" - shape="round" - variant="ghost" - color="secondary" - style={[ - a.absolute, - a.z_20, - native({ - left: -7, - }), - web({ - right: -4, - }), - ]} - onPress={() => control.close()}> - {isWeb ? ( - <X size="md" fill={t.palette.contrast_500} /> - ) : ( - <ChevronLeft size="md" fill={t.palette.contrast_500} /> - )} - </Button> + <View style={[a.relative, native(a.align_center), a.justify_center]}> <Text style={[ a.z_10, a.text_lg, - a.font_bold, + a.font_heavy, a.leading_tight, t.atoms.text_contrast_high, ]}> {title} </Text> + {isWeb ? ( + <Button + label={_(msg`Close`)} + size="small" + shape="round" + variant={isWeb ? 'ghost' : 'solid'} + color="secondary" + style={[ + a.absolute, + a.z_20, + web({right: -4}), + native({right: 0}), + native({height: 32, width: 32, borderRadius: 16}), + ]} + onPress={() => control.close()}> + <ButtonIcon icon={X} size="md" /> + </Button> + ) : null} </View> - <View style={[native([a.pt_sm]), web([a.pt_xs])]}> + <View style={[, web([a.pt_xs])]}> <SearchInput inputRef={inputRef} value={searchText} @@ -309,7 +295,6 @@ export function SearchablePeopleList({ t.atoms.border_contrast_low, t.atoms.bg, t.atoms.text_contrast_high, - t.palette.contrast_500, _, title, searchText, @@ -326,14 +311,7 @@ export function SearchablePeopleList({ keyExtractor={(item: Item) => item.key} style={[ web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), - native({ - height: '100%', - paddingHorizontal: 0, - marginTop: 0, - paddingTop: 0, - borderTopLeftRadius: 40, - borderTopRightRadius: 40, - }), + native({height: '100%'}), ]} webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} keyboardDismissMode="on-drag" @@ -396,7 +374,8 @@ function ProfileCard({ <View style={[a.flex_1, a.gap_2xs]}> <Text style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]} - numberOfLines={1}> + numberOfLines={1} + emoji> {displayName} </Text> <Text @@ -474,7 +453,7 @@ function SearchInput({ value: string onChangeText: (text: string) => void onEscape: () => void - inputRef: React.RefObject<TextInputType> + inputRef: React.RefObject<TextInput> }) { const t = useTheme() const {_} = useLingui() diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx index 01906a430..38b558343 100644 --- a/src/components/dms/dialogs/ShareViaChatDialog.tsx +++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx @@ -2,9 +2,9 @@ import React, {useCallback} from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' -import {logEvent} from 'lib/statsig/statsig' import * as Toast from '#/view/com/util/Toast' import * as Dialog from '#/components/Dialog' import {SearchablePeopleList} from './SearchablePeopleList' @@ -17,10 +17,8 @@ export function SendViaChatDialog({ onSelectChat: (chatId: string) => void }) { return ( - <Dialog.Outer - control={control} - testID="sendViaChatChatDialog" - nativeOptions={{sheet: {snapPoints: ['100%']}}}> + <Dialog.Outer control={control} testID="sendViaChatChatDialog"> + <Dialog.Handle /> <SendViaChatDialogInner control={control} onSelectChat={onSelectChat} /> </Dialog.Outer> ) diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 6dc387b23..4e3695bbf 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -2,8 +2,8 @@ import React from 'react' import {Pressable, View, ViewStyle} from 'react-native' import Animated, {LinearTransition} from 'react-native-reanimated' +import {HITSLOP_10} from '#/lib/constants' import {isNative} from '#/platform/detection' -import {HITSLOP_10} from 'lib/constants' import { atoms as a, flatten, diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx index e63cea93b..bf0d1905e 100644 --- a/src/components/moderation/LabelsOnMeDialog.tsx +++ b/src/components/moderation/LabelsOnMeDialog.tsx @@ -32,7 +32,6 @@ export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) { return ( <Dialog.Outer control={props.control}> <Dialog.Handle /> - <LabelsOnMeDialogInner {...props} /> </Dialog.Outer> ) @@ -158,23 +157,25 @@ function Label({ <Divider /> <View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}> - <Text style={[t.atoms.text_contrast_medium]}> - {isSelfLabel ? ( + {isSelfLabel ? ( + <Text style={[t.atoms.text_contrast_medium]}> <Trans>This label was applied by you.</Trans> - ) : ( - <Trans> - Source:{' '} - <InlineLinkText - label={sourceName} - to={makeProfileLink( - labeler ? labeler.creator : {did: label.src, handle: ''}, - )} - onPress={() => control.close()}> - {sourceName} - </InlineLinkText> - </Trans> - )} - </Text> + </Text> + ) : ( + <View style={{flexDirection: 'row'}}> + <Text style={[t.atoms.text_contrast_medium]}> + <Trans>Source: </Trans>{' '} + </Text> + <InlineLinkText + label={sourceName} + to={makeProfileLink( + labeler ? labeler.creator : {did: label.src, handle: ''}, + )} + onPress={() => control.close()}> + {sourceName} + </InlineLinkText> + </View> + )} </View> </View> ) @@ -236,24 +237,24 @@ function AppealForm({ return ( <> - <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}> - <Trans>Appeal "{strings.name}" label</Trans> - </Text> - <Text style={[a.text_md, a.leading_snug]}> - <Trans> - This appeal will be sent to{' '} - <InlineLinkText - label={sourceName} - to={makeProfileLink( - labeler ? labeler.creator : {did: label.src, handle: ''}, - )} - onPress={() => control.close()} - style={[a.text_md, a.leading_snug]}> - {sourceName} - </InlineLinkText> - . - </Trans> - </Text> + <View style={{flexWrap: 'wrap', flexDirection: 'row'}}> + <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}> + <Trans>Appeal "{strings.name}" label</Trans> + </Text> + <Text style={[a.text_md, a.leading_snug]}> + <Trans>This appeal will be sent to</Trans>{' '} + </Text> + <InlineLinkText + label={sourceName} + to={makeProfileLink( + labeler ? labeler.creator : {did: label.src, handle: ''}, + )} + onPress={() => control.close()} + style={[a.text_md, a.leading_snug]}> + {sourceName} + </InlineLinkText> + <Text style={[a.text_md, a.leading_snug]}>.</Text> + </View> <View style={[a.my_md]}> <Dialog.Input label={_(msg`Text input field`)} diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx index 225917853..0a1fae67e 100644 --- a/src/components/moderation/ModerationDetailsDialog.tsx +++ b/src/components/moderation/ModerationDetailsDialog.tsx @@ -141,23 +141,24 @@ function ModerationDetailsDialogInner({ {modcause?.type === 'label' && ( <View style={[a.pt_lg]}> <Divider /> - <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> - {modcause.source.type === 'user' ? ( + {modcause.source.type === 'user' ? ( + <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> <Trans>This label was applied by the author.</Trans> - ) : ( - <Trans> - This label was applied by{' '} - <InlineLinkText - label={desc.source || _(msg`an unknown labeler`)} - to={makeProfileLink({did: modcause.label.src, handle: ''})} - onPress={() => control.close()} - style={a.text_md}> - {desc.source || _(msg`an unknown labeler`)} - </InlineLinkText> - . - </Trans> - )} - </Text> + </Text> + ) : ( + <> + <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> + <Trans>This label was applied by </Trans> + </Text> + <InlineLinkText + label={desc.source || _(msg`an unknown labeler`)} + to={makeProfileLink({did: modcause.label.src, handle: ''})} + onPress={() => control.close()} + style={a.text_md}> + {desc.source || _(msg`an unknown labeler`)} + </InlineLinkText> + </> + )} </View> )} |