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/modals/index.tsx8
-rw-r--r--src/state/persisted/legacy.ts1
-rw-r--r--src/state/persisted/schema.ts16
-rw-r--r--src/state/preferences/external-embeds-prefs.tsx54
-rw-r--r--src/state/preferences/index.tsx9
-rw-r--r--src/state/queries/actor-autocomplete.ts2
-rw-r--r--src/state/queries/app-passwords.ts1
-rw-r--r--src/state/queries/invites.ts1
-rw-r--r--src/state/queries/notifications/unread.tsx4
-rw-r--r--src/state/queries/post-feed.ts4
-rw-r--r--src/state/session/index.tsx78
-rw-r--r--src/state/shell/composer.tsx1
12 files changed, 145 insertions, 34 deletions
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 81a220d1b..8c32c472a 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -6,6 +6,7 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
 import {ImageModel} from '#/state/models/media/image'
 import {GalleryModel} from '#/state/models/media/gallery'
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {EmbedPlayerSource} from '#/lib/strings/embed-player.ts'
 import {ThreadgateSetting} from '../queries/threadgate'
 
 export interface ConfirmModal {
@@ -180,6 +181,12 @@ export interface LinkWarningModal {
   href: string
 }
 
+export interface EmbedConsentModal {
+  name: 'embed-consent'
+  source: EmbedPlayerSource
+  onAccept: () => void
+}
+
 export type Modal =
   // Account
   | AddAppPasswordModal
@@ -223,6 +230,7 @@ export type Modal =
   // Generic
   | ConfirmModal
   | LinkWarningModal
+  | EmbedConsentModal
 
 const ModalContext = React.createContext<{
   isModalActive: boolean
diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts
index cdb542f5a..334ef1d92 100644
--- a/src/state/persisted/legacy.ts
+++ b/src/state/persisted/legacy.ts
@@ -109,6 +109,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
       step: legacy.onboarding?.step || defaults.onboarding.step,
     },
     hiddenPosts: defaults.hiddenPosts,
+    externalEmbeds: defaults.externalEmbeds,
   }
 }
 
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 27b1f26bd..6a26cedae 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -1,6 +1,8 @@
 import {z} from 'zod'
 import {deviceLocales} from '#/platform/detection'
 
