+14
-5
src/components/PostControls/PostMenu/PostMenuItems.tsx
+14
-5
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 {msg} from '@lingui/macro'
···
313
314
}
314
315
}
315
316
316
-
let videoUri: {uri: string; width: number; height: number; blobRef?: BlobRef; altText?: string} | undefined
317
+
let videoUri:
318
+
| {
319
+
uri: string
320
+
width: number
321
+
height: number
322
+
blobRef?: BlobRef
323
+
altText?: string
324
+
}
325
+
| undefined
317
326
let recordVideo: AppBskyEmbedVideo.Main | undefined
318
-
327
+
319
328
if (recordEmbed?.$type === 'app.bsky.embed.video') {
320
329
recordVideo = recordEmbed as AppBskyEmbedVideo.Main
321
330
} else if (recordEmbed?.$type === 'app.bsky.embed.recordWithMedia') {
···
324
333
recordVideo = media as AppBskyEmbedVideo.Main
325
334
}
326
335
}
327
-
336
+
328
337
if (post.embed?.$type === 'app.bsky.embed.video#view') {
329
338
const embed = post.embed as AppBskyEmbedVideo.View
330
339
if (recordVideo) {
···
569
578
if (!videoEmbed) return
570
579
const did = post.author.did
571
580
const cid = videoEmbed.cid
572
-
if (!did.startsWith('did:')) return
573
-
const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}`)
581
+
if (!isDid(did)) return
582
+
const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}:${string}`)
574
583
const uri = `${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`
575
584
576
585
Toast.show(_(msg({message: 'Downloading video...', context: 'toast'})))
+5
src/env/common.ts
+5
src/env/common.ts
···
111
111
*/
112
112
export const BAPP_CONFIG_DEV_BYPASS_SECRET: string =
113
113
process.env.BAPP_CONFIG_DEV_BYPASS_SECRET
114
+
115
+
export const ENV_PUBLIC_BSKY_SERVICE: string | undefined =
116
+
process.env.EXPO_PUBLIC_PUBLIC_BSKY_SERVICE
117
+
export const ENV_APPVIEW_DID_PROXY: `did:${string}#bsky_appview` | undefined =
118
+
process.env.EXPO_PUBLIC_APPVIEW_DID_PROXY
+3
-2
src/lib/api/feed/custom.ts
+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}},
+4
-2
src/lib/constants.ts
+4
-2
src/lib/constants.ts
···
3
3
4
4
import {type ProxyHeaderValue} from '#/state/session/agent'
5
5
import {BLUESKY_PROXY_DID, CHAT_PROXY_DID} from '#/env'
6
-
6
+
import {ENV_APPVIEW_DID_PROXY, ENV_PUBLIC_BSKY_SERVICE} from '#/env'
7
7
export const LOCAL_DEV_SERVICE =
8
8
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
9
9
export const STAGING_SERVICE = 'https://staging.bsky.dev'
10
10
export const BSKY_SERVICE = 'https://bsky.social'
11
11
export const BSKY_SERVICE_DID = 'did:web:bsky.social'
12
-
export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'
12
+
export const PUBLIC_BSKY_SERVICE =
13
+
ENV_PUBLIC_BSKY_SERVICE || 'https://public.api.bsky.app'
13
14
export const DEFAULT_SERVICE = BSKY_SERVICE
14
15
export const HELP_DESK_URL = `https://tangled.org/jollywhoppers.com/witchsky.app/`
15
16
export const EMBED_SERVICE = 'https://embed.bsky.app'
16
17
export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
17
18
export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download'
19
+
export const APPVIEW_DID_PROXY = ENV_APPVIEW_DID_PROXY
18
20
export const STARTER_PACK_MAX_SIZE = 150
19
21
export const CARD_ASPECT_RATIO = 1200 / 630
20
22
+2
-1
src/lib/react-query.tsx
+2
-1
src/lib/react-query.tsx
···
11
11
12
12
import {isNative} from '#/platform/detection'
13
13
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
14
+
import {PUBLIC_BSKY_SERVICE} from './constants'
14
15
15
16
// any query keys in this array will be persisted to AsyncStorage
16
17
export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
···
22
23
setTimeout(() => {
23
24
controller.abort()
24
25
}, 15e3)
25
-
const res = await fetch('https://public.api.bsky.app/xrpc/_health', {
26
+
const res = await fetch(`${PUBLIC_BSKY_SERVICE}/xrpc/_health`, {
26
27
cache: 'no-store',
27
28
signal: controller.signal,
28
29
})
+157
-1
src/screens/Settings/DeerSettings.tsx
+157
-1
src/screens/Settings/DeerSettings.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, Trans} from '@lingui/macro'
5
6
import {useLingui} from '@lingui/react'
6
7
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
7
8
9
+
import {APPVIEW_DID_PROXY} from '#/lib/constants'
8
10
import {usePalette} from '#/lib/hooks/usePalette'
9
11
import {type CommonNavigatorParams} from '#/lib/routes/types'
10
12
import {type Gate} from '#/lib/statsig/gates'
···
20
22
useConstellationInstance,
21
23
useSetConstellationInstance,
22
24
} from '#/state/preferences/constellation-instance'
25
+
import {useCustomAppViewDid} from '#/state/preferences/custom-appview-did'
23
26
import {
24
27
useDeerVerificationEnabled,
25
28
useDeerVerificationTrusted,
···
107
110
useShowLinkInHandle,
108
111
} from '#/state/preferences/show-link-in-handle.tsx'
109
112
import {useProfilesQuery} from '#/state/queries/profile'
113
+
import {findService, useDidDocument} from '#/state/queries/resolve-identity'
114
+
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
110
115
import * as SettingsList from '#/screens/Settings/components/SettingsList'
111
116
import {atoms as a, useBreakpoints} from '#/alf'
112
117
import {Admonition} from '#/components/Admonition'
···
124
129
import * as Layout from '#/components/Layout'
125
130
import {Text} from '#/components/Typography'
126
131
import {SearchProfileCard} from '../Search/components/SearchProfileCard'
127
-
128
132
type Props = NativeStackScreenProps<CommonNavigatorParams>
129
133
130
134
function ConstellationInstanceDialog({
···
201
205
)
202
206
}
203
207
208
+
function CustomAppViewDidDialog({
209
+
control,
210
+
}: {
211
+
control: Dialog.DialogControlProps
212
+
}) {
213
+
const pal = usePalette('default')
214
+
const {_} = useLingui()
215
+
216
+
const [did, setDid] = useState('')
217
+
const [, setCustomAppViewDid] = useCustomAppViewDid()
218
+
219
+
const doc = useDidDocument({did})
220
+
const bskyAppViewService =
221
+
doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView')
222
+
223
+
const submit = () => {
224
+
if (did.length === 0) {
225
+
setCustomAppViewDid(undefined)
226
+
control.close()
227
+
return
228
+
}
229
+
if (!bskyAppViewService?.serviceEndpoint) return
230
+
setCustomAppViewDid(did)
231
+
control.close()
232
+
}
233
+
234
+
return (
235
+
<Dialog.Outer
236
+
control={control}
237
+
nativeOptions={{preventExpansion: true}}
238
+
onClose={() => setDid('')}>
239
+
<Dialog.Handle />
240
+
<Dialog.ScrollableInner label={_(msg`Custom AppView Proxy DID`)}>
241
+
<View style={[a.gap_sm, a.pb_lg]}>
242
+
<Text style={[a.text_2xl, a.font_bold]}>
243
+
<Trans>Custom AppView Proxy DID</Trans>
244
+
</Text>
245
+
</View>
246
+
247
+
<View style={a.gap_lg}>
248
+
<Dialog.Input
249
+
label="Text input field"
250
+
autoFocus
251
+
style={[styles.textInput, pal.border, pal.text]}
252
+
onChangeText={value => {
253
+
setDid(value)
254
+
}}
255
+
placeholder={
256
+
APPVIEW_DID_PROXY?.substring(0, APPVIEW_DID_PROXY.indexOf('#')) ||
257
+
`did:web:api.bsky.app`
258
+
}
259
+
placeholderTextColor={pal.colors.textLight}
260
+
onSubmitEditing={submit}
261
+
accessibilityHint={_(
262
+
msg`Input the DID of the AppView to proxy requests through`,
263
+
)}
264
+
isInvalid={
265
+
!!did && !bskyAppViewService?.serviceEndpoint && !doc.isLoading
266
+
}
267
+
/>
268
+
269
+
{did && !isDid(did) && (
270
+
<View>
271
+
<ErrorMessage message={_(msg`must enter a DID`)} />
272
+
</View>
273
+
)}
274
+
275
+
{did && (did.includes('#') || did.includes('?')) && (
276
+
<View>
277
+
<ErrorMessage message={_(msg`don't include the service id`)} />
278
+
</View>
279
+
)}
280
+
281
+
{doc.isError && (
282
+
<View>
283
+
<ErrorMessage
284
+
message={
285
+
doc.error.message || _(msg`document resolution failure`)
286
+
}
287
+
/>
288
+
</View>
289
+
)}
290
+
291
+
{doc.data &&
292
+
!bskyAppViewService &&
293
+
(doc.data as {message?: string}).message && (
294
+
<View>
295
+
<ErrorMessage
296
+
message={(doc.data as {message: string}).message}
297
+
/>
298
+
</View>
299
+
)}
300
+
301
+
{doc.data && !bskyAppViewService && (
302
+
<View>
303
+
<ErrorMessage
304
+
message={_(msg`document doesn't contain #bsky_appview service`)}
305
+
/>
306
+
</View>
307
+
)}
308
+
309
+
{bskyAppViewService && (
310
+
<Text style={[a.text_sm, a.leading_snug]}>
311
+
{JSON.stringify(bskyAppViewService, null, 2)}
312
+
</Text>
313
+
)}
314
+
315
+
<View style={isWeb && [a.flex_row, a.justify_end]}>
316
+
<Button
317
+
label={_(msg`Save`)}
318
+
size="large"
319
+
onPress={submit}
320
+
variant="solid"
321
+
color={did.length > 0 ? 'primary' : 'secondary'}
322
+
disabled={
323
+
did.length !== 0 && !bskyAppViewService?.serviceEndpoint
324
+
}>
325
+
<ButtonText>
326
+
{did.length > 0 ? <Trans>Save</Trans> : <Trans>Reset</Trans>}
327
+
</ButtonText>
328
+
</Button>
329
+
</View>
330
+
</View>
331
+
332
+
<Dialog.Close />
333
+
</Dialog.ScrollableInner>
334
+
</Dialog.Outer>
335
+
)
336
+
}
337
+
204
338
function TrustedVerifiersDialog({
205
339
control,
206
340
}: {
···
335
469
[gate]: value,
336
470
})
337
471
}
472
+
const [customAppViewDid] = useCustomAppViewDid()
473
+
const setCustomAppViewDidControl = Dialog.useDialogControl()
338
474
339
475
return (
340
476
<Layout.Screen>
···
475
611
Constellation is used to supplement AppView responses for custom
476
612
verifications and nuclear block bypass, via backlinks. Current
477
613
instance: {constellationInstance}
614
+
</Trans>
615
+
</Admonition>
616
+
</SettingsList.Item>
617
+
618
+
<SettingsList.Item>
619
+
<SettingsList.ItemIcon icon={StarIcon} />
620
+
<SettingsList.ItemText>
621
+
<Trans>{`Custom AppView DID`}</Trans>
622
+
</SettingsList.ItemText>
623
+
<SettingsList.BadgeButton
624
+
label={customAppViewDid ? _(msg`Set`) : _(msg`Change`)}
625
+
onPress={() => setCustomAppViewDidControl.open()}
626
+
/>
627
+
</SettingsList.Item>
628
+
<SettingsList.Item>
629
+
<Admonition type="info" style={[a.flex_1]}>
630
+
<Trans>
631
+
Restart app after changing your AppView.
632
+
{customAppViewDid && _(` Currently ${customAppViewDid}`)}
478
633
</Trans>
479
634
</Admonition>
480
635
</SettingsList.Item>
···
801
956
</SettingsList.Container>
802
957
</Layout.Content>
803
958
<ConstellationInstanceDialog control={setConstellationInstanceControl} />
959
+
<CustomAppViewDidDialog control={setCustomAppViewDidControl} />
804
960
<TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} />
805
961
</Layout.Screen>
806
962
)
+21
src/state/preferences/custom-appview-did.tsx
+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` as `did:${string}#bsky_appview`
21
+
}
+57
-15
src/state/queries/resolve-identity.ts
+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
}
+18
-4
src/state/session/agent.ts
+18
-4
src/state/session/agent.ts
···
13
13
14
14
import {networkRetry} from '#/lib/async/retry'
15
15
import {
16
+
APPVIEW_DID_PROXY,
16
17
BLUESKY_PROXY_HEADER,
17
18
BSKY_SERVICE,
18
19
DISCOVER_SAVED_FEED,
···
25
26
import {logger} from '#/logger'
26
27
import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders'
27
28
import {emitNetworkConfirmed, emitNetworkLost} from '../events'
29
+
import {readCustomAppViewDidUri} from '../preferences/custom-appview-did'
28
30
import {addSessionErrorLog} from './logging'
29
31
import {
30
32
configureModerationForAccount,
···
39
41
configureModerationForGuest() // Side effect but only relevant for tests
40
42
41
43
const agent = new BskyAppAgent({service: PUBLIC_BSKY_SERVICE})
42
-
agent.configureProxy(BLUESKY_PROXY_HEADER.get())
44
+
const proxyDid =
45
+
readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY
46
+
agent.configureProxy(proxyDid)
43
47
return agent
44
48
}
45
49
···
77
81
}
78
82
}
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(gates, moderation, onSessionChange)
83
89
}
···
112
118
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
113
119
const moderation = configureModerationForAccount(agent, account)
114
120
115
-
agent.configureProxy(BLUESKY_PROXY_HEADER.get())
121
+
const proxyDid =
122
+
readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY
123
+
agent.configureProxy(proxyDid)
116
124
117
125
return agent.prepare(gates, moderation, onSessionChange)
118
126
}
···
201
209
logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`})
202
210
}
203
211
204
-
agent.configureProxy(BLUESKY_PROXY_HEADER.get())
212
+
const proxyDid =
213
+
readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY
214
+
agent.configureProxy(proxyDid)
205
215
206
216
return agent.prepare(gates, moderation, onSessionChange)
207
217
}
···
304
314
}
305
315
},
306
316
})
317
+
const proxyDid = readCustomAppViewDidUri() || APPVIEW_DID_PROXY
318
+
if (proxyDid) {
319
+
this.configureProxy(proxyDid)
320
+
}
307
321
}
308
322
309
323
async prepare(