diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/state/modals/index.tsx | 6 | ||||
-rw-r--r-- | src/state/persisted/schema.ts | 2 | ||||
-rw-r--r-- | src/state/preferences/in-app-browser.tsx | 79 | ||||
-rw-r--r-- | src/state/preferences/index.tsx | 5 | ||||
-rw-r--r-- | src/view/com/modals/InAppBrowserConsent.tsx | 102 | ||||
-rw-r--r-- | src/view/com/modals/LinkWarning.tsx | 6 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 12 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 18 |
9 files changed, 228 insertions, 6 deletions
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 8c32c472a..45856e108 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -187,6 +187,11 @@ export interface EmbedConsentModal { onAccept: () => void } +export interface InAppBrowserConsentModal { + name: 'in-app-browser-consent' + href: string +} + export type Modal = // Account | AddAppPasswordModal @@ -231,6 +236,7 @@ export type Modal = | ConfirmModal | LinkWarningModal | EmbedConsentModal + | InAppBrowserConsentModal const ModalContext = React.createContext<{ isModalActive: boolean diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 6a26cedae..a6f2ea06a 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -53,6 +53,7 @@ export const schema = z.object({ step: z.string(), }), hiddenPosts: z.array(z.string()).optional(), // should move to server + useInAppBrowser: z.boolean().optional(), }) export type Schema = z.infer<typeof schema> @@ -84,4 +85,5 @@ export const defaults: Schema = { step: 'Home', }, hiddenPosts: [], + useInAppBrowser: undefined, } diff --git a/src/state/preferences/in-app-browser.tsx b/src/state/preferences/in-app-browser.tsx new file mode 100644 index 000000000..628663af4 --- /dev/null +++ b/src/state/preferences/in-app-browser.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import * as persisted from '#/state/persisted' +import {Linking} from 'react-native' +import * as WebBrowser from 'expo-web-browser' +import {isNative} from '#/platform/detection' +import {useModalControls} from '../modals' + +type StateContext = persisted.Schema['useInAppBrowser'] +type SetContext = (v: persisted.Schema['useInAppBrowser']) => void + +const stateContext = React.createContext<StateContext>( + persisted.defaults.useInAppBrowser, +) +const setContext = React.createContext<SetContext>( + (_: persisted.Schema['useInAppBrowser']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState(persisted.get('useInAppBrowser')) + + const setStateWrapped = React.useCallback( + (inAppBrowser: persisted.Schema['useInAppBrowser']) => { + setState(inAppBrowser) + persisted.write('useInAppBrowser', inAppBrowser) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate(() => { + setState(persisted.get('useInAppBrowser')) + }) + }, [setStateWrapped]) + + return ( + <stateContext.Provider value={state}> + <setContext.Provider value={setStateWrapped}> + {children} + </setContext.Provider> + </stateContext.Provider> + ) +} + +export function useInAppBrowser() { + return React.useContext(stateContext) +} + +export function useSetInAppBrowser() { + return React.useContext(setContext) +} + +export function useOpenLink() { + const {openModal} = useModalControls() + const enabled = useInAppBrowser() + + const openLink = React.useCallback( + (url: string, override?: boolean) => { + if (isNative && !url.startsWith('mailto:')) { + if (override === undefined && enabled === undefined) { + openModal({ + name: 'in-app-browser-consent', + href: url, + }) + return + } else if (override ?? enabled) { + WebBrowser.openBrowserAsync(url, { + presentationStyle: + WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, + }) + return + } + } + Linking.openURL(url) + }, + [enabled, openModal], + ) + + return openLink +} diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index cc2d9244c..a442b763a 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -3,6 +3,7 @@ import {Provider as LanguagesProvider} from './languages' import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required' import {Provider as HiddenPostsProvider} from '../preferences/hidden-posts' import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' +import {Provider as InAppBrowserProvider} from './in-app-browser' export {useLanguagePrefs, useLanguagePrefsApi} from './languages' export { @@ -20,7 +21,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { <LanguagesProvider> <AltTextRequiredProvider> <ExternalEmbedsProvider> - <HiddenPostsProvider>{children}</HiddenPostsProvider> + <HiddenPostsProvider> + <InAppBrowserProvider>{children}</InAppBrowserProvider> + </HiddenPostsProvider> </ExternalEmbedsProvider> </AltTextRequiredProvider> </LanguagesProvider> diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx new file mode 100644 index 000000000..86bb46ca8 --- /dev/null +++ b/src/view/com/modals/InAppBrowserConsent.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' + +import {s} from 'lib/styles' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {ScrollView} from './util' +import {usePalette} from 'lib/hooks/usePalette' + +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useModalControls} from '#/state/modals' +import { + useOpenLink, + useSetInAppBrowser, +} from '#/state/preferences/in-app-browser' + +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="Cancel" + labelContainerStyle={{justifyContent: 'center', padding: 8}} + labelStyle={[s.f18]} + /> + </View> + </ScrollView> + ) +} + +const styles = StyleSheet.create({ + title: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 24, + marginBottom: 12, + }, + btnContainer: { + marginTop: 20, + flexDirection: 'column', + justifyContent: 'center', + rowGap: 10, + }, +}) diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx index 39e6cc3e6..81fdc7285 100644 --- a/src/view/com/modals/LinkWarning.tsx +++ b/src/view/com/modals/LinkWarning.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Linking, SafeAreaView, StyleSheet, View} from 'react-native' +import {SafeAreaView, StyleSheet, View} from 'react-native' import {ScrollView} from './util' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' @@ -12,6 +12,7 @@ import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' +import {useOpenLink} from '#/state/preferences/in-app-browser' export const snapPoints = ['50%'] @@ -21,10 +22,11 @@ export function Component({text, href}: {text: string; href: string}) { const {isMobile} = useWebMediaQueries() const {_} = useLingui() const potentiallyMisleading = isPossiblyAUrl(text) + const openLink = useOpenLink() const onPressVisit = () => { closeModal() - Linking.openURL(href) + openLink(href) } return ( diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index f9d211d07..7f814d971 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -39,6 +39,7 @@ import * as ChangeEmailModal from './ChangeEmail' import * as SwitchAccountModal from './SwitchAccount' import * as LinkWarningModal from './LinkWarning' import * as EmbedConsentModal from './EmbedConsent' +import * as InAppBrowserConsentModal from './InAppBrowserConsent' const DEFAULT_SNAPPOINTS = ['90%'] const HANDLE_HEIGHT = 24 @@ -180,6 +181,9 @@ export function ModalsContainer() { } else if (activeModal?.name === 'embed-consent') { snapPoints = EmbedConsentModal.snapPoints element = <EmbedConsentModal.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/com/util/Link.tsx b/src/view/com/util/Link.tsx index dcbec7cb4..4f898767d 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -1,6 +1,5 @@ import React, {ComponentProps, memo, useMemo} from 'react' import { - Linking, GestureResponderEvent, Platform, StyleProp, @@ -31,6 +30,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url' import {PressableWithHover} from './PressableWithHover' import FixedTouchableHighlight from '../pager/FixedTouchableHighlight' import {useModalControls} from '#/state/modals' +import {useOpenLink} from '#/state/preferences/in-app-browser' type Event = | React.MouseEvent<HTMLAnchorElement, MouseEvent> @@ -65,6 +65,7 @@ export const Link = memo(function Link({ const {closeModal} = useModalControls() const navigation = useNavigation<NavigationProp>() const anchorHref = asAnchor ? sanitizeUrl(href) : undefined + const openLink = useOpenLink() const onPress = React.useCallback( (e?: Event) => { @@ -74,11 +75,12 @@ export const Link = memo(function Link({ navigation, sanitizeUrl(href), navigationAction, + openLink, e, ) } }, - [closeModal, navigation, navigationAction, href], + [closeModal, navigation, navigationAction, href, openLink], ) if (noFeedback) { @@ -172,6 +174,7 @@ export const TextLink = memo(function TextLink({ const {...props} = useLinkProps({to: sanitizeUrl(href)}) const navigation = useNavigation<NavigationProp>() const {openModal, closeModal} = useModalControls() + const openLink = useOpenLink() if (warnOnMismatchingLabel && typeof text !== 'string') { console.error('Unable to detect mismatching label') @@ -200,6 +203,7 @@ export const TextLink = memo(function TextLink({ navigation, sanitizeUrl(href), navigationAction, + openLink, e, ) }, @@ -212,6 +216,7 @@ export const TextLink = memo(function TextLink({ text, warnOnMismatchingLabel, navigationAction, + openLink, ], ) const hrefAttrs = useMemo(() => { @@ -317,6 +322,7 @@ function onPressInner( navigation: NavigationProp, href: string, navigationAction: 'push' | 'replace' | 'navigate' = 'push', + openLink: (href: string) => void, e?: Event, ) { let shouldHandle = false @@ -345,7 +351,7 @@ function onPressInner( if (shouldHandle) { href = convertBskyAppUrlIfNeeded(href) if (newTab || href.startsWith('http') || href.startsWith('mailto')) { - Linking.openURL(href) + openLink(href) } else { closeModal() // close any active modals diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index c078e7a23..b4a3acbe3 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -70,6 +70,11 @@ import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' +import { + useInAppBrowser, + useSetInAppBrowser, +} from '#/state/preferences/in-app-browser' +import {isNative} from '#/platform/detection' function SettingsAccountCard({account}: {account: SessionAccount}) { const pal = usePalette('default') @@ -146,6 +151,8 @@ export function SettingsScreen({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const requireAltTextEnabled = useRequireAltTextEnabled() const setRequireAltTextEnabled = useSetRequireAltTextEnabled() + const inAppBrowserPref = useInAppBrowser() + const setUseInAppBrowser = useSetInAppBrowser() const onboardingDispatch = useOnboardingDispatch() const navigation = useNavigation<NavigationProp>() const {isMobile} = useWebMediaQueries() @@ -658,6 +665,17 @@ export function SettingsScreen({}: Props) { <Trans>Change handle</Trans> </Text> </TouchableOpacity> + {isNative && ( + <View style={[pal.view, styles.toggleCard]}> + <ToggleButton + type="default-light" + label={_(msg`Open links with in-app browser`)} + labelType="lg" + isSelected={inAppBrowserPref ?? false} + onPress={() => setUseInAppBrowser(!inAppBrowserPref)} + /> + </View> + )} <View style={styles.spacer20} /> <Text type="xl-bold" style={[pal.text, styles.heading]}> <Trans>Danger Zone</Trans> |