+const externalEmbedOptions = ['show', 'hide'] as const
+
 // only data needed for rendering account page
 const accountSchema = z.object({
   service: z.string(),
@@ -30,6 +32,19 @@ export const schema = z.object({
     appLanguage: z.string(),
   }),
   requireAltTextEnabled: z.boolean(), // should move to server
+  externalEmbeds: z
+    .object({
+      giphy: z.enum(externalEmbedOptions).optional(),
+      tenor: z.enum(externalEmbedOptions).optional(),
+      youtube: z.enum(externalEmbedOptions).optional(),
+      youtubeShorts: z.enum(externalEmbedOptions).optional(),
+      twitch: z.enum(externalEmbedOptions).optional(),
+      vimeo: z.enum(externalEmbedOptions).optional(),
+      spotify: z.enum(externalEmbedOptions).optional(),
+      appleMusic: z.enum(externalEmbedOptions).optional(),
+      soundcloud: z.enum(externalEmbedOptions).optional(),
+    })
+    .optional(),
   mutedThreads: z.array(z.string()), // should move to server
   invites: z.object({
     copiedInvites: z.array(z.string()),
@@ -60,6 +75,7 @@ export const defaults: Schema = {
     appLanguage: deviceLocales[0] || 'en',
   },
   requireAltTextEnabled: false,
+  externalEmbeds: {},
   mutedThreads: [],
   invites: {
     copiedInvites: [],
diff --git a/src/state/preferences/external-embeds-prefs.tsx b/src/state/preferences/external-embeds-prefs.tsx
new file mode 100644
index 000000000..0f6385fe8
--- /dev/null
+++ b/src/state/preferences/external-embeds-prefs.tsx
@@ -0,0 +1,54 @@
+import React from 'react'
+import * as persisted from '#/state/persisted'
+import {EmbedPlayerSource} from 'lib/strings/embed-player'
+
+type StateContext = persisted.Schema['externalEmbeds']
+type SetContext = (source: EmbedPlayerSource, value: 'show' | 'hide') => void
+
+const stateContext = React.createContext<StateContext>(
+  persisted.defaults.externalEmbeds,
+)
+const setContext = React.createContext<SetContext>({} as SetContext)
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [state, setState] = React.useState(persisted.get('externalEmbeds'))
+
+  const setStateWrapped = React.useCallback(
+    (source: EmbedPlayerSource, value: 'show' | 'hide') => {
+      setState(prev => {
+        persisted.write('externalEmbeds', {
+          ...prev,
+          [source]: value,
+        })
+
+        return {
+          ...prev,
+          [source]: value,
+        }
+      })
+    },
+    [setState],
+  )
+
+  React.useEffect(() => {
+    return persisted.onUpdate(() => {
+      setState(persisted.get('externalEmbeds'))
+    })
+  }, [setStateWrapped])
+
+  return (
+    <stateContext.Provider value={state}>
+      <setContext.Provider value={setStateWrapped}>
+        {children}
+      </setContext.Provider>
+    </stateContext.Provider>
+  )
+}
+
+export function useExternalEmbedsPrefs() {
+  return React.useContext(stateContext)
+}
+
+export function useSetExternalEmbedPref() {
+  return React.useContext(setContext)
+}
diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx
index 5ec659031..cc2d9244c 100644
--- a/src/state/preferences/index.tsx
+++ b/src/state/preferences/index.tsx
@@ -2,19 +2,26 @@ import React from 'react'
 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'
 
 export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
 export {
   useRequireAltTextEnabled,
   useSetRequireAltTextEnabled,
 } from './alt-text-required'
+export {
+  useExternalEmbedsPrefs,
+  useSetExternalEmbedPref,
+} from './external-embeds-prefs'
 export * from './hidden-posts'
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   return (
     <LanguagesProvider>
       <AltTextRequiredProvider>
-        <HiddenPostsProvider>{children}</HiddenPostsProvider>
+        <ExternalEmbedsProvider>
+          <HiddenPostsProvider>{children}</HiddenPostsProvider>
+        </ExternalEmbedsProvider>
       </AltTextRequiredProvider>
     </LanguagesProvider>
   )
diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts
index 785e29765..ba9f97004 100644
--- a/src/state/queries/actor-autocomplete.ts
+++ b/src/state/queries/actor-autocomplete.ts
@@ -24,6 +24,8 @@ export function useActorAutocompleteQuery(prefix: string) {
   const {data: follows, isFetching} = useMyFollowsQuery()
   const moderationOpts = useModerationOpts()
 
+  prefix = prefix.toLowerCase()
+
   return useQuery<AppBskyActorDefs.ProfileViewBasic[]>({
     staleTime: STALE.MINUTES.ONE,
     queryKey: RQKEY(prefix || ''),
diff --git a/src/state/queries/app-passwords.ts b/src/state/queries/app-passwords.ts
index 4b9e09a8d..014244f01 100644
--- a/src/state/queries/app-passwords.ts
+++ b/src/state/queries/app-passwords.ts
@@ -9,7 +9,6 @@ export const RQKEY = () => ['app-passwords']
 export function useAppPasswordsQuery() {
   return useQuery({
     staleTime: STALE.MINUTES.FIVE,
-    refetchInterval: STALE.MINUTES.ONE,
     queryKey: RQKEY(),
     queryFn: async () => {
       const res = await getAgent().com.atproto.server.listAppPasswords({})
diff --git a/src/state/queries/invites.ts b/src/state/queries/invites.ts
index bfea402e1..9ae9c707f 100644
--- a/src/state/queries/invites.ts
+++ b/src/state/queries/invites.ts
@@ -16,7 +16,6 @@ export type InviteCodesQueryResponse = Exclude<
 export function useInviteCodesQuery() {
   return useQuery({
     staleTime: STALE.MINUTES.FIVE,
-    refetchInterval: STALE.MINUTES.FIVE,
     queryKey: ['inviteCodes'],
     queryFn: async () => {
       const res = await getAgent()
diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx
index abaabbf0e..d604e8fe0 100644
--- a/src/state/queries/notifications/unread.tsx
+++ b/src/state/queries/notifications/unread.tsx
@@ -15,6 +15,7 @@ import {useMutedThreads} from '#/state/muted-threads'
 import {RQKEY as RQKEY_NOTIFS} from './feed'
 import {logger} from '#/logger'
 import {truncateAndInvalidate} from '../util'
+import {AppState} from 'react-native'
 
 const UPDATE_INTERVAL = 30 * 1e3 // 30sec
 
@@ -97,6 +98,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       async checkUnread({invalidate}: {invalidate?: boolean} = {}) {
         try {
           if (!getAgent().session) return
+          if (AppState.currentState !== 'active') {
+            return
+          }
 
           // count
           const page = await fetchPage({
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 0e943622a..dbb729133 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -1,4 +1,5 @@
 import React, {useCallback, useEffect, useRef} from 'react'
+import {AppState} from 'react-native'
 import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api'
 import {
   useInfiniteQuery,
@@ -312,6 +313,9 @@ export async function pollLatest(page: FeedPage | undefined) {
   if (!page) {
     return false
   }
+  if (AppState.currentState !== 'active') {
+    return
+  }
 
   logger.debug('usePostFeedQuery: pollLatest')
   const post = await page.api.peekLatest()
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index aa8c94ebc..17f25570e 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -102,10 +102,21 @@ function createPersistSessionHandler(
     expired: boolean
     refreshedAccount: SessionAccount
   }) => void,
+  {
+    networkErrorCallback,
+  }: {
+    networkErrorCallback?: () => void
+  } = {},
 ): AtpPersistSessionHandler {
   return function persistSession(event, session) {
     const expired = event === 'expired' || event === 'create-failed'
 
+    if (event === 'network-error') {
+      logger.warn(`session: persistSessionHandler received network-error event`)
+      networkErrorCallback?.()
+      return
+    }
+
     const refreshedAccount: SessionAccount = {
       service: account.service,
       did: session?.did || account.did,
@@ -179,16 +190,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
     [setStateAndPersist],
   )
 
+  const clearCurrentAccount = React.useCallback(() => {
+    logger.debug(
+      `session: clear current account`,
+      {},
+      logger.DebugContext.session,
+    )
+    __globalAgent = PUBLIC_BSKY_AGENT
+    queryClient.clear()
+    setStateAndPersist(s => ({
+      ...s,
+      currentAccount: undefined,
+    }))
+  }, [setStateAndPersist, queryClient])
+
   const createAccount = React.useCallback<ApiContext['createAccount']>(
     async ({service, email, password, handle, inviteCode}: any) => {
-      logger.debug(
-        `session: creating account`,
-        {
-          service,
-          handle,
-        },
-        logger.DebugContext.session,
-      )
+      logger.info(`session: creating account`, {
+        service,
+        handle,
+      })
       track('Try Create Account')
 
       const agent = new BskyAgent({service})
@@ -215,9 +236,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       }
 
       agent.setPersistSessionHandler(
-        createPersistSessionHandler(account, ({expired, refreshedAccount}) => {
-          upsertAccount(refreshedAccount, expired)
-        }),
+        createPersistSessionHandler(
+          account,
+          ({expired, refreshedAccount}) => {
+            upsertAccount(refreshedAccount, expired)
+          },
+          {networkErrorCallback: clearCurrentAccount},
+        ),
       )
 
       __globalAgent = agent
@@ -234,7 +259,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       )
       track('Create Account')
     },
-    [upsertAccount, queryClient],
+    [upsertAccount, queryClient, clearCurrentAccount],
   )
 
   const login = React.useCallback<ApiContext['login']>(
@@ -267,9 +292,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       }
 
       agent.setPersistSessionHandler(
-        createPersistSessionHandler(account, ({expired, refreshedAccount}) => {
-          upsertAccount(refreshedAccount, expired)
-        }),
+        createPersistSessionHandler(
+          account,
+          ({expired, refreshedAccount}) => {
+            upsertAccount(refreshedAccount, expired)
+          },
+          {networkErrorCallback: clearCurrentAccount},
+        ),
       )
 
       __globalAgent = agent
@@ -287,23 +316,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
       track('Sign In', {resumedSession: false})
     },
-    [upsertAccount, queryClient],
+    [upsertAccount, queryClient, clearCurrentAccount],
   )
 
-  const clearCurrentAccount = React.useCallback(() => {
-    logger.debug(
-      `session: clear current account`,
-      {},
-      logger.DebugContext.session,
-    )
-    __globalAgent = PUBLIC_BSKY_AGENT
-    queryClient.clear()
-    setStateAndPersist(s => ({
-      ...s,
-      currentAccount: undefined,
-    }))
-  }, [setStateAndPersist, queryClient])
-
   const logout = React.useCallback<ApiContext['logout']>(async () => {
     clearCurrentAccount()
     logger.debug(`session: logout`, {}, logger.DebugContext.session)
@@ -337,6 +352,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
           ({expired, refreshedAccount}) => {
             upsertAccount(refreshedAccount, expired)
           },
+          {networkErrorCallback: clearCurrentAccount},
         ),
       })
 
@@ -437,7 +453,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
         }
       }
     },
-    [upsertAccount, queryClient],
+    [upsertAccount, queryClient, clearCurrentAccount],
   )
 
   const resumeSession = React.useCallback<ApiContext['resumeSession']>(
diff --git a/src/state/shell/composer.tsx b/src/state/shell/composer.tsx
index bdf5e4a7a..9cf8ef8de 100644
--- a/src/state/shell/composer.tsx
+++ b/src/state/shell/composer.tsx
@@ -30,6 +30,7 @@ export interface ComposerOpts {
   onPost?: () => void
   quote?: ComposerOptsQuote
   mention?: string // handle of user to mention
+  openPicker?: (pos: DOMRect | undefined) => void
 }
 
 type StateContext = ComposerOpts | undefined