diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 472b59d7..89c441e9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -43,7 +43,10 @@ export function IS_PROD(url: string) { // until open federation, "production" is defined as the main server // this definition will not work once federation is enabled! // -prf - return url.startsWith('https://bsky.social') + return ( + url.startsWith('https://bsky.social') || + url.startsWith('https://api.bsky.app') + ) } export const PROD_TEAM_HANDLES = [ diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index d11e9a14..4085a52c 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -63,7 +63,6 @@ export class RootStoreModel { serialize(): unknown { return { appInfo: this.appInfo, - session: this.session.serialize(), me: this.me.serialize(), preferences: this.preferences.serialize(), } @@ -80,9 +79,6 @@ export class RootStoreModel { if (hasProp(v, 'me')) { this.me.hydrate(v.me) } - if (hasProp(v, 'session')) { - this.session.hydrate(v.session) - } if (hasProp(v, 'preferences')) { this.preferences.hydrate(v.preferences) } @@ -92,18 +88,7 @@ export class RootStoreModel { /** * Called during init to resume any stored session. */ - async attemptSessionResumption() { - logger.debug('RootStoreModel:attemptSessionResumption') - try { - await this.session.attemptSessionResumption() - logger.debug('Session initialized', { - hasSession: this.session.hasSession, - }) - this.updateSessionState() - } catch (e: any) { - logger.warn('Failed to initialize session', {error: e}) - } - } + async attemptSessionResumption() {} /** * Called by the session model. Refreshes session-oriented state. @@ -135,11 +120,10 @@ export class RootStoreModel { } /** - * Clears all session-oriented state. + * Clears all session-oriented state, previously called on LOGOUT */ clearAllSessionState() { logger.debug('RootStoreModel:clearAllSessionState') - this.session.clear() resetToTab('HomeTab') this.me.clear() } diff --git a/src/state/models/session.ts b/src/state/models/session.ts index 5b95c7d3..2c66cfdf 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -1,274 +1,26 @@ -import {makeAutoObservable, runInAction} from 'mobx' +import {makeAutoObservable} from 'mobx' import { BskyAgent, - AtpSessionEvent, - AtpSessionData, ComAtprotoServerDescribeServer as DescribeServer, } from '@atproto/api' -import normalizeUrl from 'normalize-url' -import {isObj, hasProp} from 'lib/type-guards' -import {networkRetry} from 'lib/async/retry' -import {z} from 'zod' import {RootStoreModel} from './root-store' -import {IS_PROD} from 'lib/constants' -import {track} from 'lib/analytics/analytics' -import {logger} from '#/logger' export type ServiceDescription = DescribeServer.OutputSchema -export const activeSession = z.object({ - service: z.string(), - did: z.string(), -}) -export type ActiveSession = z.infer - -export const accountData = z.object({ - service: z.string(), - refreshJwt: z.string().optional(), - accessJwt: z.string().optional(), - handle: z.string(), - did: z.string(), - email: z.string().optional(), - displayName: z.string().optional(), - aviUrl: z.string().optional(), - emailConfirmed: z.boolean().optional(), -}) -export type AccountData = z.infer - -interface AdditionalAccountData { - displayName?: string - aviUrl?: string -} - export class SessionModel { - // DEBUG - // emergency log facility to help us track down this logout issue - // remove when resolved - // -prf - _log(message: string, details?: Record) { - details = details || {} - details.state = { - data: this.data, - accounts: this.accounts.map( - a => - `${!!a.accessJwt && !!a.refreshJwt ? '✅' : '❌'} ${a.handle} (${ - a.service - })`, - ), - isResumingSession: this.isResumingSession, - } - logger.debug(message, details, logger.DebugContext.session) - } - - /** - * Currently-active session - */ - data: ActiveSession | null = null - /** - * A listing of the currently & previous sessions - */ - accounts: AccountData[] = [] - /** - * Flag to indicate if we're doing our initial-load session resumption - */ - isResumingSession = false - constructor(public rootStore: RootStoreModel) { makeAutoObservable(this, { rootStore: false, - serialize: false, - hydrate: false, hasSession: false, }) } - get currentSession() { - if (!this.data) { - return undefined - } - const {did, service} = this.data - return this.accounts.find( - account => - normalizeUrl(account.service) === normalizeUrl(service) && - account.did === did && - !!account.accessJwt && - !!account.refreshJwt, - ) + get currentSession(): any { + return undefined } get hasSession() { - return !!this.currentSession && !!this.rootStore.agent.session - } - - get hasAccounts() { - return this.accounts.length >= 1 - } - - get switchableAccounts() { - return this.accounts.filter(acct => acct.did !== this.data?.did) - } - - get emailNeedsConfirmation() { - return !this.currentSession?.emailConfirmed - } - - get isSandbox() { - if (!this.data) { - return false - } - return !IS_PROD(this.data.service) - } - - serialize(): unknown { - return { - data: this.data, - accounts: this.accounts, - } - } - - hydrate(v: unknown) { - this.accounts = [] - if (isObj(v)) { - if (hasProp(v, 'data') && activeSession.safeParse(v.data)) { - this.data = v.data as ActiveSession - } - if (hasProp(v, 'accounts') && Array.isArray(v.accounts)) { - for (const account of v.accounts) { - if (accountData.safeParse(account)) { - this.accounts.push(account as AccountData) - } - } - } - } - } - - clear() { - this.data = null - } - - /** - * Attempts to resume the previous session loaded from storage - */ - async attemptSessionResumption() { - const sess = this.currentSession - if (sess) { - this._log('SessionModel:attemptSessionResumption found stored session') - this.isResumingSession = true - try { - return await this.resumeSession(sess) - } finally { - runInAction(() => { - this.isResumingSession = false - }) - } - } else { - this._log( - 'SessionModel:attemptSessionResumption has no session to resume', - ) - } - } - - /** - * Sets the active session - */ - async setActiveSession(agent: BskyAgent, did: string) { - this._log('SessionModel:setActiveSession') - const hadSession = !!this.data - this.data = { - service: agent.service.toString(), - did, - } - await this.rootStore.handleSessionChange(agent, {hadSession}) - } - - /** - * Upserts a session into the accounts - */ - persistSession( - service: string, - did: string, - event: AtpSessionEvent, - session?: AtpSessionData, - addedInfo?: AdditionalAccountData, - ) { - this._log('SessionModel:persistSession', { - service, - did, - event, - hasSession: !!session, - }) - - const existingAccount = this.accounts.find( - account => account.service === service && account.did === did, - ) - - // fall back to any preexisting access tokens - let refreshJwt = session?.refreshJwt || existingAccount?.refreshJwt - let accessJwt = session?.accessJwt || existingAccount?.accessJwt - if (event === 'expired') { - // only clear the tokens when they're known to have expired - refreshJwt = undefined - accessJwt = undefined - } - - const newAccount = { - service, - did, - refreshJwt, - accessJwt, - - handle: session?.handle || existingAccount?.handle || '', - email: session?.email || existingAccount?.email || '', - displayName: addedInfo - ? addedInfo.displayName - : existingAccount?.displayName || '', - aviUrl: addedInfo ? addedInfo.aviUrl : existingAccount?.aviUrl || '', - emailConfirmed: session?.emailConfirmed, - } - if (!existingAccount) { - this.accounts.push(newAccount) - } else { - this.accounts = [ - newAccount, - ...this.accounts.filter( - account => !(account.service === service && account.did === did), - ), - ] - } - - // if the session expired, fire an event to let the user know - if (event === 'expired') { - this.rootStore.handleSessionDrop() - } - } - - /** - * Clears any session tokens from the accounts; used on logout. - */ - clearSessionTokens() { - this._log('SessionModel:clearSessionTokens') - this.accounts = this.accounts.map(acct => ({ - service: acct.service, - handle: acct.handle, - did: acct.did, - displayName: acct.displayName, - aviUrl: acct.aviUrl, - email: acct.email, - emailConfirmed: acct.emailConfirmed, - })) - } - - /** - * Fetches additional information about an account on load. - */ - async loadAccountInfo(agent: BskyAgent, did: string) { - const res = await agent.getProfile({actor: did}).catch(_e => undefined) - if (res) { - return { - displayName: res.data.displayName, - aviUrl: res.data.avatar, - } - } + return false } /** @@ -280,193 +32,8 @@ export class SessionModel { return res.data } - /** - * Attempt to resume a session that we still have access tokens for. - */ - async resumeSession(account: AccountData): Promise { - this._log('SessionModel:resumeSession') - if (!(account.accessJwt && account.refreshJwt && account.service)) { - this._log( - 'SessionModel:resumeSession aborted due to lack of access tokens', - ) - return false - } - - const agent = new BskyAgent({ - service: account.service, - persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => { - this.persistSession(account.service, account.did, evt, sess) - }, - }) - - try { - await networkRetry(3, () => - agent.resumeSession({ - accessJwt: account.accessJwt || '', - refreshJwt: account.refreshJwt || '', - did: account.did, - handle: account.handle, - email: account.email, - emailConfirmed: account.emailConfirmed, - }), - ) - const addedInfo = await this.loadAccountInfo(agent, account.did) - this.persistSession( - account.service, - account.did, - 'create', - agent.session, - addedInfo, - ) - this._log('SessionModel:resumeSession succeeded') - } catch (e: any) { - this._log('SessionModel:resumeSession failed', { - error: e.toString(), - }) - return false - } - - await this.setActiveSession(agent, account.did) - return true - } - - /** - * Create a new session. - */ - async login({ - service, - identifier, - password, - }: { - service: string - identifier: string - password: string - }) { - this._log('SessionModel:login') - const agent = new BskyAgent({service}) - await agent.login({identifier, password}) - if (!agent.session) { - throw new Error('Failed to establish session') - } - - const did = agent.session.did - const addedInfo = await this.loadAccountInfo(agent, did) - - this.persistSession(service, did, 'create', agent.session, addedInfo) - agent.setPersistSessionHandler( - (evt: AtpSessionEvent, sess?: AtpSessionData) => { - this.persistSession(service, did, evt, sess) - }, - ) - - await this.setActiveSession(agent, did) - this._log('SessionModel:login succeeded') - } - - async createAccount({ - service, - email, - password, - handle, - inviteCode, - }: { - service: string - email: string - password: string - handle: string - inviteCode?: string - }) { - this._log('SessionModel:createAccount') - const agent = new BskyAgent({service}) - await agent.createAccount({ - handle, - password, - email, - inviteCode, - }) - if (!agent.session) { - throw new Error('Failed to establish session') - } - - const did = agent.session.did - const addedInfo = await this.loadAccountInfo(agent, did) - - this.persistSession(service, did, 'create', agent.session, addedInfo) - agent.setPersistSessionHandler( - (evt: AtpSessionEvent, sess?: AtpSessionData) => { - this.persistSession(service, did, evt, sess) - }, - ) - - await this.setActiveSession(agent, did) - this._log('SessionModel:createAccount succeeded') - track('Create Account Successfully') - } - - /** - * Close all sessions across all accounts. - */ - async logout() { - this._log('SessionModel:logout') - // TODO - // need to evaluate why deleting the session has caused errors at times - // -prf - /*if (this.hasSession) { - this.rootStore.agent.com.atproto.session.delete().catch((e: any) => { - this.rootStore.log.warn( - '(Minor issue) Failed to delete session on the server', - e, - ) - }) - }*/ - this.clearSessionTokens() - this.rootStore.clearAllSessionState() - } - - /** - * Removes an account from the list of stored accounts. - */ - removeAccount(handle: string) { - this.accounts = this.accounts.filter(acc => acc.handle !== handle) - } - /** * Reloads the session from the server. Useful when account details change, like the handle. */ - async reloadFromServer() { - const sess = this.currentSession - if (!sess) { - return - } - const res = await this.rootStore.agent - .getProfile({actor: sess.did}) - .catch(_e => undefined) - if (res?.success) { - const updated = { - ...sess, - handle: res.data.handle, - displayName: res.data.displayName, - aviUrl: res.data.avatar, - } - runInAction(() => { - this.accounts = [ - updated, - ...this.accounts.filter( - account => - !( - account.service === updated.service && - account.did === updated.did - ), - ), - ] - }) - await this.rootStore.me.load() - } - } - - updateLocalAccountData(changes: Partial) { - this.accounts = this.accounts.map(acct => - acct.did === this.data?.did ? {...acct, ...changes} : acct, - ) - } + async reloadFromServer() {} } diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 90948b01..d0ca1013 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -1,20 +1,25 @@ import React from 'react' +import {DeviceEventEmitter} from 'react-native' import {BskyAgent, AtpPersistSessionHandler} from '@atproto/api' import {networkRetry} from '#/lib/async/retry' import {logger} from '#/logger' import * as persisted from '#/state/persisted' import {PUBLIC_BSKY_AGENT} from '#/state/queries' +import {IS_PROD} from '#/lib/constants' export type SessionAccount = persisted.PersistedAccount -export type StateContext = { +export type SessionState = { agent: BskyAgent isInitialLoad: boolean isSwitchingAccounts: boolean accounts: persisted.PersistedAccount[] currentAccount: persisted.PersistedAccount | undefined +} +export type StateContext = SessionState & { hasSession: boolean + isSandbox: boolean } export type ApiContext = { createAccount: (props: { @@ -30,15 +35,13 @@ export type ApiContext = { password: string }) => Promise logout: () => Promise - initSession: (account: persisted.PersistedAccount) => Promise - resumeSession: (account?: persisted.PersistedAccount) => Promise - removeAccount: ( - account: Partial>, - ) => void - selectAccount: (account: persisted.PersistedAccount) => Promise + initSession: (account: SessionAccount) => Promise + resumeSession: (account?: SessionAccount) => Promise + removeAccount: (account: SessionAccount) => void + selectAccount: (account: SessionAccount) => Promise updateCurrentAccount: ( account: Partial< - Pick + Pick >, ) => void clearCurrentAccount: () => void @@ -46,11 +49,12 @@ export type ApiContext = { const StateContext = React.createContext({ agent: PUBLIC_BSKY_AGENT, - hasSession: false, isInitialLoad: true, isSwitchingAccounts: false, accounts: [], currentAccount: undefined, + hasSession: false, + isSandbox: false, }) const ApiContext = React.createContext({ @@ -94,6 +98,8 @@ function createPersistSessionHandler( logger.DebugContext.session, ) + if (expired) DeviceEventEmitter.emit('session-dropped') + persistSessionCallback({ expired, refreshedAccount, @@ -103,9 +109,8 @@ function createPersistSessionHandler( export function Provider({children}: React.PropsWithChildren<{}>) { const isDirty = React.useRef(false) - const [state, setState] = React.useState({ + const [state, setState] = React.useState({ agent: PUBLIC_BSKY_AGENT, - hasSession: false, isInitialLoad: true, // try to resume the session first isSwitchingAccounts: false, accounts: persisted.get('session').accounts, @@ -113,7 +118,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) const setStateAndPersist = React.useCallback( - (fn: (prev: StateContext) => StateContext) => { + (fn: (prev: SessionState) => SessionState) => { isDirty.current = true setState(fn) }, @@ -312,9 +317,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { setStateAndPersist(s => { return { ...s, - accounts: s.accounts.filter( - a => !(a.did === account.did || a.handle === account.handle), - ), + accounts: s.accounts.filter(a => a.did !== account.did), } }) }, @@ -431,6 +434,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { () => ({ ...state, hasSession: !!state.currentAccount, + isSandbox: state.currentAccount + ? !IS_PROD(state.currentAccount?.service) + : false, }), [state], ) diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx index c4a3b158..8c94ef2d 100644 --- a/src/view/com/auth/login/ChooseAccountForm.tsx +++ b/src/view/com/auth/login/ChooseAccountForm.tsx @@ -5,7 +5,6 @@ import {useAnalytics} from 'lib/analytics/analytics' import {Text} from '../../util/text/Text' import {UserAvatar} from '../../util/UserAvatar' import {s} from 'lib/styles' -import {AccountData} from 'state/models/session' import {usePalette} from 'lib/hooks/usePalette' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -62,7 +61,7 @@ export const ChooseAccountForm = ({ onSelectAccount, onPressBack, }: { - onSelectAccount: (account?: AccountData) => void + onSelectAccount: (account?: SessionAccount) => void onPressBack: () => void }) => { const {track, screen} = useAnalytics() diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx index de00d6ca..27d08812 100644 --- a/src/view/com/auth/login/Login.tsx +++ b/src/view/com/auth/login/Login.tsx @@ -4,7 +4,6 @@ import {useAnalytics} from 'lib/analytics/analytics' import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' import {useStores, DEFAULT_SERVICE} from 'state/index' import {ServiceDescription} from 'state/models/session' -import {AccountData} from 'state/models/session' import {usePalette} from 'lib/hooks/usePalette' import {logger} from '#/logger' import {ChooseAccountForm} from './ChooseAccountForm' @@ -14,7 +13,7 @@ import {SetNewPasswordForm} from './SetNewPasswordForm' import {PasswordUpdatedForm} from './PasswordUpdatedForm' import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' -import {useSession} from '#/state/session' +import {useSession, SessionAccount} from '#/state/session' enum Forms { Login, @@ -41,7 +40,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { accounts.length ? Forms.ChooseAccount : Forms.Login, ) - const onSelectAccount = (account?: AccountData) => { + const onSelectAccount = (account?: SessionAccount) => { if (account?.service) { setServiceUrl(account.service) } diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index ffae6cbf..6a846f67 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -23,6 +23,7 @@ import useAppState from 'react-native-appstate-hook' import {logger} from '#/logger' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useSession} from '#/state/session' export const FeedPage = observer(function FeedPageImpl({ testID, @@ -38,6 +39,7 @@ export const FeedPage = observer(function FeedPageImpl({ renderEndOfFeed?: () => JSX.Element }) { const store = useStores() + const {isSandbox} = useSession() const pal = usePalette('default') const {_} = useLingui() const {isDesktop} = useWebMediaQueries() @@ -140,7 +142,7 @@ export const FeedPage = observer(function FeedPageImpl({ style={[pal.text, {fontWeight: 'bold'}]} text={ <> - {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} + {isSandbox ? 'SANDBOX' : 'Bluesky'}{' '} {hasNew && ( - }, [isDesktop, pal.view, pal.text, pal.textLight, store, hasNew, _]) + }, [ + isDesktop, + pal.view, + pal.text, + pal.textLight, + store, + hasNew, + _, + isSandbox, + ]) return ( diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx index 05d0da37..3481b861 100644 --- a/src/view/com/modals/SwitchAccount.tsx +++ b/src/view/com/modals/SwitchAccount.tsx @@ -64,7 +64,7 @@ function SwitchAccountCard({account}: {account: SessionAccount}) { ) : ( - + )} ) diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 8c29ad6a..d79bfe94 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -19,12 +19,14 @@ import {useLingui} from '@lingui/react' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useShellLayout} from '#/state/shell/shell-layout' +import {useSession} from '#/state/session' export const FeedsTabBar = observer(function FeedsTabBarImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { const pal = usePalette('default') const store = useStores() + const {isSandbox} = useSession() const {_} = useLingui() const setDrawerOpen = useSetDrawerOpen() const items = useHomeTabs(store.preferences.pinnedFeeds) @@ -59,7 +61,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( - {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'} + {isSandbox ? 'SANDBOX' : 'Bluesky'} { - await store.session.login({ + await login({ service: 'http://localhost:3000', identifier: 'alice.test', password: 'hunter2', }) } const onPressSignInBob = async () => { - await store.session.login({ + await login({ service: 'http://localhost:3000', identifier: 'bob.test', password: 'hunter2', @@ -45,7 +47,7 @@ export function TestCtrls() { /> store.session.logout()} + onPress={() => logout()} accessibilityRole="button" style={BTN} /> diff --git a/src/view/com/util/AccountDropdownBtn.tsx b/src/view/com/util/AccountDropdownBtn.tsx index 158ed9b6..96ce678f 100644 --- a/src/view/com/util/AccountDropdownBtn.tsx +++ b/src/view/com/util/AccountDropdownBtn.tsx @@ -8,11 +8,11 @@ import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {DropdownItem, NativeDropdown} from './forms/NativeDropdown' import * as Toast from '../../com/util/Toast' -import {useSessionApi} from '#/state/session' +import {useSessionApi, SessionAccount} from '#/state/session' import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' -export function AccountDropdownBtn({handle}: {handle: string}) { +export function AccountDropdownBtn({account}: {account: SessionAccount}) { const pal = usePalette('default') const {removeAccount} = useSessionApi() const {_} = useLingui() @@ -21,7 +21,7 @@ export function AccountDropdownBtn({handle}: {handle: string}) { { label: 'Remove account', onPress: () => { - removeAccount({handle}) + removeAccount(account) Toast.show('Account removed from quick access') }, icon: { diff --git a/src/view/com/util/PostSandboxWarning.tsx b/src/view/com/util/PostSandboxWarning.tsx index 21f5f7b9..b2375c70 100644 --- a/src/view/com/util/PostSandboxWarning.tsx +++ b/src/view/com/util/PostSandboxWarning.tsx @@ -1,13 +1,13 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {Text} from './text/Text' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' +import {useSession} from '#/state/session' export function PostSandboxWarning() { - const store = useStores() + const {isSandbox} = useSession() const pal = usePalette('default') - if (store.session.isSandbox) { + if (isSandbox) { return ( ) : ( - + )} ) diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 99e1d7d9..609348e4 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -47,6 +47,57 @@ import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useSetDrawerOpen} from '#/state/shell' import {useModalControls} from '#/state/modals' +import {useSession, SessionAccount} from '#/state/session' +import {useProfileQuery} from '#/state/queries/profile' + +export function DrawerProfileCard({ + account, + onPressProfile, +}: { + account: SessionAccount + onPressProfile: () => void +}) { + const {_} = useLingui() + const pal = usePalette('default') + const {data: profile} = useProfileQuery({did: account.did}) + + return ( + + + + {profile?.displayName || account.handle} + + + @{account.handle} + + + + {formatCountShortOnly(profile?.followersCount ?? 0)} + {' '} + {pluralize(profile?.followersCount || 0, 'follower')} ·{' '} + + {formatCountShortOnly(profile?.followsCount ?? 0)} + {' '} + following + + + ) +} export const DrawerContent = observer(function DrawerContentImpl() { const theme = useTheme() @@ -58,6 +109,7 @@ export const DrawerContent = observer(function DrawerContentImpl() { const {track} = useAnalytics() const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = useNavigationTabState() + const {currentAccount} = useSession() const {notifications} = store.me @@ -135,11 +187,11 @@ export const DrawerContent = observer(function DrawerContentImpl() { track('Menu:FeedbackClicked') Linking.openURL( FEEDBACK_FORM_URL({ - email: store.session.currentSession?.email, - handle: store.session.currentSession?.handle, + email: currentAccount?.email, + handle: currentAccount?.handle, }), ) - }, [track, store.session.currentSession]) + }, [track, currentAccount]) const onPressHelp = React.useCallback(() => { track('Menu:HelpClicked') @@ -159,42 +211,12 @@ export const DrawerContent = observer(function DrawerContentImpl() { - - - - {store.me.displayName || store.me.handle} - - - @{store.me.handle} - - - - {formatCountShortOnly(store.me.followersCount ?? 0)} - {' '} - {pluralize(store.me.followersCount || 0, 'follower')} ·{' '} - - {formatCountShortOnly(store.me.followsCount ?? 0)} - {' '} - following - - + )} diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index cb62b4be..98f54c7e 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -17,10 +17,9 @@ import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' export const DesktopRightNav = observer(function DesktopRightNavImpl() { - const store = useStores() const pal = usePalette('default') const palError = usePalette('error') - const {hasSession, currentAccount} = useSession() + const {isSandbox, hasSession, currentAccount} = useSession() const {isTablet} = useWebMediaQueries() if (isTablet) { @@ -32,7 +31,7 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { {hasSession && } {hasSession && } - {store.session.isSandbox ? ( + {isSandbox ? ( SANDBOX. Posts and accounts are not permanent.