From 998ee2998657e82395fe4462809c376ffbd03ed8 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 12 Jan 2024 10:19:56 -0800 Subject: [PATCH 1/6] (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 --- package.json | 2 +- src/state/modals/index.tsx | 6 ++ src/state/persisted/schema.ts | 2 + src/state/preferences/in-app-browser.tsx | 79 +++++++++++++++ src/state/preferences/index.tsx | 5 +- src/view/com/modals/InAppBrowserConsent.tsx | 102 ++++++++++++++++++++ src/view/com/modals/LinkWarning.tsx | 6 +- src/view/com/modals/Modal.tsx | 4 + src/view/com/util/Link.tsx | 12 ++- src/view/screens/Settings.tsx | 18 ++++ yarn.lock | 85 +++++++++++++--- 11 files changed, 299 insertions(+), 22 deletions(-) create mode 100644 src/state/preferences/in-app-browser.tsx create mode 100644 src/view/com/modals/InAppBrowserConsent.tsx diff --git a/package.json b/package.json index 7e63ad9a62..6572d868bf 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "expo-system-ui": "~2.9.2", "expo-task-manager": "~11.7.0", "expo-updates": "~0.24.5", + "expo-web-browser": "^12.5.0", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", "js-sha256": "^0.9.0", @@ -148,7 +149,6 @@ "react-native-get-random-values": "~1.8.0", "react-native-haptic-feedback": "^1.14.0", "react-native-image-crop-picker": "^0.38.1", - "react-native-inappbrowser-reborn": "^3.6.3", "react-native-ios-context-menu": "^1.15.3", "react-native-linear-gradient": "^2.6.2", "react-native-pager-view": "6.2.2", diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 8c32c472a6..45856e1084 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 6a26cedae5..a6f2ea06a6 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 @@ -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 0000000000..628663af49 --- /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( + persisted.defaults.useInAppBrowser, +) +const setContext = React.createContext( + (_: 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 ( + + + {children} + + + ) +} + +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 cc2d9244c3..a442b763ad 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<{}>) { - {children} + + {children} + diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx new file mode 100644 index 0000000000..86bb46ca81 --- /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 ( + + + How should we open this link? + + + + Your choice will be saved, but can be changed later in settings. + + + + + + ) : null +} + +const styles = StyleSheet.create({ + infoBar: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 16, + paddingVertical: 12, + marginHorizontal: 10, + marginBottom: 10, + }, +}) From c0261fc940c11be48eeb296400af4a3f5d5b6b7d Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Fri, 12 Jan 2024 10:25:00 -0800 Subject: [PATCH 3/6] package.json: cp --verbose doesn't exist on macos (#2501) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6572d868bf..79597c8246 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "build-web": "expo export:web && node ./scripts/post-web-build.js && cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/", + "build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/", "build-all": "yarn intl:build && eas build --platform all", "start": "expo start --dev-client", "start:prod": "expo start --dev-client --no-dev --minify", From 11f6ff7cbe1e057fad9a3476d52e6a6ec64b2234 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 12 Jan 2024 12:39:21 -0600 Subject: [PATCH 4/6] Ensure sessions and tokens are synced between tabs (#2498) * Ensure sessions and tokens are synced between tabs * Send clear account to sentry for monitoring --- src/state/session/index.tsx | 60 +++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 65cb12b469..f707c1598d 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -193,11 +193,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const clearCurrentAccount = React.useCallback(() => { - logger.debug( - `session: clear current account`, - {}, - logger.DebugContext.session, - ) + logger.warn(`session: clear current account`) __globalAgent = PUBLIC_BSKY_AGENT queryClient.clear() setStateAndPersist(s => ({ @@ -322,8 +318,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const logout = React.useCallback(async () => { + logger.info(`session: logout`) clearCurrentAccount() - logger.debug(`session: logout`, {}, logger.DebugContext.session) setStateAndPersist(s => { return { ...s, @@ -551,30 +547,36 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return persisted.onUpdate(() => { const session = persisted.get('session') - logger.debug(`session: onUpdate`, {}, logger.DebugContext.session) + logger.info(`session: persisted onUpdate`, {}) - if (session.currentAccount) { + if (session.currentAccount && session.currentAccount.refreshJwt) { if (session.currentAccount?.did !== state.currentAccount?.did) { - logger.debug( - `session: switching account`, - { - from: { - did: state.currentAccount?.did, - handle: state.currentAccount?.handle, - }, - to: { - did: session.currentAccount.did, - handle: session.currentAccount.handle, - }, + logger.info(`session: persisted onUpdate, switching accounts`, { + from: { + did: state.currentAccount?.did, + handle: state.currentAccount?.handle, }, - logger.DebugContext.session, - ) + to: { + did: session.currentAccount.did, + handle: session.currentAccount.handle, + }, + }) initSession(session.currentAccount) + } else { + logger.info(`session: persisted onUpdate, updating session`, {}) + + /* + * Use updated session in this tab's agent. Do not call + * upsertAccount, since that will only persist the session that's + * already persisted, and we'll get a loop between tabs. + */ + // @ts-ignore we checked for `refreshJwt` above + __globalAgent.session = session.currentAccount } } else if (!session.currentAccount && state.currentAccount) { logger.debug( - `session: logging out`, + `session: persisted onUpdate, logging out`, { did: state.currentAccount?.did, handle: state.currentAccount?.handle, @@ -582,10 +584,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) { logger.DebugContext.session, ) + /* + * No need to do a hard logout here. If we reach this, tokens for this + * account have already been cleared either by an `expired` event + * handled by `persistSession` (which nukes this accounts tokens only), + * or by a `logout` call which nukes all accounts tokens) + */ clearCurrentAccount() } + + setState(s => ({ + ...s, + accounts: session.accounts, + currentAccount: session.currentAccount, + })) }) - }, [state, clearCurrentAccount, initSession]) + }, [state, setState, clearCurrentAccount, initSession]) const stateContext = React.useMemo( () => ({ From a0e345fa015b4cb85e3e8fb8b6646493d7d2e0ae Mon Sep 17 00:00:00 2001 From: Mary <148872143+mary-ext@users.noreply.github.com> Date: Sat, 13 Jan 2024 01:42:28 +0700 Subject: [PATCH 5/6] fix: set html lang according to app language (#2496) --- src/locale/i18n.web.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/locale/i18n.web.ts b/src/locale/i18n.web.ts index 819f58eb75..a6f0e158d1 100644 --- a/src/locale/i18n.web.ts +++ b/src/locale/i18n.web.ts @@ -62,6 +62,9 @@ export async function dynamicActivate(locale: AppLanguage) { export async function useLocaleLanguage() { const {appLanguage} = useLanguagePrefs() useEffect(() => { - dynamicActivate(sanitizeAppLanguageSetting(appLanguage)) + const sanitizedLanguage = sanitizeAppLanguageSetting(appLanguage) + + document.documentElement.lang = sanitizedLanguage + dynamicActivate(sanitizedLanguage) }, [appLanguage]) } From 40c757afa9bf8f53da858a213227288d9affa5f7 Mon Sep 17 00:00:00 2001 From: Mary <148872143+mary-ext@users.noreply.github.com> Date: Sat, 13 Jan 2024 01:44:24 +0700 Subject: [PATCH 6/6] fix: truncate long email address (#2493) --- src/view/screens/Settings.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index b4a3acbe3c..1f117b45bc 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -320,8 +320,14 @@ export function SettingsScreen({}: Props) { /> )} - - {currentAccount.email || '(no email)'}{' '} + + {currentAccount.email || '(no email)'} openModal({name: 'change-email'})}>