about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/components/Link.tsx28
-rw-r--r--src/components/dialogs/Context.tsx12
-rw-r--r--src/components/dialogs/LinkWarning.tsx161
-rw-r--r--src/state/modals/index.tsx10
-rw-r--r--src/view/com/modals/LinkWarning.tsx180
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/modals/Modal.web.tsx3
-rw-r--r--src/view/com/util/Link.tsx11
-rw-r--r--src/view/shell/index.tsx2
-rw-r--r--src/view/shell/index.web.tsx2
10 files changed, 200 insertions, 213 deletions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 49c9c5235..d0f8678ff 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -24,6 +24,7 @@ import {Button, type ButtonProps} from '#/components/Button'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {Text, type TextProps} from '#/components/Typography'
 import {router} from '#/routes'
+import {useGlobalDialogsControlContext} from './dialogs/Context'
 
 /**
  * Only available within a `Link`, since that inherits from `Button`.
@@ -111,7 +112,8 @@ export function useLink({
   }
 
   const isExternal = isExternalUrl(href)
-  const {openModal, closeModal} = useModalControls()
+  const {closeModal} = useModalControls()
+  const {linkWarningDialogControl} = useGlobalDialogsControlContext()
   const openLink = useOpenLink()
 
   const onPress = React.useCallback(
@@ -132,10 +134,9 @@ export function useLink({
       }
 
       if (requiresWarning) {
-        openModal({
-          name: 'link-warning',
-          text: displayText,
-          href: href,
+        linkWarningDialogControl.open({
+          displayText,
+          href,
         })
       } else {
         if (isExternal) {
@@ -176,13 +177,13 @@ export function useLink({
       displayText,
       isExternal,
       href,
-      openModal,
       openLink,
       closeModal,
       action,
       navigation,
       overridePresentation,
       shouldProxy,
+      linkWarningDialogControl,
     ],
   )
 
@@ -195,16 +196,21 @@ export function useLink({
     )
 
     if (requiresWarning) {
-      openModal({
-        name: 'link-warning',
-        text: displayText,
-        href: href,
+      linkWarningDialogControl.open({
+        displayText,
+        href,
         share: true,
       })
     } else {
       shareUrl(href)
     }
-  }, [disableMismatchWarning, displayText, href, isExternal, openModal])
+  }, [
+    disableMismatchWarning,
+    displayText,
+    href,
+    isExternal,
+    linkWarningDialogControl,
+  ])
 
   const onLongPress = React.useCallback(
     (e: GestureResponderEvent) => {
diff --git a/src/components/dialogs/Context.tsx b/src/components/dialogs/Context.tsx
index 728044325..1ee4d2739 100644
--- a/src/components/dialogs/Context.tsx
+++ b/src/components/dialogs/Context.tsx
@@ -17,6 +17,11 @@ type ControlsContext = {
   signinDialogControl: Control
   inAppBrowserConsentControl: StatefulControl<string>
   emailDialogControl: StatefulControl<Screen>
+  linkWarningDialogControl: StatefulControl<{
+    href: string
+    displayText: string
+    share?: boolean
+  }>
 }
 
 const ControlsContext = createContext<ControlsContext | null>(null)
@@ -36,6 +41,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   const signinDialogControl = Dialog.useDialogControl()
   const inAppBrowserConsentControl = useStatefulDialogControl<string>()
   const emailDialogControl = useStatefulDialogControl<Screen>()
+  const linkWarningDialogControl = useStatefulDialogControl<{
+    href: string
+    displayText: string
+    share?: boolean
+  }>()
 
   const ctx = useMemo<ControlsContext>(
     () => ({
@@ -43,12 +53,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       signinDialogControl,
       inAppBrowserConsentControl,
       emailDialogControl,
+      linkWarningDialogControl,
     }),
     [
       mutedWordsDialogControl,
       signinDialogControl,
       inAppBrowserConsentControl,
       emailDialogControl,
+      linkWarningDialogControl,
     ],
   )
 
diff --git a/src/components/dialogs/LinkWarning.tsx b/src/components/dialogs/LinkWarning.tsx
new file mode 100644
index 000000000..9ae871812
--- /dev/null
+++ b/src/components/dialogs/LinkWarning.tsx
@@ -0,0 +1,161 @@
+import {useCallback, useMemo} 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 {shareUrl} from '#/lib/sharing'
+import {isPossiblyAUrl, splitApexDomain} from '#/lib/strings/url-helpers'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {Text} from '#/components/Typography'
+import {useGlobalDialogsControlContext} from './Context'
+
+export function LinkWarningDialog() {
+  const {linkWarningDialogControl} = useGlobalDialogsControlContext()
+
+  return (
+    <Dialog.Outer
+      control={linkWarningDialogControl.control}
+      nativeOptions={{preventExpansion: true}}
+      webOptions={{alignCenter: true}}
+      onClose={linkWarningDialogControl.clear}>
+      <Dialog.Handle />
+      <InAppBrowserConsentInner link={linkWarningDialogControl.value} />
+    </Dialog.Outer>
+  )
+}
+
+function InAppBrowserConsentInner({
+  link,
+}: {
+  link?: {href: string; displayText: string; share?: boolean}
+}) {
+  const control = Dialog.useDialogContext()
+  const {_} = useLingui()
+  const t = useTheme()
+  const openLink = useOpenLink()
+  const {gtMobile} = useBreakpoints()
+
+  const potentiallyMisleading = useMemo(
+    () => link && isPossiblyAUrl(link.displayText),
+    [link],
+  )
+
+  const onPressVisit = useCallback(() => {
+    control.close(() => {
+      if (!link) return
+      if (link.share) {
+        shareUrl(link.href)
+      } else {
+        openLink(link.href, undefined, true)
+      }
+    })
+  }, [control, link, openLink])
+
+  const onCancel = useCallback(() => {
+    control.close()
+  }, [control])
+
+  return (
+    <Dialog.ScrollableInner
+      style={web({maxWidth: 450})}
+      label={
+        potentiallyMisleading
+          ? _(msg`Potentially misleading link warning`)
+          : _(msg`Leaving Bluesky`)
+      }>
+      <View style={[a.gap_2xl]}>
+        <View style={[a.gap_sm]}>
+          <Text style={[a.font_heavy, a.text_2xl]}>
+            {potentiallyMisleading ? (
+              <Trans>Potentially misleading link</Trans>
+            ) : (
+              <Trans>Leaving Bluesky</Trans>
+            )}
+          </Text>
+          <Text style={[t.atoms.text_contrast_high, a.text_md, a.leading_snug]}>
+            <Trans>This link is taking you to the following website:</Trans>
+          </Text>
+          {link && <LinkBox href={link.href} />}
+          {potentiallyMisleading && (
+            <Text
+              style={[t.atoms.text_contrast_high, a.text_md, a.leading_snug]}>
+              <Trans>Make sure this is where you intend to go!</Trans>
+            </Text>
+          )}
+        </View>
+        <View
+          style={[
+            a.flex_1,
+            a.gap_sm,
+            gtMobile && [a.flex_row_reverse, a.justify_start],
+          ]}>
+          <Button
+            label={link?.share ? _(msg`Share link`) : _(msg`Visit site`)}
+            accessibilityHint={_(msg`Opens link ${link?.href ?? ''}`)}
+            onPress={onPressVisit}
+            size="large"
+            variant="solid"
+            color={potentiallyMisleading ? 'secondary_inverted' : 'primary'}>
+            <ButtonText>
+              {link?.share ? (
+                <Trans>Share link</Trans>
+              ) : (
+                <Trans>Visit site</Trans>
+              )}
+            </ButtonText>
+          </Button>
+          <Button
+            label={_(msg`Go back`)}
+            onPress={onCancel}
+            size="large"
+            variant="ghost"
+            color="secondary">
+            <ButtonText>
+              <Trans>Go back</Trans>
+            </ButtonText>
+          </Button>
+        </View>
+      </View>
+      <Dialog.Close />
+    </Dialog.ScrollableInner>
+  )
+}
+
+function LinkBox({href}: {href: string}) {
+  const t = useTheme()
+  const [scheme, hostname, rest] = useMemo(() => {
+    try {
+      const urlp = new URL(href)
+      const [subdomain, apexdomain] = splitApexDomain(urlp.hostname)
+      return [
+        urlp.protocol + '//' + subdomain,
+        apexdomain,
+        urlp.pathname.replace(/\/$/, '') + urlp.search + urlp.hash,
+      ]
+    } catch {
+      return ['', href, '']
+    }
+  }, [href])
+  return (
+    <View
+      style={[
+        t.atoms.bg,
+        t.atoms.border_contrast_medium,
+        a.px_md,
+        {paddingVertical: 10},
+        a.rounded_sm,
+        a.border,
+      ]}>
+      <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
+        {scheme}
+        <Text style={[a.text_md, a.leading_snug, t.atoms.text, a.font_bold]}>
+          {hostname}
+        </Text>
+        {rest}
+      </Text>
+    </View>
+  )
+}
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 7ebcec4c7..a2cc63745 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -43,13 +43,6 @@ export interface ChangePasswordModal {
   name: 'change-password'
 }
 
-export interface LinkWarningModal {
-  name: 'link-warning'
-  text: string
-  href: string
-  share?: boolean
-}
-
 export type Modal =
   // Account
   | DeleteAccountModal
@@ -67,9 +60,6 @@ export type Modal =
   | WaitlistModal
   | InviteCodesModal
 
-  // Generic
-  | LinkWarningModal
-
 const ModalContext = React.createContext<{
   isModalActive: boolean
   activeModals: Modal[]
diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx
deleted file mode 100644
index b0bf76ede..000000000
--- a/src/view/com/modals/LinkWarning.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-import React from 'react'
-import {SafeAreaView, StyleSheet, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {useOpenLink} from '#/lib/hooks/useOpenLink'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {shareUrl} from '#/lib/sharing'
-import {isPossiblyAUrl, splitApexDomain} from '#/lib/strings/url-helpers'
-import {colors, s} from '#/lib/styles'
-import {isWeb} from '#/platform/detection'
-import {useModalControls} from '#/state/modals'
-import {Button} from '#/view/com/util/forms/Button'
-import {Text} from '#/view/com/util/text/Text'
-import {ScrollView} from './util'
-
-export const snapPoints = ['50%']
-
-export function Component({
-  text,
-  href,
-  share,
-}: {
-  text: string
-  href: string
-  share?: boolean
-}) {
-  const pal = usePalette('default')
-  const {closeModal} = useModalControls()
-  const {isMobile} = useWebMediaQueries()
-  const {_} = useLingui()
-  const potentiallyMisleading = isPossiblyAUrl(text)
-  const openLink = useOpenLink()
-
-  const onPressVisit = () => {
-    closeModal()
-    if (share) {
-      shareUrl(href)
-    } else {
-      openLink(href, false, true)
-    }
-  }
-
-  return (
-    <SafeAreaView style={[s.flex1, pal.view]}>
-      <ScrollView
-        testID="linkWarningModal"
-        style={[s.flex1, isMobile && {paddingHorizontal: 18}]}>
-        <View style={styles.titleSection}>
-          {potentiallyMisleading ? (
-            <>
-              <FontAwesomeIcon
-                icon="circle-exclamation"
-                color={pal.colors.text}
-                size={18}
-              />
-              <Text type="title-lg" style={[pal.text, styles.title]}>
-                <Trans>Potentially Misleading Link</Trans>
-              </Text>
-            </>
-          ) : (
-            <Text type="title-lg" style={[pal.text, styles.title]}>
-              <Trans>Leaving Bluesky</Trans>
-            </Text>
-          )}
-        </View>
-
-        <View style={{gap: 10}}>
-          <Text type="lg" style={pal.text}>
-            <Trans>This link is taking you to the following website:</Trans>
-          </Text>
-
-          <LinkBox href={href} />
-
-          {potentiallyMisleading && (
-            <Text type="lg" style={pal.text}>
-              <Trans>Make sure this is where you intend to go!</Trans>
-            </Text>
-          )}
-        </View>
-
-        <View style={[styles.btnContainer, isMobile && {paddingBottom: 40}]}>
-          <Button
-            testID="confirmBtn"
-            type="primary"
-            onPress={onPressVisit}
-            accessibilityLabel={share ? _(msg`Share Link`) : _(msg`Visit Site`)}
-            accessibilityHint={
-              share
-                ? _(msg`Shares the linked website`)
-                : _(msg`Opens the linked website`)
-            }
-            label={share ? _(msg`Share Link`) : _(msg`Visit Site`)}
-            labelContainerStyle={{justifyContent: 'center', padding: 4}}
-            labelStyle={[s.f18]}
-          />
-          <Button
-            testID="cancelBtn"
-            type="default"
-            onPress={() => {
-              closeModal()
-            }}
-            accessibilityLabel={_(msg`Cancel`)}
-            accessibilityHint={_(msg`Cancels opening the linked website`)}
-            label={_(msg`Cancel`)}
-            labelContainerStyle={{justifyContent: 'center', padding: 4}}
-            labelStyle={[s.f18]}
-          />
-        </View>
-      </ScrollView>
-    </SafeAreaView>
-  )
-}
-
-function LinkBox({href}: {href: string}) {
-  const pal = usePalette('default')
-  const [scheme, hostname, rest] = React.useMemo(() => {
-    try {
-      const urlp = new URL(href)
-      const [subdomain, apexdomain] = splitApexDomain(urlp.hostname)
-      return [
-        urlp.protocol + '//' + subdomain,
-        apexdomain,
-        urlp.pathname + urlp.search + urlp.hash,
-      ]
-    } catch {
-      return ['', href, '']
-    }
-  }, [href])
-  return (
-    <View style={[pal.view, pal.border, styles.linkBox]}>
-      <Text type="lg" style={pal.textLight}>
-        {scheme}
-        <Text type="lg-bold" style={pal.text}>
-          {hostname}
-        </Text>
-        {rest}
-      </Text>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    paddingBottom: isWeb ? 0 : 40,
-  },
-  titleSection: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-    alignItems: 'center',
-    gap: 6,
-    paddingTop: isWeb ? 0 : 4,
-    paddingBottom: isWeb ? 14 : 10,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: '600',
-  },
-  linkBox: {
-    paddingHorizontal: 12,
-    paddingVertical: 10,
-    borderRadius: 6,
-    borderWidth: 1,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.blue3,
-  },
-  btnContainer: {
-    paddingTop: 20,
-    gap: 6,
-  },
-})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 1ec4052d0..f9afd183e 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -13,7 +13,6 @@ import * as DeleteAccountModal from './DeleteAccount'
 import * as InviteCodesModal from './InviteCodes'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
-import * as LinkWarningModal from './LinkWarning'
 import * as UserAddRemoveListsModal from './UserAddRemoveLists'
 
 const DEFAULT_SNAPPOINTS = ['90%']
@@ -68,9 +67,6 @@ export function ModalsContainer() {
   } else if (activeModal?.name === 'change-password') {
     snapPoints = ChangePasswordModal.snapPoints
     element = <ChangePasswordModal.Component />
-  } else if (activeModal?.name === 'link-warning') {
-    snapPoints = LinkWarningModal.snapPoints
-    element = <LinkWarningModal.Component {...activeModal} />
   } else {
     return null
   }
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 3374c3132..3eb744380 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -12,7 +12,6 @@ import * as DeleteAccountModal from './DeleteAccount'
 import * as InviteCodesModal from './InviteCodes'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
-import * as LinkWarningModal from './LinkWarning'
 import * as UserAddRemoveLists from './UserAddRemoveLists'
 
 export function ModalsContainer() {
@@ -65,8 +64,6 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <PostLanguagesSettingsModal.Component />
   } else if (modal.name === 'change-password') {
     element = <ChangePasswordModal.Component />
-  } else if (modal.name === 'link-warning') {
-    element = <LinkWarningModal.Component {...modal} />
   } else {
     return null
   }
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 3a0bf6f6d..a9c12ba0e 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -30,6 +30,7 @@ import {emitSoftReset} from '#/state/events'
 import {useModalControls} from '#/state/modals'
 import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper'
 import {useTheme} from '#/alf'
+import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
 import {router} from '../../../routes'
 import {PressableWithHover} from './PressableWithHover'
 import {Text} from './text/Text'
@@ -189,7 +190,8 @@ export const TextLink = memo(function TextLink({
   onBeforePress?: () => void
 } & TextProps) {
   const navigation = useNavigationDeduped()
-  const {openModal, closeModal} = useModalControls()
+  const {closeModal} = useModalControls()
+  const {linkWarningDialogControl} = useGlobalDialogsControlContext()
   const openLink = useOpenLink()
 
   if (!disableMismatchWarning && typeof text !== 'string') {
@@ -211,9 +213,8 @@ export const TextLink = memo(function TextLink({
         linkRequiresWarning(href, typeof text === 'string' ? text : '')
       if (requiresWarning) {
         e?.preventDefault?.()
-        openModal({
-          name: 'link-warning',
-          text: typeof text === 'string' ? text : '',
+        linkWarningDialogControl.open({
+          displayText: typeof text === 'string' ? text : '',
           href,
         })
       }
@@ -245,13 +246,13 @@ export const TextLink = memo(function TextLink({
       onBeforePress,
       onPressProp,
       closeModal,
-      openModal,
       navigation,
       href,
       text,
       disableMismatchWarning,
       navigationAction,
       openLink,
+      linkWarningDialogControl,
     ],
   )
   const hrefAttrs = useMemo(() => {
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index cd328c457..8c08ec0c0 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -27,6 +27,7 @@ import {atoms as a, select, useTheme} from '#/alf'
 import {setSystemUITheme} from '#/alf/util/systemUI'
 import {EmailDialog} from '#/components/dialogs/EmailDialog'
 import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent'
+import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
 import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
 import {SigninDialog} from '#/components/dialogs/Signin'
 import {Outlet as PortalOutlet} from '#/components/Portal'
@@ -155,6 +156,7 @@ function ShellInner() {
       <SigninDialog />
       <EmailDialog />
       <InAppBrowserConsentDialog />
+      <LinkWarningDialog />
       <Lightbox />
       <PortalOutlet />
       <BottomSheetOutlet />
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index a7ff76d61..8969d68f8 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -18,6 +18,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal'
 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
 import {atoms as a, select, useTheme} from '#/alf'
 import {EmailDialog} from '#/components/dialogs/EmailDialog'
+import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
 import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
 import {SigninDialog} from '#/components/dialogs/Signin'
 import {Outlet as PortalOutlet} from '#/components/Portal'
@@ -69,6 +70,7 @@ function ShellInner() {
       <MutedWordsDialog />
       <SigninDialog />
       <EmailDialog />
+      <LinkWarningDialog />
       <Lightbox />
       <PortalOutlet />