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

Configure Feed

Select the types of activity you want to include in your feed.

feat: custom appview support

YIPPEE!
adds a patch from blacksky to stop put/getPreferences from failing (from being proxied with anything besides bluesky's atproto-proxy)
also added automatic restart after changing appview

Revert "revert pdsAgent commits & remove custom AppView support" (commit cccfbd0b7b1b42da3b8c9fa27353542aec283b69)

xan.lol 873d4cd6 9e435a24

+416 -72
+30
patches/@atproto+api+0.18.18.patch
··· 1 + diff --git a/node_modules/@atproto/api/dist/agent.js b/node_modules/@atproto/api/dist/agent.js 2 + index 634e463..57b5c74 100644 3 + --- a/node_modules/@atproto/api/dist/agent.js 4 + +++ b/node_modules/@atproto/api/dist/agent.js 5 + @@ -593,7 +593,7 @@ class Agent extends xrpc_1.XrpcClient { 6 + hideAllFeeds: false, 7 + }, 8 + }; 9 + - const res = await this.app.bsky.actor.getPreferences({}); 10 + + const res = await this.app.bsky.actor.getPreferences({}, {headers: {'atproto-proxy': ''}}); 11 + const labelPrefs = []; 12 + for (const pref of res.data.preferences) { 13 + if (predicate.isValidAdultContentPref(pref)) { 14 + @@ -1275,14 +1275,14 @@ class Agent extends xrpc_1.XrpcClient { 15 + async updatePreferences(cb) { 16 + try { 17 + await __classPrivateFieldGet(this, _Agent_prefsLock, "f").acquireAsync(); 18 + - const res = await this.app.bsky.actor.getPreferences({}); 19 + + const res = await this.app.bsky.actor.getPreferences({}, {headers: {'atproto-proxy': ''}}); 20 + const newPrefs = cb(res.data.preferences); 21 + if (newPrefs === false) { 22 + return res.data.preferences; 23 + } 24 + await this.app.bsky.actor.putPreferences({ 25 + preferences: newPrefs, 26 + - }); 27 + + }, {headers: {'atproto-proxy': ''}}); 28 + return newPrefs; 29 + } 30 + finally {
+9 -10
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' ··· 609 610 if (!videoEmbed) return 610 611 const did = post.author.did 611 612 const cid = videoEmbed.cid 612 - if (!did.startsWith('did:')) return 613 - const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}`) 613 + if (!isDid(did)) return 614 + const pdsUrl = await resolvePdsServiceUrl(did) 614 615 const uri = `${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}` 615 616 616 617 Toast.show(l({message: 'Downloading video...', context: 'toast'})) ··· 624 625 type: 'success', 625 626 }) 626 627 else 627 - Toast.show( 628 - l({message: 'Failed to download video', context: 'toast'}), 629 - {type: 'error'}, 630 - ) 628 + Toast.show(l({message: 'Failed to download video', context: 'toast'}), { 629 + type: 'error', 630 + }) 631 631 } 632 632 633 633 const onPressDownloadGif = async () => { ··· 644 644 type: 'success', 645 645 }) 646 646 else 647 - Toast.show( 648 - l({message: 'Failed to download GIF', context: 'toast'}), 649 - {type: 'error'}, 650 - ) 647 + Toast.show(l({message: 'Failed to download GIF', context: 'toast'}), { 648 + type: 'error', 649 + }) 651 650 } 652 651 653 652 const isEmbedGif = () => {
+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, ··· 182 183 } 183 184 184 185 try { 185 - await agent.com.atproto.repo.applyWrites({ 186 + await pdsAgent(agent).com.atproto.repo.applyWrites({ 186 187 repo: agent.assertDid, 187 188 writes: writes, 188 189 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 = 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 })
+158 -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' ··· 18 19 useConstellationInstance, 19 20 useSetConstellationInstance, 20 21 } from '#/state/preferences/constellation-instance' 22 + import { 23 + useCustomAppViewDid, 24 + useSetCustomAppViewDid, 25 + } from '#/state/preferences/custom-appview-did' 21 26 import { 22 27 useDeerVerificationEnabled, 23 28 useDeerVerificationTrusted, ··· 152 157 useSetHandleInLinks, 153 158 } from '#/state/preferences/use-handle-in-links' 154 159 import {useProfilesQuery} from '#/state/queries/profile' 160 + import {findService, useDidDocument} from '#/state/queries/resolve-identity' 161 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 155 162 import * as SettingsList from '#/screens/Settings/components/SettingsList' 156 163 import {atoms as a, useBreakpoints} from '#/alf' 157 164 import {Admonition} from '#/components/Admonition' ··· 256 263 ) 257 264 } 258 265 266 + function CustomAppViewDidDialog({ 267 + control, 268 + }: { 269 + control: Dialog.DialogControlProps 270 + }) { 271 + const pal = usePalette('default') 272 + const {_} = useLingui() 273 + 274 + const [customAppViewDid] = useCustomAppViewDid() 275 + const [did, setDid] = useState(customAppViewDid ?? '') 276 + const setCustomAppViewDid = useSetCustomAppViewDid() 277 + 278 + const doc = useDidDocument({did}) 279 + const bskyAppViewService = 280 + doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView') 281 + 282 + const submit = () => { 283 + if (did.length === 0) { 284 + control.close(() => { 285 + setCustomAppViewDid(undefined) 286 + }) 287 + return 288 + } 289 + if (!bskyAppViewService?.serviceEndpoint) return 290 + control.close(() => { 291 + setCustomAppViewDid(did) 292 + }) 293 + } 294 + 295 + return ( 296 + <Dialog.Outer 297 + control={control} 298 + nativeOptions={{preventExpansion: true}} 299 + onClose={() => setDid(customAppViewDid ?? '')}> 300 + <Dialog.Handle /> 301 + <Dialog.ScrollableInner label={_(msg`Custom AppView Proxy DID`)}> 302 + <View style={[a.gap_sm, a.pb_lg]}> 303 + <Text style={[a.text_2xl, a.font_bold]}> 304 + <Trans>Custom AppView Proxy DID</Trans> 305 + </Text> 306 + </View> 307 + 308 + <View style={a.gap_lg}> 309 + <Dialog.Input 310 + label="Text input field" 311 + autoFocus 312 + style={[styles.textInput, pal.border, pal.text]} 313 + onChangeText={value => { 314 + setDid(value) 315 + }} 316 + placeholder={ 317 + APPVIEW_DID_PROXY?.substring(0, APPVIEW_DID_PROXY.indexOf('#')) || 318 + `did:web:api.bsky.app` 319 + } 320 + placeholderTextColor={pal.colors.textLight} 321 + onSubmitEditing={submit} 322 + accessibilityHint={_( 323 + msg`Input the DID of the AppView to proxy requests through`, 324 + )} 325 + isInvalid={ 326 + !!did && !bskyAppViewService?.serviceEndpoint && !doc.isLoading 327 + } 328 + defaultValue={customAppViewDid ?? ''} 329 + /> 330 + 331 + {did && !isDid(did) && ( 332 + <View> 333 + <ErrorMessage message={_(msg`must enter a DID`)} /> 334 + </View> 335 + )} 336 + 337 + {did && (did.includes('#') || did.includes('?')) && ( 338 + <View> 339 + <ErrorMessage message={_(msg`don't include the service id`)} /> 340 + </View> 341 + )} 342 + 343 + {doc.isError && ( 344 + <View> 345 + <ErrorMessage 346 + message={ 347 + doc.error.message || _(msg`document resolution failure`) 348 + } 349 + /> 350 + </View> 351 + )} 352 + 353 + {doc.data && 354 + !bskyAppViewService && 355 + (doc.data as {message?: string}).message && ( 356 + <View> 357 + <ErrorMessage 358 + message={(doc.data as {message: string}).message} 359 + /> 360 + </View> 361 + )} 362 + 363 + {doc.data && !bskyAppViewService && ( 364 + <View> 365 + <ErrorMessage 366 + message={_(msg`document doesn't contain #bsky_appview service`)} 367 + /> 368 + </View> 369 + )} 370 + 371 + {bskyAppViewService && ( 372 + <Text style={[a.text_sm, a.leading_snug]}> 373 + {JSON.stringify(bskyAppViewService, null, 2)} 374 + </Text> 375 + )} 376 + 377 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 378 + <Button 379 + label={_(msg`Save`)} 380 + size="large" 381 + onPress={submit} 382 + variant="solid" 383 + color={did.length > 0 ? 'primary' : 'secondary'} 384 + disabled={ 385 + did.length !== 0 && !bskyAppViewService?.serviceEndpoint 386 + }> 387 + <ButtonText> 388 + {did.length > 0 ? <Trans>Save</Trans> : <Trans>Reset</Trans>} 389 + </ButtonText> 390 + </Button> 391 + </View> 392 + </View> 393 + 394 + <Dialog.Close /> 395 + </Dialog.ScrollableInner> 396 + </Dialog.Outer> 397 + ) 398 + } 399 + 259 400 function LibreTranslateInstanceDialog({ 260 401 control, 261 402 }: { ··· 836 977 const autoLikeOnRepost = useAutoLikeOnRepost() 837 978 const setAutoLikeOnRepost = useSetAutoLikeOnRepost() 838 979 980 + const [customAppViewDid] = useCustomAppViewDid() 981 + const setCustomAppViewDidControl = Dialog.useDialogControl() 982 + 839 983 return ( 840 984 <Layout.Screen> 841 985 <Layout.Header.Outer> ··· 1010 1154 /> 1011 1155 </SettingsList.Item> 1012 1156 1013 - <SettingsList.Divider /> 1014 - 1015 1157 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1016 1158 <SettingsList.ItemIcon icon={PaintRollerIcon} /> 1017 1159 <SettingsList.ItemText> ··· 1499 1641 1500 1642 <SettingsList.Divider /> 1501 1643 1644 + <SettingsList.Item> 1645 + <SettingsList.ItemIcon icon={StarIcon} /> 1646 + <SettingsList.ItemText> 1647 + <Trans>{`Custom AppView DID`}</Trans> 1648 + </SettingsList.ItemText> 1649 + <SettingsList.BadgeButton 1650 + label={customAppViewDid ? _(msg`Change`) : _(msg`Set`)} 1651 + onPress={() => setCustomAppViewDidControl.open()} 1652 + /> 1653 + </SettingsList.Item> 1654 + 1655 + <SettingsList.Divider /> 1656 + 1502 1657 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1503 1658 <SettingsList.ItemIcon icon={RaisingHandIcon} /> 1504 1659 <SettingsList.ItemText> ··· 1543 1698 </SettingsList.Container> 1544 1699 </Layout.Content> 1545 1700 <ConstellationInstanceDialog control={setConstellationInstanceControl} /> 1701 + <CustomAppViewDidDialog control={setCustomAppViewDidControl} /> 1546 1702 <TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} /> 1547 1703 <LibreTranslateInstanceDialog 1548 1704 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 = 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 {atoms as a, useBreakpoints, useTheme} from '#/alf' 11 12 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 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 = 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()
+41
src/state/preferences/custom-appview-did.tsx
··· 1 + import React from 'react' 2 + import {reloadAppAsync} from 'expo' 3 + import {isDid} from '@atproto/api' 4 + 5 + import {IS_WEB} from '#/env' 6 + import {device, useStorage} from '#/storage' 7 + 8 + export function useCustomAppViewDid() { 9 + const [customAppViewDid = undefined, setCustomAppViewDid] = useStorage( 10 + device, 11 + ['customAppViewDid'], 12 + ) 13 + 14 + return [customAppViewDid, setCustomAppViewDid] as const 15 + } 16 + 17 + export function useSetCustomAppViewDid() { 18 + const [, setCustomAppViewDid] = useCustomAppViewDid() 19 + 20 + return React.useCallback( 21 + (customAppViewDid: string | undefined) => { 22 + setCustomAppViewDid(customAppViewDid) 23 + 24 + if (IS_WEB) { 25 + window.location.reload() 26 + } else { 27 + void reloadAppAsync() 28 + } 29 + }, 30 + [setCustomAppViewDid], 31 + ) 32 + } 33 + 34 + export function readCustomAppViewDidUri() { 35 + const maybeDid = device.get(['customAppViewDid']) 36 + if (!maybeDid || !isDid(maybeDid)) { 37 + return undefined 38 + } 39 + 40 + return `${maybeDid}#bsky_appview` 41 + }
+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 ··· 77 81 // after session is attached 78 82 const aa = prefetchAgeAssuranceData({agent}) 79 83 80 - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) 84 + const proxyDid = 85 + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY 86 + agent.configureProxy(proxyDid) 81 87 82 88 return agent.prepare({ 83 89 resolvers: [gates, moderation, aa], ··· 116 122 const moderation = configureModerationForAccount(agent, account) 117 123 const aa = prefetchAgeAssuranceData({agent}) 118 124 119 - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) 125 + const proxyDid = 126 + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY 127 + agent.configureProxy(proxyDid) 120 128 121 129 return agent.prepare({ 122 130 resolvers: [gates, moderation, aa], ··· 223 231 }), 224 232 getAge(birthDate) < 18 && 225 233 networkRetry(3, () => { 226 - return agent.com.atproto.repo.putRecord({ 234 + return pdsAgent(agent).com.atproto.repo.putRecord({ 227 235 repo: account.did, 228 236 collection: 'chat.bsky.actor.declaration', 229 237 rkey: 'self', ··· 288 296 logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`}) 289 297 } 290 298 291 - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) 299 + const proxyDid = 300 + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY 301 + agent.configureProxy(proxyDid) 292 302 293 303 return agent.prepare({ 294 304 resolvers: [gates, moderation, aa], ··· 400 410 } 401 411 }, 402 412 }) 413 + const proxyDid = readCustomAppViewDidUri() || APPVIEW_DID_PROXY 414 + if (proxyDid) { 415 + this.configureProxy(proxyDid) 416 + } 403 417 } 404 418 405 419 async prepare({ ··· 431 445 dispose() { 432 446 this.sessionManager.session = undefined 433 447 this.persistSessionHandler = undefined 448 + } 449 + 450 + cloneWithoutProxy(): BskyAgent { 451 + const cloned = new BskyAgent({service: this.serviceUrl.toString()}) 452 + cloned.sessionManager.session = this.sessionManager.session 453 + return cloned 434 454 } 435 455 } 436 456
+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', ··· 455 456 } 456 457 return agent 457 458 } 459 + 460 + export function useBlankPrefAuthedAgent(): BskyAgent { 461 + const agent = useContext(AgentContext) 462 + if (!agent) { 463 + throw Error('useAgent() must be below <SessionProvider>.') 464 + } 465 + 466 + return useMemo(() => { 467 + return (agent as BskyAppAgent).cloneWithoutProxy() 468 + }, [agent]) 469 + }
+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.