(optional) In app browser (#2490)
* add expo web browser + modal * add in app browser option to settings * don't show toggle on web * Tweak browser-choice UIs --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
parent
b147f7ae8a
commit
998ee29986
11 changed files with 299 additions and 22 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
79
src/state/preferences/in-app-browser.tsx
Normal file
79
src/state/preferences/in-app-browser.tsx
Normal file
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
|
|
102
src/view/com/modals/InAppBrowserConsent.tsx
Normal file
102
src/view/com/modals/InAppBrowserConsent.tsx
Normal file
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue