about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/Button.tsx24
-rw-r--r--src/components/Dialog/context.ts6
-rw-r--r--src/components/dialogs/Context.tsx54
-rw-r--r--src/components/dialogs/InAppBrowserConsent.tsx111
-rw-r--r--src/lib/hooks/useOpenLink.ts23
-rw-r--r--src/state/modals/index.tsx6
-rw-r--r--src/view/com/modals/InAppBrowserConsent.tsx99
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/shell/index.tsx2
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 />