diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-06-18 16:17:54 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-06-18 06:17:54 -0700 |
commit | 4da86e5864e10b14880900b78cb94d33c199b7da (patch) | |
tree | 2ace2cc361d2c84a145b9e163c06733f0b6ef241 /src | |
parent | dd86402763518ae94ced8274dda886f92ec7b51e (diff) | |
download | voidsky-4da86e5864e10b14880900b78cb94d33c199b7da.tar.zst |
Modernise link warning dialog (#8243)
* add link warning dialog * add copy for if sharing * delete old modal * get web working
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Link.tsx | 28 | ||||
-rw-r--r-- | src/components/dialogs/Context.tsx | 12 | ||||
-rw-r--r-- | src/components/dialogs/LinkWarning.tsx | 161 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 10 | ||||
-rw-r--r-- | src/view/com/modals/LinkWarning.tsx | 180 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 11 | ||||
-rw-r--r-- | src/view/shell/index.tsx | 2 | ||||
-rw-r--r-- | src/view/shell/index.web.tsx | 2 |
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 /> |