From 9721bbaeb3a420f431a409c14d921f14edb8f64c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 11 Apr 2024 16:25:00 -0500 Subject: [PATCH] Cleanup, feedback --- src/lib/constants.ts | 1 + src/state/persisted/index.ts | 6 +- src/state/persisted/schema.ts | 21 ++++-- src/state/queries/index.ts | 4 +- src/state/session/index.tsx | 130 +++++++++++++++++++--------------- 5 files changed, 92 insertions(+), 70 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 401c39362b..3732098f39 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -4,6 +4,7 @@ export const LOCAL_DEV_SERVICE = Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' export const STAGING_SERVICE = 'https://staging.bsky.dev' export const BSKY_SERVICE = 'https://bsky.social' +export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app' export const DEFAULT_SERVICE = BSKY_SERVICE const HELP_DESK_LANG = 'en-us' export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts index 268beb6659..4e0aafd822 100644 --- a/src/state/persisted/index.ts +++ b/src/state/persisted/index.ts @@ -6,11 +6,7 @@ import {migrate} from '#/state/persisted/legacy' import {defaults, Schema} from '#/state/persisted/schema' import * as store from '#/state/persisted/store' -export type { - PersistedAccount, - PersistedCurrentAccount, - Schema, -} from '#/state/persisted/schema' +export type {PersistedAccount, Schema} from '#/state/persisted/schema' export {defaults} from '#/state/persisted/schema' const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL') diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 368e3fb2aa..8f00542dfa 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -4,7 +4,10 @@ import {deviceLocales} from '#/platform/detection' const externalEmbedOptions = ['show', 'hide'] as const -// only data needed for rendering account page +/** + * A account persisted to storage. Stored in the `accounts[]` array. Contains + * base account info and access tokens. + */ const accountSchema = z.object({ service: z.string(), did: z.string(), @@ -17,17 +20,25 @@ const accountSchema = z.object({ }) export type PersistedAccount = z.infer -const currentAccountSchema = z.object({ - did: z.string(), +/** + * The current account. Stored in the `currentAccount` field. + * + * In previous versions, this included tokens and other info. Now, it's used + * only to reference the `did` field, and all other fields are marked as + * optional. They should be considered deprecated and not used, but are kept + * here for backwards compat. + */ +const currentAccountScheme = accountSchema.extend({ + service: z.string().optional(), + handle: z.string().optional(), }) -export type PersistedCurrentAccount = z.infer export const schema = z.object({ colorMode: z.enum(['system', 'light', 'dark']), darkTheme: z.enum(['dim', 'dark']).optional(), session: z.object({ accounts: z.array(accountSchema), - currentAccount: currentAccountSchema.optional(), + currentAccount: currentAccountScheme.optional(), }), reminders: z.object({ lastEmailConfirm: z.string().optional(), diff --git a/src/state/queries/index.ts b/src/state/queries/index.ts index e7c5f577b7..e30528ca13 100644 --- a/src/state/queries/index.ts +++ b/src/state/queries/index.ts @@ -1,7 +1,9 @@ import {BskyAgent} from '@atproto/api' +import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' + export const PUBLIC_BSKY_AGENT = new BskyAgent({ - service: 'https://public.api.bsky.app', + service: PUBLIC_BSKY_SERVICE, }) export const STALE = { diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index fbea122ac6..51b361e166 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -5,12 +5,12 @@ import {jwtDecode} from 'jwt-decode' import {track} from '#/lib/analytics/analytics' import {networkRetry} from '#/lib/async/retry' import {IS_TEST_USER} from '#/lib/constants' +import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' import {logEvent, LogEvents} from '#/lib/statsig/statsig' import {hasProp} from '#/lib/type-guards' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' -import {PUBLIC_BSKY_AGENT} from '#/state/queries' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import * as Toast from '#/view/com/util/Toast' @@ -18,10 +18,16 @@ import {IS_DEV} from '#/env' import {emitSessionDropped} from '../events' import {readLabelers} from './agent-config' +/** + * Only used for the initial agent values in state and context. Replaced + * immediately, and should not be reused. + */ +const INITIAL_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE}) + /** * @deprecated use `agent` from `useSession` instead */ -let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT +let __globalAgent: BskyAgent = INITIAL_AGENT /** * NOTE @@ -36,6 +42,7 @@ export function getAgent() { } export type SessionAccount = persisted.PersistedAccount +export type CurrentAccount = Omit export type StateContext = { currentAgent: BskyAgent @@ -44,10 +51,10 @@ export type StateContext = { hasSession: boolean accounts: SessionAccount[] /** - * This value is derived from `BskyAgent.session` and should contain the full - * account object persisted to storage, minus the access tokens. + * Contains the full account object persisted to storage, minus access + * tokens. */ - currentAccount: Omit | undefined + currentAccount: CurrentAccount | undefined } export type ApiContext = { @@ -98,7 +105,7 @@ export type ApiContext = { } const StateContext = React.createContext({ - currentAgent: PUBLIC_BSKY_AGENT, + currentAgent: INITIAL_AGENT, isInitialLoad: true, isSwitchingAccounts: false, accounts: [], @@ -133,15 +140,6 @@ function agentToSessionAccount(agent: BskyAgent): SessionAccount | undefined { } } -function agentToCurrentAccount( - agent: BskyAgent, -): StateContext['currentAccount'] { - const sessionAccount = agentToSessionAccount(agent) - delete sessionAccount?.accessJwt - delete sessionAccount?.refreshJwt - return sessionAccount -} - function sessionAccountToAgentSession( account: SessionAccount, ): BskyAgent['session'] { @@ -158,16 +156,20 @@ function sessionAccountToAgentSession( export function Provider({children}: React.PropsWithChildren<{}>) { const isDirty = React.useRef(false) const [currentAgent, setCurrentAgent] = - React.useState(PUBLIC_BSKY_AGENT) + React.useState(INITIAL_AGENT) const [accounts, setAccounts] = React.useState( persisted.get('session').accounts, ) const [isInitialLoad, setIsInitialLoad] = React.useState(true) const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false) - const currentAccount = React.useMemo( - () => agentToCurrentAccount(currentAgent), + const currentAccountDid = React.useMemo( + () => currentAgent.session?.did, [currentAgent], ) + const currentAccount = React.useMemo( + () => accounts.find(a => a.did === currentAccountDid), + [accounts, currentAccountDid], + ) const persistNextUpdate = React.useCallback( () => (isDirty.current = true), @@ -187,10 +189,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const clearCurrentAccount = React.useCallback(() => { logger.warn(`session: clear current account`) + + // immediate clear this so any pending requests don't use it + currentAgent.setPersistSessionHandler(() => {}) + persistNextUpdate() - setCurrentAgent(PUBLIC_BSKY_AGENT) - BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) - }, [persistNextUpdate, setCurrentAgent]) + + const newAgent = new BskyAgent({service: PUBLIC_BSKY_SERVICE}) + setCurrentAgent(newAgent) + configureModeration(newAgent) + }, [currentAgent, persistNextUpdate, setCurrentAgent]) React.useMemo(() => { currentAgent.setPersistSessionHandler(event => { @@ -436,17 +444,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) { >(async () => { const {accounts: persistedAccounts} = persisted.get('session') const selectedAccount = persistedAccounts.find( - a => a.did === currentAccount?.did, + a => a.did === currentAccountDid, ) if (!selectedAccount) return - await currentAgent.resumeSession( - sessionAccountToAgentSession(selectedAccount)!, - ) + + // update and swap agent to trigger render refresh + const newAgent = currentAgent.clone() + await newAgent.resumeSession(sessionAccountToAgentSession(selectedAccount)!) + const refreshedAccount = agentToSessionAccount(newAgent) persistNextUpdate() - upsertAndPersistAccount(agentToSessionAccount(currentAgent)!) - setCurrentAgent(currentAgent.clone()) + upsertAndPersistAccount(refreshedAccount!) + setCurrentAgent(newAgent) + configureModeration(newAgent, refreshedAccount) }, [ - currentAccount, + currentAccountDid, currentAgent, setCurrentAgent, persistNextUpdate, @@ -475,11 +486,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { isDirty.current = false persisted.write('session', { accounts, - currentAccount: currentAccount - ? { - did: currentAccount.did, - } - : undefined, + currentAccount, }) } }, [accounts, currentAccount]) @@ -502,17 +509,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) if (selectedAccount && selectedAccount.refreshJwt) { - if (selectedAccount?.did !== currentAccount?.did) { + if (selectedAccount?.did !== currentAccountDid) { logger.debug( `session: persisted onUpdate, switching accounts`, { from: { - did: currentAccount?.did, - handle: currentAccount?.handle, + did: currentAccountDid, }, to: { did: selectedAccount.did, - handle: selectedAccount.handle, }, }, logger.DebugContext.session, @@ -526,12 +531,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) { logger.DebugContext.session, ) // console.log('UPDATE', { refreshJwt: selectedAccount.refreshJwt.slice(-10) }) - // updates silently, all subsequent calls will use the new session - currentAgent.session = sessionAccountToAgentSession(selectedAccount) - // replace agent to re-derive currentAccount and trigger rerender with fresh data - setCurrentAgent(currentAgent.clone()) + const newAgent = currentAgent.clone() + newAgent.session = sessionAccountToAgentSession(selectedAccount) + configureModeration(newAgent, selectedAccount) + setCurrentAgent(newAgent) } - } else if (!selectedAccount && currentAccount) { + } else if (!selectedAccount && currentAccountDid) { logger.debug( `session: persisted onUpdate, logging out`, {}, @@ -548,7 +553,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }) }, [ - currentAccount, + currentAccountDid, setAccounts, clearCurrentAccount, initSession, @@ -614,25 +619,32 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) } -async function configureModeration(agent: BskyAgent, account: SessionAccount) { - if (IS_TEST_USER(account.handle)) { - const did = ( - await agent - .resolveHandle({handle: 'mod-authority.test'}) - .catch(_ => undefined) - )?.data.did - if (did) { - console.warn('USING TEST ENV MODERATION') - BskyAgent.configure({appLabelers: [did]}) +async function configureModeration(agent: BskyAgent, account?: SessionAccount) { + if (account) { + if (IS_TEST_USER(account.handle)) { + const did = ( + await agent + .resolveHandle({handle: 'mod-authority.test'}) + .catch(_ => undefined) + )?.data.did + if (did) { + console.warn('USING TEST ENV MODERATION') + BskyAgent.configure({appLabelers: [did]}) + } + } else { + BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) + + if (account) { + const labelerDids = await readLabelers(account.did).catch(_ => {}) + if (labelerDids) { + agent.configureLabelersHeader( + labelerDids.filter(did => did !== BSKY_LABELER_DID), + ) + } + } } } else { BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) - const labelerDids = await readLabelers(account.did).catch(_ => {}) - if (labelerDids) { - agent.configureLabelersHeader( - labelerDids.filter(did => did !== BSKY_LABELER_DID), - ) - } } }