tangled
alpha
login
or
join now
jeanmachine.dev
/
witchsky.app
forked from
jollywhoppers.com/witchsky.app
0
fork
atom
Bluesky app fork with some witchin' additions 💫
0
fork
atom
overview
issues
pulls
pipelines
feat: pds badge for identifying service providers.
jeanmachine.dev
1 week ago
ace97b6b
dd8e3e0e
+776
-96
23 changed files
expand all
collapse all
unified
split
src
components
AccountList.tsx
PdsBadge.tsx
PdsDialog.tsx
ProfileCard.tsx
ProfileHoverCard
index.web.tsx
dms
MessagesListHeader.tsx
icons
Fediverse.tsx
screens
Messages
components
ChatListItem.tsx
PostThread
components
ThreadItemAnchor.tsx
Profile
Header
DisplayName.tsx
ProfileHeaderStandard.tsx
Settings
RunesSettings.tsx
Settings.tsx
state
persisted
schema.ts
preferences
index.tsx
pds-label.tsx
queries
pds-label.ts
view
com
composer
ComposerReplyTo.tsx
text-input
mobile
Autocomplete.tsx
notifications
NotificationFeedItem.tsx
util
PostMeta.tsx
icons
index.tsx
shell
Drawer.tsx
+2
src/components/AccountList.tsx
···
17
17
import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
18
18
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron'
19
19
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
20
20
+
import {PdsBadge} from '#/components/PdsBadge'
20
21
import {Text} from '#/components/Typography'
21
22
import {useSimpleVerificationState} from '#/components/verification'
22
23
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
167
168
profile?.displayName || profile?.handle || account.handle,
168
169
)}
169
170
</Text>
171
171
+
<PdsBadge did={account.did} size="sm" />
170
172
{verification.showBadge && (
171
173
<View>
172
174
<VerificationCheck
+123
src/components/PdsBadge.tsx
···
1
1
+
import {View} from 'react-native'
2
2
+
import {msg} from '@lingui/macro'
3
3
+
import {useLingui} from '@lingui/react'
4
4
+
5
5
+
import {usePdsLabelEnabled, usePdsLabelHideBskyPds} from '#/state/preferences/pds-label'
6
6
+
import {usePdsLabelQuery} from '#/state/queries/pds-label'
7
7
+
import {atoms as a, useBreakpoints} from '#/alf'
8
8
+
import {Button} from '#/components/Button'
9
9
+
import * as Dialog from '#/components/Dialog'
10
10
+
import {FaviconOrGlobe, PdsDialog} from '#/components/PdsDialog'
11
11
+
12
12
+
export function PdsBadge({
13
13
+
did,
14
14
+
size,
15
15
+
}: {
16
16
+
did: string
17
17
+
size: 'lg' | 'md' | 'sm'
18
18
+
}) {
19
19
+
const enabled = usePdsLabelEnabled()
20
20
+
const hideBskyPds = usePdsLabelHideBskyPds()
21
21
+
const {data, isLoading} = usePdsLabelQuery(enabled ? did : undefined)
22
22
+
23
23
+
if (!enabled) return null
24
24
+
if (isLoading) return <PdsBadgeLoading size={size} />
25
25
+
if (!data) return null
26
26
+
if (hideBskyPds && data.isBsky) return null
27
27
+
28
28
+
return (
29
29
+
<PdsBadgeInner
30
30
+
pdsUrl={data.pdsUrl}
31
31
+
faviconUrl={data.faviconUrl}
32
32
+
isBsky={data.isBsky}
33
33
+
isBridged={data.isBridged}
34
34
+
size={size}
35
35
+
/>
36
36
+
)
37
37
+
}
38
38
+
39
39
+
function PdsBadgeLoading({size}: {size: 'lg' | 'md' | 'sm'}) {
40
40
+
const {gtPhone} = useBreakpoints()
41
41
+
let dimensions = 12
42
42
+
if (size === 'lg') {
43
43
+
dimensions = gtPhone ? 20 : 18
44
44
+
} else if (size === 'md') {
45
45
+
dimensions = 14
46
46
+
}
47
47
+
return (
48
48
+
<View style={{width: dimensions, height: dimensions}}>
49
49
+
<FaviconOrGlobe
50
50
+
faviconUrl=""
51
51
+
isBsky={false}
52
52
+
isBridged={false}
53
53
+
size={dimensions}
54
54
+
borderRadius={dimensions / 4}
55
55
+
/>
56
56
+
</View>
57
57
+
)
58
58
+
}
59
59
+
60
60
+
function PdsBadgeInner({
61
61
+
pdsUrl,
62
62
+
faviconUrl,
63
63
+
isBsky,
64
64
+
isBridged,
65
65
+
size,
66
66
+
}: {
67
67
+
pdsUrl: string
68
68
+
faviconUrl: string
69
69
+
isBsky: boolean
70
70
+
isBridged: boolean
71
71
+
size: 'lg' | 'md' | 'sm'
72
72
+
}) {
73
73
+
const {_} = useLingui()
74
74
+
const {gtPhone} = useBreakpoints()
75
75
+
const dialogControl = Dialog.useDialogControl()
76
76
+
77
77
+
let dimensions = 12
78
78
+
if (size === 'lg') {
79
79
+
dimensions = gtPhone ? 20 : 18
80
80
+
} else if (size === 'md') {
81
81
+
dimensions = 14
82
82
+
}
83
83
+
84
84
+
return (
85
85
+
<>
86
86
+
<Button
87
87
+
label={_(msg`View PDS information`)}
88
88
+
hitSlop={20}
89
89
+
onPress={evt => {
90
90
+
evt.preventDefault()
91
91
+
dialogControl.open()
92
92
+
}}>
93
93
+
{({hovered}) => (
94
94
+
<View
95
95
+
style={[
96
96
+
a.justify_center,
97
97
+
a.align_center,
98
98
+
a.transition_transform,
99
99
+
{
100
100
+
width: dimensions,
101
101
+
height: dimensions,
102
102
+
transform: [{scale: hovered ? 1.1 : 1}],
103
103
+
},
104
104
+
]}>
105
105
+
<FaviconOrGlobe
106
106
+
faviconUrl={faviconUrl}
107
107
+
isBsky={isBsky}
108
108
+
isBridged={isBridged}
109
109
+
size={dimensions}
110
110
+
borderRadius={dimensions / 4}
111
111
+
/>
112
112
+
</View>
113
113
+
)}
114
114
+
</Button>
115
115
+
116
116
+
<PdsDialog
117
117
+
control={dialogControl}
118
118
+
pdsUrl={pdsUrl}
119
119
+
faviconUrl={faviconUrl}
120
120
+
/>
121
121
+
</>
122
122
+
)
123
123
+
}
+263
src/components/PdsDialog.tsx
···
1
1
+
import {useState} from 'react'
2
2
+
import {Image, View} from 'react-native'
3
3
+
import {
4
4
+
FontAwesomeIcon,
5
5
+
type FontAwesomeIconStyle,
6
6
+
} from '@fortawesome/react-native-fontawesome'
7
7
+
import {msg, Trans} from '@lingui/macro'
8
8
+
import {useLingui} from '@lingui/react'
9
9
+
10
10
+
import {isBridgedPdsUrl, isBskyPdsUrl} from '#/state/queries/pds-label'
11
11
+
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
12
12
+
import {Button, ButtonText} from '#/components/Button'
13
13
+
import * as Dialog from '#/components/Dialog'
14
14
+
import {Fediverse as FediverseIcon} from '#/components/icons/Fediverse'
15
15
+
import {Mark as BskyMark} from '#/components/icons/Logo'
16
16
+
import {InlineLinkText} from '#/components/Link'
17
17
+
import {Text} from '#/components/Typography'
18
18
+
19
19
+
function formatBskyPdsDisplayName(hostname: string): string {
20
20
+
const match = hostname.match(/^([^.]+)\.([^.]+)\.host\.bsky\.network$/)
21
21
+
if (match) {
22
22
+
const name = match[1].charAt(0).toUpperCase() + match[1].slice(1)
23
23
+
const rawRegion = match[2]
24
24
+
const region = rawRegion
25
25
+
.replace(/^us-east$/, 'US East')
26
26
+
.replace(/^us-west$/, 'US West')
27
27
+
.replace(/^eu-west$/, 'EU West')
28
28
+
.replace(
29
29
+
/^ap-(.+)$/,
30
30
+
(_match: string, r: string) =>
31
31
+
`AP ${r.charAt(0).toUpperCase()}${r.slice(1)}`,
32
32
+
)
33
33
+
return `${name} (${region})`
34
34
+
}
35
35
+
if (hostname === 'bsky.social') return 'Bluesky Social'
36
36
+
return hostname
37
37
+
}
38
38
+
39
39
+
export function PdsDialog({
40
40
+
control,
41
41
+
pdsUrl,
42
42
+
faviconUrl,
43
43
+
}: {
44
44
+
control: Dialog.DialogControlProps
45
45
+
pdsUrl: string
46
46
+
faviconUrl: string
47
47
+
}) {
48
48
+
const {_} = useLingui()
49
49
+
const {gtMobile} = useBreakpoints()
50
50
+
51
51
+
let hostname = pdsUrl
52
52
+
try {
53
53
+
hostname = new URL(pdsUrl).hostname
54
54
+
} catch {}
55
55
+
56
56
+
const isBsky = isBskyPdsUrl(pdsUrl)
57
57
+
const isBridged = isBridgedPdsUrl(pdsUrl)
58
58
+
const displayName = isBsky ? formatBskyPdsDisplayName(hostname) : hostname
59
59
+
60
60
+
return (
61
61
+
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
62
62
+
<Dialog.Handle />
63
63
+
<Dialog.ScrollableInner
64
64
+
label={_(msg`PDS Information`)}
65
65
+
style={[
66
66
+
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
67
67
+
]}>
68
68
+
<View style={[a.gap_md, a.pb_lg]}>
69
69
+
<View style={[a.flex_row, a.align_center, a.gap_md]}>
70
70
+
<FaviconOrGlobe
71
71
+
faviconUrl={faviconUrl}
72
72
+
isBsky={isBsky}
73
73
+
isBridged={isBridged}
74
74
+
size={36}
75
75
+
/>
76
76
+
<View style={[a.flex_1]}>
77
77
+
<Text
78
78
+
style={[a.text_2xl, a.font_semi_bold, a.leading_tight]}
79
79
+
numberOfLines={1}>
80
80
+
{displayName}
81
81
+
</Text>
82
82
+
{isBsky && (
83
83
+
<Text style={[a.text_sm]}>
84
84
+
<Trans>Bluesky-hosted PDS</Trans>
85
85
+
</Text>
86
86
+
)}
87
87
+
{isBridged && (
88
88
+
<Text style={[a.text_sm]}>
89
89
+
<Trans>Fediverse bridge</Trans>
90
90
+
</Text>
91
91
+
)}
92
92
+
</View>
93
93
+
</View>
94
94
+
95
95
+
<Text style={[a.text_md, a.leading_snug]}>
96
96
+
<Trans>
97
97
+
This account's data is stored on a Personal Data Server (PDS):{' '}
98
98
+
<InlineLinkText
99
99
+
to={pdsUrl}
100
100
+
label={displayName}
101
101
+
style={[a.text_md, a.font_semi_bold]}>
102
102
+
{displayName}
103
103
+
</InlineLinkText>
104
104
+
{'. '}A PDS is where your posts, follows, and other data live on
105
105
+
the AT Protocol network.
106
106
+
</Trans>
107
107
+
</Text>
108
108
+
109
109
+
{isBridged && (
110
110
+
<Text style={[a.text_md, a.leading_snug]}>
111
111
+
<Trans>
112
112
+
This account is bridged from the Fediverse via{' '}
113
113
+
<InlineLinkText
114
114
+
to="https://fed.brid.gy"
115
115
+
label="Bridgy Fed"
116
116
+
style={[a.text_md, a.font_semi_bold]}>
117
117
+
Bridgy Fed
118
118
+
</InlineLinkText>
119
119
+
. Their original account lives on a Fediverse platform such as
120
120
+
Mastodon.
121
121
+
</Trans>
122
122
+
</Text>
123
123
+
)}
124
124
+
125
125
+
{!isBsky && !isBridged && (
126
126
+
<Text style={[a.text_md, a.leading_snug]}>
127
127
+
<Trans>
128
128
+
This account is self-hosted or uses a third-party PDS provider.
129
129
+
</Trans>
130
130
+
</Text>
131
131
+
)}
132
132
+
</View>
133
133
+
134
134
+
<View
135
135
+
style={[
136
136
+
a.w_full,
137
137
+
a.gap_sm,
138
138
+
gtMobile
139
139
+
? [a.flex_row, a.flex_row_reverse, a.justify_start]
140
140
+
: [a.flex_col],
141
141
+
]}>
142
142
+
<Button
143
143
+
label={_(msg`Close dialog`)}
144
144
+
size="small"
145
145
+
variant="solid"
146
146
+
color="primary"
147
147
+
onPress={() => control.close()}>
148
148
+
<ButtonText>
149
149
+
<Trans>Close</Trans>
150
150
+
</ButtonText>
151
151
+
</Button>
152
152
+
</View>
153
153
+
154
154
+
<Dialog.Close />
155
155
+
</Dialog.ScrollableInner>
156
156
+
</Dialog.Outer>
157
157
+
)
158
158
+
}
159
159
+
160
160
+
export function FaviconOrGlobe({
161
161
+
faviconUrl,
162
162
+
isBsky,
163
163
+
isBridged,
164
164
+
size,
165
165
+
borderRadius,
166
166
+
}: {
167
167
+
faviconUrl: string
168
168
+
isBsky: boolean
169
169
+
isBridged: boolean
170
170
+
size: number
171
171
+
borderRadius?: number
172
172
+
}) {
173
173
+
const t = useTheme()
174
174
+
const [imgError, setImgError] = useState(false)
175
175
+
const resolvedBorderRadius = borderRadius ?? size / 5
176
176
+
177
177
+
if (isBsky) {
178
178
+
return (
179
179
+
<View
180
180
+
style={[
181
181
+
a.align_center,
182
182
+
a.justify_center,
183
183
+
a.overflow_hidden,
184
184
+
{
185
185
+
width: size,
186
186
+
height: size,
187
187
+
borderRadius: resolvedBorderRadius,
188
188
+
backgroundColor: '#0085ff',
189
189
+
},
190
190
+
]}>
191
191
+
<BskyMark width={size * 0.65} style={{color: '#fff'}} />
192
192
+
</View>
193
193
+
)
194
194
+
}
195
195
+
196
196
+
if (isBridged) {
197
197
+
return (
198
198
+
<View
199
199
+
style={[
200
200
+
a.align_center,
201
201
+
a.justify_center,
202
202
+
a.overflow_hidden,
203
203
+
{
204
204
+
width: size,
205
205
+
height: size,
206
206
+
borderRadius: resolvedBorderRadius,
207
207
+
backgroundColor: '#6364FF',
208
208
+
},
209
209
+
]}>
210
210
+
<FediverseIcon
211
211
+
width={Math.round(size * 0.7)}
212
212
+
style={{color: '#fff'}}
213
213
+
/>
214
214
+
</View>
215
215
+
)
216
216
+
}
217
217
+
218
218
+
if (!imgError && faviconUrl) {
219
219
+
return (
220
220
+
<View
221
221
+
style={[
222
222
+
a.overflow_hidden,
223
223
+
a.align_center,
224
224
+
a.justify_center,
225
225
+
{
226
226
+
width: size,
227
227
+
height: size,
228
228
+
borderRadius: resolvedBorderRadius,
229
229
+
backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
230
230
+
},
231
231
+
]}>
232
232
+
<Image
233
233
+
source={{uri: faviconUrl}}
234
234
+
style={{width: size, height: size}}
235
235
+
onError={() => setImgError(true)}
236
236
+
accessibilityIgnoresInvertColors
237
237
+
/>
238
238
+
</View>
239
239
+
)
240
240
+
}
241
241
+
242
242
+
return (
243
243
+
<View
244
244
+
style={[
245
245
+
a.align_center,
246
246
+
a.justify_center,
247
247
+
{
248
248
+
width: size,
249
249
+
height: size,
250
250
+
borderRadius: resolvedBorderRadius,
251
251
+
backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
252
252
+
},
253
253
+
]}>
254
254
+
<FontAwesomeIcon
255
255
+
icon="database"
256
256
+
size={Math.round(size * 0.6)}
257
257
+
style={
258
258
+
{color: t.atoms.text_contrast_medium.color} as FontAwesomeIconStyle
259
259
+
}
260
260
+
/>
261
261
+
</View>
262
262
+
)
263
263
+
}
+12
src/components/ProfileCard.tsx
···
40
40
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
41
41
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
42
42
import {Link as InternalLink, type LinkProps} from '#/components/Link'
43
43
+
import {PdsBadge} from '#/components/PdsBadge'
43
44
import * as Pills from '#/components/Pills'
44
45
import {RichText} from '#/components/RichText'
45
46
import {Text} from '#/components/Typography'
···
259
260
numberOfLines={1}>
260
261
{forceLTR(name)}
261
262
</Text>
263
263
+
<View
264
264
+
style={[
265
265
+
a.pl_2xs,
266
266
+
a.self_center,
267
267
+
{marginTop: platform({default: 0, android: -1})},
268
268
+
]}>
269
269
+
<PdsBadge did={profile.did} size="sm" />
270
270
+
</View>
262
271
{verification.showBadge && (
263
272
<View
264
273
style={[
···
318
327
numberOfLines={1}>
319
328
{name}
320
329
</Text>
330
330
+
<View style={[a.pl_xs]}>
331
331
+
<PdsBadge did={profile.did} size="sm" />
332
332
+
</View>
321
333
{verification.showBadge && (
322
334
<View style={[a.pl_xs]}>
323
335
<VerificationCheck
+12
-4
src/components/ProfileHoverCard/index.web.tsx
···
38
38
} from '#/components/KnownFollowers'
39
39
import {InlineLinkText, Link} from '#/components/Link'
40
40
import {Loader} from '#/components/Loader'
41
41
+
import {PdsBadge} from '#/components/PdsBadge'
41
42
import * as Pills from '#/components/Pills'
42
43
import {Portal} from '#/components/Portal'
43
44
import {RichText} from '#/components/RichText'
···
546
547
moderation.ui('displayName'),
547
548
)}
548
549
</Text>
550
550
+
<View style={[a.pl_xs, {marginTop: -2}]}>
551
551
+
<PdsBadge did={profile.did} size="sm" />
552
552
+
</View>
549
553
{verification.showBadge && (
550
554
<View
551
555
style={[
···
581
585
582
586
{!isBlockedUser && (
583
587
<>
584
584
-
{disableFollowersMetrics && disableFollowingMetrics ? ( null ) :
588
588
+
{disableFollowersMetrics && disableFollowingMetrics ? null : (
585
589
<View style={[a.flex_row, a.flex_wrap, a.gap_md, a.pt_xs]}>
586
590
{!disableFollowersMetrics ? (
587
591
<InlineLinkText
···
589
593
label={`${followers} ${pluralizedFollowers}`}
590
594
style={[t.atoms.text]}
591
595
onPress={hide}>
592
592
-
<Text style={[a.text_md, a.font_semi_bold]}>{followers} </Text>
596
596
+
<Text style={[a.text_md, a.font_semi_bold]}>
597
597
+
{followers}{' '}
598
598
+
</Text>
593
599
<Text style={[t.atoms.text_contrast_medium]}>
594
600
{pluralizedFollowers}
595
601
</Text>
···
601
607
label={_(msg`${following} following`)}
602
608
style={[t.atoms.text]}
603
609
onPress={hide}>
604
604
-
<Text style={[a.text_md, a.font_semi_bold]}>{following} </Text>
610
610
+
<Text style={[a.text_md, a.font_semi_bold]}>
611
611
+
{following}{' '}
612
612
+
</Text>
605
613
<Text style={[t.atoms.text_contrast_medium]}>
606
614
{pluralizedFollowings}
607
615
</Text>
608
616
</InlineLinkText>
609
617
) : null}
610
618
</View>
611
611
-
}
619
619
+
)}
612
620
613
621
{profile.description?.trim() && !moderation.ui('profileView').blur ? (
614
622
<View style={[a.pt_md]}>
+4
src/components/dms/MessagesListHeader.tsx
···
20
20
import * as Layout from '#/components/Layout'
21
21
import {Link} from '#/components/Link'
22
22
import {PostAlerts} from '#/components/moderation/PostAlerts'
23
23
+
import {PdsBadge} from '#/components/PdsBadge'
23
24
import {Text} from '#/components/Typography'
24
25
import {useSimpleVerificationState} from '#/components/verification'
25
26
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
161
162
numberOfLines={1}>
162
163
{displayName}
163
164
</Text>
165
165
+
<View style={[a.pl_xs]}>
166
166
+
<PdsBadge did={profile.did} size="sm" />
167
167
+
</View>
164
168
{verification.showBadge && (
165
169
<View style={[a.pl_xs]}>
166
170
<VerificationCheck
+6
src/components/icons/Fediverse.tsx
···
1
1
+
import {createSinglePathSVG} from './TEMPLATE'
2
2
+
3
3
+
export const Fediverse = createSinglePathSVG({
4
4
+
path: 'M426.8 590.9C407.1 590.4 389.3 579.3 380.2 561.8C371.2 544.4 372.3 523.4 383.2 507C394.1 490.6 413 481.5 432.6 483.1C452.3 483.6 470.1 494.7 479.2 512.2C488.2 529.6 487.1 550.6 476.2 567C465.3 583.4 446.4 592.5 426.8 590.9zM376.7 510.3C371.2 521.2 369.3 533.6 371.1 545.7L200.7 518.4C206.2 507.5 208.2 495.1 206.4 483L376.7 510.3zM144.7 545.6C125.1 545.1 107.3 533.9 98.3 516.5C89.2 499 90.4 478.1 101.3 461.7C112.1 445.4 131 436.2 150.6 437.8C170.2 438.3 188 449.5 197 466.9C206.1 484.4 204.9 505.3 194 521.7C183.2 538 164.3 547.2 144.7 545.6zM402.4 484.2C391.5 489.8 382.7 498.6 377 509.5L306.4 438.6L340 421.6L402.4 484.3zM518.1 325C526.8 333.6 537.9 339.3 550 341.4L471.4 494.8C462.7 486.2 451.6 480.5 439.5 478.4L518.1 325zM408.7 283.3L439.2 478.4C427.1 476.5 414.7 478.3 403.8 483.7L371.6 277.4L408.8 283.4zM382.4 392.9L206.2 482.2C204.2 470.1 198.6 459 190 450.2L376.6 355.6L382.4 392.8zM229.7 370.9L189.4 449.6C180.7 441 169.6 435.3 157.5 433.3L203.1 344.3L229.7 371zM156.7 433C144.6 431.2 132.3 433.2 121.3 438.6L94.7 268.3C106.8 270.1 119.2 268.2 130.1 262.7L156.7 433zM303.8 385.2L270.2 402.2L130.8 262.3C141.7 256.7 150.5 247.9 156.2 237L303.8 385.2zM501.3 292.4C503.3 304.5 508.9 315.6 517.5 324.3L428.2 369.5L422.4 332.3L501.3 292.3zM556.9 336.7C537.3 336.2 519.5 325 510.5 307.6C501.4 290.1 502.6 269.2 513.5 252.8C524.3 236.5 543.2 227.3 562.8 228.9C582.4 229.4 600.2 240.6 609.2 258C618.3 275.5 617.1 296.4 606.2 312.8C595.4 329.1 576.5 338.3 556.9 336.7zM316.6 122.7C325.3 131.3 336.4 137 348.4 139L253.1 325.1L226.5 298.4L316.5 122.6zM506.9 256.1C501.4 267 499.4 279.4 501.2 291.4L294.8 258.3L312 224.8L507 256.1zM100.7 263.6C81.1 263.1 63.3 251.9 54.3 234.5C45.2 217 46.4 196.1 57.3 179.7C68.1 163.4 87 154.2 106.6 155.8C126.2 156.3 144 167.5 153 184.9C162.1 202.4 160.9 223.3 150 239.7C139.2 256 120.3 265.2 100.7 263.6zM532.7 230.2C521.8 235.8 513 244.6 507.3 255.5L385.5 133.3C396.4 127.7 405.2 118.9 410.9 108L532.6 230.2zM261.3 216.6L244.1 250.1L156.7 236.1C162.1 225.2 164.1 212.8 162.2 200.7L261.2 216.6zM400.8 232.5L363.6 226.5L350 139.3C362.1 141 374.5 139 385.3 133.4L400.8 232.5zM299.8 90.2C301.8 102.3 307.4 113.4 316 122.1L162.1 200.1C160.1 188 154.5 176.9 145.9 168.2L299.8 90.2zM355.4 134.5C335.7 134 317.9 122.9 308.8 105.4C299.8 88 300.9 67 311.8 50.6C322.7 34.2 341.6 25.1 361.2 26.7C380.9 27.2 398.7 38.3 407.8 55.8C416.8 73.2 415.7 94.2 404.8 110.6C393.9 127 375 136.1 355.4 134.5z',
5
5
+
viewBox: '0 0 640 640',
6
6
+
})
+4
src/screens/Messages/components/ChatListItem.tsx
···
41
41
import {Link} from '#/components/Link'
42
42
import {useMenuControl} from '#/components/Menu'
43
43
import {PostAlerts} from '#/components/moderation/PostAlerts'
44
44
+
import {PdsBadge} from '#/components/PdsBadge'
44
45
import {createPortalGroup} from '#/components/Portal'
45
46
import {Text} from '#/components/Typography'
46
47
import {useSimpleVerificationState} from '#/components/verification'
···
417
418
]}>
418
419
{displayName}
419
420
</Text>
421
421
+
</View>
422
422
+
<View style={[a.pl_xs, a.self_center]}>
423
423
+
<PdsBadge did={profile.did} size="sm" />
420
424
</View>
421
425
{verification.showBadge && (
422
426
<View style={[a.pl_xs, a.self_center]}>
+4
-1
src/screens/PostThread/components/ThreadItemAnchor.tsx
···
57
57
import {ContentHider} from '#/components/moderation/ContentHider'
58
58
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
59
59
import {PostAlerts} from '#/components/moderation/PostAlerts'
60
60
+
import {PdsBadge} from '#/components/PdsBadge'
60
61
import {type AppModerationCause} from '#/components/Pills'
61
62
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
62
63
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
···
381
382
)}
382
383
</Text>
383
384
384
384
-
<View style={[a.pl_xs]}>
385
385
+
<View
386
386
+
style={[a.pl_xs, a.flex_row, a.gap_2xs, a.align_center]}>
387
387
+
<PdsBadge did={post.author.did} size="md" />
385
388
<VerificationCheckButton profile={authorShadow} size="md" />
386
389
</View>
387
390
</View>
+5
src/screens/Profile/Header/DisplayName.tsx
···
5
5
import {sanitizeHandle} from '#/lib/strings/handles'
6
6
import {type Shadow} from '#/state/cache/types'
7
7
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
8
8
+
import {PdsBadge} from '#/components/PdsBadge'
8
9
import {Text} from '#/components/Typography'
9
10
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
10
11
···
36
37
<View
37
38
style={[
38
39
a.pl_xs,
40
40
+
a.flex_row,
41
41
+
a.gap_2xs,
42
42
+
a.align_center,
39
43
{
40
44
marginTop: platform({ios: 2}),
41
45
},
42
46
]}>
47
47
+
<PdsBadge did={profile.did} size="lg" />
43
48
<VerificationCheckButton profile={profile} size="lg" />
44
49
</View>
45
50
</Text>
+10
-1
src/screens/Profile/Header/ProfileHeaderStandard.tsx
···
42
42
shouldShowKnownFollowers,
43
43
} from '#/components/KnownFollowers'
44
44
import {Link} from '#/components/Link'
45
45
+
import {PdsBadge} from '#/components/PdsBadge'
45
46
import * as Prompt from '#/components/Prompt'
46
47
import {RichText} from '#/components/RichText'
47
48
import * as Toast from '#/components/Toast'
···
162
163
profile.displayName || sanitizeHandle(profile.handle),
163
164
moderation.ui('displayName'),
164
165
)}
165
165
-
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
166
166
+
<View
167
167
+
style={[
168
168
+
a.pl_xs,
169
169
+
a.flex_row,
170
170
+
a.gap_2xs,
171
171
+
a.align_center,
172
172
+
{marginTop: platform({ios: 2})},
173
173
+
]}>
174
174
+
<PdsBadge did={profile.did} size="lg" />
166
175
<VerificationCheckButton profile={profile} size="lg" />
167
176
</View>
168
177
</Text>
+45
-4
src/screens/Settings/RunesSettings.tsx
···
5
5
import {useLingui} from '@lingui/react'
6
6
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
7
7
8
8
-
import { DEFAULT_ALT_TEXT_AI_MODEL } from '#/lib/constants'
8
8
+
import {DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants'
9
9
import {usePalette} from '#/lib/hooks/usePalette'
10
10
import {type CommonNavigatorParams} from '#/lib/routes/types'
11
11
import {dynamicActivate} from '#/locale/i18n'
···
114
114
useSetOpenRouterApiKey,
115
115
useSetOpenRouterModel,
116
116
} from '#/state/preferences/openrouter'
117
117
+
import {
118
118
+
usePdsLabelEnabled,
119
119
+
usePdsLabelHideBskyPds,
120
120
+
useSetPdsLabelEnabled,
121
121
+
useSetPdsLabelHideBskyPds,
122
122
+
} from '#/state/preferences/pds-label'
117
123
import {
118
124
usePostReplacement,
119
125
useSetPostReplacement,
···
708
714
const deerVerificationEnabled = useDeerVerificationEnabled()
709
715
const setDeerVerificationEnabled = useSetDeerVerificationEnabled()
710
716
717
717
+
const pdsLabelEnabled = usePdsLabelEnabled()
718
718
+
const setPdsLabelEnabled = useSetPdsLabelEnabled()
719
719
+
const pdsLabelHideBskyPds = usePdsLabelHideBskyPds()
720
720
+
const setPdsLabelHideBskyPds = useSetPdsLabelHideBskyPds()
721
721
+
711
722
const repostCarouselEnabled = useRepostCarouselEnabled()
712
723
const setRepostCarouselEnabled = useSetRepostCarouselEnabled()
713
724
···
760
771
</Toggle.Item>
761
772
<Toggle.Item
762
773
name="use_handle_in_links"
763
763
-
label={_(msg`Use handles in profile links instead of DIDs (requires restart)`)}
774
774
+
label={_(
775
775
+
msg`Use handles in profile links instead of DIDs (requires restart)`,
776
776
+
)}
764
777
value={handleInLinks ?? false}
765
778
onChange={value => setHandleInLinks(value)}
766
779
style={[a.w_full]}>
···
909
922
<Trans>Tweaks</Trans>
910
923
</SettingsList.ItemText>
911
924
<Toggle.Item
925
925
+
name="pds_label_badge"
926
926
+
label={_(
927
927
+
msg`Show a PDS badge next to the display name on profiles`,
928
928
+
)}
929
929
+
value={pdsLabelEnabled}
930
930
+
onChange={value => setPdsLabelEnabled(value)}
931
931
+
style={[a.w_full]}>
932
932
+
<Toggle.LabelText style={[a.flex_1]}>
933
933
+
<Trans>
934
934
+
Show a PDS badge next to the display name on profiles
935
935
+
</Trans>
936
936
+
</Toggle.LabelText>
937
937
+
<Toggle.Platform />
938
938
+
</Toggle.Item>
939
939
+
{pdsLabelEnabled && (
940
940
+
<Toggle.Item
941
941
+
name="pds_label_hide_bsky"
942
942
+
label={_(msg`Hide PDS badge for Bluesky-hosted accounts`)}
943
943
+
value={pdsLabelHideBskyPds}
944
944
+
onChange={value => setPdsLabelHideBskyPds(value)}
945
945
+
style={[a.w_full]}>
946
946
+
<Toggle.LabelText style={[a.flex_1]}>
947
947
+
<Trans>Hide PDS badge for Bluesky-hosted accounts</Trans>
948
948
+
</Toggle.LabelText>
949
949
+
<Toggle.Platform />
950
950
+
</Toggle.Item>
951
951
+
)}
952
952
+
953
953
+
<Toggle.Item
912
954
name="repost_carousel"
913
955
label={_(msg`Combine reposts into a horizontal carousel`)}
914
956
value={repostCarouselEnabled}
···
1186
1228
<SettingsList.Item>
1187
1229
<Admonition type="info" style={[a.flex_1]}>
1188
1230
<Trans>
1189
1189
-
Current model:{' '}
1190
1190
-
{openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '}
1231
1231
+
Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '}
1191
1232
<InlineLinkText
1192
1233
to="https://openrouter.ai/models?fmt=cards&input_modalities=image&order=most-popular"
1193
1234
label="openrouter.ai">
+17
-11
src/screens/Settings/Settings.tsx
···
6
6
import {useLingui} from '@lingui/react'
7
7
import {Trans} from '@lingui/react/macro'
8
8
import {useNavigation} from '@react-navigation/native'
9
9
-
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
9
9
+
import type * as nativeStack from '@react-navigation/native-stack'
10
10
11
11
import {HELP_DESK_URL} from '#/lib/constants'
12
12
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
···
62
62
import * as Layout from '#/components/Layout'
63
63
import {Loader} from '#/components/Loader'
64
64
import * as Menu from '#/components/Menu'
65
65
+
import {PdsBadge} from '#/components/PdsBadge'
65
66
import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config'
66
67
import * as Prompt from '#/components/Prompt'
67
68
import {Text} from '#/components/Typography'
···
76
77
import {device, useStorage} from '#/storage'
77
78
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
78
79
79
79
-
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
80
80
+
type Props = nativeStack.NativeStackScreenProps<
81
81
+
CommonNavigatorParams,
82
82
+
'Settings'
83
83
+
>
80
84
export function SettingsScreen({}: Props) {
81
85
const ax = useAnalytics()
82
86
const {_} = useLingui()
···
372
376
]}>
373
377
{displayName}
374
378
</Text>
375
375
-
{shouldShowVerificationCheckButton(verificationState) && (
376
376
-
<View
377
377
-
style={[
378
378
-
{
379
379
-
marginTop: platform({web: 8, ios: 8, android: 10}),
380
380
-
},
381
381
-
]}>
379
379
+
<View
380
380
+
style={[
381
381
+
a.flex_row,
382
382
+
a.gap_2xs,
383
383
+
a.align_center,
384
384
+
{marginTop: platform({web: 8, ios: 8, android: 10})},
385
385
+
]}>
386
386
+
<PdsBadge did={shadow.did} size="lg" />
387
387
+
{shouldShowVerificationCheckButton(verificationState) && (
382
388
<VerificationCheckButton profile={shadow} size="lg" />
383
383
-
</View>
384
384
-
)}
389
389
+
)}
390
390
+
</View>
385
391
</View>
386
392
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
387
393
{sanitizeHandle(profile.handle, '@')}
+10
src/state/persisted/schema.ts
···
173
173
.optional(),
174
174
highQualityImages: z.boolean().optional(),
175
175
hideUnreplyablePosts: z.boolean().optional(),
176
176
+
pdsLabel: z
177
177
+
.object({
178
178
+
enabled: z.boolean(),
179
179
+
hideBskyPds: z.boolean(),
180
180
+
})
181
181
+
.optional(),
176
182
177
183
postReplacement: z.object({
178
184
enabled: z.boolean().optional(),
···
304
310
},
305
311
highQualityImages: false,
306
312
hideUnreplyablePosts: false,
313
313
+
pdsLabel: {
314
314
+
enabled: false,
315
315
+
hideBskyPds: true,
316
316
+
},
307
317
showExternalShareButtons: false,
308
318
translationServicePreference: 'google',
309
319
libreTranslateInstance: 'https://libretranslate.com/',
+78
-75
src/state/preferences/index.tsx
···
37
37
import {Provider as NoAppLabelersProvider} from './no-app-labelers'
38
38
import {Provider as NoDiscoverProvider} from './no-discover-fallback'
39
39
import {Provider as OpenRouterProvider} from './openrouter'
40
40
+
import {Provider as PdsLabelProvider} from './pds-label'
40
41
import {Provider as PostNameReplacementProvider} from './post-name-replacement.tsx'
41
42
import {Provider as RepostCarouselProvider} from './repost-carousel-enabled'
42
43
import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle'
···
96
97
<ConstellationProvider>
97
98
<ConstellationInstanceProvider>
98
99
<DeerVerificationProvider>
99
99
-
<NoDiscoverProvider>
100
100
-
<ShowLinkInHandleProvider>
101
101
-
<UseHandleInLinksProvider>
102
102
-
<LargeAltBadgeProvider>
103
103
-
<ExternalEmbedsProvider>
104
104
-
<HiddenPostsProvider>
105
105
-
<HighQualityImagesProvider>
106
106
-
<InAppBrowserProvider>
107
107
-
<DisableHapticsProvider>
108
108
-
<AutoplayProvider>
109
109
-
<UsedStarterPacksProvider>
110
110
-
<SubtitlesProvider>
111
111
-
<TrendingSettingsProvider>
112
112
-
<RepostCarouselProvider>
113
113
-
<KawaiiProvider>
114
114
-
<HideFeedsPromoTabProvider>
115
115
-
<DisableViaRepostNotificationProvider>
116
116
-
<DisableLikesMetricsProvider>
117
117
-
<DisableRepostsMetricsProvider>
118
118
-
<DisableQuotesMetricsProvider>
119
119
-
<DisableSavesMetricsProvider>
120
120
-
<DisableReplyMetricsProvider>
121
121
-
<DisableFollowersMetricsProvider>
122
122
-
<DisableFollowingMetricsProvider>
123
123
-
<DisableFollowedByMetricsProvider>
124
124
-
<DisablePostsMetricsProvider>
125
125
-
<HideSimilarAccountsRecommProvider>
126
126
-
<HideUnreplyablePostsProvider>
127
127
-
<EnableSquareAvatarsProvider>
128
128
-
<EnableSquareButtonsProvider>
129
129
-
<PostNameReplacementProvider>
130
130
-
<DisableVerifyEmailReminderProvider>
131
131
-
<TranslationServicePreferenceProvider>
132
132
-
<OpenRouterProvider>
133
133
-
<DisableComposerPromptProvider>
134
134
-
<DiscoverContextEnabledProvider>
135
135
-
{
136
136
-
children
137
137
-
}
138
138
-
</DiscoverContextEnabledProvider>
139
139
-
</DisableComposerPromptProvider>
140
140
-
</OpenRouterProvider>
141
141
-
</TranslationServicePreferenceProvider>
142
142
-
</DisableVerifyEmailReminderProvider>
143
143
-
</PostNameReplacementProvider>
144
144
-
</EnableSquareButtonsProvider>
145
145
-
</EnableSquareAvatarsProvider>
146
146
-
</HideUnreplyablePostsProvider>
147
147
-
</HideSimilarAccountsRecommProvider>
148
148
-
</DisablePostsMetricsProvider>
149
149
-
</DisableFollowedByMetricsProvider>
150
150
-
</DisableFollowingMetricsProvider>
151
151
-
</DisableFollowersMetricsProvider>
152
152
-
</DisableReplyMetricsProvider>
153
153
-
</DisableSavesMetricsProvider>
154
154
-
</DisableQuotesMetricsProvider>
155
155
-
</DisableRepostsMetricsProvider>
156
156
-
</DisableLikesMetricsProvider>
157
157
-
</DisableViaRepostNotificationProvider>
158
158
-
</HideFeedsPromoTabProvider>
159
159
-
</KawaiiProvider>
160
160
-
</RepostCarouselProvider>
161
161
-
</TrendingSettingsProvider>
162
162
-
</SubtitlesProvider>
163
163
-
</UsedStarterPacksProvider>
164
164
-
</AutoplayProvider>
165
165
-
</DisableHapticsProvider>
166
166
-
</InAppBrowserProvider>
167
167
-
</HighQualityImagesProvider>
168
168
-
</HiddenPostsProvider>
169
169
-
</ExternalEmbedsProvider>
170
170
-
</LargeAltBadgeProvider>
171
171
-
</UseHandleInLinksProvider>
172
172
-
</ShowLinkInHandleProvider>
173
173
-
</NoDiscoverProvider>
100
100
+
<PdsLabelProvider>
101
101
+
<NoDiscoverProvider>
102
102
+
<ShowLinkInHandleProvider>
103
103
+
<UseHandleInLinksProvider>
104
104
+
<LargeAltBadgeProvider>
105
105
+
<ExternalEmbedsProvider>
106
106
+
<HiddenPostsProvider>
107
107
+
<HighQualityImagesProvider>
108
108
+
<InAppBrowserProvider>
109
109
+
<DisableHapticsProvider>
110
110
+
<AutoplayProvider>
111
111
+
<UsedStarterPacksProvider>
112
112
+
<SubtitlesProvider>
113
113
+
<TrendingSettingsProvider>
114
114
+
<RepostCarouselProvider>
115
115
+
<KawaiiProvider>
116
116
+
<HideFeedsPromoTabProvider>
117
117
+
<DisableViaRepostNotificationProvider>
118
118
+
<DisableLikesMetricsProvider>
119
119
+
<DisableRepostsMetricsProvider>
120
120
+
<DisableQuotesMetricsProvider>
121
121
+
<DisableSavesMetricsProvider>
122
122
+
<DisableReplyMetricsProvider>
123
123
+
<DisableFollowersMetricsProvider>
124
124
+
<DisableFollowingMetricsProvider>
125
125
+
<DisableFollowedByMetricsProvider>
126
126
+
<DisablePostsMetricsProvider>
127
127
+
<HideSimilarAccountsRecommProvider>
128
128
+
<HideUnreplyablePostsProvider>
129
129
+
<EnableSquareAvatarsProvider>
130
130
+
<EnableSquareButtonsProvider>
131
131
+
<PostNameReplacementProvider>
132
132
+
<DisableVerifyEmailReminderProvider>
133
133
+
<TranslationServicePreferenceProvider>
134
134
+
<OpenRouterProvider>
135
135
+
<DisableComposerPromptProvider>
136
136
+
<DiscoverContextEnabledProvider>
137
137
+
{
138
138
+
children
139
139
+
}
140
140
+
</DiscoverContextEnabledProvider>
141
141
+
</DisableComposerPromptProvider>
142
142
+
</OpenRouterProvider>
143
143
+
</TranslationServicePreferenceProvider>
144
144
+
</DisableVerifyEmailReminderProvider>
145
145
+
</PostNameReplacementProvider>
146
146
+
</EnableSquareButtonsProvider>
147
147
+
</EnableSquareAvatarsProvider>
148
148
+
</HideUnreplyablePostsProvider>
149
149
+
</HideSimilarAccountsRecommProvider>
150
150
+
</DisablePostsMetricsProvider>
151
151
+
</DisableFollowedByMetricsProvider>
152
152
+
</DisableFollowingMetricsProvider>
153
153
+
</DisableFollowersMetricsProvider>
154
154
+
</DisableReplyMetricsProvider>
155
155
+
</DisableSavesMetricsProvider>
156
156
+
</DisableQuotesMetricsProvider>
157
157
+
</DisableRepostsMetricsProvider>
158
158
+
</DisableLikesMetricsProvider>
159
159
+
</DisableViaRepostNotificationProvider>
160
160
+
</HideFeedsPromoTabProvider>
161
161
+
</KawaiiProvider>
162
162
+
</RepostCarouselProvider>
163
163
+
</TrendingSettingsProvider>
164
164
+
</SubtitlesProvider>
165
165
+
</UsedStarterPacksProvider>
166
166
+
</AutoplayProvider>
167
167
+
</DisableHapticsProvider>
168
168
+
</InAppBrowserProvider>
169
169
+
</HighQualityImagesProvider>
170
170
+
</HiddenPostsProvider>
171
171
+
</ExternalEmbedsProvider>
172
172
+
</LargeAltBadgeProvider>
173
173
+
</UseHandleInLinksProvider>
174
174
+
</ShowLinkInHandleProvider>
175
175
+
</NoDiscoverProvider>
176
176
+
</PdsLabelProvider>
174
177
</DeerVerificationProvider>
175
178
</ConstellationInstanceProvider>
176
179
</ConstellationProvider>
+75
src/state/preferences/pds-label.tsx
···
1
1
+
import React from 'react'
2
2
+
3
3
+
import * as persisted from '#/state/persisted'
4
4
+
5
5
+
type StateContext = persisted.Schema['pdsLabel']
6
6
+
type SetContext = (v: persisted.Schema['pdsLabel']) => void
7
7
+
8
8
+
const stateContext = React.createContext<StateContext>(
9
9
+
persisted.defaults.pdsLabel,
10
10
+
)
11
11
+
const setContext = React.createContext<SetContext>(
12
12
+
(_: persisted.Schema['pdsLabel']) => {},
13
13
+
)
14
14
+
15
15
+
export function Provider({children}: React.PropsWithChildren<{}>) {
16
16
+
const [state, setState] = React.useState(persisted.get('pdsLabel'))
17
17
+
18
18
+
const setStateWrapped = React.useCallback(
19
19
+
(pdsLabel: persisted.Schema['pdsLabel']) => {
20
20
+
setState(pdsLabel)
21
21
+
persisted.write('pdsLabel', pdsLabel)
22
22
+
},
23
23
+
[setState],
24
24
+
)
25
25
+
26
26
+
React.useEffect(() => {
27
27
+
return persisted.onUpdate('pdsLabel', next => {
28
28
+
setState(next)
29
29
+
})
30
30
+
}, [setStateWrapped])
31
31
+
32
32
+
return (
33
33
+
<stateContext.Provider value={state}>
34
34
+
<setContext.Provider value={setStateWrapped}>
35
35
+
{children}
36
36
+
</setContext.Provider>
37
37
+
</stateContext.Provider>
38
38
+
)
39
39
+
}
40
40
+
41
41
+
export function usePdsLabel() {
42
42
+
return React.useContext(stateContext) ?? persisted.defaults.pdsLabel!
43
43
+
}
44
44
+
45
45
+
export function usePdsLabelEnabled() {
46
46
+
return usePdsLabel().enabled
47
47
+
}
48
48
+
49
49
+
export function usePdsLabelHideBskyPds() {
50
50
+
return usePdsLabel().hideBskyPds
51
51
+
}
52
52
+
53
53
+
export function useSetPdsLabel() {
54
54
+
return React.useContext(setContext)
55
55
+
}
56
56
+
57
57
+
export function useSetPdsLabelEnabled() {
58
58
+
const pdsLabel = usePdsLabel()
59
59
+
const setPdsLabel = useSetPdsLabel()
60
60
+
61
61
+
return React.useMemo(
62
62
+
() => (enabled: boolean) => setPdsLabel({...pdsLabel, enabled}),
63
63
+
[pdsLabel, setPdsLabel],
64
64
+
)
65
65
+
}
66
66
+
67
67
+
export function useSetPdsLabelHideBskyPds() {
68
68
+
const pdsLabel = usePdsLabel()
69
69
+
const setPdsLabel = useSetPdsLabel()
70
70
+
71
71
+
return React.useMemo(
72
72
+
() => (hideBskyPds: boolean) => setPdsLabel({...pdsLabel, hideBskyPds}),
73
73
+
[pdsLabel, setPdsLabel],
74
74
+
)
75
75
+
}
+78
src/state/queries/pds-label.ts
···
1
1
+
import {useQuery} from '@tanstack/react-query'
2
2
+
3
3
+
import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity'
4
4
+
5
5
+
const BSKY_PDS_HOSTNAMES = ['bsky.social', 'staging.bsky.dev']
6
6
+
const BSKY_PDS_SUFFIX = '.bsky.network'
7
7
+
const BRIDGY_FED_HOSTNAME = 'atproto.brid.gy'
8
8
+
9
9
+
export function isBskyPdsUrl(url: string): boolean {
10
10
+
try {
11
11
+
const hostname = new URL(url).hostname
12
12
+
return (
13
13
+
BSKY_PDS_HOSTNAMES.includes(hostname) ||
14
14
+
hostname.endsWith(BSKY_PDS_SUFFIX)
15
15
+
)
16
16
+
} catch {
17
17
+
return false
18
18
+
}
19
19
+
}
20
20
+
21
21
+
export function isBridgedPdsUrl(url: string): boolean {
22
22
+
try {
23
23
+
return new URL(url).hostname === BRIDGY_FED_HOSTNAME
24
24
+
} catch {
25
25
+
return false
26
26
+
}
27
27
+
}
28
28
+
29
29
+
async function fetchFaviconUrl(pdsUrl: string): Promise<string> {
30
30
+
let origin = ''
31
31
+
try {
32
32
+
origin = new URL(pdsUrl).origin
33
33
+
} catch {
34
34
+
return ''
35
35
+
}
36
36
+
try {
37
37
+
const res = await fetch(origin, {headers: {Accept: 'text/html'}})
38
38
+
if (res.ok) {
39
39
+
const html = await res.text()
40
40
+
// Match <link rel="icon"> or <link rel="shortcut icon"> in either attribute order
41
41
+
const match =
42
42
+
html.match(
43
43
+
/<link[^>]+rel=["'](?:shortcut icon|icon)["'][^>]*href=["']([^"']+)["']/i,
44
44
+
) ||
45
45
+
html.match(
46
46
+
/<link[^>]+href=["']([^"']+)["'][^>]*rel=["'](?:shortcut icon|icon)["']/i,
47
47
+
)
48
48
+
if (match) {
49
49
+
const href = match[1]
50
50
+
if (href.startsWith('http')) return href
51
51
+
if (href.startsWith('//')) return `https:${href}`
52
52
+
if (href.startsWith('/')) return `${origin}${href}`
53
53
+
return `${origin}/${href}`
54
54
+
}
55
55
+
}
56
56
+
} catch {}
57
57
+
return `${origin}/favicon.ico`
58
58
+
}
59
59
+
60
60
+
export const RQKEY_ROOT = 'pds-label'
61
61
+
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
62
62
+
63
63
+
export function usePdsLabelQuery(did: string | undefined) {
64
64
+
return useQuery({
65
65
+
queryKey: RQKEY(did ?? ''),
66
66
+
queryFn: async () => {
67
67
+
if (!did) return null
68
68
+
const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}`)
69
69
+
const isBsky = isBskyPdsUrl(pdsUrl)
70
70
+
const isBridged = isBridgedPdsUrl(pdsUrl)
71
71
+
const faviconUrl =
72
72
+
isBsky || isBridged ? '' : await fetchFaviconUrl(pdsUrl)
73
73
+
return {pdsUrl, isBsky, isBridged, faviconUrl}
74
74
+
},
75
75
+
enabled: !!did,
76
76
+
staleTime: 1000 * 60 * 60, // 1 hour
77
77
+
})
78
78
+
}
+4
src/view/com/composer/ComposerReplyTo.tsx
···
16
16
import {type ComposerOptsPostRef} from '#/state/shell/composer'
17
17
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
18
18
import {atoms as a, useTheme, web} from '#/alf'
19
19
+
import {PdsBadge} from '#/components/PdsBadge'
19
20
import {QuoteEmbed} from '#/components/Post/Embed'
20
21
import {Text} from '#/components/Typography'
21
22
import {useSimpleVerificationState} from '#/components/verification'
···
116
117
sanitizeHandle(replyTo.author.handle),
117
118
)}
118
119
</Text>
120
120
+
<View style={[a.pl_xs]}>
121
121
+
<PdsBadge did={replyTo.author.did} size="sm" />
122
122
+
</View>
119
123
{verification.showBadge && (
120
124
<View style={[a.pl_xs]}>
121
125
<VerificationCheck
+4
src/view/com/composer/text-input/mobile/Autocomplete.tsx
···
9
9
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
10
10
import {UserAvatar} from '#/view/com/util/UserAvatar'
11
11
import {atoms as a, platform, useTheme} from '#/alf'
12
12
+
import {PdsBadge} from '#/components/PdsBadge'
12
13
import {Text} from '#/components/Typography'
13
14
import {useSimpleVerificationState} from '#/components/verification'
14
15
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
115
116
numberOfLines={1}>
116
117
{displayName}
117
118
</Text>
119
119
+
<View style={[{marginTop: platform({android: -2})}]}>
120
120
+
<PdsBadge did={profile.did} size="sm" />
121
121
+
</View>
118
122
{state.isVerified && (
119
123
<View
120
124
style={[
+4
src/view/com/notifications/NotificationFeedItem.tsx
···
65
65
import {VerifiedCheck} from '#/components/icons/VerifiedCheck'
66
66
import {InlineLinkText, Link} from '#/components/Link'
67
67
import * as MediaPreview from '#/components/MediaPreview'
68
68
+
import {PdsBadge} from '#/components/PdsBadge'
68
69
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
69
70
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
70
71
import {SubtleHover} from '#/components/SubtleHover'
···
1060
1061
author.profile.displayName || author.profile.handle,
1061
1062
)}
1062
1063
</Text>
1064
1064
+
<View style={[a.pl_xs, a.self_center]}>
1065
1065
+
<PdsBadge did={author.profile.did} size="sm" />
1066
1066
+
</View>
1063
1067
{verification.showBadge && (
1064
1068
<View style={[a.pl_xs, a.self_center]}>
1065
1069
<VerificationCheck
+11
src/view/com/util/PostMeta.tsx
···
16
16
import {unstableCacheProfileView} from '#/state/queries/profile'
17
17
import {atoms as a, platform, useTheme, web} from '#/alf'
18
18
import {WebOnlyInlineLinkText} from '#/components/Link'
19
19
+
import {PdsBadge} from '#/components/PdsBadge'
19
20
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
20
21
import {Text} from '#/components/Typography'
21
22
import {useSimpleVerificationState} from '#/components/verification'
···
113
114
),
114
115
)}
115
116
</MaybeLinkText>
117
117
+
<View
118
118
+
style={[
119
119
+
a.pl_2xs,
120
120
+
a.self_center,
121
121
+
{
122
122
+
marginTop: platform({web: 1, ios: 0, android: -1}),
123
123
+
},
124
124
+
]}>
125
125
+
<PdsBadge did={author.did} size="sm" />
126
126
+
</View>
116
127
{verification.showBadge && (
117
128
<View
118
129
style={[
+3
src/view/icons/index.tsx
···
54
54
import {faClipboardCheck} from '@fortawesome/free-solid-svg-icons/faClipboardCheck'
55
55
import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
56
56
import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash'
57
57
+
import {faDatabase} from '@fortawesome/free-solid-svg-icons/faDatabase'
57
58
import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload'
58
59
import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis'
59
60
import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope'
···
150
151
faCommentSlash,
151
152
faComments,
152
153
faCompass,
154
154
+
faDatabase,
153
155
faDownload,
154
156
faEllipsis,
155
157
faEnvelope,
···
160
162
faFire,
161
163
faFlask,
162
164
faFloppyDisk,
165
165
+
163
166
faGear,
164
167
faGlobe,
165
168
faHand,
+2
src/view/shell/Drawer.tsx
···
57
57
} from '#/components/icons/UserCircle'
58
58
import {InlineLinkText} from '#/components/Link'
59
59
import {Text} from '#/components/Typography'
60
60
+
import {PdsBadge} from '#/components/PdsBadge'
60
61
import {useSimpleVerificationState} from '#/components/verification'
61
62
import {VerificationCheck} from '#/components/verification/VerificationCheck'
62
63
import {IS_WEB} from '#/env'
···
104
105
numberOfLines={1}>
105
106
{profile?.displayName || account.handle}
106
107
</Text>
108
108
+
<PdsBadge did={account.did} size="md" />
107
109
{verification.showBadge && (
108
110
<View
109
111
style={{