Compare commits

...

15 Commits

Author SHA1 Message Date
Eric Bailey 07d88e21fb Port over new pdsUrl handling 2024-04-25 14:52:56 -05:00
Eric Bailey 13124e5676 Export for parity 2024-04-25 14:35:54 -05:00
Eric Bailey fe0d150706 Improve toasts, log caught error, during switch account 2024-04-25 14:33:54 -05:00
Eric Bailey 2c85c04591 Handle thrown errors from initSession during login 2024-04-25 14:33:54 -05:00
Eric Bailey 0688c65df8 Add session v2 2024-04-25 14:33:54 -05:00
Eric Bailey 5137b70610 Replace missing export 2024-04-25 14:33:30 -05:00
Eric Bailey 0916ec732f Add a-a test 2024-04-25 14:09:11 -05:00
Eric Bailey a7e3433ecc Memoize getAgent method 2024-04-25 14:07:57 -05:00
Eric Bailey 7cdd3de3f6 Hook it up 2024-04-25 13:18:03 -05:00
Eric Bailey 679f613831 Drill into notifications handlers
(cherry picked from commit 7ac9e500866732e1f2e205bbe96e70db331e5ffb)
2024-04-25 11:45:31 -05:00
Eric Bailey 717304eb14 Drill agent into Onboarding/util
(cherry picked from commit 2ba68eb5e446a694730b720f2a5b3307eb0914ef)
2024-04-25 11:44:23 -05:00
Eric Bailey e7297a2c84 Drill into notifications/util
(cherry picked from commit 84b535ed54f4fe93debcd198809bb184519c3507)
2024-04-25 11:42:39 -05:00
Eric Bailey 50da4dc728 Drill into whenAppViewReady
(cherry picked from commit e290e5be3df509bdd9d0e626a164996c9dee3636)
2024-04-25 11:41:29 -05:00
Eric Bailey 1061710255 Drill agent into feed apis 2024-04-25 10:58:08 -05:00
Eric Bailey 38aa11adea Update to desired post-feed usage 2024-04-25 10:43:05 -05:00
69 changed files with 2167 additions and 998 deletions
+3 -1
View File
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {ReportOption} from '#/lib/moderation/useReportOptions'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, native, useTheme} from '#/alf'
@@ -35,6 +35,7 @@ export function SubmitView({
}) {
const t = useTheme()
const {_} = useLingui()
const {getAgent} = useAgent()
const [details, setDetails] = React.useState<string>('')
const [submitting, setSubmitting] = React.useState<boolean>(false)
const [selectedServices, setSelectedServices] = React.useState<string[]>([
@@ -90,6 +91,7 @@ export function SubmitView({
selectedServices,
onSubmitComplete,
setError,
getAgent,
])
return (
+3 -2
View File
@@ -1,12 +1,13 @@
import React from 'react'
import {RichText as RichTextAPI} from '@atproto/api'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
export function useRichText(text: string): [RichTextAPI, boolean] {
const [prevText, setPrevText] = React.useState(text)
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
const {getAgent} = useAgent()
if (text !== prevText) {
setPrevText(text)
setRawRT(new RichTextAPI({text}))
@@ -27,7 +28,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
return () => {
ignore = true
}
}, [text])
}, [text, getAgent])
const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving]
}
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -173,6 +173,7 @@ function AppealForm({
const {gtMobile} = useBreakpoints()
const [details, setDetails] = React.useState('')
const isAccountReport = 'did' in subject
const {getAgent} = useAgent()
const onSubmit = async () => {
try {
+17 -4
View File
@@ -1,15 +1,28 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class AuthorFeedAPI implements FeedAPI {
constructor(public params: GetAuthorFeed.QueryParams) {}
agent: BskyAgent
params: GetAuthorFeed.QueryParams
constructor({
agent,
feedParams,
}: {
agent: BskyAgent
feedParams: GetAuthorFeed.QueryParams
}) {
this.agent = agent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().getAuthorFeed({
const res = await this.agent.getAuthorFeed({
...this.params,
limit: 1,
})
@@ -23,7 +36,7 @@ export class AuthorFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getAuthorFeed({
const res = await this.agent.getAuthorFeed({
...this.params,
cursor,
limit,
+23 -7
View File
@@ -2,18 +2,30 @@ import {
AppBskyFeedDefs,
AppBskyFeedGetFeed as GetCustomFeed,
AtpAgent,
BskyAgent,
} from '@atproto/api'
import {getContentLanguages} from '#/state/preferences/languages'
import {getAgent} from '#/state/session'
import {FeedAPI, FeedAPIResponse} from './types'
export class CustomFeedAPI implements FeedAPI {
constructor(public params: GetCustomFeed.QueryParams) {}
agent: BskyAgent
params: GetCustomFeed.QueryParams
constructor({
agent,
feedParams,
}: {
agent: BskyAgent
feedParams: GetCustomFeed.QueryParams
}) {
this.agent = agent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const contentLangs = getContentLanguages().join(',')
const res = await getAgent().app.bsky.feed.getFeed(
const res = await this.agent.app.bsky.feed.getFeed(
{
...this.params,
limit: 1,
@@ -31,15 +43,19 @@ export class CustomFeedAPI implements FeedAPI {
limit: number
}): Promise<FeedAPIResponse> {
const contentLangs = getContentLanguages().join(',')
const agent = getAgent()
const res = agent.session
? await getAgent().app.bsky.feed.getFeed(
const res = this.agent.session
? await this.agent.app.bsky.feed.getFeed(
{
...this.params,
cursor,
limit,
},
{headers: {'Accept-Language': contentLangs}},
{
headers: {
'Accept-Language': contentLangs,
},
},
)
: await loggedOutFetch({...this.params, cursor, limit})
if (res.success) {
+9 -5
View File
@@ -1,12 +1,16 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class FollowingFeedAPI implements FeedAPI {
constructor() {}
agent: BskyAgent
constructor({agent}: {agent: BskyAgent}) {
this.agent = agent
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().getTimeline({
const res = await this.agent.getTimeline({
limit: 1,
})
return res.data.feed[0]
@@ -19,7 +23,7 @@ export class FollowingFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getTimeline({
const res = await this.agent.getTimeline({
cursor,
limit,
})
+18 -9
View File
@@ -1,8 +1,9 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {FollowingFeedAPI} from './following'
import {CustomFeedAPI} from './custom'
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {CustomFeedAPI} from './custom'
import {FollowingFeedAPI} from './following'
import {FeedAPI, FeedAPIResponse} from './types'
// HACK
// the feed API does not include any facilities for passing down
@@ -26,19 +27,27 @@ export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = {
}
export class HomeFeedAPI implements FeedAPI {
agent: BskyAgent
following: FollowingFeedAPI
discover: CustomFeedAPI
usingDiscover = false
itemCursor = 0
constructor() {
this.following = new FollowingFeedAPI()
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
constructor({agent}: {agent: BskyAgent}) {
this.agent = agent
this.following = new FollowingFeedAPI({agent})
this.discover = new CustomFeedAPI({
agent,
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
})
}
reset() {
this.following = new FollowingFeedAPI()
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
this.following = new FollowingFeedAPI({agent: this.agent})
this.discover = new CustomFeedAPI({
agent: this.agent,
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
})
this.usingDiscover = false
this.itemCursor = 0
}
+17 -4
View File
@@ -1,15 +1,28 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetActorLikes as GetActorLikes,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class LikesFeedAPI implements FeedAPI {
constructor(public params: GetActorLikes.QueryParams) {}
agent: BskyAgent
params: GetActorLikes.QueryParams
constructor({
agent,
feedParams,
}: {
agent: BskyAgent
feedParams: GetActorLikes.QueryParams
}) {
this.agent = agent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().getActorLikes({
const res = await this.agent.getActorLikes({
...this.params,
limit: 1,
})
@@ -23,7 +36,7 @@ export class LikesFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getActorLikes({
const res = await this.agent.getActorLikes({
...this.params,
cursor,
limit,
+17 -4
View File
@@ -1,15 +1,28 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetListFeed as GetListFeed,
BskyAgent,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class ListFeedAPI implements FeedAPI {
constructor(public params: GetListFeed.QueryParams) {}
agent: BskyAgent
params: GetListFeed.QueryParams
constructor({
agent,
feedParams,
}: {
agent: BskyAgent
feedParams: GetListFeed.QueryParams
}) {
this.agent = agent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().app.bsky.feed.getListFeed({
const res = await this.agent.app.bsky.feed.getListFeed({
...this.params,
limit: 1,
})
@@ -23,7 +36,7 @@ export class ListFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().app.bsky.feed.getListFeed({
const res = await this.agent.app.bsky.feed.getListFeed({
...this.params,
cursor,
limit,
+72 -17
View File
@@ -1,31 +1,51 @@
import {AppBskyFeedDefs, AppBskyFeedGetTimeline} from '@atproto/api'
import {AppBskyFeedDefs, AppBskyFeedGetTimeline, BskyAgent} from '@atproto/api'
import shuffle from 'lodash.shuffle'
import {timeout} from 'lib/async/timeout'
import {getContentLanguages} from '#/state/preferences/languages'
import {FeedParams} from '#/state/queries/post-feed'
import {bundleAsync} from 'lib/async/bundle'
import {timeout} from 'lib/async/timeout'
import {feedUriToHref} from 'lib/strings/url-helpers'
import {FeedTuner} from '../feed-manip'
import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
import {FeedParams} from '#/state/queries/post-feed'
import {FeedTunerFn} from '../feed-manip'
import {getAgent} from '#/state/session'
import {getContentLanguages} from '#/state/preferences/languages'
import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
const REQUEST_WAIT_MS = 500 // 500ms
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
export class MergeFeedAPI implements FeedAPI {
agent: BskyAgent
params: FeedParams
feedTuners: FeedTunerFn[]
following: MergeFeedSource_Following
customFeeds: MergeFeedSource_Custom[] = []
feedCursor = 0
itemCursor = 0
sampleCursor = 0
constructor(public params: FeedParams, public feedTuners: FeedTunerFn[]) {
this.following = new MergeFeedSource_Following(this.feedTuners)
constructor({
agent,
feedParams,
feedTuners,
}: {
agent: BskyAgent
feedParams: FeedParams
feedTuners: FeedTunerFn[]
}) {
this.agent = agent
this.params = feedParams
this.feedTuners = feedTuners
this.following = new MergeFeedSource_Following({
agent: this.agent,
feedTuners: this.feedTuners,
})
}
reset() {
this.following = new MergeFeedSource_Following(this.feedTuners)
this.following = new MergeFeedSource_Following({
agent: this.agent,
feedTuners: this.feedTuners,
})
this.customFeeds = []
this.feedCursor = 0
this.itemCursor = 0
@@ -33,7 +53,12 @@ export class MergeFeedAPI implements FeedAPI {
if (this.params.mergeFeedSources) {
this.customFeeds = shuffle(
this.params.mergeFeedSources.map(
feedUri => new MergeFeedSource_Custom(feedUri, this.feedTuners),
feedUri =>
new MergeFeedSource_Custom({
agent: this.agent,
feedUri,
feedTuners: this.feedTuners,
}),
),
)
} else {
@@ -42,7 +67,7 @@ export class MergeFeedAPI implements FeedAPI {
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().getTimeline({
const res = await this.agent.getTimeline({
limit: 1,
})
return res.data.feed[0]
@@ -136,12 +161,23 @@ export class MergeFeedAPI implements FeedAPI {
}
class MergeFeedSource {
agent: BskyAgent
feedTuners: FeedTunerFn[]
sourceInfo: ReasonFeedSource | undefined
cursor: string | undefined = undefined
queue: AppBskyFeedDefs.FeedViewPost[] = []
hasMore = true
constructor(public feedTuners: FeedTunerFn[]) {}
constructor({
agent,
feedTuners,
}: {
agent: BskyAgent
feedTuners: FeedTunerFn[]
}) {
this.agent = agent
this.feedTuners = feedTuners
}
get numReady() {
return this.queue.length
@@ -203,7 +239,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
cursor: string | undefined,
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
const res = await getAgent().getTimeline({cursor, limit})
const res = await this.agent.getTimeline({cursor, limit})
// run the tuner pre-emptively to ensure better mixing
const slices = this.tuner.tune(res.data.feed, {
dryRun: false,
@@ -215,10 +251,25 @@ class MergeFeedSource_Following extends MergeFeedSource {
}
class MergeFeedSource_Custom extends MergeFeedSource {
agent: BskyAgent
minDate: Date
feedUri: string
constructor(public feedUri: string, public feedTuners: FeedTunerFn[]) {
super(feedTuners)
constructor({
agent,
feedUri,
feedTuners,
}: {
agent: BskyAgent
feedUri: string
feedTuners: FeedTunerFn[]
}) {
super({
agent,
feedTuners,
})
this.agent = agent
this.feedUri = feedUri
this.sourceInfo = {
$type: 'reasonFeedSource',
uri: feedUri,
@@ -233,13 +284,17 @@ class MergeFeedSource_Custom extends MergeFeedSource {
): Promise<AppBskyFeedGetTimeline.Response> {
try {
const contentLangs = getContentLanguages().join(',')
const res = await getAgent().app.bsky.feed.getFeed(
const res = await this.agent.app.bsky.feed.getFeed(
{
cursor,
limit,
feed: this.feedUri,
},
{headers: {'Accept-Language': contentLangs}},
{
headers: {
'Accept-Language': contentLangs,
},
},
)
// NOTE
// some custom feeds fail to enforce the pagination limit
+14 -5
View File
@@ -1,6 +1,9 @@
import {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {SessionAccount, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
@@ -8,6 +11,7 @@ import * as Toast from '#/view/com/util/Toast'
import {LogEvents} from '../statsig/statsig'
export function useAccountSwitcher() {
const {_} = useLingui()
const {track} = useAnalytics()
const {selectAccount, clearCurrentAccount} = useSessionApi()
const {requestSwitchToAccount} = useLoggedOutViewControls()
@@ -31,21 +35,26 @@ export function useAccountSwitcher() {
}
await selectAccount(account, logContext)
setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`)
Toast.show(_(msg`Signed in as @${account.handle}`))
}, 100)
} else {
requestSwitchToAccount({requestedAccount: account.did})
Toast.show(
`Please sign in as @${account.handle}`,
_(msg`Please sign in as @${account.handle}`),
'circle-exclamation',
)
}
} catch (e) {
Toast.show('Sorry! We need you to enter your password.')
} catch (e: any) {
logger.error(`switch account: selectAccount failed`, {
message: e.message,
})
clearCurrentAccount() // back user out to login
setTimeout(() => {
Toast.show(_(msg`Sorry! We need you to enter your password.`))
}, 100)
}
},
[track, clearCurrentAccount, selectAccount, requestSwitchToAccount],
[_, track, clearCurrentAccount, selectAccount, requestSwitchToAccount],
)
return {onPressSwitchAccount}
+6 -3
View File
@@ -1,12 +1,13 @@
import {useEffect} from 'react'
import * as Notifications from 'expo-notifications'
import {BskyAgent} from '@atproto/api'
import {QueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
import {truncateAndInvalidate} from '#/state/queries/util'
import {getAgent, SessionAccount} from '#/state/session'
import {SessionAccount} from '#/state/session'
import {track} from 'lib/analytics/analytics'
import {devicePlatform, isIOS} from 'platform/detection'
import {resetToTab} from '../../Navigation'
@@ -18,6 +19,7 @@ const SERVICE_DID = (serviceUrl?: string) =>
: 'did:web:api.bsky.app'
export async function requestPermissionsAndRegisterToken(
agent: BskyAgent,
account: SessionAccount,
) {
// request notifications permission once the user has logged in
@@ -29,7 +31,7 @@ export async function requestPermissionsAndRegisterToken(
// register the push token with the server
const token = await Notifications.getDevicePushTokenAsync()
try {
await getAgent().api.app.bsky.notification.registerPush({
await agent.api.app.bsky.notification.registerPush({
serviceDid: SERVICE_DID(account.service),
platform: devicePlatform,
token: token.data,
@@ -49,6 +51,7 @@ export async function requestPermissionsAndRegisterToken(
}
export function registerTokenChangeHandler(
agent: BskyAgent,
account: SessionAccount,
): () => void {
// listens for new changes to the push token
@@ -60,7 +63,7 @@ export function registerTokenChangeHandler(
logger.DebugContext.notifications,
)
try {
await getAgent().api.app.bsky.notification.registerPush({
await agent.api.app.bsky.notification.registerPush({
serviceDid: SERVICE_DID(account.service),
platform: devicePlatform,
token: newToken.data,
+17 -10
View File
@@ -1,20 +1,20 @@
import React from 'react'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useOnboardingDispatch} from '#/state/shell'
import {getAgent, isSessionDeactivated, useSessionApi} from '#/state/session'
import {logger} from '#/logger'
import {pluralize} from '#/lib/strings/helpers'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Text, P} from '#/components/Typography'
import {pluralize} from '#/lib/strings/helpers'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {ScrollView} from '#/view/com/util/Views'
import {Loader} from '#/components/Loader'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Loader} from '#/components/Loader'
import {P, Text} from '#/components/Typography'
const COL_WIDTH = 400
@@ -25,6 +25,7 @@ export function Deactivated() {
const {gtMobile} = useBreakpoints()
const onboardingDispatch = useOnboardingDispatch()
const {logout} = useSessionApi()
const {getAgent} = useAgent()
const [isProcessing, setProcessing] = React.useState(false)
const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>(
@@ -56,7 +57,13 @@ export function Deactivated() {
} finally {
setProcessing(false)
}
}, [setProcessing, setEstimatedTime, setPlaceInQueue, onboardingDispatch])
}, [
setProcessing,
setEstimatedTime,
setPlaceInQueue,
onboardingDispatch,
getAgent,
])
React.useEffect(() => {
checkStatus()
+17 -9
View File
@@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
@@ -38,15 +39,22 @@ export const ChooseAccountForm = ({
setShowLoggedOut(false)
Toast.show(_(msg`Already signed in as @${account.handle}`))
} else {
await initSession(account)
logEvent('account:loggedIn', {
logContext: 'ChooseAccountForm',
withPassword: false,
})
track('Sign In', {resumedSession: true})
setTimeout(() => {
Toast.show(_(msg`Signed in as @${account.handle}`))
}, 100)
try {
await initSession(account)
logEvent('account:loggedIn', {
logContext: 'ChooseAccountForm',
withPassword: false,
})
track('Sign In', {resumedSession: true})
setTimeout(() => {
Toast.show(_(msg`Signed in as @${account.handle}`))
}, 100)
} catch (e: any) {
logger.error('choose account: initSession failed', {
message: e.message,
})
onSelectAccount(account)
}
}
} else {
onSelectAccount(account)
+4 -2
View File
@@ -8,7 +8,7 @@ import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {
DescriptionText,
@@ -38,6 +38,7 @@ export function StepFinished() {
const onboardDispatch = useOnboardingDispatch()
const [saving, setSaving] = React.useState(false)
const {mutateAsync: saveFeeds} = useSetSaveFeedsMutation()
const {getAgent} = useAgent()
const finishOnboarding = React.useCallback(async () => {
setSaving(true)
@@ -57,6 +58,7 @@ export function StepFinished() {
try {
await Promise.all([
bulkWriteFollows(
getAgent(),
suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID),
),
// these must be serial
@@ -80,7 +82,7 @@ export function StepFinished() {
track('OnboardingV2:StepFinished:End')
track('OnboardingV2:Complete')
logEvent('onboarding:finished:nextPressed', {})
}, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track])
}, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track, getAgent])
React.useEffect(() => {
track('OnboardingV2:StepFinished:Start')
@@ -8,7 +8,7 @@ import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {capitalize} from '#/lib/strings/capitalize'
import {logger} from '#/logger'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {
DescriptionText,
@@ -39,6 +39,7 @@ export function StepInterests() {
state.interestsStepResults.selectedInterests.map(i => i),
)
const onboardDispatch = useOnboardingDispatch()
const {getAgent} = useAgent()
const {isLoading, isError, error, data, refetch, isFetching} = useQuery({
queryKey: ['interests'],
queryFn: async () => {
+11 -7
View File
@@ -1,7 +1,10 @@
import {AppBskyGraphFollow, AppBskyGraphGetFollows} from '@atproto/api'
import {
AppBskyGraphFollow,
AppBskyGraphGetFollows,
BskyAgent,
} from '@atproto/api'
import {until} from '#/lib/async/until'
import {getAgent} from '#/state/session'
import {PRIMARY_FEEDS} from './StepAlgoFeeds'
function shuffle(array: any) {
@@ -63,8 +66,8 @@ export function aggregateInterestItems(
return Array.from(new Set(results)).slice(0, 20)
}
export async function bulkWriteFollows(dids: string[]) {
const session = getAgent().session
export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
const session = agent.session
if (!session) {
throw new Error(`bulkWriteFollows failed: no session`)
@@ -83,14 +86,15 @@ export async function bulkWriteFollows(dids: string[]) {
value: r,
}))
await getAgent().com.atproto.repo.applyWrites({
await agent.com.atproto.repo.applyWrites({
repo: session.did,
writes: followWrites,
})
await whenFollowsIndexed(session.did, res => !!res.data.follows.length)
await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length)
}
async function whenFollowsIndexed(
agent: BskyAgent,
actor: string,
fn: (res: AppBskyGraphGetFollows.Response) => boolean,
) {
@@ -99,7 +103,7 @@ async function whenFollowsIndexed(
1e3, // 1s delay between tries
fn,
() =>
getAgent().app.bsky.graph.getFollows({
agent.app.bsky.graph.getFollows({
actor,
limit: 1,
}),
+3 -1
View File
@@ -9,7 +9,7 @@ import {FEEDBACK_FORM_URL} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {createFullHandle} from '#/lib/strings/handles'
import {useServiceQuery} from '#/state/queries/service'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
import {
initialState,
@@ -35,6 +35,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
const [state, dispatch] = React.useReducer(reducer, initialState)
const submit = useSubmitSignup({state, dispatch})
const {gtMobile} = useBreakpoints()
const {getAgent} = useAgent()
const {
data: serviceInfo,
@@ -113,6 +114,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
state.serviceDescription?.phoneVerificationRequired,
state.userDomain,
submit,
getAgent,
])
const onBackPress = React.useCallback(() => {
+4 -2
View File
@@ -5,7 +5,7 @@ import {useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute} from '#/lib/moderation'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
const DEFAULT_MOD_OPTS = {
@@ -18,6 +18,7 @@ export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
export function useActorAutocompleteQuery(prefix: string) {
const moderationOpts = useModerationOpts()
const {getAgent} = useAgent()
prefix = prefix.toLowerCase()
@@ -46,6 +47,7 @@ export type ActorAutocompleteFn = ReturnType<typeof useActorAutocompleteFn>
export function useActorAutocompleteFn() {
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const {getAgent} = useAgent()
return React.useCallback(
async ({query, limit = 8}: {query: string; limit?: number}) => {
@@ -74,7 +76,7 @@ export function useActorAutocompleteFn() {
moderationOpts || DEFAULT_MOD_OPTS,
)
},
[queryClient, moderationOpts],
[queryClient, moderationOpts, getAgent],
)
}
+2 -1
View File
@@ -2,7 +2,7 @@ import {AppBskyActorDefs} from '@atproto/api'
import {QueryClient, useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'actor-search'
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
@@ -14,6 +14,7 @@ export function useActorSearch({
query: string
enabled?: boolean
}) {
const {getAgent} = useAgent()
return useQuery<AppBskyActorDefs.ProfileView[]>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(query || ''),
+4 -1
View File
@@ -2,12 +2,13 @@ import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '../session'
import {useAgent} from '../session'
const RQKEY_ROOT = 'app-passwords'
export const RQKEY = () => [RQKEY_ROOT]
export function useAppPasswordsQuery() {
const {getAgent} = useAgent()
return useQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(),
@@ -20,6 +21,7 @@ export function useAppPasswordsQuery() {
export function useAppPasswordCreateMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<
ComAtprotoServerCreateAppPassword.OutputSchema,
Error,
@@ -42,6 +44,7 @@ export function useAppPasswordCreateMutation() {
export function useAppPasswordDeleteMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, Error, {name: string}>({
mutationFn: async ({name}) => {
await getAgent().com.atproto.server.revokeAppPassword({
+5 -1
View File
@@ -17,7 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {router} from '#/routes'
export type FeedSourceFeedInfo = {
@@ -140,6 +140,7 @@ export function getAvatarTypeFromUri(uri: string) {
export function useFeedSourceInfoQuery({uri}: {uri: string}) {
const type = getFeedTypeFromUri(uri)
const {getAgent} = useAgent()
return useQuery({
staleTime: STALE.INFINITY,
@@ -166,6 +167,7 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
export const useGetPopularFeedsQueryKey = ['getPopularFeeds']
export function useGetPopularFeedsQuery() {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema,
Error,
@@ -187,6 +189,7 @@ export function useGetPopularFeedsQuery() {
}
export function useSearchPopularFeedsMutation() {
const {getAgent} = useAgent()
return useMutation({
mutationFn: async (query: string) => {
const res = await getAgent().app.bsky.unspecced.getPopularFeedGenerators({
@@ -238,6 +241,7 @@ const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
export function usePinnedFeedsInfos() {
const {hasSession} = useSession()
const {getAgent} = useAgent()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const pinnedUris = preferences?.feeds?.pinned ?? []
+6 -3
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const handleQueryKeyRoot = 'handle'
const fetchHandleQueryKey = (handleOrDid: string) => [
@@ -14,6 +14,7 @@ const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
export function useFetchHandle() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return React.useCallback(
async (handleOrDid: string) => {
@@ -27,12 +28,13 @@ export function useFetchHandle() {
}
return handleOrDid
},
[queryClient],
[queryClient, getAgent],
)
}
export function useUpdateHandleMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation({
mutationFn: async ({handle}: {handle: string}) => {
@@ -48,6 +50,7 @@ export function useUpdateHandleMutation() {
export function useFetchDid() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return React.useCallback(
async (handleOrDid: string) => {
@@ -64,6 +67,6 @@ export function useFetchDid() {
},
})
},
[queryClient],
[queryClient, getAgent],
)
}
+2 -1
View File
@@ -3,7 +3,7 @@ import {useQuery} from '@tanstack/react-query'
import {cleanError} from '#/lib/strings/errors'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
return invite.available - invite.uses.length > 0 && !invite.disabled
@@ -16,6 +16,7 @@ export type InviteCodesQueryResponse = Exclude<
undefined
>
export function useInviteCodesQuery() {
const {getAgent} = useAgent()
return useQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: [inviteCodesQueryKeyRoot],
+5 -1
View File
@@ -5,7 +5,7 @@ import {z} from 'zod'
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
import {STALE} from '#/state/queries'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const labelerInfoQueryKeyRoot = 'labeler-info'
export const labelerInfoQueryKey = (did: string) => [
@@ -31,6 +31,7 @@ export function useLabelerInfoQuery({
did?: string
enabled?: boolean
}) {
const {getAgent} = useAgent()
return useQuery({
enabled: !!did && enabled !== false,
queryKey: labelerInfoQueryKey(did as string),
@@ -45,6 +46,7 @@ export function useLabelerInfoQuery({
}
export function useLabelersInfoQuery({dids}: {dids: string[]}) {
const {getAgent} = useAgent()
return useQuery({
enabled: !!dids.length,
queryKey: labelersInfoQueryKey(dids),
@@ -56,6 +58,7 @@ export function useLabelersInfoQuery({dids}: {dids: string[]}) {
}
export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
const {getAgent} = useAgent()
return useQuery({
enabled: !!dids.length,
queryKey: labelersDetailedInfoQueryKey(dids),
@@ -73,6 +76,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
export function useLabelerSubscriptionMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation({
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
+3 -1
View File
@@ -1,8 +1,9 @@
import {useMutation} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
export function useLikeMutation() {
const {getAgent} = useAgent()
return useMutation({
mutationFn: async ({uri, cid}: {uri: string; cid: string}) => {
const res = await getAgent().like(uri, cid)
@@ -12,6 +13,7 @@ export function useLikeMutation() {
}
export function useUnlikeMutation() {
const {getAgent} = useAgent()
return useMutation({
mutationFn: async ({uri}: {uri: string}) => {
await getAgent().deleteLike(uri)
+2 -1
View File
@@ -7,7 +7,7 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'list-members'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListMembersQuery(uri: string) {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyGraphGetList.OutputSchema,
Error,
+4 -1
View File
@@ -19,7 +19,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
const SANITY_PAGE_LIMIT = 1000
@@ -40,6 +40,7 @@ export interface ListMembersip {
*/
export function useDangerousListMembershipsQuery() {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
return useQuery<ListMembersip[]>({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(),
@@ -91,6 +92,7 @@ export function getMembership(
export function useListMembershipAddMutation() {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const queryClient = useQueryClient()
return useMutation<
{uri: string; cid: string},
@@ -149,6 +151,7 @@ export function useListMembershipAddMutation() {
export function useListMembershipRemoveMutation() {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const queryClient = useQueryClient()
return useMutation<
void,
+50 -22
View File
@@ -4,6 +4,7 @@ import {
AppBskyGraphGetList,
AppBskyGraphList,
AtUri,
BskyAgent,
Facet,
} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
@@ -12,7 +13,7 @@ import chunk from 'lodash.chunk'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {STALE} from '#/state/queries'
import {getAgent, useSession} from '../session'
import {useAgent, useSession} from '../session'
import {invalidate as invalidateMyLists} from './my-lists'
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
@@ -20,6 +21,7 @@ const RQKEY_ROOT = 'list'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListQuery(uri?: string) {
const {getAgent} = useAgent()
return useQuery<AppBskyGraphDefs.ListView, Error>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''),
@@ -47,6 +49,7 @@ export interface ListCreateMutateParams {
export function useListCreateMutation() {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>(
{
async mutationFn({
@@ -85,9 +88,13 @@ export function useListCreateMutation() {
)
// wait for the appview to update
await whenAppViewReady(res.uri, (v: AppBskyGraphGetList.Response) => {
return typeof v?.data?.list.uri === 'string'
})
await whenAppViewReady(
getAgent(),
res.uri,
(v: AppBskyGraphGetList.Response) => {
return typeof v?.data?.list.uri === 'string'
},
)
return res
},
onSuccess() {
@@ -109,6 +116,7 @@ export interface ListMetadataMutateParams {
}
export function useListMetadataMutation() {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const queryClient = useQueryClient()
return useMutation<
{uri: string; cid: string},
@@ -150,12 +158,16 @@ export function useListMetadataMutation() {
).data
// wait for the appview to update
await whenAppViewReady(res.uri, (v: AppBskyGraphGetList.Response) => {
const list = v.data.list
return (
list.name === record.name && list.description === record.description
)
})
await whenAppViewReady(
getAgent(),
res.uri,
(v: AppBskyGraphGetList.Response) => {
const list = v.data.list
return (
list.name === record.name && list.description === record.description
)
},
)
return res
},
onSuccess(data, variables) {
@@ -172,6 +184,7 @@ export function useListMetadataMutation() {
export function useListDeleteMutation() {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const queryClient = useQueryClient()
return useMutation<void, Error, {uri: string}>({
mutationFn: async ({uri}) => {
@@ -220,9 +233,13 @@ export function useListDeleteMutation() {
}
// wait for the appview to update
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
return !v?.success
})
await whenAppViewReady(
getAgent(),
uri,
(v: AppBskyGraphGetList.Response) => {
return !v?.success
},
)
},
onSuccess() {
invalidateMyLists(queryClient)
@@ -236,6 +253,7 @@ export function useListDeleteMutation() {
export function useListMuteMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, Error, {uri: string; mute: boolean}>({
mutationFn: async ({uri, mute}) => {
if (mute) {
@@ -244,9 +262,13 @@ export function useListMuteMutation() {
await getAgent().unmuteModList(uri)
}
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
return Boolean(v?.data.list.viewer?.muted) === mute
})
await whenAppViewReady(
getAgent(),
uri,
(v: AppBskyGraphGetList.Response) => {
return Boolean(v?.data.list.viewer?.muted) === mute
},
)
},
onSuccess(data, variables) {
queryClient.invalidateQueries({
@@ -258,6 +280,7 @@ export function useListMuteMutation() {
export function useListBlockMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, Error, {uri: string; block: boolean}>({
mutationFn: async ({uri, block}) => {
if (block) {
@@ -266,11 +289,15 @@ export function useListBlockMutation() {
await getAgent().unblockModList(uri)
}
await whenAppViewReady(uri, (v: AppBskyGraphGetList.Response) => {
return block
? typeof v?.data.list.viewer?.blocked === 'string'
: !v?.data.list.viewer?.blocked
})
await whenAppViewReady(
getAgent(),
uri,
(v: AppBskyGraphGetList.Response) => {
return block
? typeof v?.data.list.viewer?.blocked === 'string'
: !v?.data.list.viewer?.blocked
},
)
},
onSuccess(data, variables) {
queryClient.invalidateQueries({
@@ -281,6 +308,7 @@ export function useListBlockMutation() {
}
async function whenAppViewReady(
agent: BskyAgent,
uri: string,
fn: (res: AppBskyGraphGetList.Response) => boolean,
) {
@@ -289,7 +317,7 @@ async function whenAppViewReady(
1e3, // 1s delay between tries
fn,
() =>
getAgent().app.bsky.graph.getList({
agent.app.bsky.graph.getList({
list: uri,
limit: 1,
}),
+2 -1
View File
@@ -6,13 +6,14 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'my-blocked-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyBlockedAccountsQuery() {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyGraphGetBlocks.OutputSchema,
Error,
+2 -1
View File
@@ -3,7 +3,7 @@ import {QueryClient, useQuery} from '@tanstack/react-query'
import {accumulate} from '#/lib/async/accumulate'
import {STALE} from '#/state/queries'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
export type MyListsFilter =
| 'all'
@@ -16,6 +16,7 @@ export const RQKEY = (filter: MyListsFilter) => [RQKEY_ROOT, filter]
export function useMyListsQuery(filter: MyListsFilter) {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
return useQuery<AppBskyGraphDefs.ListView[]>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(filter),
+2 -1
View File
@@ -6,13 +6,14 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'my-muted-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyMutedAccountsQuery() {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyGraphGetMutes.OutputSchema,
Error,
+3
View File
@@ -27,6 +27,7 @@ import {
} from '@tanstack/react-query'
import {useMutedThreads} from '#/state/muted-threads'
import {useAgent} from '#/state/session'
import {STALE} from '..'
import {useModerationOpts} from '../preferences'
import {embedViewRecordToPostView, getEmbeddedPost} from '../util'
@@ -46,6 +47,7 @@ export function RQKEY() {
}
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const {getAgent} = useAgent()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads()
@@ -71,6 +73,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
if (!page) {
page = (
await fetchPage({
agent: getAgent(),
limit: PAGE_SIZE,
cursor: pageParam,
queryClient,
+4 -2
View File
@@ -12,7 +12,7 @@ import BroadcastChannel from '#/lib/broadcast'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useMutedThreads} from '#/state/muted-threads'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {useModerationOpts} from '../preferences'
import {truncateAndInvalidate} from '../util'
import {RQKEY as RQKEY_NOTIFS} from './feed'
@@ -46,6 +46,7 @@ const apiContext = React.createContext<ApiContext>({
export function Provider({children}: React.PropsWithChildren<{}>) {
const {hasSession} = useSession()
const {getAgent} = useAgent()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads()
@@ -144,6 +145,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// count
const {page, indexedAt: lastIndexed} = await fetchPage({
agent: getAgent(),
cursor: undefined,
limit: 40,
queryClient,
@@ -196,7 +198,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
},
}
}, [setNumUnread, queryClient, moderationOpts, threadMutes])
}, [setNumUnread, queryClient, moderationOpts, threadMutes, getAgent])
checkUnreadRef.current = api.checkUnread
return (
+15 -13
View File
@@ -1,18 +1,19 @@
import {
AppBskyNotificationListNotifications,
ModerationOpts,
moderateNotification,
AppBskyEmbedRecord,
AppBskyFeedDefs,
AppBskyFeedLike,
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedLike,
AppBskyEmbedRecord,
AppBskyNotificationListNotifications,
BskyAgent,
moderateNotification,
ModerationOpts,
} from '@atproto/api'
import chunk from 'lodash.chunk'
import {QueryClient} from '@tanstack/react-query'
import {getAgent} from '../../session'
import chunk from 'lodash.chunk'
import {precacheProfile} from '../profile'
import {NotificationType, FeedNotification, FeedPage} from './types'
import {FeedNotification, FeedPage, NotificationType} from './types'
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
const MS_1HR = 1e3 * 60 * 60
@@ -22,6 +23,7 @@ const MS_2DAY = MS_1HR * 48
// =
export async function fetchPage({
agent,
cursor,
limit,
queryClient,
@@ -29,6 +31,7 @@ export async function fetchPage({
threadMutes,
fetchAdditionalData,
}: {
agent: BskyAgent
cursor: string | undefined
limit: number
queryClient: QueryClient
@@ -36,7 +39,7 @@ export async function fetchPage({
threadMutes: string[]
fetchAdditionalData: boolean
}): Promise<{page: FeedPage; indexedAt: string | undefined}> {
const res = await getAgent().listNotifications({
const res = await agent.listNotifications({
limit,
cursor,
})
@@ -53,7 +56,7 @@ export async function fetchPage({
// we fetch subjects of notifications (usually posts) now instead of lazily
// in the UI to avoid relayouts
if (fetchAdditionalData) {
const subjects = await fetchSubjects(notifsGrouped)
const subjects = await fetchSubjects(agent, notifsGrouped)
for (const notif of notifsGrouped) {
if (notif.subjectUri) {
notif.subject = subjects.get(notif.subjectUri)
@@ -137,6 +140,7 @@ export function groupNotifications(
}
async function fetchSubjects(
agent: BskyAgent,
groupedNotifs: FeedNotification[],
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
const uris = new Set<string>()
@@ -148,9 +152,7 @@ async function fetchSubjects(
const uriChunks = chunk(Array.from(uris), 25)
const postsChunks = await Promise.all(
uriChunks.map(uris =>
getAgent()
.app.bsky.feed.getPosts({uris})
.then(res => res.data.posts),
agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts),
),
)
const map = new Map<string, AppBskyFeedDefs.PostView>()
+39 -17
View File
@@ -4,6 +4,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
BskyAgent,
ModerationDecision,
} from '@atproto/api'
import {
@@ -19,7 +20,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {getAgent} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {AuthorFeedAPI} from 'lib/api/feed/author'
import {CustomFeedAPI} from 'lib/api/feed/custom'
import {FollowingFeedAPI} from 'lib/api/feed/following'
@@ -104,6 +105,8 @@ export function usePostFeedQuery(
const queryClient = useQueryClient()
const feedTuners = useFeedTuners(feedDesc)
const moderationOpts = useModerationOpts()
const {hasSession} = useSession()
const {getAgent} = useAgent()
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
const lastRun = useRef<{
data: InfiniteData<FeedPageUnselected>
@@ -135,11 +138,17 @@ export function usePostFeedQuery(
queryKey: RQKEY(feedDesc, params),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
logger.debug('usePostFeedQuery', {feedDesc, cursor: pageParam?.cursor})
const agent = getAgent()
const {api, cursor} = pageParam
? pageParam
: {
api: createApi(feedDesc, params || {}, feedTuners),
api: createApi({
feedDesc,
feedParams: params || {},
feedTuners,
agent,
}),
cursor: undefined,
}
@@ -153,7 +162,7 @@ export function usePostFeedQuery(
* moderations happen later, which results in some posts being shown and
* some not.
*/
if (!getAgent().session) {
if (!hasSession) {
assertSomePostsPassModeration(res.feed)
}
@@ -365,34 +374,47 @@ export async function pollLatest(page: FeedPage | undefined) {
return false
}
function createApi(
feedDesc: FeedDescriptor,
params: FeedParams,
feedTuners: FeedTunerFn[],
) {
function createApi({
agent,
feedDesc,
feedParams,
feedTuners,
}: {
agent: BskyAgent
feedDesc: FeedDescriptor
feedParams: FeedParams
feedTuners: FeedTunerFn[]
}) {
if (feedDesc === 'home') {
if (params.mergeFeedEnabled) {
return new MergeFeedAPI(params, feedTuners)
if (feedParams.mergeFeedEnabled) {
return new MergeFeedAPI({
agent,
feedParams,
feedTuners,
})
} else {
return new HomeFeedAPI()
return new HomeFeedAPI({agent})
}
} else if (feedDesc === 'following') {
return new FollowingFeedAPI()
return new FollowingFeedAPI({agent})
} else if (feedDesc.startsWith('author')) {
const [_, actor, filter] = feedDesc.split('|')
return new AuthorFeedAPI({actor, filter})
return new AuthorFeedAPI({agent, feedParams: {actor, filter}})
} else if (feedDesc.startsWith('likes')) {
const [_, actor] = feedDesc.split('|')
return new LikesFeedAPI({actor})
return new LikesFeedAPI({agent, feedParams: {actor}})
} else if (feedDesc.startsWith('feedgen')) {
const [_, feed] = feedDesc.split('|')
return new CustomFeedAPI({feed})
return new CustomFeedAPI({
agent,
feedParams: {feed},
})
} else if (feedDesc.startsWith('list')) {
const [_, list] = feedDesc.split('|')
return new ListFeedAPI({list})
return new ListFeedAPI({agent, feedParams: {list}})
} else {
// shouldnt happen
return new FollowingFeedAPI()
return new FollowingFeedAPI({agent})
}
}
+2 -1
View File
@@ -6,7 +6,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'liked-by'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function useLikedByQuery(resolvedUri: string | undefined) {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyFeedGetLikes.OutputSchema,
Error,
+2 -1
View File
@@ -6,7 +6,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'post-reposted-by'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function usePostRepostedByQuery(resolvedUri: string | undefined) {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyFeedGetRepostedBy.OutputSchema,
Error,
+2 -1
View File
@@ -7,7 +7,7 @@ import {
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from 'state/queries/search-posts'
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
@@ -66,6 +66,7 @@ export type ThreadNode =
export function usePostThreadQuery(uri: string | undefined) {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useQuery<ThreadNode, Error>({
gcTime: 0,
queryKey: RQKEY(uri || ''),
+9 -2
View File
@@ -7,13 +7,14 @@ import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {Shadow} from '#/state/cache/types'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {findProfileQueryData} from './profile'
const RQKEY_ROOT = 'post'
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
export function usePostQuery(uri: string | undefined) {
const {getAgent} = useAgent()
return useQuery<AppBskyFeedDefs.PostView>({
queryKey: RQKEY(uri || ''),
async queryFn() {
@@ -30,6 +31,7 @@ export function usePostQuery(uri: string | undefined) {
export function useGetPost() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useCallback(
async ({uri}: {uri: string}) => {
return queryClient.fetchQuery({
@@ -56,7 +58,7 @@ export function useGetPost() {
},
})
},
[queryClient],
[queryClient, getAgent],
)
}
@@ -125,6 +127,7 @@ function usePostLikeMutation(
const {currentAccount} = useSession()
const queryClient = useQueryClient()
const postAuthor = post.author
const {getAgent} = useAgent()
return useMutation<
{uri: string}, // responds with the uri of the like
Error,
@@ -162,6 +165,7 @@ function usePostLikeMutation(
function usePostUnlikeMutation(
logContext: LogEvents['post:unlike']['logContext'],
) {
const {getAgent} = useAgent()
return useMutation<void, Error, {postUri: string; likeUri: string}>({
mutationFn: ({likeUri}) => {
logEvent('post:unlike', {logContext})
@@ -234,6 +238,7 @@ export function usePostRepostMutationQueue(
function usePostRepostMutation(
logContext: LogEvents['post:repost']['logContext'],
) {
const {getAgent} = useAgent()
return useMutation<
{uri: string}, // responds with the uri of the repost
Error,
@@ -252,6 +257,7 @@ function usePostRepostMutation(
function usePostUnrepostMutation(
logContext: LogEvents['post:unrepost']['logContext'],
) {
const {getAgent} = useAgent()
return useMutation<void, Error, {postUri: string; repostUri: string}>({
mutationFn: ({repostUri}) => {
logEvent('post:unrepost', {logContext})
@@ -265,6 +271,7 @@ function usePostUnrepostMutation(
export function usePostDeleteMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, Error, {uri: string}>({
mutationFn: async ({uri}) => {
await getAgent().deletePost(uri)
+17 -1
View File
@@ -22,7 +22,7 @@ import {
ThreadViewPreferences,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config'
export * from '#/state/queries/preferences/const'
@@ -33,6 +33,7 @@ const preferencesQueryKeyRoot = 'getPreferences'
export const preferencesQueryKey = [preferencesQueryKeyRoot]
export function usePreferencesQuery() {
const {getAgent} = useAgent()
return useQuery({
staleTime: STALE.SECONDS.FIFTEEN,
structuralSharing: true,
@@ -118,6 +119,7 @@ export function useModerationOpts() {
export function useClearPreferencesMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation({
mutationFn: async () => {
@@ -131,6 +133,7 @@ export function useClearPreferencesMutation() {
}
export function usePreferencesSetContentLabelMutation() {
const {getAgent} = useAgent()
const queryClient = useQueryClient()
return useMutation<
@@ -150,6 +153,7 @@ export function usePreferencesSetContentLabelMutation() {
export function useSetContentLabelMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation({
mutationFn: async ({
@@ -172,6 +176,7 @@ export function useSetContentLabelMutation() {
export function usePreferencesSetAdultContentMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, unknown, {enabled: boolean}>({
mutationFn: async ({enabled}) => {
@@ -186,6 +191,7 @@ export function usePreferencesSetAdultContentMutation() {
export function usePreferencesSetBirthDateMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, unknown, {birthDate: Date}>({
mutationFn: async ({birthDate}: {birthDate: Date}) => {
@@ -200,6 +206,7 @@ export function usePreferencesSetBirthDateMutation() {
export function useSetFeedViewPreferencesMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
mutationFn: async prefs => {
@@ -214,6 +221,7 @@ export function useSetFeedViewPreferencesMutation() {
export function useSetThreadViewPreferencesMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
mutationFn: async prefs => {
@@ -228,6 +236,7 @@ export function useSetThreadViewPreferencesMutation() {
export function useSetSaveFeedsMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<
void,
@@ -246,6 +255,7 @@ export function useSetSaveFeedsMutation() {
export function useSaveFeedMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, unknown, {uri: string}>({
mutationFn: async ({uri}) => {
@@ -261,6 +271,7 @@ export function useSaveFeedMutation() {
export function useRemoveFeedMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, unknown, {uri: string}>({
mutationFn: async ({uri}) => {
@@ -276,6 +287,7 @@ export function useRemoveFeedMutation() {
export function usePinFeedMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, unknown, {uri: string}>({
mutationFn: async ({uri}) => {
@@ -291,6 +303,7 @@ export function usePinFeedMutation() {
export function useUnpinFeedMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, unknown, {uri: string}>({
mutationFn: async ({uri}) => {
@@ -306,6 +319,7 @@ export function useUnpinFeedMutation() {
export function useUpsertMutedWordsMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation({
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
@@ -320,6 +334,7 @@ export function useUpsertMutedWordsMutation() {
export function useUpdateMutedWordMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
@@ -334,6 +349,7 @@ export function useUpdateMutedWordMutation() {
export function useRemoveMutedWordMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
+2 -1
View File
@@ -1,7 +1,7 @@
import {AppBskyFeedGetActorFeeds} from '@atproto/api'
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -15,6 +15,7 @@ export function useProfileFeedgensQuery(
opts?: {enabled?: boolean},
) {
const enabled = opts?.enabled !== false
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyFeedGetActorFeeds.OutputSchema,
Error,
+2 -1
View File
@@ -6,7 +6,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -15,6 +15,7 @@ const RQKEY_ROOT = 'profile-followers'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileFollowersQuery(did: string | undefined) {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyGraphGetFollowers.OutputSchema,
Error,
+2 -1
View File
@@ -7,7 +7,7 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -17,6 +17,7 @@ const RQKEY_ROOT = 'profile-follows'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileFollowsQuery(did: string | undefined) {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyGraphGetFollows.OutputSchema,
Error,
+2 -1
View File
@@ -1,7 +1,7 @@
import {AppBskyGraphGetLists} from '@atproto/api'
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -11,6 +11,7 @@ export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
const enabled = opts?.enabled !== false
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyGraphGetLists.OutputSchema,
Error,
+16 -3
View File
@@ -8,6 +8,7 @@ import {
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AtUri,
BskyAgent,
} from '@atproto/api'
import {
QueryClient,
@@ -25,7 +26,7 @@ import {Shadow} from '#/state/cache/types'
import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {updateProfileShadow} from '../cache/profile-shadow'
import {getAgent, useSession} from '../session'
import {useAgent, useSession} from '../session'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {ThreadNode} from './post-thread'
@@ -53,6 +54,7 @@ export function useProfileQuery({
staleTime?: number
}) {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
// WARNING
// this staleTime is load-bearing
@@ -77,6 +79,7 @@ export function useProfileQuery({
}
export function useProfilesQuery({handles}: {handles: string[]}) {
const {getAgent} = useAgent()
return useQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: profilesQueryKey(handles),
@@ -88,6 +91,7 @@ export function useProfilesQuery({handles}: {handles: string[]}) {
}
export function usePrefetchProfileQuery() {
const {getAgent} = useAgent()
const queryClient = useQueryClient()
const prefetchProfileQuery = useCallback(
async (did: string) => {
@@ -99,7 +103,7 @@ export function usePrefetchProfileQuery() {
},
})
},
[queryClient],
[queryClient, getAgent],
)
return prefetchProfileQuery
}
@@ -115,6 +119,7 @@ interface ProfileUpdateParams {
}
export function useProfileUpdateMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, Error, ProfileUpdateParams>({
mutationFn: async ({
profile,
@@ -154,6 +159,7 @@ export function useProfileUpdateMutation() {
return existing
})
await whenAppViewReady(
getAgent(),
profile.did,
checkCommitted ||
(res => {
@@ -255,6 +261,7 @@ function useProfileFollowMutation(
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
) {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
@@ -281,6 +288,7 @@ function useProfileFollowMutation(
function useProfileUnfollowMutation(
logContext: LogEvents['profile:unfollow']['logContext'],
) {
const {getAgent} = useAgent()
return useMutation<void, Error, {did: string; followUri: string}>({
mutationFn: async ({followUri}) => {
logEvent('profile:unfollow', {logContext})
@@ -341,6 +349,7 @@ export function useProfileMuteMutationQueue(
function useProfileMuteMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await getAgent().mute(did)
@@ -353,6 +362,7 @@ function useProfileMuteMutation() {
function useProfileUnmuteMutation() {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await getAgent().unmute(did)
@@ -419,6 +429,7 @@ export function useProfileBlockMutationQueue(
function useProfileBlockMutation() {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
@@ -439,6 +450,7 @@ function useProfileBlockMutation() {
function useProfileUnblockMutation() {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string; blockUri: string}>({
mutationFn: async ({blockUri}) => {
@@ -516,6 +528,7 @@ export function precacheThreadPostProfiles(
}
async function whenAppViewReady(
agent: BskyAgent,
actor: string,
fn: (res: AppBskyActorGetProfile.Response) => boolean,
) {
@@ -523,7 +536,7 @@ async function whenAppViewReady(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() => getAgent().app.bsky.actor.getProfile({actor}),
() => agent.app.bsky.actor.getProfile({actor}),
)
}
+2 -1
View File
@@ -2,7 +2,7 @@ import {AppBskyActorDefs, AtUri} from '@atproto/api'
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
const RQKEY_ROOT = 'resolved-did'
@@ -24,6 +24,7 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
export function useResolveDidQuery(didOrHandle: string | undefined) {
const queryClient = useQueryClient()
const {getAgent} = useAgent()
return useQuery<string, Error>({
staleTime: STALE.HOURS.ONE,
+2 -1
View File
@@ -6,7 +6,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
const searchPostsQueryKeyRoot = 'search-posts'
@@ -25,6 +25,7 @@ export function useSearchPostsQuery({
sort?: 'top' | 'latest'
enabled?: boolean
}) {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyFeedSearchPosts.OutputSchema,
Error,
+2 -1
View File
@@ -2,12 +2,13 @@ import {AppBskyFeedGetSuggestedFeeds} from '@atproto/api'
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const suggestedFeedsQueryKeyRoot = 'suggestedFeeds'
export const suggestedFeedsQueryKey = [suggestedFeedsQueryKeyRoot]
export function useSuggestedFeedsQuery() {
const {getAgent} = useAgent()
return useInfiniteQuery<
AppBskyFeedGetSuggestedFeeds.OutputSchema,
Error,
+3 -1
View File
@@ -14,7 +14,7 @@ import {
import {STALE} from '#/state/queries'
import {useModerationOpts} from '#/state/queries/preferences'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
const suggestedFollowsQueryKeyRoot = 'suggested-follows'
const suggestedFollowsQueryKey = [suggestedFollowsQueryKeyRoot]
@@ -27,6 +27,7 @@ const suggestedFollowsByActorQueryKey = (did: string) => [
export function useSuggestedFollowsQuery() {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const moderationOpts = useModerationOpts()
return useInfiniteQuery<
@@ -77,6 +78,7 @@ export function useSuggestedFollowsQuery() {
}
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
const {getAgent} = useAgent()
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
queryKey: suggestedFollowsByActorQueryKey(did),
queryFn: async () => {
+12 -765
View File
@@ -1,768 +1,15 @@
import React from 'react'
import {
AtpPersistSessionHandler,
BSKY_LABELER_DID,
BskyAgent,
} from '@atproto/api'
import {jwtDecode} from 'jwt-decode'
import * as V1 from '#/state/session/v1'
import * as V2 from '#/state/session/v2'
import {track} from '#/lib/analytics/analytics'
import {networkRetry} from '#/lib/async/retry'
import {IS_TEST_USER} from '#/lib/constants'
import {logEvent, LogEvents, tryFetchGates} 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 {useCloseAllActiveElements} from '#/state/util'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {IS_DEV} from '#/env'
import {emitSessionDropped} from '../events'
import {readLabelers} from './agent-config'
export type {CurrentAccount, SessionAccount} from '#/state/session/types'
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
const isV2 = false
/**
* NOTE
* Never hold on to the object returned by this function.
* Call `getAgent()` at the time of invocation to ensure
* that you never have a stale agent.
*/
export function getAgent() {
return __globalAgent
}
export type SessionAccount = persisted.PersistedAccount
export type SessionState = {
isInitialLoad: boolean
isSwitchingAccounts: boolean
accounts: SessionAccount[]
currentAccount: SessionAccount | undefined
}
export type StateContext = SessionState & {
hasSession: boolean
}
export type ApiContext = {
createAccount: (props: {
service: string
email: string
password: string
handle: string
inviteCode?: string
verificationPhone?: string
verificationCode?: string
}) => Promise<void>
login: (
props: {
service: string
identifier: string
password: string
authFactorToken?: string | undefined
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* A full logout. Clears the `currentAccount` from session, AND removes
* access tokens from all accounts, so that returning as any user will
* require a full login.
*/
logout: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => Promise<void>
/**
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
* clear access tokens from accounts, allowing the user to return to their
* other accounts without logging in.
*
* Used when adding a new account, deleting an account.
*/
clearCurrentAccount: () => void
initSession: (account: SessionAccount) => Promise<void>
resumeSession: (account?: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
selectAccount: (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
updateCurrentAccount: (
account: Partial<
Pick<
SessionAccount,
'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor'
>
>,
) => void
}
const StateContext = React.createContext<StateContext>({
isInitialLoad: true,
isSwitchingAccounts: false,
accounts: [],
currentAccount: undefined,
hasSession: false,
})
const ApiContext = React.createContext<ApiContext>({
createAccount: async () => {},
login: async () => {},
logout: async () => {},
initSession: async () => {},
resumeSession: async () => {},
removeAccount: () => {},
selectAccount: async () => {},
updateCurrentAccount: () => {},
clearCurrentAccount: () => {},
})
function createPersistSessionHandler(
agent: BskyAgent,
account: SessionAccount,
persistSessionCallback: (props: {
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,
handle: session?.handle || account.handle,
email: session?.email || account.email,
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
deactivated: isSessionDeactivated(session?.accessJwt),
pdsUrl: agent.pdsUrl?.toString(),
/*
* Tokens are undefined if the session expires, or if creation fails for
* any reason e.g. tokens are invalid, network error, etc.
*/
refreshJwt: session?.refreshJwt,
accessJwt: session?.accessJwt,
}
logger.debug(`session: persistSession`, {
event,
deactivated: refreshedAccount.deactivated,
})
if (expired) {
logger.warn(`session: expired`)
emitSessionDropped()
}
/*
* If the session expired, or it was successfully created/updated, we want
* to update/persist the data.
*
* If the session creation failed, it could be a network error, or it could
* be more serious like an invalid token(s). We can't differentiate, so in
* order to allow the user to get a fresh token (if they need it), we need
* to persist this data and wipe their tokens, effectively logging them
* out.
*/
persistSessionCallback({
expired,
refreshedAccount,
})
}
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const isDirty = React.useRef(false)
const [state, setState] = React.useState<SessionState>({
isInitialLoad: true,
isSwitchingAccounts: false,
accounts: persisted.get('session').accounts,
currentAccount: undefined, // assume logged out to start
})
const setStateAndPersist = React.useCallback(
(fn: (prev: SessionState) => SessionState) => {
isDirty.current = true
setState(fn)
},
[setState],
)
const upsertAccount = React.useCallback(
(account: SessionAccount, expired = false) => {
setStateAndPersist(s => {
return {
...s,
currentAccount: expired ? undefined : account,
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
}
})
},
[setStateAndPersist],
)
const clearCurrentAccount = React.useCallback(() => {
logger.warn(`session: clear current account`)
__globalAgent = PUBLIC_BSKY_AGENT
setStateAndPersist(s => ({
...s,
currentAccount: undefined,
}))
}, [setStateAndPersist])
const createAccount = React.useCallback<ApiContext['createAccount']>(
async ({
service,
email,
password,
handle,
inviteCode,
verificationPhone,
verificationCode,
}: any) => {
logger.info(`session: creating account`)
track('Try Create Account')
logEvent('account:create:begin', {})
const agent = new BskyAgent({service})
await agent.createAccount({
handle,
password,
email,
inviteCode,
verificationPhone,
verificationCode,
})
if (!agent.session) {
throw new Error(`session: createAccount failed to establish a session`)
}
const fetchingGates = tryFetchGates(
agent.session.did,
'prefer-fresh-gates',
)
const deactivated = isSessionDeactivated(agent.session.accessJwt)
if (!deactivated) {
/*dont await*/ agent.upsertProfile(_existing => {
return {
displayName: '',
// HACKFIX
// creating a bunch of identical profile objects is breaking the relay
// tossing this unspecced field onto it to reduce the size of the problem
// -prf
createdAt: new Date().toISOString(),
}
})
}
const account: SessionAccount = {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email!, // TODO this is always defined?
emailConfirmed: false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
deactivated,
pdsUrl: agent.pdsUrl?.toString(),
}
await configureModeration(agent, account)
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
__globalAgent = agent
await fetchingGates
upsertAccount(account)
logger.debug(`session: created account`, {}, logger.DebugContext.session)
track('Create Account')
logEvent('account:create:success', {})
},
[upsertAccount, clearCurrentAccount],
)
const login = React.useCallback<ApiContext['login']>(
async ({service, identifier, password, authFactorToken}, logContext) => {
logger.debug(`session: login`, {}, logger.DebugContext.session)
const agent = new BskyAgent({service})
await agent.login({identifier, password, authFactorToken})
if (!agent.session) {
throw new Error(`session: login failed to establish a session`)
}
const fetchingGates = tryFetchGates(
agent.session.did,
'prefer-fresh-gates',
)
const account: SessionAccount = {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
emailAuthFactor: agent.session.emailAuthFactor,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
deactivated: isSessionDeactivated(agent.session.accessJwt),
pdsUrl: agent.pdsUrl?.toString(),
}
await configureModeration(agent, account)
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
__globalAgent = agent
// @ts-ignore
if (IS_DEV && isWeb) window.agent = agent
await fetchingGates
upsertAccount(account)
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true})
},
[upsertAccount, clearCurrentAccount],
)
const logout = React.useCallback<ApiContext['logout']>(
async logContext => {
logger.debug(`session: logout`)
clearCurrentAccount()
setStateAndPersist(s => {
return {
...s,
accounts: s.accounts.map(a => ({
...a,
refreshJwt: undefined,
accessJwt: undefined,
})),
}
})
logEvent('account:loggedOut', {logContext})
},
[clearCurrentAccount, setStateAndPersist],
)
const initSession = React.useCallback<ApiContext['initSession']>(
async account => {
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
const agent = new BskyAgent({service: account.service})
// restore the correct PDS URL if available
if (account.pdsUrl) {
agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl)
}
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
// @ts-ignore
if (IS_DEV && isWeb) window.agent = agent
await configureModeration(agent, account)
let canReusePrevSession = false
try {
if (account.accessJwt) {
const decoded = jwtDecode(account.accessJwt)
if (decoded.exp) {
const didExpire = Date.now() >= decoded.exp * 1000
if (!didExpire) {
canReusePrevSession = true
}
}
}
} catch (e) {
logger.error(`session: could not decode jwt`)
}
const prevSession = {
accessJwt: account.accessJwt || '',
refreshJwt: account.refreshJwt || '',
did: account.did,
handle: account.handle,
deactivated:
isSessionDeactivated(account.accessJwt) || account.deactivated,
}
if (canReusePrevSession) {
logger.debug(`session: attempting to reuse previous session`)
agent.session = prevSession
__globalAgent = agent
await fetchingGates
upsertAccount(account)
if (prevSession.deactivated) {
// don't attempt to resume
// use will be taken to the deactivated screen
logger.debug(`session: reusing session for deactivated account`)
return
}
// Intentionally not awaited to unblock the UI:
resumeSessionWithFreshAccount()
.then(freshAccount => {
if (JSON.stringify(account) !== JSON.stringify(freshAccount)) {
logger.info(
`session: reuse of previous session returned a fresh account, upserting`,
)
upsertAccount(freshAccount)
}
})
.catch(e => {
/*
* Note: `agent.persistSession` is also called when this fails, and
* we handle that failure via `createPersistSessionHandler`
*/
logger.info(`session: resumeSessionWithFreshAccount failed`, {
message: e,
})
__globalAgent = PUBLIC_BSKY_AGENT
})
} else {
logger.debug(`session: attempting to resume using previous session`)
try {
const freshAccount = await resumeSessionWithFreshAccount()
__globalAgent = agent
await fetchingGates
upsertAccount(freshAccount)
} catch (e) {
/*
* Note: `agent.persistSession` is also called when this fails, and
* we handle that failure via `createPersistSessionHandler`
*/
logger.info(`session: resumeSessionWithFreshAccount failed`, {
message: e,
})
__globalAgent = PUBLIC_BSKY_AGENT
}
}
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
logger.debug(`session: resumeSessionWithFreshAccount`)
await networkRetry(1, () => agent.resumeSession(prevSession))
/*
* If `agent.resumeSession` fails above, it'll throw. This is just to
* make TypeScript happy.
*/
if (!agent.session) {
throw new Error(`session: initSession failed to establish a session`)
}
// ensure changes in handle/email etc are captured on reload
return {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
emailAuthFactor: agent.session.emailAuthFactor || false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
deactivated: isSessionDeactivated(agent.session.accessJwt),
pdsUrl: agent.pdsUrl?.toString(),
}
}
},
[upsertAccount, clearCurrentAccount],
)
const resumeSession = React.useCallback<ApiContext['resumeSession']>(
async account => {
try {
if (account) {
await initSession(account)
}
} catch (e) {
logger.error(`session: resumeSession failed`, {message: e})
} finally {
setState(s => ({
...s,
isInitialLoad: false,
}))
}
},
[initSession],
)
const removeAccount = React.useCallback<ApiContext['removeAccount']>(
account => {
setStateAndPersist(s => {
return {
...s,
accounts: s.accounts.filter(a => a.did !== account.did),
}
})
},
[setStateAndPersist],
)
const updateCurrentAccount = React.useCallback<
ApiContext['updateCurrentAccount']
>(
account => {
setStateAndPersist(s => {
const currentAccount = s.currentAccount
// ignore, should never happen
if (!currentAccount) return s
const updatedAccount = {
...currentAccount,
handle: account.handle || currentAccount.handle,
email: account.email || currentAccount.email,
emailConfirmed:
account.emailConfirmed !== undefined
? account.emailConfirmed
: currentAccount.emailConfirmed,
emailAuthFactor:
account.emailAuthFactor !== undefined
? account.emailAuthFactor
: currentAccount.emailAuthFactor,
}
return {
...s,
currentAccount: updatedAccount,
accounts: [
updatedAccount,
...s.accounts.filter(a => a.did !== currentAccount.did),
],
}
})
},
[setStateAndPersist],
)
const selectAccount = React.useCallback<ApiContext['selectAccount']>(
async (account, logContext) => {
setState(s => ({...s, isSwitchingAccounts: true}))
try {
await initSession(account)
setState(s => ({...s, isSwitchingAccounts: false}))
logEvent('account:loggedIn', {logContext, withPassword: false})
} catch (e) {
// reset this in case of error
setState(s => ({...s, isSwitchingAccounts: false}))
// but other listeners need a throw
throw e
}
},
[setState, initSession],
)
React.useEffect(() => {
if (isDirty.current) {
isDirty.current = false
persisted.write('session', {
accounts: state.accounts,
currentAccount: state.currentAccount,
})
}
}, [state])
React.useEffect(() => {
return persisted.onUpdate(() => {
const session = persisted.get('session')
logger.debug(`session: persisted onUpdate`, {})
if (session.currentAccount && session.currentAccount.refreshJwt) {
if (session.currentAccount?.did !== state.currentAccount?.did) {
logger.debug(`session: persisted onUpdate, switching accounts`, {
from: {
did: state.currentAccount?.did,
handle: state.currentAccount?.handle,
},
to: {
did: session.currentAccount.did,
handle: session.currentAccount.handle,
},
})
initSession(session.currentAccount)
} else {
logger.debug(`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: persisted onUpdate, logging out`,
{},
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, setState, clearCurrentAccount, initSession])
const stateContext = React.useMemo(
() => ({
...state,
hasSession: !!state.currentAccount,
}),
[state],
)
const api = React.useMemo(
() => ({
createAccount,
login,
logout,
initSession,
resumeSession,
removeAccount,
selectAccount,
updateCurrentAccount,
clearCurrentAccount,
}),
[
createAccount,
login,
logout,
initSession,
resumeSession,
removeAccount,
selectAccount,
updateCurrentAccount,
clearCurrentAccount,
],
)
return (
<StateContext.Provider value={stateContext}>
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
</StateContext.Provider>
)
}
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]})
}
} 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),
)
}
}
}
export function useSession() {
return React.useContext(StateContext)
}
export function useSessionApi() {
return React.useContext(ApiContext)
}
export function useRequireAuth() {
const {hasSession} = useSession()
const closeAll = useCloseAllActiveElements()
const {signinDialogControl} = useGlobalDialogsControlContext()
return React.useCallback(
(fn: () => void) => {
if (hasSession) {
fn()
} else {
closeAll()
signinDialogControl.open()
}
},
[hasSession, signinDialogControl, closeAll],
)
}
export function isSessionDeactivated(accessJwt: string | undefined) {
if (accessJwt) {
const sessData = jwtDecode(accessJwt)
return (
hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated'
)
}
return false
}
export const useAgent = isV2 ? V2.useAgent : V1.useAgent
export const Provider = isV2 ? V2.Provider : V1.Provider
export const useSession = isV2 ? V2.useSession : V1.useSession
export const useSessionApi = isV2 ? V2.useSessionApi : V1.useSessionApi
export const useRequireAuth = isV2 ? V2.useRequireAuth : V1.useRequireAuth
export const isSessionDeactivated = isV2
? V2.isSessionDeactivated
: V1.isSessionDeactivated
+88
View File
@@ -0,0 +1,88 @@
import {BskyAgent} from '@atproto/api'
import {LogEvents} from '#/lib/statsig/statsig'
import {PersistedAccount} from '#/state/persisted'
/**
* Alias for `PersistedAccount` from persisted storage.
*/
export type SessionAccount = PersistedAccount
/**
* Subset of `SessionAccount` that excludes tokens.
*/
export type CurrentAccount = Omit<SessionAccount, 'accessJwt' | 'refreshJwt'>
/**
* Context shape returned from `useSession()`
*/
export type SessionStateContext = {
currentAgent: BskyAgent
isInitialLoad: boolean
isSwitchingAccounts: boolean
hasSession: boolean
accounts: SessionAccount[]
/**
* Contains the full account object persisted to storage, minus access
* tokens.
*/
currentAccount: CurrentAccount | undefined
}
/**
* Context shape returned from `useSessionApi()`
*/
export type SessionApiContext = {
createAccount: (props: {
service: string
email: string
password: string
handle: string
inviteCode?: string
verificationPhone?: string
verificationCode?: string
}) => Promise<void>
login: (
props: {
service: string
identifier: string
password: string
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* A full logout. Clears the `currentAccount` from session, AND removes
* access tokens from all accounts, so that returning as any user will
* require a full login.
*/
logout: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => Promise<void>
/**
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
* clear access tokens from accounts, allowing the user to return to their
* other accounts without logging in.
*
* Used when adding a new account, deleting an account.
*/
clearCurrentAccount: () => void
initSession: (account: SessionAccount) => Promise<void>
resumeSession: (account?: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
selectAccount: (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* Refreshes the BskyAgent's session and derive a fresh `currentAccount`
*/
refreshSession: () => void
/**
* @deprecated Use `refreshSession` instead.
*/
updateCurrentAccount: (
account: Partial<
Pick<SessionAccount, 'handle' | 'email' | 'emailConfirmed'>
>,
) => void
}
+180
View File
@@ -0,0 +1,180 @@
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {jwtDecode} from 'jwt-decode'
import {IS_TEST_USER} from '#/lib/constants'
import {hasProp} from '#/lib/type-guards'
import {logger} from '#/logger'
import * as persisted from '#/state/persisted'
import {readLabelers} from '#/state/session/agent-config'
import {SessionAccount, SessionApiContext} from '#/state/session/types'
export function isSessionDeactivated(accessJwt: string | undefined) {
if (accessJwt) {
const sessData = jwtDecode(accessJwt)
return (
hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated'
)
}
return false
}
export function readLastActiveAccount() {
const {currentAccount, accounts} = persisted.get('session')
return accounts.find(a => a.did === currentAccount?.did)
}
export function agentToSessionAccount(
agent: BskyAgent,
): SessionAccount | undefined {
if (!agent.session) return undefined
return {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed,
deactivated: isSessionDeactivated(agent.session.accessJwt),
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
pdsUrl: agent.pdsUrl?.toString(),
}
}
export function sessionAccountToAgentSession(
account: SessionAccount,
): BskyAgent['session'] {
return {
did: account.did,
handle: account.handle,
email: account.email,
emailConfirmed: account.emailConfirmed,
accessJwt: account.accessJwt || '',
refreshJwt: account.refreshJwt || '',
}
}
export 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]})
}
}
export function isSessionExpired(account: SessionAccount) {
let canReusePrevSession = false
try {
if (account.accessJwt) {
const decoded = jwtDecode(account.accessJwt)
if (decoded.exp) {
const didExpire = Date.now() >= decoded.exp * 1000
if (!didExpire) {
canReusePrevSession = true
}
}
}
} catch (e) {
logger.error(`session: could not decode jwt`)
}
return !canReusePrevSession
}
export async function createAgentAndLogin({
service,
identifier,
password,
}: {
service: string
identifier: string
password: string
}) {
const agent = new BskyAgent({service})
await agent.login({identifier, password})
if (!agent.session) {
throw new Error(`session: login failed to establish a session`)
}
const account = agentToSessionAccount(agent)!
await configureModeration(agent, account)
return {
agent,
account,
}
}
export async function createAgentAndCreateAccount({
service,
email,
password,
handle,
inviteCode,
verificationPhone,
verificationCode,
}: Parameters<SessionApiContext['createAccount']>[0]) {
const agent = new BskyAgent({service})
await agent.createAccount({
handle,
password,
email,
inviteCode,
verificationPhone,
verificationCode,
})
if (!agent.session) {
throw new Error(`session: createAccount failed to establish a session`)
}
const deactivated = isSessionDeactivated(agent.session.accessJwt)
if (!deactivated) {
/*dont await*/ agent.upsertProfile(_existing => {
return {
displayName: '',
// HACKFIX
// creating a bunch of identical profile objects is breaking the relay
// tossing this unspecced field onto it to reduce the size of the problem
// -prf
createdAt: new Date().toISOString(),
}
})
}
const account = agentToSessionAccount(agent)!
await configureModeration(agent, account)
return {
agent,
account,
}
}
+769
View File
@@ -0,0 +1,769 @@
import React from 'react'
import {
AtpPersistSessionHandler,
BSKY_LABELER_DID,
BskyAgent,
} from '@atproto/api'
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 {logEvent, LogEvents, tryFetchGates} 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 {useCloseAllActiveElements} from '#/state/util'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {IS_DEV} from '#/env'
import {emitSessionDropped} from '../events'
import {readLabelers} from './agent-config'
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
export function useAgent() {
return React.useMemo(
() => ({
getAgent() {
return __globalAgent
},
}),
[],
)
}
export type SessionAccount = persisted.PersistedAccount
export type SessionState = {
isInitialLoad: boolean
isSwitchingAccounts: boolean
accounts: SessionAccount[]
currentAccount: SessionAccount | undefined
}
export type StateContext = SessionState & {
hasSession: boolean
}
export type ApiContext = {
createAccount: (props: {
service: string
email: string
password: string
handle: string
inviteCode?: string
verificationPhone?: string
verificationCode?: string
}) => Promise<void>
login: (
props: {
service: string
identifier: string
password: string
authFactorToken?: string | undefined
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* A full logout. Clears the `currentAccount` from session, AND removes
* access tokens from all accounts, so that returning as any user will
* require a full login.
*/
logout: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => Promise<void>
/**
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
* clear access tokens from accounts, allowing the user to return to their
* other accounts without logging in.
*
* Used when adding a new account, deleting an account.
*/
clearCurrentAccount: () => void
initSession: (account: SessionAccount) => Promise<void>
resumeSession: (account?: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
selectAccount: (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
updateCurrentAccount: (
account: Partial<
Pick<
SessionAccount,
'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor'
>
>,
) => void
}
const StateContext = React.createContext<StateContext>({
isInitialLoad: true,
isSwitchingAccounts: false,
accounts: [],
currentAccount: undefined,
hasSession: false,
})
const ApiContext = React.createContext<ApiContext>({
createAccount: async () => {},
login: async () => {},
logout: async () => {},
initSession: async () => {},
resumeSession: async () => {},
removeAccount: () => {},
selectAccount: async () => {},
updateCurrentAccount: () => {},
clearCurrentAccount: () => {},
})
function createPersistSessionHandler(
agent: BskyAgent,
account: SessionAccount,
persistSessionCallback: (props: {
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,
handle: session?.handle || account.handle,
email: session?.email || account.email,
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
deactivated: isSessionDeactivated(session?.accessJwt),
pdsUrl: agent.pdsUrl?.toString(),
/*
* Tokens are undefined if the session expires, or if creation fails for
* any reason e.g. tokens are invalid, network error, etc.
*/
refreshJwt: session?.refreshJwt,
accessJwt: session?.accessJwt,
}
logger.debug(`session: persistSession`, {
event,
deactivated: refreshedAccount.deactivated,
})
if (expired) {
logger.warn(`session: expired`)
emitSessionDropped()
}
/*
* If the session expired, or it was successfully created/updated, we want
* to update/persist the data.
*
* If the session creation failed, it could be a network error, or it could
* be more serious like an invalid token(s). We can't differentiate, so in
* order to allow the user to get a fresh token (if they need it), we need
* to persist this data and wipe their tokens, effectively logging them
* out.
*/
persistSessionCallback({
expired,
refreshedAccount,
})
}
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const isDirty = React.useRef(false)
const [state, setState] = React.useState<SessionState>({
isInitialLoad: true,
isSwitchingAccounts: false,
accounts: persisted.get('session').accounts,
currentAccount: undefined, // assume logged out to start
})
const setStateAndPersist = React.useCallback(
(fn: (prev: SessionState) => SessionState) => {
isDirty.current = true
setState(fn)
},
[setState],
)
const upsertAccount = React.useCallback(
(account: SessionAccount, expired = false) => {
setStateAndPersist(s => {
return {
...s,
currentAccount: expired ? undefined : account,
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
}
})
},
[setStateAndPersist],
)
const clearCurrentAccount = React.useCallback(() => {
logger.warn(`session: clear current account`)
__globalAgent = PUBLIC_BSKY_AGENT
setStateAndPersist(s => ({
...s,
currentAccount: undefined,
}))
}, [setStateAndPersist])
const createAccount = React.useCallback<ApiContext['createAccount']>(
async ({
service,
email,
password,
handle,
inviteCode,
verificationPhone,
verificationCode,
}: any) => {
logger.info(`session: creating account`)
track('Try Create Account')
logEvent('account:create:begin', {})
const agent = new BskyAgent({service})
await agent.createAccount({
handle,
password,
email,
inviteCode,
verificationPhone,
verificationCode,
})
if (!agent.session) {
throw new Error(`session: createAccount failed to establish a session`)
}
const fetchingGates = tryFetchGates(
agent.session.did,
'prefer-fresh-gates',
)
const deactivated = isSessionDeactivated(agent.session.accessJwt)
if (!deactivated) {
/*dont await*/ agent.upsertProfile(_existing => {
return {
displayName: '',
// HACKFIX
// creating a bunch of identical profile objects is breaking the relay
// tossing this unspecced field onto it to reduce the size of the problem
// -prf
createdAt: new Date().toISOString(),
}
})
}
const account: SessionAccount = {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email!, // TODO this is always defined?
emailConfirmed: false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
deactivated,
pdsUrl: agent.pdsUrl?.toString(),
}
await configureModeration(agent, account)
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
__globalAgent = agent
await fetchingGates
upsertAccount(account)
logger.debug(`session: created account`, {}, logger.DebugContext.session)
track('Create Account')
logEvent('account:create:success', {})
},
[upsertAccount, clearCurrentAccount],
)
const login = React.useCallback<ApiContext['login']>(
async ({service, identifier, password, authFactorToken}, logContext) => {
logger.debug(`session: login`, {}, logger.DebugContext.session)
const agent = new BskyAgent({service})
await agent.login({identifier, password, authFactorToken})
if (!agent.session) {
throw new Error(`session: login failed to establish a session`)
}
const fetchingGates = tryFetchGates(
agent.session.did,
'prefer-fresh-gates',
)
const account: SessionAccount = {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
emailAuthFactor: agent.session.emailAuthFactor,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
deactivated: isSessionDeactivated(agent.session.accessJwt),
pdsUrl: agent.pdsUrl?.toString(),
}
await configureModeration(agent, account)
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
__globalAgent = agent
// @ts-ignore
if (IS_DEV && isWeb) window.agent = agent
await fetchingGates
upsertAccount(account)
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true})
},
[upsertAccount, clearCurrentAccount],
)
const logout = React.useCallback<ApiContext['logout']>(
async logContext => {
logger.debug(`session: logout`)
clearCurrentAccount()
setStateAndPersist(s => {
return {
...s,
accounts: s.accounts.map(a => ({
...a,
refreshJwt: undefined,
accessJwt: undefined,
})),
}
})
logEvent('account:loggedOut', {logContext})
},
[clearCurrentAccount, setStateAndPersist],
)
const initSession = React.useCallback<ApiContext['initSession']>(
async account => {
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
const agent = new BskyAgent({service: account.service})
// restore the correct PDS URL if available
if (account.pdsUrl) {
agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl)
}
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
// @ts-ignore
if (IS_DEV && isWeb) window.agent = agent
await configureModeration(agent, account)
let canReusePrevSession = false
try {
if (account.accessJwt) {
const decoded = jwtDecode(account.accessJwt)
if (decoded.exp) {
const didExpire = Date.now() >= decoded.exp * 1000
if (!didExpire) {
canReusePrevSession = true
}
}
}
} catch (e) {
logger.error(`session: could not decode jwt`)
}
const prevSession = {
accessJwt: account.accessJwt || '',
refreshJwt: account.refreshJwt || '',
did: account.did,
handle: account.handle,
deactivated:
isSessionDeactivated(account.accessJwt) || account.deactivated,
}
if (canReusePrevSession) {
logger.debug(`session: attempting to reuse previous session`)
agent.session = prevSession
__globalAgent = agent
await fetchingGates
upsertAccount(account)
if (prevSession.deactivated) {
// don't attempt to resume
// use will be taken to the deactivated screen
logger.debug(`session: reusing session for deactivated account`)
return
}
// Intentionally not awaited to unblock the UI:
resumeSessionWithFreshAccount()
.then(freshAccount => {
if (JSON.stringify(account) !== JSON.stringify(freshAccount)) {
logger.info(
`session: reuse of previous session returned a fresh account, upserting`,
)
upsertAccount(freshAccount)
}
})
.catch(e => {
/*
* Note: `agent.persistSession` is also called when this fails, and
* we handle that failure via `createPersistSessionHandler`
*/
logger.info(`session: resumeSessionWithFreshAccount failed`, {
message: e,
})
__globalAgent = PUBLIC_BSKY_AGENT
})
} else {
logger.debug(`session: attempting to resume using previous session`)
try {
const freshAccount = await resumeSessionWithFreshAccount()
__globalAgent = agent
await fetchingGates
upsertAccount(freshAccount)
} catch (e) {
/*
* Note: `agent.persistSession` is also called when this fails, and
* we handle that failure via `createPersistSessionHandler`
*/
logger.info(`session: resumeSessionWithFreshAccount failed`, {
message: e,
})
__globalAgent = PUBLIC_BSKY_AGENT
}
}
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
logger.debug(`session: resumeSessionWithFreshAccount`)
await networkRetry(1, () => agent.resumeSession(prevSession))
/*
* If `agent.resumeSession` fails above, it'll throw. This is just to
* make TypeScript happy.
*/
if (!agent.session) {
throw new Error(`session: initSession failed to establish a session`)
}
// ensure changes in handle/email etc are captured on reload
return {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
emailAuthFactor: agent.session.emailAuthFactor || false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
deactivated: isSessionDeactivated(agent.session.accessJwt),
pdsUrl: agent.pdsUrl?.toString(),
}
}
},
[upsertAccount, clearCurrentAccount],
)
const resumeSession = React.useCallback<ApiContext['resumeSession']>(
async account => {
try {
if (account) {
await initSession(account)
}
} catch (e) {
logger.error(`session: resumeSession failed`, {message: e})
} finally {
setState(s => ({
...s,
isInitialLoad: false,
}))
}
},
[initSession],
)
const removeAccount = React.useCallback<ApiContext['removeAccount']>(
account => {
setStateAndPersist(s => {
return {
...s,
accounts: s.accounts.filter(a => a.did !== account.did),
}
})
},
[setStateAndPersist],
)
const updateCurrentAccount = React.useCallback<
ApiContext['updateCurrentAccount']
>(
account => {
setStateAndPersist(s => {
const currentAccount = s.currentAccount
// ignore, should never happen
if (!currentAccount) return s
const updatedAccount = {
...currentAccount,
handle: account.handle || currentAccount.handle,
email: account.email || currentAccount.email,
emailConfirmed:
account.emailConfirmed !== undefined
? account.emailConfirmed
: currentAccount.emailConfirmed,
emailAuthFactor:
account.emailAuthFactor !== undefined
? account.emailAuthFactor
: currentAccount.emailAuthFactor,
}
return {
...s,
currentAccount: updatedAccount,
accounts: [
updatedAccount,
...s.accounts.filter(a => a.did !== currentAccount.did),
],
}
})
},
[setStateAndPersist],
)
const selectAccount = React.useCallback<ApiContext['selectAccount']>(
async (account, logContext) => {
setState(s => ({...s, isSwitchingAccounts: true}))
try {
await initSession(account)
setState(s => ({...s, isSwitchingAccounts: false}))
logEvent('account:loggedIn', {logContext, withPassword: false})
} catch (e) {
// reset this in case of error
setState(s => ({...s, isSwitchingAccounts: false}))
// but other listeners need a throw
throw e
}
},
[setState, initSession],
)
React.useEffect(() => {
if (isDirty.current) {
isDirty.current = false
persisted.write('session', {
accounts: state.accounts,
currentAccount: state.currentAccount,
})
}
}, [state])
React.useEffect(() => {
return persisted.onUpdate(() => {
const session = persisted.get('session')
logger.debug(`session: persisted onUpdate`, {})
if (session.currentAccount && session.currentAccount.refreshJwt) {
if (session.currentAccount?.did !== state.currentAccount?.did) {
logger.debug(`session: persisted onUpdate, switching accounts`, {
from: {
did: state.currentAccount?.did,
handle: state.currentAccount?.handle,
},
to: {
did: session.currentAccount.did,
handle: session.currentAccount.handle,
},
})
initSession(session.currentAccount)
} else {
logger.debug(`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: persisted onUpdate, logging out`,
{},
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, setState, clearCurrentAccount, initSession])
const stateContext = React.useMemo(
() => ({
...state,
hasSession: !!state.currentAccount,
}),
[state],
)
const api = React.useMemo(
() => ({
createAccount,
login,
logout,
initSession,
resumeSession,
removeAccount,
selectAccount,
updateCurrentAccount,
clearCurrentAccount,
}),
[
createAccount,
login,
logout,
initSession,
resumeSession,
removeAccount,
selectAccount,
updateCurrentAccount,
clearCurrentAccount,
],
)
return (
<StateContext.Provider value={stateContext}>
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
</StateContext.Provider>
)
}
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]})
}
} 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),
)
}
}
}
export function useSession() {
return React.useContext(StateContext)
}
export function useSessionApi() {
return React.useContext(ApiContext)
}
export function useRequireAuth() {
const {hasSession} = useSession()
const closeAll = useCloseAllActiveElements()
const {signinDialogControl} = useGlobalDialogsControlContext()
return React.useCallback(
(fn: () => void) => {
if (hasSession) {
fn()
} else {
closeAll()
signinDialogControl.open()
}
},
[hasSession, signinDialogControl, closeAll],
)
}
export function isSessionDeactivated(accessJwt: string | undefined) {
if (accessJwt) {
const sessData = jwtDecode(accessJwt)
return (
hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated'
)
}
return false
}
+565
View File
@@ -0,0 +1,565 @@
import React from 'react'
import {BskyAgent} from '@atproto/api'
import {track} from '#/lib/analytics/analytics'
import {networkRetry} from '#/lib/async/retry'
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
import {
SessionAccount,
SessionApiContext,
SessionStateContext,
} from '#/state/session/types'
import {
agentToSessionAccount,
configureModeration,
createAgentAndCreateAccount,
createAgentAndLogin,
isSessionExpired,
sessionAccountToAgentSession,
} from '#/state/session/util'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import * as Toast from '#/view/com/util/Toast'
import {IS_DEV} from '#/env'
import {emitSessionDropped} from '../events'
export type {CurrentAccount, SessionAccount} from '#/state/session/types'
export {isSessionDeactivated} from '#/state/session/util'
/**
* 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})
const StateContext = React.createContext<SessionStateContext>({
currentAgent: INITIAL_AGENT,
isInitialLoad: true,
isSwitchingAccounts: false,
accounts: [],
currentAccount: undefined,
hasSession: false,
})
const ApiContext = React.createContext<SessionApiContext>({
createAccount: async () => {},
login: async () => {},
logout: async () => {},
initSession: async () => {},
resumeSession: async () => {},
removeAccount: () => {},
selectAccount: async () => {},
refreshSession: () => {},
clearCurrentAccount: () => {},
updateCurrentAccount: async () => {},
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const isDirty = React.useRef(false)
const [currentAgent, setCurrentAgent] =
React.useState<BskyAgent>(INITIAL_AGENT)
const [accounts, setAccounts] = React.useState<SessionAccount[]>(
persisted.get('session').accounts,
)
const [isInitialLoad, setIsInitialLoad] = React.useState(true)
const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false)
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),
[],
)
const upsertAndPersistAccount = React.useCallback(
(account: SessionAccount) => {
persistNextUpdate()
setAccounts(accounts => [
account,
...accounts.filter(a => a.did !== account.did),
])
},
[setAccounts, persistNextUpdate],
)
const clearCurrentAccount = React.useCallback(() => {
logger.warn(`session: clear current account`)
// immediate clear this so any pending requests don't use it
currentAgent.setPersistSessionHandler(() => {})
persistNextUpdate()
const newAgent = new BskyAgent({service: PUBLIC_BSKY_SERVICE})
setCurrentAgent(newAgent)
configureModeration(newAgent)
}, [currentAgent, persistNextUpdate, setCurrentAgent])
React.useEffect(() => {
/*
* This method is continually overwritten when `currentAgent` and dependent
* methods local to this file change, so that the freshest agent and
* handlers are always used.
*/
currentAgent.setPersistSessionHandler(event => {
logger.debug(
`session: persistSession`,
{event},
logger.DebugContext.session,
)
const expired = event === 'expired' || event === 'create-failed'
/*
* Special case for a network error that occurs when calling
* `resumeSession`, which happens on page load, when switching
* accounts, or when refreshing user session data.
*
* When this occurs, we drop the user back out to the login screen, but
* we don't clear tokens, allowing them to quickly log back in when their
* connection improves.
*/
if (event === 'network-error') {
logger.warn(
`session: persistSessionHandler received network-error event`,
)
emitSessionDropped()
clearCurrentAccount()
setTimeout(() => {
Toast.show(`Your internet connection is unstable. Please try again.`)
}, 100)
return
}
/*
* If the session was expired naturally, we want to drop the user back
* out to log in.
*/
if (expired) {
logger.warn(`session: expired`)
emitSessionDropped()
clearCurrentAccount()
setTimeout(() => {
Toast.show(`Sorry! We need you to enter your password.`)
}, 100)
}
/**
* The updated account object, derived from the updated session we just
* received from this callback.
*/
const refreshedAccount = agentToSessionAccount(currentAgent)
if (refreshedAccount) {
/*
* If the session expired naturally, or it was otherwise successfully
* created/updated, we want to update/persist the data.
*/
upsertAndPersistAccount(refreshedAccount)
} else {
/*
* This should never happen based on current `AtpAgent` handling, but
* it's here for TypeScript, and should result in the same handling as
* a session expiration.
*/
logger.error(`session: persistSession failed to get refreshed account`)
emitSessionDropped()
clearCurrentAccount()
setTimeout(() => {
Toast.show(`Sorry! We need you to enter your password.`)
}, 100)
}
})
}, [currentAgent, clearCurrentAccount, upsertAndPersistAccount])
const createAccount = React.useCallback<SessionApiContext['createAccount']>(
async ({
service,
email,
password,
handle,
inviteCode,
verificationPhone,
verificationCode,
}: any) => {
logger.info(`session: creating account`)
track('Try Create Account')
logEvent('account:create:begin', {})
const {agent, account} = await createAgentAndCreateAccount({
service,
handle,
password,
email,
inviteCode,
verificationPhone,
verificationCode,
})
setCurrentAgent(agent)
upsertAndPersistAccount(account)
logger.debug(`session: created account`, {}, logger.DebugContext.session)
track('Create Account')
logEvent('account:create:success', {})
},
[upsertAndPersistAccount],
)
const login = React.useCallback<SessionApiContext['login']>(
async ({service, identifier, password}, logContext) => {
logger.debug(`session: login`, {}, logger.DebugContext.session)
const {agent, account} = await createAgentAndLogin({
service,
identifier,
password,
})
setCurrentAgent(agent)
upsertAndPersistAccount(account)
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true})
},
[upsertAndPersistAccount],
)
const logout = React.useCallback<SessionApiContext['logout']>(
async logContext => {
logger.debug(`session: logout`)
clearCurrentAccount()
persistNextUpdate()
setAccounts(accounts =>
accounts.map(a => ({
...a,
accessJwt: undefined,
refreshJwt: undefined,
})),
)
logEvent('account:loggedOut', {logContext})
},
[clearCurrentAccount, persistNextUpdate, setAccounts],
)
const initSession = React.useCallback<SessionApiContext['initSession']>(
async account => {
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
const newAgent = new BskyAgent({
service: account.service,
})
// restore the correct PDS URL if available
if (account.pdsUrl) {
newAgent.pdsUrl = newAgent.api.xrpc.uri = new URL(account.pdsUrl)
}
const prevSession = {
...account,
accessJwt: account.accessJwt || '',
refreshJwt: account.refreshJwt || '',
}
/**
* Optimistically update moderation services so that when the new agent
* is applied, they're ready.
*
* If session resumption fails, this will be reset by
* `clearCurrentAccount`.
*/
await configureModeration(newAgent, account)
if (isSessionExpired(account)) {
/*
* If session is expired, attempt to refresh the session using the
* refresh token via `resumeSession`
*/
logger.debug(
`session: attempting to resumeSession using previous session`,
{},
logger.DebugContext.session,
)
await networkRetry(1, () => newAgent.resumeSession(prevSession))
setCurrentAgent(newAgent)
upsertAndPersistAccount(agentToSessionAccount(newAgent)!)
} else {
/*
* If the session is not expired, assume we can reuse it.
*/
logger.debug(
`session: attempting to reuse previous session`,
{},
logger.DebugContext.session,
)
newAgent.session = prevSession
setCurrentAgent(newAgent)
upsertAndPersistAccount(account)
}
},
[upsertAndPersistAccount],
)
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
async account => {
try {
if (account) {
await initSession(account)
}
} catch (e) {
logger.error(`session: resumeSession failed`, {message: e})
} finally {
setIsInitialLoad(false)
}
},
[initSession, setIsInitialLoad],
)
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => {
persistNextUpdate()
setAccounts(accounts => accounts.filter(a => a.did !== account.did))
},
[setAccounts, persistNextUpdate],
)
const refreshSession = React.useCallback<
SessionApiContext['refreshSession']
>(async () => {
const {accounts: persistedAccounts} = persisted.get('session')
const selectedAccount = persistedAccounts.find(
a => a.did === currentAccountDid,
)
if (!selectedAccount) return
// update and swap agent to trigger render refresh
const newAgent = currentAgent.clone()
await newAgent.resumeSession(sessionAccountToAgentSession(selectedAccount)!)
const refreshedAccount = agentToSessionAccount(newAgent)
persistNextUpdate()
upsertAndPersistAccount(refreshedAccount!)
setCurrentAgent(newAgent)
configureModeration(newAgent, refreshedAccount)
}, [
currentAccountDid,
currentAgent,
setCurrentAgent,
persistNextUpdate,
upsertAndPersistAccount,
])
const updateCurrentAccount = React.useCallback(async () => {
await refreshSession()
}, [refreshSession])
const selectAccount = React.useCallback<SessionApiContext['selectAccount']>(
async (account, logContext) => {
setIsSwitchingAccounts(true)
try {
await initSession(account)
setIsSwitchingAccounts(false)
logEvent('account:loggedIn', {logContext, withPassword: false})
} catch (e) {
// reset this in case of error
setIsSwitchingAccounts(false)
// but other listeners need a throw
throw e
}
},
[setIsSwitchingAccounts, initSession],
)
React.useEffect(() => {
if (isDirty.current) {
isDirty.current = false
persisted.write('session', {
accounts,
currentAccount,
})
}
}, [accounts, currentAccount])
React.useEffect(() => {
return persisted.onUpdate(async () => {
const persistedSession = persisted.get('session')
logger.debug(
`session: persisted onUpdate`,
{},
logger.DebugContext.session,
)
/*
* Accounts are already persisted on other side of broadcast, but we need
* to update them in memory in this tab.
*/
setAccounts(persistedSession.accounts)
const selectedAccount = persistedSession.accounts.find(
a => a.did === persistedSession.currentAccount?.did,
)
if (selectedAccount && selectedAccount.refreshJwt) {
if (selectedAccount?.did !== currentAccountDid) {
logger.debug(
`session: persisted onUpdate, switching accounts`,
{
from: {
did: currentAccountDid,
},
to: {
did: selectedAccount.did,
},
},
logger.DebugContext.session,
)
await initSession(selectedAccount)
} else {
logger.debug(
`session: persisted onUpdate, updating session`,
{},
logger.DebugContext.session,
)
/*
* Create a new agent for the same account, with updated data from
* other side of broadcast. Update on state to re-derive
* `currentAccount` and re-render the app.
*/
const newAgent = currentAgent.clone()
newAgent.session = sessionAccountToAgentSession(selectedAccount)
configureModeration(newAgent, selectedAccount)
setCurrentAgent(newAgent)
}
} else if (!selectedAccount && currentAccountDid) {
logger.debug(
`session: persisted onUpdate, logging out`,
{},
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()
}
})
}, [
currentAccountDid,
setAccounts,
clearCurrentAccount,
initSession,
currentAgent,
setCurrentAgent,
])
const stateContext = React.useMemo(
() => ({
currentAgent,
isInitialLoad,
isSwitchingAccounts,
currentAccount,
accounts,
hasSession: Boolean(currentAccount),
}),
[
currentAgent,
isInitialLoad,
isSwitchingAccounts,
accounts,
currentAccount,
],
)
const api = React.useMemo(
() => ({
createAccount,
login,
logout,
initSession,
resumeSession,
removeAccount,
selectAccount,
refreshSession,
clearCurrentAccount,
updateCurrentAccount,
}),
[
createAccount,
login,
logout,
initSession,
resumeSession,
removeAccount,
selectAccount,
refreshSession,
clearCurrentAccount,
updateCurrentAccount,
],
)
if (IS_DEV && isWeb) {
// @ts-ignore
window.agent = currentAgent
}
return (
<StateContext.Provider value={stateContext}>
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
</StateContext.Provider>
)
}
export function useSession() {
return React.useContext(StateContext)
}
export function useSessionApi() {
return React.useContext(ApiContext)
}
export function useRequireAuth() {
const {hasSession} = useSession()
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAll = useCloseAllActiveElements()
return React.useCallback(
(fn: () => void) => {
if (hasSession) {
fn()
} else {
closeAll()
setShowLoggedOut(true)
}
},
[hasSession, setShowLoggedOut, closeAll],
)
}
export function useAgent() {
const {currentAgent} = useSession()
return React.useMemo(
() => ({
getAgent() {
return currentAgent
},
}),
[currentAgent],
)
}
+2 -1
View File
@@ -32,7 +32,7 @@ import {
import {useProfileQuery} from '#/state/queries/profile'
import {Gif} from '#/state/queries/tenor'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
import * as apilib from 'lib/api/index'
@@ -83,6 +83,7 @@ export const ComposePost = observer(function ComposePost({
imageUris: initImageUris,
}: Props) {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
const {isModalActive} = useModals()
const {closeComposer} = useComposerControls()
@@ -1,12 +1,14 @@
import {useState, useEffect} from 'react'
import {useEffect, useState} from 'react'
import {useAgent} from '#/state/session'
import * as apilib from 'lib/api/index'
import {getLinkMeta} from 'lib/link-meta/link-meta'
import {ComposerOpts} from 'state/shell/composer'
import {getAgent} from '#/state/session'
export function useExternalLinkFetch({}: {
setQuote: (opts: ComposerOpts['quote']) => void
}) {
const {getAgent} = useAgent()
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
undefined,
)
@@ -39,7 +41,7 @@ export function useExternalLinkFetch({}: {
})
}
return cleanup
}, [extLink])
}, [extLink, getAgent])
return {extLink, setExtLink}
}
+13 -11
View File
@@ -1,24 +1,25 @@
import {useState, useEffect} from 'react'
import {ImageModel} from 'state/models/media/image'
import {useEffect, useState} from 'react'
import {logger} from '#/logger'
import {useFetchDid} from '#/state/queries/handle'
import {useGetPost} from '#/state/queries/post'
import {useAgent} from '#/state/session'
import * as apilib from 'lib/api/index'
import {getLinkMeta} from 'lib/link-meta/link-meta'
import {POST_IMG_MAX} from 'lib/constants'
import {
getPostAsQuote,
getFeedAsEmbed,
getListAsEmbed,
getPostAsQuote,
} from 'lib/link-meta/bsky'
import {getLinkMeta} from 'lib/link-meta/link-meta'
import {downloadAndResize} from 'lib/media/manip'
import {
isBskyPostUrl,
isBskyCustomFeedUrl,
isBskyListUrl,
isBskyPostUrl,
} from 'lib/strings/url-helpers'
import {ImageModel} from 'state/models/media/image'
import {ComposerOpts} from 'state/shell/composer'
import {POST_IMG_MAX} from 'lib/constants'
import {logger} from '#/logger'
import {getAgent} from '#/state/session'
import {useGetPost} from '#/state/queries/post'
import {useFetchDid} from '#/state/queries/handle'
export function useExternalLinkFetch({
setQuote,
@@ -30,6 +31,7 @@ export function useExternalLinkFetch({
)
const getPost = useGetPost()
const fetchDid = useFetchDid()
const {getAgent} = useAgent()
useEffect(() => {
let aborted = false
@@ -135,7 +137,7 @@ export function useExternalLinkFetch({
})
}
return cleanup
}, [extLink, setQuote, getPost, fetchDid])
}, [extLink, setQuote, getPost, fetchDid, getAgent])
return {extLink, setExtLink}
}
+13 -11
View File
@@ -1,19 +1,20 @@
import React, {useState} from 'react'
import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
import {ScrollView, TextInput} from './util'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {ErrorMessage} from '../util/error/ErrorMessage'
import * as Toast from '../util/Toast'
import {s, colors} from 'lib/styles'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useSession, useSessionApi, getAgent} from '#/state/session'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {ScrollView, TextInput} from './util'
enum Stages {
InputEmail,
@@ -26,6 +27,7 @@ export const snapPoints = ['90%']
export function Component() {
const pal = usePalette('default')
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {updateCurrentAccount} = useSessionApi()
const {_} = useLingui()
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
+2 -1
View File
@@ -16,8 +16,8 @@ import {useModalControls} from '#/state/modals'
import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {useServiceQuery} from '#/state/queries/service'
import {
getAgent,
SessionAccount,
useAgent,
useSession,
useSessionApi,
} from '#/state/session'
@@ -40,6 +40,7 @@ export type Props = {onChanged: () => void}
export function Component(props: Props) {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {
isLoading,
data: serviceInfo,
+15 -13
View File
@@ -6,24 +6,25 @@ import {
TouchableOpacity,
View,
} from 'react-native'
import {ScrollView} from './util'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {TextInput} from './util'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {s, colors} from 'lib/styles'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useAgent, useSession} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {isAndroid, isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError, isNetworkError} from 'lib/strings/errors'
import {checkAndFormatResetCode} from 'lib/strings/password'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useSession, getAgent} from '#/state/session'
import * as EmailValidator from 'email-validator'
import {logger} from '#/logger'
import {colors, s} from 'lib/styles'
import {isAndroid, isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import {ScrollView} from './util'
import {TextInput} from './util'
enum Stages {
RequestCode,
@@ -36,6 +37,7 @@ export const snapPoints = isAndroid ? ['90%'] : ['45%']
export function Component() {
const pal = usePalette('default')
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {_} = useLingui()
const [stage, setStage] = useState<Stages>(Stages.RequestCode)
const [isProcessing, setIsProcessing] = useState<boolean>(false)
+3 -1
View File
@@ -25,7 +25,7 @@ import {
useListCreateMutation,
useListMetadataMutation,
} from '#/state/queries/list'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -62,6 +62,7 @@ export function Component({
const {_} = useLingui()
const listCreateMutation = useListCreateMutation()
const listMetadataMutation = useListMetadataMutation()
const {getAgent} = useAgent()
const activePurpose = useMemo(() => {
if (list?.purpose) {
@@ -228,6 +229,7 @@ export function Component({
listMetadataMutation,
listCreateMutation,
_,
getAgent,
])
return (
+2 -1
View File
@@ -11,7 +11,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
@@ -30,6 +30,7 @@ export function Component({}: {}) {
const pal = usePalette('default')
const theme = useTheme()
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {clearCurrentAccount, removeAccount} = useSessionApi()
const {_} = useLingui()
const {closeModal} = useModalControls()
+2 -1
View File
@@ -13,7 +13,7 @@ import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
@@ -41,6 +41,7 @@ export function Component({
onSuccess?: () => void
}) {
const pal = usePalette('default')
const {getAgent} = useAgent()
const {currentAccount} = useSession()
const {updateCurrentAccount} = useSessionApi()
const {_} = useLingui()
+3 -2
View File
@@ -19,7 +19,7 @@ import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {useModerationOpts} from '#/state/queries/preferences'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
@@ -472,6 +472,7 @@ function ProfileScreenLoaded({
}
function useRichText(text: string): [RichTextAPI, boolean] {
const {getAgent} = useAgent()
const [prevText, setPrevText] = React.useState(text)
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
@@ -495,7 +496,7 @@ function useRichText(text: string): [RichTextAPI, boolean] {
return () => {
ignore = true
}
}, [text])
}, [text, getAgent])
const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving]
}
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {isNative} from '#/platform/detection'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
@@ -31,6 +31,7 @@ export function DisableEmail2FADialog({
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const {updateCurrentAccount} = useSessionApi()
const {getAgent} = useAgent()
const [stage, setStage] = useState<Stages>(Stages.Email)
const [confirmationCode, setConfirmationCode] = useState<string>('')
+3 -2
View File
@@ -3,7 +3,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {useDialogControl} from '#/components/Dialog'
import {DisableEmail2FADialog} from './DisableEmail2FADialog'
@@ -14,6 +14,7 @@ export function Email2FAToggle() {
const {updateCurrentAccount} = useSessionApi()
const {openModal} = useModalControls()
const disableDialogCtrl = useDialogControl()
const {getAgent} = useAgent()
const enableEmailAuthFactor = React.useCallback(async () => {
if (currentAccount?.email) {
@@ -25,7 +26,7 @@ export function Email2FAToggle() {
emailAuthFactor: true,
})
}
}, [currentAccount, updateCurrentAccount])
}, [currentAccount, updateCurrentAccount, getAgent])
const onToggle = React.useCallback(() => {
if (!currentAccount) {
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -19,6 +19,7 @@ export function ExportCarDialog({
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const downloadUrl = React.useMemo(() => {
const agent = getAgent()
@@ -30,7 +31,7 @@ export function ExportCarDialog({
url.pathname = '/xrpc/com.atproto.sync.getRepo'
url.searchParams.set('did', agent.session.did)
return url.toString()
}, [currentAccount])
}, [currentAccount, getAgent])
return (
<Dialog.Outer control={control}>
+11 -4
View File
@@ -13,7 +13,7 @@ import * as NavigationBar from 'expo-navigation-bar'
import {StatusBar} from 'expo-status-bar'
import {useNavigationState} from '@react-navigation/native'
import {useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {
useIsDrawerOpen,
useIsDrawerSwipeDisabled,
@@ -57,6 +57,7 @@ function ShellInner() {
)
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
const {hasSession, currentAccount} = useSession()
const {getAgent} = useAgent()
const closeAnyActiveElement = useCloseAnyActiveElement()
const {importantForAccessibility} = useDialogStateContext()
// start undefined
@@ -78,11 +79,17 @@ function ShellInner() {
// only runs when did changes
if (currentAccount && currentAccountDid.current !== currentAccount.did) {
currentAccountDid.current = currentAccount.did
notifications.requestPermissionsAndRegisterToken(currentAccount)
const unsub = notifications.registerTokenChangeHandler(currentAccount)
notifications.requestPermissionsAndRegisterToken(
getAgent(),
currentAccount,
)
const unsub = notifications.registerTokenChangeHandler(
getAgent(),
currentAccount,
)
return unsub
}
}, [currentAccount])
}, [currentAccount, getAgent])
return (
<>