about summary refs log tree commit diff
path: root/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/state')
-rw-r--r--src/state/dialogs/index.tsx44
-rw-r--r--src/state/modals/index.tsx6
-rw-r--r--src/state/persisted/__tests__/migrate.test.ts6
-rw-r--r--src/state/persisted/legacy.ts4
-rw-r--r--src/state/persisted/schema.ts2
-rw-r--r--src/state/preferences/in-app-browser.tsx79
-rw-r--r--src/state/preferences/index.tsx5
-rw-r--r--src/state/queries/feed.ts3
-rw-r--r--src/state/queries/notifications/unread.tsx2
-rw-r--r--src/state/queries/post-feed.ts7
-rw-r--r--src/state/session/index.tsx80
11 files changed, 205 insertions, 33 deletions
diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx
new file mode 100644
index 000000000..4cafaa086
--- /dev/null
+++ b/src/state/dialogs/index.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+import {DialogControlProps} from '#/components/Dialog'
+
+const DialogContext = React.createContext<{
+  activeDialogs: React.MutableRefObject<
+    Map<string, React.MutableRefObject<DialogControlProps>>
+  >
+}>({
+  activeDialogs: {
+    current: new Map(),
+  },
+})
+
+const DialogControlContext = React.createContext<{
+  closeAllDialogs(): void
+}>({
+  closeAllDialogs: () => {},
+})
+
+export function useDialogStateContext() {
+  return React.useContext(DialogContext)
+}
+
+export function useDialogStateControlContext() {
+  return React.useContext(DialogControlContext)
+}
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const activeDialogs = React.useRef<
+    Map<string, React.MutableRefObject<DialogControlProps>>
+  >(new Map())
+  const closeAllDialogs = React.useCallback(() => {
+    activeDialogs.current.forEach(dialog => dialog.current.close())
+  }, [])
+  const context = React.useMemo(() => ({activeDialogs}), [])
+  const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs])
+  return (
+    <DialogContext.Provider value={context}>
+      <DialogControlContext.Provider value={controls}>
+        {children}
+      </DialogControlContext.Provider>
+    </DialogContext.Provider>
+  )
+}
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/__tests__/migrate.test.ts b/src/state/persisted/__tests__/migrate.test.ts
index d42580efd..2435ed24f 100644
--- a/src/state/persisted/__tests__/migrate.test.ts
+++ b/src/state/persisted/__tests__/migrate.test.ts
@@ -26,7 +26,7 @@ test('migrate: fresh install', async () => {
 
   expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
   expect(read).toHaveBeenCalledTimes(1)
-  expect(logger.log).toHaveBeenCalledWith(
+  expect(logger.info).toHaveBeenCalledWith(
     'persisted state: no migration needed',
   )
 })
@@ -38,7 +38,7 @@ test('migrate: fresh install, existing new storage', async () => {
 
   expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
   expect(read).toHaveBeenCalledTimes(1)
-  expect(logger.log).toHaveBeenCalledWith(
+  expect(logger.info).toHaveBeenCalledWith(
     'persisted state: no migration needed',
   )
 })
@@ -68,7 +68,7 @@ test('migrate: has legacy data', async () => {
   await migrate()
 
   expect(write).toHaveBeenCalledWith(transform(fixtures.LEGACY_DATA_DUMP))
-  expect(logger.log).toHaveBeenCalledWith(
+  expect(logger.info).toHaveBeenCalledWith(
     'persisted state: migrated legacy storage',
   )
 })
diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts
index 334ef1d92..097d6bc5c 100644
--- a/src/state/persisted/legacy.ts
+++ b/src/state/persisted/legacy.ts
@@ -164,14 +164,14 @@ export async function migrate() {
 
       if (validate.success) {
         await write(newData)
-        logger.log('persisted state: migrated legacy storage')
+        logger.info('persisted state: migrated legacy storage')
       } else {
         logger.error('persisted state: legacy data failed validation', {
           error: validate.error,
         })
       }
     } else {
-      logger.log('persisted state: no migration needed')
+      logger.info('persisted state: no migration needed')
     }
   } catch (e: any) {
     logger.error(e, {
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/state/queries/feed.ts b/src/state/queries/feed.ts
index 7a55b4e18..4acc7179a 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -272,7 +272,8 @@ export function usePinnedFeedsInfos(): {
                   },
                 })
               } catch (e) {
-                logger.warn(`usePinnedFeedsInfos: failed to fetch ${uri}`, {
+                // expected failure
+                logger.info(`usePinnedFeedsInfos: failed to fetch ${uri}`, {
                   error: e,
                 })
               }
diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx
index 49bb5a29d..a96b56225 100644
--- a/src/state/queries/notifications/unread.tsx
+++ b/src/state/queries/notifications/unread.tsx
@@ -167,7 +167,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
           }
           broadcast.postMessage({event: unreadCountStr})
         } catch (e) {
-          logger.error('Failed to check unread notifications', {error: e})
+          logger.warn('Failed to check unread notifications', {error: e})
         }
       },
 
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index dbb729133..82acf3974 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -18,6 +18,7 @@ import {LikesFeedAPI} from 'lib/api/feed/likes'
 import {CustomFeedAPI} from 'lib/api/feed/custom'
 import {ListFeedAPI} from 'lib/api/feed/list'
 import {MergeFeedAPI} from 'lib/api/feed/merge'
+import {HomeFeedAPI} from '#/lib/api/feed/home'
 import {logger} from '#/logger'
 import {STALE} from '#/state/queries'
 import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
@@ -338,7 +339,11 @@ function createApi(
   feedTuners: FeedTunerFn[],
 ) {
   if (feedDesc === 'home') {
-    return new MergeFeedAPI(params, feedTuners)
+    if (params.mergeFeedEnabled) {
+      return new MergeFeedAPI(params, feedTuners)
+    } else {
+      return new HomeFeedAPI()
+    }
   } else if (feedDesc === 'following') {
     return new FollowingFeedAPI()
   } else if (feedDesc.startsWith('author')) {
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 65cb12b46..e49bc2b39 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -44,6 +44,8 @@ export type ApiContext = {
     password: string
     handle: string
     inviteCode?: string
+    verificationPhone?: string
+    verificationCode?: string
   }) => Promise<void>
   login: (props: {
     service: string
@@ -193,11 +195,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 => ({
@@ -207,7 +205,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   }, [setStateAndPersist, queryClient])
 
   const createAccount = React.useCallback<ApiContext['createAccount']>(
-    async ({service, email, password, handle, inviteCode}: any) => {
+    async ({
+      service,
+      email,
+      password,
+      handle,
+      inviteCode,
+      verificationPhone,
+      verificationCode,
+    }: any) => {
       logger.info(`session: creating account`, {
         service,
         handle,
@@ -221,12 +227,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
         password,
         email,
         inviteCode,
+        verificationPhone,
+        verificationCode,
       })
 
       if (!agent.session) {
         throw new Error(`session: createAccount failed to establish a session`)
       }
 
+      /*dont await*/ agent.upsertProfile(_existing => {
+        return {
+          displayName: handle,
+        }
+      })
+
       const account: SessionAccount = {
         service: agent.service.toString(),
         did: agent.session.did,
@@ -322,8 +336,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   const logout = React.useCallback<ApiContext['logout']>(async () => {
+    logger.info(`session: logout`)
     clearCurrentAccount()
-    logger.debug(`session: logout`, {}, logger.DebugContext.session)
     setStateAndPersist(s => {
       return {
         ...s,
@@ -551,30 +565,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 +602,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(
     () => ({