Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

feat: custom AppView support again

This reverts commit cccfbd0b7b1b42da3b8c9fa27353542aec283b69

it's in the same state as before, but in my limited testing, there were no bugs. definitely still needs improvement.

xan.lol b8410bff b39e0799

verified
+362 -64
+3 -2
src/components/PostControls/PostMenu/PostMenuItems.tsx
··· 17 17 type AppBskyFeedThreadgate, 18 18 AtUri, 19 19 type BlobRef, 20 + isDid, 20 21 type RichText as RichTextAPI, 21 22 } from '@atproto/api' 22 23 import {plural} from '@lingui/core/macro' ··· 605 606 if (!videoEmbed) return 606 607 const did = post.author.did 607 608 const cid = videoEmbed.cid 608 - if (!did.startsWith('did:')) return 609 - const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}`) 609 + if (!isDid(did)) return 610 + const pdsUrl = await resolvePdsServiceUrl(did) 610 611 const uri = `${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}` 611 612 612 613 Toast.show(l({message: 'Downloading video...', context: 'toast'}))
+2 -1
src/components/dialogs/EmailDialog/data/useConfirmEmail.ts
··· 1 1 import {useMutation} from '@tanstack/react-query' 2 2 3 3 import {useAgent, useSession} from '#/state/session' 4 + import {pdsAgent} from '#/state/session/agent' 4 5 5 6 export function useConfirmEmail({ 6 7 onSuccess, ··· 15 16 throw new Error('No email found for the current account') 16 17 } 17 18 18 - await agent.com.atproto.server.confirmEmail({ 19 + await pdsAgent(agent).com.atproto.server.confirmEmail({ 19 20 email: currentAccount.email.trim(), 20 21 token: token.trim(), 21 22 })
+2 -1
src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts
··· 1 1 import {useMutation} from '@tanstack/react-query' 2 2 3 3 import {useAgent, useSession} from '#/state/session' 4 + import {pdsAgent} from '#/state/session/agent' 4 5 5 6 export function useManageEmail2FA() { 6 7 const agent = useAgent() ··· 17 18 throw new Error('No email found for the current account') 18 19 } 19 20 20 - await agent.com.atproto.server.updateEmail({ 21 + await pdsAgent(agent).com.atproto.server.updateEmail({ 21 22 email: currentAccount.email, 22 23 emailAuthFactor: enabled, 23 24 token,
+3 -1
src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts
··· 1 1 import {useMutation} from '@tanstack/react-query' 2 2 3 3 import {useAgent} from '#/state/session' 4 + import {pdsAgent} from '#/state/session/agent' 4 5 5 6 export function useRequestEmailUpdate() { 6 7 const agent = useAgent() 7 8 8 9 return useMutation({ 9 10 mutationFn: async () => { 10 - return (await agent.com.atproto.server.requestEmailUpdate()).data 11 + return (await pdsAgent(agent).com.atproto.server.requestEmailUpdate()) 12 + .data 11 13 }, 12 14 }) 13 15 }
+2 -1
src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts
··· 1 1 import {useMutation} from '@tanstack/react-query' 2 2 3 3 import {useAgent} from '#/state/session' 4 + import {pdsAgent} from '#/state/session/agent' 4 5 5 6 export function useRequestEmailVerification() { 6 7 const agent = useAgent() 7 8 8 9 return useMutation({ 9 10 mutationFn: async () => { 10 - await agent.com.atproto.server.requestEmailConfirmation() 11 + await pdsAgent(agent).com.atproto.server.requestEmailConfirmation() 11 12 }, 12 13 }) 13 14 }
+5 -1
src/components/dialogs/EmailDialog/data/useUpdateEmail.ts
··· 1 1 import {useMutation} from '@tanstack/react-query' 2 2 3 3 import {useAgent} from '#/state/session' 4 + import {pdsAgent} from '#/state/session/agent' 4 5 import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate' 5 6 6 7 async function updateEmailAndRefreshSession( ··· 8 9 email: string, 9 10 token?: string, 10 11 ) { 11 - await agent.com.atproto.server.updateEmail({email: email.trim(), token}) 12 + await pdsAgent(agent).com.atproto.server.updateEmail({ 13 + email: email.trim(), 14 + token, 15 + }) 12 16 await agent.resumeSession(agent.session!) 13 17 } 14 18
+2 -1
src/components/intents/VerifyEmailIntentDialog.tsx
··· 5 5 import {Trans} from '@lingui/react/macro' 6 6 7 7 import {useAgent, useSession} from '#/state/session' 8 + import {pdsAgent} from '#/state/session/agent' 8 9 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 9 10 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 10 11 import * as Dialog from '#/components/Dialog' ··· 52 53 53 54 const onPressResendEmail = async () => { 54 55 setSending(true) 55 - await agent.com.atproto.server.requestEmailConfirmation() 56 + await pdsAgent(agent).com.atproto.server.requestEmailConfirmation() 56 57 setSending(false) 57 58 setStatus('resent') 58 59 }
+6
src/env/common.ts
··· 141 141 export const APP_CONFIG_URL = IS_DEV 142 142 ? (APP_CONFIG_DEV_URL ?? APP_CONFIG_PROD_URL) 143 143 : APP_CONFIG_PROD_URL 144 + 145 + export const ENV_PUBLIC_BSKY_SERVICE: string | undefined = 146 + process.env.EXPO_PUBLIC_PUBLIC_BSKY_SERVICE 147 + export const ENV_APPVIEW_DID_PROXY: 148 + | `did:${string}:${string}#bsky_appview` 149 + | undefined = process.env.EXPO_PUBLIC_APPVIEW_DID_PROXY
+3 -2
src/lib/api/feed/custom.ts
··· 5 5 jsonStringToLex, 6 6 } from '@atproto/api' 7 7 8 + import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' 8 9 import { 9 10 getAppLanguageAsContentLanguage, 10 11 getContentLanguages, ··· 120 121 121 122 // manually construct fetch call so we can add the `lang` cache-busting param 122 123 let res = await fetch( 123 - `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ 124 + `${PUBLIC_BSKY_SERVICE}/xrpc/app.bsky.feed.getFeed?feed=${feed}${ 124 125 cursor ? `&cursor=${cursor}` : '' 125 126 }&limit=${limit}&lang=${contentLangs}`, 126 127 { ··· 140 141 141 142 // no data, try again with language headers removed 142 143 res = await fetch( 143 - `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ 144 + `${PUBLIC_BSKY_SERVICE}/xrpc/app.bsky.feed.getFeed?feed=${feed}${ 144 145 cursor ? `&cursor=${cursor}` : '' 145 146 }&limit=${limit}`, 146 147 {method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}},
+2 -1
src/lib/api/index.ts
··· 38 38 createThreadgateRecord, 39 39 threadgateAllowUISettingToAllowRecordValue, 40 40 } from '#/state/queries/threadgate' 41 + import {pdsAgent} from '#/state/session/agent' 41 42 import { 42 43 type EmbedDraft, 43 44 type PostDraft, ··· 176 177 } 177 178 178 179 try { 179 - await agent.com.atproto.repo.applyWrites({ 180 + await pdsAgent(agent).com.atproto.repo.applyWrites({ 180 181 repo: agent.assertDid, 181 182 writes: writes, 182 183 validate: true,
+9 -3
src/lib/constants.ts
··· 2 2 import {type AppBskyActorDefs, BSKY_LABELER_DID} from '@atproto/api' 3 3 4 4 import {type ProxyHeaderValue} from '#/state/session/agent' 5 - import {BLUESKY_PROXY_DID, CHAT_PROXY_DID} from '#/env' 6 - 5 + import { 6 + BLUESKY_PROXY_DID, 7 + CHAT_PROXY_DID, 8 + ENV_APPVIEW_DID_PROXY, 9 + ENV_PUBLIC_BSKY_SERVICE, 10 + } from '#/env' 7 11 export const LOCAL_DEV_SERVICE = 8 12 Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' 9 13 export const STAGING_SERVICE = 'https://staging.bsky.dev' 10 14 export const BSKY_SERVICE = 'https://bsky.social' 11 15 export const BSKY_SERVICE_DID = 'did:web:bsky.social' 12 - export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app' 16 + export const PUBLIC_BSKY_SERVICE = 17 + ENV_PUBLIC_BSKY_SERVICE || 'https://public.api.bsky.app' 13 18 export const DEFAULT_SERVICE = BSKY_SERVICE 14 19 export const HELP_DESK_URL = `https://tangled.org/jollywhoppers.com/witchsky.app/` 15 20 export const EMBED_SERVICE = 'https://embed.bsky.app' 16 21 export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` 17 22 export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download' 23 + export const APPVIEW_DID_PROXY = ENV_APPVIEW_DID_PROXY 18 24 export const STARTER_PACK_MAX_SIZE = 150 19 25 export const CARD_ASPECT_RATIO = 1200 / 630 20 26
+2 -1
src/lib/generate-starterpack.ts
··· 15 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 16 import {enforceLen} from '#/lib/strings/helpers' 17 17 import {useAgent} from '#/state/session' 18 + import {pdsAgent} from '#/state/session/agent' 18 19 import type * as bsky from '#/types/bsky' 19 20 20 21 export const createStarterPackList = async ({ ··· 44 45 }, 45 46 ) 46 47 if (!list) throw new Error('List creation failed') 47 - await agent.com.atproto.repo.applyWrites({ 48 + await pdsAgent(agent).com.atproto.repo.applyWrites({ 48 49 repo: agent.session!.did, 49 50 writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})), 50 51 })
+4 -1
src/lib/media/video/upload.shared.ts
··· 5 5 import {VIDEO_SERVICE_DID} from '#/lib/constants' 6 6 import {UploadLimitError} from '#/lib/media/video/errors' 7 7 import {getServiceAuthAudFromUrl} from '#/lib/strings/url-helpers' 8 + import {pdsAgent} from '#/state/session/agent' 8 9 import {createVideoAgent} from './util' 9 10 10 11 export async function getServiceAuthToken({ ··· 22 23 if (!pdsAud) { 23 24 throw new Error('Agent does not have a PDS URL') 24 25 } 25 - const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth({ 26 + const {data: serviceAuth} = await pdsAgent( 27 + agent, 28 + ).com.atproto.server.getServiceAuth({ 26 29 aud: aud ?? pdsAud, 27 30 lxm, 28 31 exp,
+2 -1
src/lib/react-query.tsx
··· 13 13 import {PERSISTED_QUERY_ROOT} from '#/state/queries' 14 14 import * as env from '#/env' 15 15 import {IS_NATIVE, IS_WEB} from '#/env' 16 + import {PUBLIC_BSKY_SERVICE} from './constants' 16 17 17 18 declare global { 18 19 interface Window { ··· 27 28 setTimeout(() => { 28 29 controller.abort() 29 30 }, 15e3) 30 - const res = await fetch('https://public.api.bsky.app/xrpc/_health', { 31 + const res = await fetch(`${PUBLIC_BSKY_SERVICE}/xrpc/_health`, { 31 32 cache: 'no-store', 32 33 signal: controller.signal, 33 34 })
+2 -1
src/screens/Deactivated.tsx
··· 14 14 useSession, 15 15 useSessionApi, 16 16 } from '#/state/session' 17 + import {pdsAgent} from '#/state/session/agent' 17 18 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 18 19 import {Logo} from '#/view/icons/Logo' 19 20 import {atoms as a, useTheme} from '#/alf' ··· 70 71 const handleActivate = React.useCallback(async () => { 71 72 try { 72 73 setPending(true) 73 - await agent.com.atproto.server.activateAccount() 74 + await pdsAgent(agent).com.atproto.server.activateAccount() 74 75 await queryClient.resetQueries() 75 76 await agent.resumeSession(agent.session!) 76 77 } catch (e: any) {
+2 -1
src/screens/Onboarding/util.ts
··· 10 10 import chunk from 'lodash.chunk' 11 11 12 12 import {until} from '#/lib/async/until' 13 + import {pdsAgent} from '#/state/session/agent' 13 14 14 15 export async function bulkWriteFollows( 15 16 agent: BskyAgent, ··· 41 42 42 43 const chunks = chunk(followWrites, 50) 43 44 for (const chunk of chunks) { 44 - await agent.com.atproto.repo.applyWrites({ 45 + await pdsAgent(agent).com.atproto.repo.applyWrites({ 45 46 repo: session.did, 46 47 writes: chunk, 47 48 })
+160 -2
src/screens/Settings/RunesSettings.tsx
··· 1 1 import {useState} from 'react' 2 2 import {View} from 'react-native' 3 + import {isDid} from '@atproto/api' 3 4 import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' 4 5 import {msg} from '@lingui/core/macro' 5 6 import {useLingui} from '@lingui/react' 6 7 import {Trans} from '@lingui/react/macro' 7 8 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 8 9 9 - import {DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants' 10 + import {APPVIEW_DID_PROXY, DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants' 10 11 import {usePalette} from '#/lib/hooks/usePalette' 11 12 import {type CommonNavigatorParams} from '#/lib/routes/types' 12 13 import {dynamicActivate} from '#/locale/i18n' ··· 18 19 useConstellationInstance, 19 20 useSetConstellationInstance, 20 21 } from '#/state/preferences/constellation-instance' 22 + import {useCustomAppViewDid} from '#/state/preferences/custom-appview-did' 21 23 import { 22 24 useDeerVerificationEnabled, 23 25 useDeerVerificationTrusted, ··· 152 154 useSetHandleInLinks, 153 155 } from '#/state/preferences/use-handle-in-links' 154 156 import {useProfilesQuery} from '#/state/queries/profile' 157 + import {findService, useDidDocument} from '#/state/queries/resolve-identity' 158 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 155 159 import * as SettingsList from '#/screens/Settings/components/SettingsList' 156 160 import {atoms as a, useBreakpoints} from '#/alf' 157 161 import {Admonition} from '#/components/Admonition' ··· 173 177 import {Text} from '#/components/Typography' 174 178 import {IS_WEB} from '#/env' 175 179 import {SearchProfileCard} from '../Search/components/SearchProfileCard' 176 - 177 180 type Props = NativeStackScreenProps<CommonNavigatorParams> 178 181 179 182 function ConstellationInstanceDialog({ ··· 538 541 ) 539 542 } 540 543 544 + function CustomAppViewDidDialog({ 545 + control, 546 + }: { 547 + control: Dialog.DialogControlProps 548 + }) { 549 + const pal = usePalette('default') 550 + const {_} = useLingui() 551 + 552 + const [did, setDid] = useState('') 553 + const [, setCustomAppViewDid] = useCustomAppViewDid() 554 + 555 + const doc = useDidDocument({did}) 556 + const bskyAppViewService = 557 + doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView') 558 + 559 + const submit = () => { 560 + if (did.length === 0) { 561 + setCustomAppViewDid(undefined) 562 + control.close() 563 + return 564 + } 565 + if (!bskyAppViewService?.serviceEndpoint) return 566 + setCustomAppViewDid(did) 567 + control.close() 568 + } 569 + 570 + return ( 571 + <Dialog.Outer 572 + control={control} 573 + nativeOptions={{preventExpansion: true}} 574 + onClose={() => setDid('')}> 575 + <Dialog.Handle /> 576 + <Dialog.ScrollableInner label={_(msg`Custom AppView Proxy DID`)}> 577 + <View style={[a.gap_sm, a.pb_lg]}> 578 + <Text style={[a.text_2xl, a.font_bold]}> 579 + <Trans>Custom AppView Proxy DID</Trans> 580 + </Text> 581 + </View> 582 + 583 + <View style={a.gap_lg}> 584 + <Dialog.Input 585 + label="Text input field" 586 + autoFocus 587 + style={[styles.textInput, pal.border, pal.text]} 588 + onChangeText={value => { 589 + setDid(value) 590 + }} 591 + placeholder={ 592 + APPVIEW_DID_PROXY?.substring(0, APPVIEW_DID_PROXY.indexOf('#')) || 593 + `did:web:api.bsky.app` 594 + } 595 + placeholderTextColor={pal.colors.textLight} 596 + onSubmitEditing={submit} 597 + accessibilityHint={_( 598 + msg`Input the DID of the AppView to proxy requests through`, 599 + )} 600 + isInvalid={ 601 + !!did && !bskyAppViewService?.serviceEndpoint && !doc.isLoading 602 + } 603 + /> 604 + 605 + {did && !isDid(did) && ( 606 + <View> 607 + <ErrorMessage message={_(msg`must enter a DID`)} /> 608 + </View> 609 + )} 610 + 611 + {did && (did.includes('#') || did.includes('?')) && ( 612 + <View> 613 + <ErrorMessage message={_(msg`don't include the service id`)} /> 614 + </View> 615 + )} 616 + 617 + {doc.isError && ( 618 + <View> 619 + <ErrorMessage 620 + message={ 621 + doc.error.message || _(msg`document resolution failure`) 622 + } 623 + /> 624 + </View> 625 + )} 626 + 627 + {doc.data && 628 + !bskyAppViewService && 629 + (doc.data as {message?: string}).message && ( 630 + <View> 631 + <ErrorMessage 632 + message={(doc.data as {message: string}).message} 633 + /> 634 + </View> 635 + )} 636 + 637 + {doc.data && !bskyAppViewService && ( 638 + <View> 639 + <ErrorMessage 640 + message={_(msg`document doesn't contain #bsky_appview service`)} 641 + /> 642 + </View> 643 + )} 644 + 645 + {bskyAppViewService && ( 646 + <Text style={[a.text_sm, a.leading_snug]}> 647 + {JSON.stringify(bskyAppViewService, null, 2)} 648 + </Text> 649 + )} 650 + 651 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 652 + <Button 653 + label={_(msg`Save`)} 654 + size="large" 655 + onPress={submit} 656 + variant="solid" 657 + color={did.length > 0 ? 'primary' : 'secondary'} 658 + disabled={ 659 + did.length !== 0 && !bskyAppViewService?.serviceEndpoint 660 + }> 661 + <ButtonText> 662 + {did.length > 0 ? <Trans>Save</Trans> : <Trans>Reset</Trans>} 663 + </ButtonText> 664 + </Button> 665 + </View> 666 + </View> 667 + 668 + <Dialog.Close /> 669 + </Dialog.ScrollableInner> 670 + </Dialog.Outer> 671 + ) 672 + } 673 + 541 674 function TrustedVerifiersDialog({ 542 675 control, 543 676 }: { ··· 828 961 const openRouterModel = useOpenRouterModel() 829 962 const setOpenRouterModelControl = Dialog.useDialogControl() 830 963 const openRouterConfigured = useOpenRouterConfigured() 964 + 965 + const [customAppViewDid] = useCustomAppViewDid() 966 + const setCustomAppViewDidControl = Dialog.useDialogControl() 831 967 832 968 return ( 833 969 <Layout.Screen> ··· 1481 1617 1482 1618 <SettingsList.Divider /> 1483 1619 1620 + <SettingsList.Item> 1621 + <SettingsList.ItemIcon icon={StarIcon} /> 1622 + <SettingsList.ItemText> 1623 + <Trans>{`Custom AppView DID`}</Trans> 1624 + </SettingsList.ItemText> 1625 + <SettingsList.BadgeButton 1626 + label={customAppViewDid ? _(msg`Set`) : _(msg`Change`)} 1627 + onPress={() => setCustomAppViewDidControl.open()} 1628 + /> 1629 + </SettingsList.Item> 1630 + <SettingsList.Item> 1631 + <Admonition type="info" style={[a.flex_1]}> 1632 + <Trans> 1633 + Restart app after changing your AppView. 1634 + {customAppViewDid && _(msg` Currently ${customAppViewDid}`)} 1635 + </Trans> 1636 + </Admonition> 1637 + </SettingsList.Item> 1638 + 1639 + <SettingsList.Divider /> 1640 + 1484 1641 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1485 1642 <SettingsList.ItemIcon icon={RaisingHandIcon} /> 1486 1643 <SettingsList.ItemText> ··· 1525 1682 </SettingsList.Container> 1526 1683 </Layout.Content> 1527 1684 <ConstellationInstanceDialog control={setConstellationInstanceControl} /> 1685 + <CustomAppViewDidDialog control={setCustomAppViewDidControl} /> 1528 1686 <TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} /> 1529 1687 <LibreTranslateInstanceDialog 1530 1688 control={setLibreTranslateInstanceControl}
+3 -2
src/screens/Settings/components/ChangePasswordDialog.tsx
··· 9 9 import {checkAndFormatResetCode} from '#/lib/strings/password' 10 10 import {logger} from '#/logger' 11 11 import {useAgent, useSession} from '#/state/session' 12 + import {pdsAgent} from '#/state/session/agent' 12 13 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 13 14 import {android, atoms as a, web} from '#/alf' 14 15 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 85 86 setError('') 86 87 setIsProcessing(true) 87 88 try { 88 - await agent.com.atproto.server.requestPasswordReset({ 89 + await pdsAgent(agent).com.atproto.server.requestPasswordReset({ 89 90 email: currentAccount.email, 90 91 }) 91 92 setStage(Stages.ChangePassword) ··· 129 130 setError('') 130 131 setIsProcessing(true) 131 132 try { 132 - await agent.com.atproto.server.resetPassword({ 133 + await pdsAgent(agent).com.atproto.server.resetPassword({ 133 134 token: formattedCode, 134 135 password: newPassword, 135 136 })
+2 -1
src/screens/Settings/components/DeactivateAccountDialog.tsx
··· 6 6 7 7 import {logger} from '#/logger' 8 8 import {useAgent, useSessionApi} from '#/state/session' 9 + import {pdsAgent} from '#/state/session/agent' 9 10 import {atoms as a, useTheme} from '#/alf' 10 11 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 11 12 import {type DialogOuterProps} from '#/components/Dialog' ··· 42 43 const handleDeactivate = React.useCallback(async () => { 43 44 try { 44 45 setPending(true) 45 - await agent.com.atproto.server.deactivateAccount({}) 46 + await pdsAgent(agent).com.atproto.server.deactivateAccount({}) 46 47 control.close(() => { 47 48 logoutCurrentAccount('Deactivated') 48 49 })
+3 -2
src/screens/Settings/components/DisableEmail2FADialog.tsx
··· 6 6 7 7 import {cleanError} from '#/lib/strings/errors' 8 8 import {useAgent, useSession} from '#/state/session' 9 + import {pdsAgent} from '#/state/session/agent' 9 10 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 10 11 import * as Toast from '#/view/com/util/Toast' 11 12 import {atoms as a, useBreakpoints, useTheme} from '#/alf' ··· 42 43 setError('') 43 44 setIsProcessing(true) 44 45 try { 45 - await agent.com.atproto.server.requestEmailUpdate() 46 + await pdsAgent(agent).com.atproto.server.requestEmailUpdate() 46 47 setStage(Stages.ConfirmCode) 47 48 } catch (e) { 48 49 setError(cleanError(String(e))) ··· 56 57 setIsProcessing(true) 57 58 try { 58 59 if (currentAccount?.email) { 59 - await agent.com.atproto.server.updateEmail({ 60 + await pdsAgent(agent).com.atproto.server.updateEmail({ 60 61 email: currentAccount.email, 61 62 token: confirmationCode.trim(), 62 63 emailAuthFactor: false,
+2 -1
src/screens/Settings/components/ExportCarDialog.tsx
··· 8 8 import {saveBytesToDisk} from '#/lib/media/manip' 9 9 import {logger} from '#/logger' 10 10 import {useAgent} from '#/state/session' 11 + import {pdsAgent} from '#/state/session/agent' 11 12 import {atoms as a, useTheme, web} from '#/alf' 12 13 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 13 14 import * as Dialog from '#/components/Dialog' ··· 34 35 try { 35 36 setLoading('repo') 36 37 const did = agent.session.did 37 - const downloadRes = await agent.com.atproto.sync.getRepo({did}) 38 + const downloadRes = await pdsAgent(agent).com.atproto.sync.getRepo({did}) 38 39 const saveRes = await saveBytesToDisk( 39 40 'repo.car', 40 41 downloadRes.data,
+2 -1
src/screens/SignupQueued.tsx
··· 8 8 9 9 import {logger} from '#/logger' 10 10 import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' 11 + import {pdsAgent} from '#/state/session/agent' 11 12 import {useOnboardingDispatch} from '#/state/shell' 12 13 import {Logo} from '#/view/icons/Logo' 13 14 import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' ··· 38 39 const checkStatus = React.useCallback(async () => { 39 40 setProcessing(true) 40 41 try { 41 - const res = await agent.com.atproto.temp.checkSignupQueue() 42 + const res = await pdsAgent(agent).com.atproto.temp.checkSignupQueue() 42 43 if (res.data.activated) { 43 44 // ready to go, exchange the access token for a usable one and kick off onboarding 44 45 await agent.sessionManager.refreshSession()
+21
src/state/preferences/custom-appview-did.tsx
··· 1 + import {isDid} from '@atproto/api' 2 + 3 + import {device, useStorage} from '#/storage' 4 + 5 + export function useCustomAppViewDid() { 6 + const [customAppViewDid = undefined, setCustomAppViewDid] = useStorage( 7 + device, 8 + ['customAppViewDid'], 9 + ) 10 + 11 + return [customAppViewDid, setCustomAppViewDid] as const 12 + } 13 + 14 + export function readCustomAppViewDidUri() { 15 + const maybeDid = device.get(['customAppViewDid']) 16 + if (!maybeDid || !isDid(maybeDid)) { 17 + return undefined 18 + } 19 + 20 + return `${maybeDid}#bsky_appview` 21 + }
+4 -3
src/state/queries/app-passwords.ts
··· 3 3 4 4 import {STALE} from '#/state/queries' 5 5 import {useAgent} from '../session' 6 + import {pdsAgent} from '../session/agent' 6 7 7 8 const RQKEY_ROOT = 'app-passwords' 8 9 export const RQKEY = () => [RQKEY_ROOT] ··· 13 14 staleTime: STALE.MINUTES.FIVE, 14 15 queryKey: RQKEY(), 15 16 queryFn: async () => { 16 - const res = await agent.com.atproto.server.listAppPasswords({}) 17 + const res = await pdsAgent(agent).com.atproto.server.listAppPasswords({}) 17 18 return res.data.passwords 18 19 }, 19 20 }) ··· 29 30 >({ 30 31 mutationFn: async ({name, privileged}) => { 31 32 return ( 32 - await agent.com.atproto.server.createAppPassword({ 33 + await pdsAgent(agent).com.atproto.server.createAppPassword({ 33 34 name, 34 35 privileged, 35 36 }) ··· 48 49 const agent = useAgent() 49 50 return useMutation<void, Error, {name: string}>({ 50 51 mutationFn: async ({name}) => { 51 - await agent.com.atproto.server.revokeAppPassword({ 52 + await pdsAgent(agent).com.atproto.server.revokeAppPassword({ 52 53 name, 53 54 }) 54 55 },
+3 -2
src/state/queries/list.ts
··· 17 17 import {type ImageMeta} from '#/state/gallery' 18 18 import {STALE} from '#/state/queries' 19 19 import {useAgent, useSession} from '#/state/session' 20 + import {pdsAgent} from '#/state/session/agent' 20 21 import {invalidate as invalidateMyLists} from './my-lists' 21 22 import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists' 22 23 ··· 152 153 record.avatar = undefined 153 154 } 154 155 const res = ( 155 - await agent.com.atproto.repo.putRecord({ 156 + await pdsAgent(agent).com.atproto.repo.putRecord({ 156 157 repo: currentAccount.did, 157 158 collection: 'app.bsky.graph.list', 158 159 rkey, ··· 231 232 232 233 // apply in chunks 233 234 for (const writesChunk of chunk(writes, 10)) { 234 - await agent.com.atproto.repo.applyWrites({ 235 + await pdsAgent(agent).com.atproto.repo.applyWrites({ 235 236 repo: currentAccount.did, 236 237 writes: writesChunk, 237 238 })
+3 -2
src/state/queries/messages/actor-declaration.ts
··· 3 3 4 4 import {logger} from '#/logger' 5 5 import {useAgent, useSession} from '#/state/session' 6 + import {pdsAgent} from '#/state/session/agent' 6 7 import {RQKEY as PROFILE_RKEY} from '../profile' 7 8 8 9 export function useUpdateActorDeclaration({ ··· 19 20 return useMutation({ 20 21 mutationFn: async (allowIncoming: 'all' | 'none' | 'following') => { 21 22 if (!currentAccount) throw new Error('Not signed in') 22 - const result = await agent.com.atproto.repo.putRecord({ 23 + const result = await pdsAgent(agent).com.atproto.repo.putRecord({ 23 24 repo: currentAccount.did, 24 25 collection: 'chat.bsky.actor.declaration', 25 26 rkey: 'self', ··· 69 70 return useMutation({ 70 71 mutationFn: async () => { 71 72 if (!currentAccount) throw new Error('Not signed in') 72 - const result = await agent.api.com.atproto.repo.deleteRecord({ 73 + const result = await pdsAgent(agent).com.atproto.repo.deleteRecord({ 73 74 repo: currentAccount.did, 74 75 collection: 'chat.bsky.actor.declaration', 75 76 rkey: 'self',
+2 -1
src/state/queries/postgate/index.ts
··· 21 21 POSTGATE_COLLECTION, 22 22 } from '#/state/queries/postgate/util' 23 23 import {useAgent} from '#/state/session' 24 + import {pdsAgent} from '#/state/session/agent' 24 25 import * as bsky from '#/types/bsky' 25 26 26 27 export async function getPostgateRecord({ ··· 96 97 const postUrip = new AtUri(postUri) 97 98 98 99 await networkRetry(2, () => 99 - agent.api.com.atproto.repo.putRecord({ 100 + pdsAgent(agent).com.atproto.repo.putRecord({ 100 101 repo: agent.session!.did, 101 102 collection: POSTGATE_COLLECTION, 102 103 rkey: postUrip.rkey,
+4 -3
src/state/queries/preferences/index.ts
··· 23 23 type ThreadViewPreferences, 24 24 type UsePreferencesQueryResponse, 25 25 } from '#/state/queries/preferences/types' 26 - import {useAgent} from '#/state/session' 26 + import {useBlankPrefAuthedAgent as useAgent} from '#/state/session' 27 + import {pdsAgent} from '#/state/session/agent' 27 28 import {saveLabelers} from '#/state/session/agent-config' 28 29 import {useAgeAssurance} from '#/ageAssurance' 29 30 import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util' ··· 49 50 if (!agent.did) { 50 51 return DEFAULT_LOGGED_OUT_PREFERENCES 51 52 } else { 52 - const res = await agent.getPreferences() 53 + const res = await pdsAgent(agent).getPreferences() 53 54 54 55 // save to local storage to ensure there are labels on initial requests 55 56 saveLabelers( ··· 113 114 114 115 return useMutation({ 115 116 mutationFn: async () => { 116 - await agent.app.bsky.actor.putPreferences({preferences: []}) 117 + await pdsAgent(agent).app.bsky.actor.putPreferences({preferences: []}) 117 118 // triggers a refetch 118 119 await queryClient.invalidateQueries({ 119 120 queryKey: preferencesQueryKey,
+57 -15
src/state/queries/resolve-identity.ts
··· 1 + import {type Did, isDid} from '@atproto/api' 2 + import {useQuery} from '@tanstack/react-query' 3 + 4 + import {STALE} from '.' 1 5 import {LRU} from './direct-fetch-record' 6 + const RQKEY_ROOT = 'resolve-identity' 7 + export const RQKEY = (did: string) => [RQKEY_ROOT, did] 2 8 3 - const serviceCache = new LRU<`did:${string}`, string>() 9 + // this isn't trusted... 10 + export type DidDocument = { 11 + '@context'?: string[] 12 + id?: string 13 + alsoKnownAs?: string[] 14 + verificationMethod?: VerificationMethod[] 15 + service?: Service[] 16 + } 4 17 5 - export async function resolvePdsServiceUrl(did: `did:${string}`) { 18 + export type VerificationMethod = { 19 + id?: string 20 + type?: string 21 + controller?: string 22 + publicKeyMultibase?: string 23 + } 24 + 25 + export type Service = { 26 + id?: string 27 + type?: string 28 + serviceEndpoint?: string 29 + } 30 + 31 + const serviceCache = new LRU<Did, DidDocument>() 32 + 33 + export async function resolveDidDocument(did: Did) { 6 34 return await serviceCache.getOrTryInsertWith(did, async () => { 7 35 const docUrl = did.startsWith('did:plc:') 8 36 ? `https://plc.directory/${did}` 9 37 : `https://${did.substring(8)}/.well-known/did.json` 10 38 11 - // TODO: validate! 12 - const doc: { 13 - service: { 14 - serviceEndpoint: string 15 - type: string 16 - }[] 17 - } = await (await fetch(docUrl)).json() 18 - const service = doc.service.find( 19 - s => s.type === 'AtprotoPersonalDataServer', 20 - )?.serviceEndpoint 39 + // TODO: we should probably validate this... 40 + return await (await fetch(docUrl)).json() 41 + }) 42 + } 21 43 22 - if (service === undefined) 23 - throw new Error(`could not find a service for ${did}`) 24 - return service 44 + export function findService(doc: DidDocument, id: string, type?: string) { 45 + // probably not defensive enough, but we don't have atproto/did as a dep... 46 + if (!Array.isArray(doc?.service)) return 47 + return doc.service.find( 48 + s => s?.serviceEndpoint && s?.id === id && (!type || s?.type === type), 49 + ) 50 + } 51 + 52 + export async function resolvePdsServiceUrl(did: Did) { 53 + const doc = await resolveDidDocument(did) 54 + return findService(doc, '#atproto_pds', 'AtprotoPersonalDataServer') 55 + ?.serviceEndpoint 56 + } 57 + 58 + export function useDidDocument({did}: {did: string}) { 59 + return useQuery<DidDocument | undefined>({ 60 + staleTime: STALE.HOURS.ONE, 61 + queryKey: RQKEY(did || ''), 62 + async queryFn() { 63 + if (!isDid(did)) return undefined 64 + return await resolveDidDocument(did) 65 + }, 66 + enabled: isDid(did) && !(did.includes('#') || did.includes('?')), 25 67 }) 26 68 }
+4 -3
src/state/queries/starter-packs.ts
··· 27 27 import {STALE} from '#/state/queries/index' 28 28 import {invalidateListMembersQuery} from '#/state/queries/list-members' 29 29 import {useAgent} from '#/state/session' 30 + import {pdsAgent} from '#/state/session/agent' 30 31 import * as bsky from '#/types/bsky' 31 32 32 33 const RQKEY_ROOT = 'starter-pack' ··· 203 204 if (removedItems.length !== 0) { 204 205 const chunks = chunk(removedItems, 50) 205 206 for (const chunk of chunks) { 206 - await agent.com.atproto.repo.applyWrites({ 207 + await pdsAgent(agent).com.atproto.repo.applyWrites({ 207 208 repo: agent.session!.did, 208 209 writes: chunk.map(i => ({ 209 210 $type: 'com.atproto.repo.applyWrites#delete', ··· 220 221 if (addedProfiles.length > 0) { 221 222 const chunks = chunk(addedProfiles, 50) 222 223 for (const chunk of chunks) { 223 - await agent.com.atproto.repo.applyWrites({ 224 + await pdsAgent(agent).com.atproto.repo.applyWrites({ 224 225 repo: agent.session!.did, 225 226 writes: chunk.map(p => ({ 226 227 $type: 'com.atproto.repo.applyWrites#create', ··· 237 238 } 238 239 239 240 const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey 240 - await agent.com.atproto.repo.putRecord({ 241 + await pdsAgent(agent).com.atproto.repo.putRecord({ 241 242 repo: agent.session!.did, 242 243 collection: 'app.bsky.graph.starterpack', 243 244 rkey,
+2 -1
src/state/queries/threadgate/index.ts
··· 18 18 } from '#/state/queries/threadgate/util' 19 19 import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread' 20 20 import {useAgent} from '#/state/session' 21 + import {pdsAgent} from '#/state/session/agent' 21 22 import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' 22 23 import * as bsky from '#/types/bsky' 23 24 ··· 162 163 }) 163 164 164 165 await networkRetry(2, () => 165 - agent.api.com.atproto.repo.putRecord({ 166 + pdsAgent(agent).com.atproto.repo.putRecord({ 166 167 repo: agent.session!.did, 167 168 collection: 'app.bsky.feed.threadgate', 168 169 rkey: postUrip.rkey,
+25 -5
src/state/session/agent.ts
··· 15 15 16 16 import {networkRetry} from '#/lib/async/retry' 17 17 import { 18 + APPVIEW_DID_PROXY, 18 19 BLUESKY_PROXY_HEADER, 19 20 BSKY_SERVICE, 20 21 DISCOVER_SAVED_FEED, ··· 33 34 } from '#/ageAssurance/data' 34 35 import {features} from '#/analytics' 35 36 import {emitNetworkConfirmed, emitNetworkLost} from '../events' 37 + import {readCustomAppViewDidUri} from '../preferences/custom-appview-did' 36 38 import {addSessionErrorLog} from './logging' 37 39 import { 38 40 configureModerationForAccount, ··· 47 49 configureModerationForGuest() // Side effect but only relevant for tests 48 50 49 51 const agent = new BskyAppAgent({service: PUBLIC_BSKY_SERVICE}) 50 - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) 52 + const proxyDid = 53 + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY 54 + agent.configureProxy(proxyDid) 51 55 return agent 52 56 } 53 57 ··· 90 94 // after session is attached 91 95 const aa = prefetchAgeAssuranceData({agent}) 92 96 93 - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) 97 + const proxyDid = 98 + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY 99 + agent.configureProxy(proxyDid) 94 100 95 101 return agent.prepare({ 96 102 resolvers: [gates, moderation, aa], ··· 129 135 const moderation = configureModerationForAccount(agent, account) 130 136 const aa = prefetchAgeAssuranceData({agent}) 131 137 132 - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) 138 + const proxyDid = 139 + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY 140 + agent.configureProxy(proxyDid) 133 141 134 142 return agent.prepare({ 135 143 resolvers: [gates, moderation, aa], ··· 236 244 }), 237 245 getAge(birthDate) < 18 && 238 246 networkRetry(3, () => { 239 - return agent.com.atproto.repo.putRecord({ 247 + return pdsAgent(agent).com.atproto.repo.putRecord({ 240 248 repo: account.did, 241 249 collection: 'chat.bsky.actor.declaration', 242 250 rkey: 'self', ··· 301 309 logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`}) 302 310 } 303 311 304 - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) 312 + const proxyDid = 313 + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY 314 + agent.configureProxy(proxyDid) 305 315 306 316 return agent.prepare({ 307 317 resolvers: [gates, moderation, aa], ··· 407 417 } 408 418 }, 409 419 }) 420 + const proxyDid = readCustomAppViewDidUri() || APPVIEW_DID_PROXY 421 + if (proxyDid) { 422 + this.configureProxy(proxyDid) 423 + } 410 424 } 411 425 412 426 async prepare({ ··· 438 452 dispose() { 439 453 this.sessionManager.session = undefined 440 454 this.persistSessionHandler = undefined 455 + } 456 + 457 + cloneWithoutProxy(): BskyAgent { 458 + const cloned = new BskyAgent({service: this.serviceUrl.toString()}) 459 + cloned.sessionManager.session = this.sessionManager.session 460 + return cloned 441 461 } 442 462 } 443 463
+13 -1
src/state/session/index.tsx
··· 22 22 createAgentAndCreateAccount, 23 23 createAgentAndLogin, 24 24 createAgentAndResume, 25 + pdsAgent, 25 26 sessionAccountToSession, 26 27 } from './agent' 27 28 import {type Action, getInitialState, reducer, type State} from './reducer' ··· 278 279 >(async () => { 279 280 const agent = state.currentAgentState.agent as BskyAppAgent 280 281 const signal = cancelPendingTask() 281 - const {data} = await agent.com.atproto.server.getSession() 282 + const {data} = await pdsAgent(agent).com.atproto.server.getSession() 282 283 if (signal.aborted) return 283 284 store.dispatch({ 284 285 type: 'partial-refresh-session', ··· 449 450 } 450 451 return agent 451 452 } 453 + 454 + export function useBlankPrefAuthedAgent(): BskyAgent { 455 + const agent = useContext(AgentContext) 456 + if (!agent) { 457 + throw Error('useAgent() must be below <SessionProvider>.') 458 + } 459 + 460 + return useMemo(() => { 461 + return (agent as BskyAppAgent).cloneWithoutProxy() 462 + }, [agent]) 463 + }
+1
src/storage/schema.ts
··· 63 63 deerGateCache: string 64 64 activitySubscriptionsNudged?: boolean 65 65 threadgateNudged?: boolean 66 + customAppViewDid: string | undefined 66 67 67 68 /** 68 69 * Policy update overlays. New IDs are required for each new announcement.