diff options
-rw-r--r-- | src/components/Button.tsx | 24 | ||||
-rw-r--r-- | src/components/Dialog/context.ts | 6 | ||||
-rw-r--r-- | src/components/dialogs/Context.tsx | 54 | ||||
-rw-r--r-- | src/components/dialogs/InAppBrowserConsent.tsx | 111 | ||||
-rw-r--r-- | src/lib/hooks/useOpenLink.ts | 23 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 6 | ||||
-rw-r--r-- | src/view/com/modals/InAppBrowserConsent.tsx | 99 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/shell/index.tsx | 2 |
9 files changed, 188 insertions, 141 deletions
diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 123e6ee42..2d6ddc834 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,23 +1,23 @@ import React from 'react' import { - AccessibilityProps, - GestureResponderEvent, - MouseEvent, - NativeSyntheticEvent, + type AccessibilityProps, + type GestureResponderEvent, + type MouseEvent, + type NativeSyntheticEvent, Pressable, - PressableProps, - StyleProp, + type PressableProps, + type StyleProp, StyleSheet, - TargetedEvent, - TextProps, - TextStyle, + type TargetedEvent, + type TextProps, + type TextStyle, View, - ViewStyle, + type ViewStyle, } from 'react-native' import {LinearGradient} from 'expo-linear-gradient' import {atoms as a, flatten, select, tokens, useTheme} from '#/alf' -import {Props as SVGIconProps} from '#/components/icons/common' +import {type Props as SVGIconProps} from '#/components/icons/common' import {Text} from '#/components/Typography' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' @@ -597,7 +597,7 @@ export function useSharedButtonTextStyles() { if (variant === 'solid' || variant === 'gradient') { if (!disabled) { baseStyles.push({ - color: t.palette.contrast_100, + color: t.palette.contrast_50, }) } else { baseStyles.push({ diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 331ff3f33..eb892403f 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -2,9 +2,9 @@ import React from 'react' import {useDialogStateContext} from '#/state/dialogs' import { - DialogContextProps, - DialogControlRefProps, - DialogOuterProps, + type DialogContextProps, + type DialogControlRefProps, + type DialogOuterProps, } from '#/components/Dialog/types' import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types' diff --git a/src/components/dialogs/Context.tsx b/src/components/dialogs/Context.tsx index c9dff9a99..fda904b8b 100644 --- a/src/components/dialogs/Context.tsx +++ b/src/components/dialogs/Context.tsx @@ -1,32 +1,66 @@ -import React from 'react' +import {createContext, useContext, useMemo, useState} from 'react' import * as Dialog from '#/components/Dialog' -type Control = Dialog.DialogOuterProps['control'] +type Control = Dialog.DialogControlProps + +export type StatefulControl<T> = { + control: Control + open: (value: T) => void + clear: () => void + value: T | undefined +} type ControlsContext = { mutedWordsDialogControl: Control signinDialogControl: Control + inAppBrowserConsentControl: StatefulControl<string> } -const ControlsContext = React.createContext({ - mutedWordsDialogControl: {} as Control, - signinDialogControl: {} as Control, -}) +const ControlsContext = createContext<ControlsContext | null>(null) export function useGlobalDialogsControlContext() { - return React.useContext(ControlsContext) + const ctx = useContext(ControlsContext) + if (!ctx) { + throw new Error( + 'useGlobalDialogsControlContext must be used within a Provider', + ) + } + return ctx } export function Provider({children}: React.PropsWithChildren<{}>) { const mutedWordsDialogControl = Dialog.useDialogControl() const signinDialogControl = Dialog.useDialogControl() - const ctx = React.useMemo<ControlsContext>( - () => ({mutedWordsDialogControl, signinDialogControl}), - [mutedWordsDialogControl, signinDialogControl], + const inAppBrowserConsentControl = useStatefulDialogControl<string>() + + const ctx = useMemo<ControlsContext>( + () => ({ + mutedWordsDialogControl, + signinDialogControl, + inAppBrowserConsentControl, + }), + [mutedWordsDialogControl, signinDialogControl, inAppBrowserConsentControl], ) return ( <ControlsContext.Provider value={ctx}>{children}</ControlsContext.Provider> ) } + +function useStatefulDialogControl<T>(initialValue?: T): StatefulControl<T> { + const [value, setValue] = useState(initialValue) + const control = Dialog.useDialogControl() + return useMemo( + () => ({ + control, + open: (v: T) => { + setValue(v) + control.open() + }, + clear: () => setValue(initialValue), + value, + }), + [control, value, initialValue], + ) +} diff --git a/src/components/dialogs/InAppBrowserConsent.tsx b/src/components/dialogs/InAppBrowserConsent.tsx new file mode 100644 index 000000000..4459c64db --- /dev/null +++ b/src/components/dialogs/InAppBrowserConsent.tsx @@ -0,0 +1,111 @@ +import {useCallback} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useOpenLink} from '#/lib/hooks/useOpenLink' +import {isWeb} from '#/platform/detection' +import {useSetInAppBrowser} from '#/state/preferences/in-app-browser' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {SquareArrowTopRight_Stroke2_Corner0_Rounded as External} from '#/components/icons/SquareArrowTopRight' +import {Text} from '#/components/Typography' +import {useGlobalDialogsControlContext} from './Context' + +export function InAppBrowserConsentDialog() { + const {inAppBrowserConsentControl} = useGlobalDialogsControlContext() + + if (isWeb) return null + + return ( + <Dialog.Outer + control={inAppBrowserConsentControl.control} + nativeOptions={{preventExpansion: true}} + onClose={inAppBrowserConsentControl.clear}> + <Dialog.Handle /> + <InAppBrowserConsentInner href={inAppBrowserConsentControl.value} /> + </Dialog.Outer> + ) +} + +function InAppBrowserConsentInner({href}: {href?: string}) { + const control = Dialog.useDialogContext() + const {_} = useLingui() + const t = useTheme() + const setInAppBrowser = useSetInAppBrowser() + const openLink = useOpenLink() + + const onUseIAB = useCallback(() => { + control.close(() => { + setInAppBrowser(true) + if (href) { + openLink(href, true) + } + }) + }, [control, setInAppBrowser, href, openLink]) + + const onUseLinking = useCallback(() => { + control.close(() => { + setInAppBrowser(false) + if (href) { + openLink(href, false) + } + }) + }, [control, setInAppBrowser, href, openLink]) + + const onCancel = useCallback(() => { + control.close() + }, [control]) + + return ( + <Dialog.ScrollableInner label={_(msg`How should we open this link?`)}> + <View style={[a.gap_2xl]}> + <View style={[a.gap_sm]}> + <Text style={[a.font_heavy, a.text_2xl]}> + <Trans>How should we open this link?</Trans> + </Text> + <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_md]}> + <Trans> + Your choice will be remembered for future links. You can change it + at any time in settings. + </Trans> + </Text> + </View> + <View style={[a.gap_sm]}> + <Button + label={_(msg`Use in-app browser`)} + onPress={onUseIAB} + size="large" + variant="solid" + color="primary"> + <ButtonText> + <Trans>Use in-app browser</Trans> + </ButtonText> + </Button> + <Button + label={_(msg`Use my default browser`)} + onPress={onUseLinking} + size="large" + variant="solid" + color="secondary"> + <ButtonText> + <Trans>Use my default browser</Trans> + </ButtonText> + <ButtonIcon position="right" icon={External} /> + </Button> + <Button + label={_(msg`Cancel`)} + onPress={onCancel} + size="large" + variant="ghost" + color="secondary"> + <ButtonText> + <Trans>Cancel</Trans> + </ButtonText> + </Button> + </View> + </View> + </Dialog.ScrollableInner> + ) +} diff --git a/src/lib/hooks/useOpenLink.ts b/src/lib/hooks/useOpenLink.ts index a949dacc6..28c1bca3d 100644 --- a/src/lib/hooks/useOpenLink.ts +++ b/src/lib/hooks/useOpenLink.ts @@ -12,16 +12,18 @@ import { toNiceDomain, } from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' -import {useModalControls} from '#/state/modals' import {useInAppBrowser} from '#/state/preferences/in-app-browser' import {useTheme} from '#/alf' +import {useDialogContext} from '#/components/Dialog' import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' export function useOpenLink() { - const {openModal} = useModalControls() const enabled = useInAppBrowser() const t = useTheme() const sheetWrapper = useSheetWrapper() + const dialogContext = useDialogContext() + const {inAppBrowserConsentControl} = useGlobalDialogsControlContext() const openLink = useCallback( async (url: string, override?: boolean, shouldProxy?: boolean) => { @@ -42,10 +44,17 @@ export function useOpenLink() { if (isNative && !url.startsWith('mailto:')) { if (override === undefined && enabled === undefined) { - openModal({ - name: 'in-app-browser-consent', - href: url, - }) + // consent dialog is a global dialog, and while it's possible to nest dialogs, + // the actual components need to be nested. sibling dialogs on iOS are not supported. + // thus, check if we're in a dialog, and if so, close the existing dialog before opening the + // consent dialog -sfn + if (dialogContext.isWithinDialog) { + dialogContext.close(() => { + inAppBrowserConsentControl.open(url) + }) + } else { + inAppBrowserConsentControl.open(url) + } return } else if (override ?? enabled) { await sheetWrapper( @@ -62,7 +71,7 @@ export function useOpenLink() { } Linking.openURL(url) }, - [enabled, openModal, t, sheetWrapper], + [enabled, inAppBrowserConsentControl, t, sheetWrapper, dialogContext], ) return openLink diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 45c4fb467..f79f6213f 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -66,11 +66,6 @@ export interface LinkWarningModal { share?: boolean } -export interface InAppBrowserConsentModal { - name: 'in-app-browser-consent' - href: string -} - export type Modal = // Account | DeleteAccountModal @@ -96,7 +91,6 @@ export type Modal = // Generic | LinkWarningModal - | InAppBrowserConsentModal const ModalContext = React.createContext<{ isModalActive: boolean diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx deleted file mode 100644 index 105edfbc6..000000000 --- a/src/view/com/modals/InAppBrowserConsent.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useOpenLink} from '#/lib/hooks/useOpenLink' -import {usePalette} from '#/lib/hooks/usePalette' -import {s} from '#/lib/styles' -import {useModalControls} from '#/state/modals' -import {useSetInAppBrowser} from '#/state/preferences/in-app-browser' -import {ScrollView} from '#/view/com/modals/util' -import {Button} from '#/view/com/util/forms/Button' -import {Text} from '#/view/com/util/text/Text' - -export const snapPoints = [350] - -export function Component({href}: {href: string}) { - const pal = usePalette('default') - const {closeModal} = useModalControls() - const {_} = useLingui() - const setInAppBrowser = useSetInAppBrowser() - const openLink = useOpenLink() - - const onUseIAB = React.useCallback(() => { - setInAppBrowser(true) - closeModal() - openLink(href, true) - }, [closeModal, setInAppBrowser, href, openLink]) - - const onUseLinking = React.useCallback(() => { - setInAppBrowser(false) - closeModal() - openLink(href, false) - }, [closeModal, setInAppBrowser, href, openLink]) - - return ( - <ScrollView - testID="inAppBrowserConsentModal" - style={[s.flex1, pal.view, {paddingHorizontal: 20, paddingTop: 10}]}> - <Text style={[pal.text, styles.title]}> - <Trans>How should we open this link?</Trans> - </Text> - <Text style={pal.text}> - <Trans> - Your choice will be saved, but can be changed later in settings. - </Trans> - </Text> - <View style={[styles.btnContainer]}> - <Button - testID="confirmBtn" - type="inverted" - onPress={onUseIAB} - accessibilityLabel={_(msg`Use in-app browser`)} - accessibilityHint="" - label={_(msg`Use in-app browser`)} - labelContainerStyle={{justifyContent: 'center', padding: 8}} - labelStyle={[s.f18]} - /> - <Button - testID="confirmBtn" - type="inverted" - onPress={onUseLinking} - accessibilityLabel={_(msg`Use my default browser`)} - accessibilityHint="" - label={_(msg`Use my default browser`)} - labelContainerStyle={{justifyContent: 'center', padding: 8}} - labelStyle={[s.f18]} - /> - <Button - testID="cancelBtn" - type="default" - onPress={() => { - closeModal() - }} - accessibilityLabel={_(msg`Cancel`)} - accessibilityHint="" - label={_(msg`Cancel`)} - labelContainerStyle={{justifyContent: 'center', padding: 8}} - labelStyle={[s.f18]} - /> - </View> - </ScrollView> - ) -} - -const styles = StyleSheet.create({ - title: { - textAlign: 'center', - fontWeight: '600', - fontSize: 24, - marginBottom: 12, - }, - btnContainer: { - marginTop: 20, - flexDirection: 'column', - justifyContent: 'center', - rowGap: 10, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index d0b50c857..8fd927f16 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -11,7 +11,6 @@ import * as ChangePasswordModal from './ChangePassword' import * as CreateOrEditListModal from './CreateOrEditList' import * as DeleteAccountModal from './DeleteAccount' import * as EditProfileModal from './EditProfile' -import * as InAppBrowserConsentModal from './InAppBrowserConsent' import * as InviteCodesModal from './InviteCodes' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' @@ -76,9 +75,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'link-warning') { snapPoints = LinkWarningModal.snapPoints element = <LinkWarningModal.Component {...activeModal} /> - } else if (activeModal?.name === 'in-app-browser-consent') { - snapPoints = InAppBrowserConsentModal.snapPoints - element = <InAppBrowserConsentModal.Component {...activeModal} /> } else { return null } diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 3d3a5520c..1e34f6da5 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -25,6 +25,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {setSystemUITheme} from '#/alf/util/systemUI' +import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {SigninDialog} from '#/components/dialogs/Signin' import {Outlet as PortalOutlet} from '#/components/Portal' @@ -151,6 +152,7 @@ function ShellInner() { <ModalsContainer /> <MutedWordsDialog /> <SigninDialog /> + <InAppBrowserConsentDialog /> <Lightbox /> <PortalOutlet /> <BottomSheetOutlet /> |