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
7c1beca1
dd8e3e0e
+796
-95
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
import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
18
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron'
19
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
0
20
import {Text} from '#/components/Typography'
21
import {useSimpleVerificationState} from '#/components/verification'
22
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
167
profile?.displayName || profile?.handle || account.handle,
168
)}
169
</Text>
0
170
{verification.showBadge && (
171
<View>
172
<VerificationCheck
···
17
import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
18
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron'
19
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
20
+
import {PdsBadge} from '#/components/PdsBadge'
21
import {Text} from '#/components/Typography'
22
import {useSimpleVerificationState} from '#/components/verification'
23
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
168
profile?.displayName || profile?.handle || account.handle,
169
)}
170
</Text>
171
+
<PdsBadge did={account.did} size="sm" />
172
{verification.showBadge && (
173
<View>
174
<VerificationCheck
+152
src/components/PdsBadge.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import {View} from 'react-native'
2
+
import {msg} from '@lingui/macro'
3
+
import {useLingui} from '@lingui/react'
4
+
5
+
import {
6
+
usePdsLabelEnabled,
7
+
usePdsLabelHideBskyPds,
8
+
} from '#/state/preferences/pds-label'
9
+
import {usePdsLabelQuery} from '#/state/queries/pds-label'
10
+
import {atoms as a, useBreakpoints} from '#/alf'
11
+
import {Button} from '#/components/Button'
12
+
import * as Dialog from '#/components/Dialog'
13
+
import {FaviconOrGlobe, PdsDialog} from '#/components/PdsDialog'
14
+
import {IS_WEB} from '#/env'
15
+
16
+
export function PdsBadge({
17
+
did,
18
+
size,
19
+
interactive = true,
20
+
}: {
21
+
did: string
22
+
size: 'lg' | 'md' | 'sm'
23
+
interactive?: boolean
24
+
}) {
25
+
const enabled = usePdsLabelEnabled()
26
+
const hideBskyPds = usePdsLabelHideBskyPds()
27
+
const {data, isLoading} = usePdsLabelQuery(enabled ? did : undefined)
28
+
29
+
if (!enabled) return null
30
+
if (isLoading) return <PdsBadgeLoading size={size} />
31
+
if (!data) return null
32
+
if (hideBskyPds && data.isBsky) return null
33
+
34
+
return (
35
+
<PdsBadgeInner
36
+
pdsUrl={data.pdsUrl}
37
+
faviconUrl={data.faviconUrl}
38
+
isBsky={data.isBsky}
39
+
isBridged={data.isBridged}
40
+
size={size}
41
+
interactive={interactive}
42
+
/>
43
+
)
44
+
}
45
+
46
+
function PdsBadgeLoading({size}: {size: 'lg' | 'md' | 'sm'}) {
47
+
const {gtPhone} = useBreakpoints()
48
+
let dimensions = 12
49
+
if (size === 'lg') {
50
+
dimensions = gtPhone ? 20 : 18
51
+
} else if (size === 'md') {
52
+
dimensions = 14
53
+
}
54
+
return (
55
+
<View style={{width: dimensions, height: dimensions}}>
56
+
<FaviconOrGlobe
57
+
faviconUrl=""
58
+
isBsky={false}
59
+
isBridged={false}
60
+
size={dimensions}
61
+
borderRadius={dimensions / 4}
62
+
/>
63
+
</View>
64
+
)
65
+
}
66
+
67
+
function PdsBadgeInner({
68
+
pdsUrl,
69
+
faviconUrl,
70
+
isBsky,
71
+
isBridged,
72
+
size,
73
+
interactive,
74
+
}: {
75
+
pdsUrl: string
76
+
faviconUrl: string
77
+
isBsky: boolean
78
+
isBridged: boolean
79
+
size: 'lg' | 'md' | 'sm'
80
+
interactive: boolean
81
+
}) {
82
+
const {_} = useLingui()
83
+
const {gtPhone} = useBreakpoints()
84
+
const dialogControl = Dialog.useDialogControl()
85
+
86
+
let dimensions = 12
87
+
if (size === 'lg') {
88
+
dimensions = gtPhone ? 20 : 18
89
+
} else if (size === 'md') {
90
+
dimensions = 14
91
+
}
92
+
93
+
const icon = (
94
+
<FaviconOrGlobe
95
+
faviconUrl={faviconUrl}
96
+
isBsky={isBsky}
97
+
isBridged={isBridged}
98
+
size={dimensions}
99
+
borderRadius={dimensions / 4}
100
+
/>
101
+
)
102
+
103
+
if (!interactive) {
104
+
return (
105
+
<View
106
+
style={[
107
+
a.justify_center,
108
+
a.align_center,
109
+
{width: dimensions, height: dimensions},
110
+
]}>
111
+
{icon}
112
+
</View>
113
+
)
114
+
}
115
+
116
+
return (
117
+
<>
118
+
<Button
119
+
label={_(msg`View PDS information`)}
120
+
hitSlop={20}
121
+
onPress={evt => {
122
+
evt.preventDefault()
123
+
dialogControl.open()
124
+
if (IS_WEB) {
125
+
;(document.activeElement as HTMLElement | null)?.blur()
126
+
}
127
+
}}>
128
+
{({hovered}) => (
129
+
<View
130
+
style={[
131
+
a.justify_center,
132
+
a.align_center,
133
+
a.transition_transform,
134
+
{
135
+
width: dimensions,
136
+
height: dimensions,
137
+
transform: [{scale: hovered ? 1.1 : 1}],
138
+
},
139
+
]}>
140
+
{icon}
141
+
</View>
142
+
)}
143
+
</Button>
144
+
145
+
<PdsDialog
146
+
control={dialogControl}
147
+
pdsUrl={pdsUrl}
148
+
faviconUrl={faviconUrl}
149
+
/>
150
+
</>
151
+
)
152
+
}
+260
src/components/PdsDialog.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import {useState} from 'react'
2
+
import {Image, View} from 'react-native'
3
+
import {
4
+
FontAwesomeIcon,
5
+
type FontAwesomeIconStyle,
6
+
} from '@fortawesome/react-native-fontawesome'
7
+
import {msg, Trans} from '@lingui/macro'
8
+
import {useLingui} from '@lingui/react'
9
+
10
+
import {isBridgedPdsUrl, isBskyPdsUrl} from '#/state/queries/pds-label'
11
+
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
12
+
import {Button, ButtonText} from '#/components/Button'
13
+
import * as Dialog from '#/components/Dialog'
14
+
import {Fediverse as FediverseIcon} from '#/components/icons/Fediverse'
15
+
import {Mark as BskyMark} from '#/components/icons/Logo'
16
+
import {InlineLinkText} from '#/components/Link'
17
+
import {Text} from '#/components/Typography'
18
+
19
+
function formatBskyPdsDisplayName(hostname: string): string {
20
+
const match = hostname.match(/^([^.]+)\.([^.]+)\.host\.bsky\.network$/)
21
+
if (match) {
22
+
const name = match[1].charAt(0).toUpperCase() + match[1].slice(1)
23
+
const rawRegion = match[2]
24
+
const region = rawRegion
25
+
.replace(/^us-east$/, 'US East')
26
+
.replace(/^us-west$/, 'US West')
27
+
.replace(/^eu-west$/, 'EU West')
28
+
.replace(
29
+
/^ap-(.+)$/,
30
+
(_match: string, r: string) =>
31
+
`AP ${r.charAt(0).toUpperCase()}${r.slice(1)}`,
32
+
)
33
+
return `${name} (${region})`
34
+
}
35
+
if (hostname === 'bsky.social') return 'Bluesky Social'
36
+
return hostname
37
+
}
38
+
39
+
export function PdsDialog({
40
+
control,
41
+
pdsUrl,
42
+
faviconUrl,
43
+
}: {
44
+
control: Dialog.DialogControlProps
45
+
pdsUrl: string
46
+
faviconUrl: string
47
+
}) {
48
+
const {_} = useLingui()
49
+
const {gtMobile} = useBreakpoints()
50
+
51
+
let hostname = pdsUrl
52
+
try {
53
+
hostname = new URL(pdsUrl).hostname
54
+
} catch {}
55
+
56
+
const isBsky = isBskyPdsUrl(pdsUrl)
57
+
const isBridged = isBridgedPdsUrl(pdsUrl)
58
+
const displayName = isBsky ? formatBskyPdsDisplayName(hostname) : hostname
59
+
60
+
return (
61
+
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
62
+
<Dialog.Handle />
63
+
<Dialog.ScrollableInner
64
+
label={_(msg`PDS Information`)}
65
+
style={[
66
+
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
67
+
]}>
68
+
<View style={[a.gap_md, a.pb_lg]}>
69
+
<View style={[a.flex_row, a.align_center, a.gap_md]}>
70
+
<FaviconOrGlobe
71
+
faviconUrl={faviconUrl}
72
+
isBsky={isBsky}
73
+
isBridged={isBridged}
74
+
size={36}
75
+
/>
76
+
<View style={[a.flex_1]}>
77
+
<Text
78
+
style={[a.text_2xl, a.font_semi_bold, a.leading_tight]}
79
+
numberOfLines={1}>
80
+
{displayName}
81
+
</Text>
82
+
{isBsky && (
83
+
<Text style={[a.text_sm]}>
84
+
<Trans>Bluesky-hosted PDS</Trans>
85
+
</Text>
86
+
)}
87
+
{isBridged && (
88
+
<Text style={[a.text_sm]}>
89
+
<Trans>Fediverse bridge</Trans>
90
+
</Text>
91
+
)}
92
+
</View>
93
+
</View>
94
+
95
+
<Text style={[a.text_md, a.leading_snug]}>
96
+
<Trans>
97
+
This account's data is stored on a Personal Data Server (PDS):{' '}
98
+
<InlineLinkText
99
+
to={pdsUrl}
100
+
label={displayName}
101
+
style={[a.text_md, a.font_semi_bold]}>
102
+
{displayName}
103
+
</InlineLinkText>
104
+
{'. '}A PDS is where your posts, follows, and other data live on
105
+
the AT Protocol network.
106
+
</Trans>
107
+
</Text>
108
+
109
+
{isBridged && (
110
+
<Text style={[a.text_md, a.leading_snug]}>
111
+
<Trans>
112
+
This account is bridged from the Fediverse via{' '}
113
+
<InlineLinkText
114
+
to="https://fed.brid.gy"
115
+
label="Bridgy Fed"
116
+
style={[a.text_md, a.font_semi_bold]}>
117
+
Bridgy Fed
118
+
</InlineLinkText>
119
+
. Their original account lives on a Fediverse platform such as
120
+
Mastodon.
121
+
</Trans>
122
+
</Text>
123
+
)}
124
+
125
+
{!isBsky && !isBridged && (
126
+
<Text style={[a.text_md, a.leading_snug]}>
127
+
<Trans>
128
+
This account is self-hosted or uses a third-party PDS provider.
129
+
</Trans>
130
+
</Text>
131
+
)}
132
+
</View>
133
+
134
+
<View
135
+
style={[
136
+
a.w_full,
137
+
a.gap_sm,
138
+
gtMobile
139
+
? [a.flex_row, a.flex_row_reverse, a.justify_start]
140
+
: [a.flex_col],
141
+
]}>
142
+
<Button
143
+
label={_(msg`Close dialog`)}
144
+
size="small"
145
+
variant="solid"
146
+
color="primary"
147
+
onPress={() => control.close()}>
148
+
<ButtonText>
149
+
<Trans>Close</Trans>
150
+
</ButtonText>
151
+
</Button>
152
+
</View>
153
+
154
+
<Dialog.Close />
155
+
</Dialog.ScrollableInner>
156
+
</Dialog.Outer>
157
+
)
158
+
}
159
+
160
+
export function FaviconOrGlobe({
161
+
faviconUrl,
162
+
isBsky,
163
+
isBridged,
164
+
size,
165
+
borderRadius,
166
+
}: {
167
+
faviconUrl: string
168
+
isBsky: boolean
169
+
isBridged: boolean
170
+
size: number
171
+
borderRadius?: number
172
+
}) {
173
+
const t = useTheme()
174
+
const [imgError, setImgError] = useState(false)
175
+
const resolvedBorderRadius = borderRadius ?? size / 5
176
+
177
+
if (isBsky) {
178
+
return (
179
+
<View
180
+
style={[
181
+
a.align_center,
182
+
a.justify_center,
183
+
a.overflow_hidden,
184
+
{
185
+
width: size,
186
+
height: size,
187
+
borderRadius: resolvedBorderRadius,
188
+
backgroundColor: '#0085ff',
189
+
},
190
+
]}>
191
+
<BskyMark width={Math.round(size * 0.8)} style={{color: '#fff'}} />
192
+
</View>
193
+
)
194
+
}
195
+
196
+
if (isBridged) {
197
+
return (
198
+
<View
199
+
style={[
200
+
a.align_center,
201
+
a.justify_center,
202
+
a.overflow_hidden,
203
+
{
204
+
width: size,
205
+
height: size,
206
+
borderRadius: resolvedBorderRadius,
207
+
backgroundColor: '#6364FF',
208
+
},
209
+
]}>
210
+
<FediverseIcon width={Math.round(size * 0.8)} style={{color: '#fff'}} />
211
+
</View>
212
+
)
213
+
}
214
+
215
+
if (!imgError && faviconUrl) {
216
+
return (
217
+
<View
218
+
style={[
219
+
a.overflow_hidden,
220
+
a.align_center,
221
+
a.justify_center,
222
+
{
223
+
width: size,
224
+
height: size,
225
+
borderRadius: resolvedBorderRadius,
226
+
backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
227
+
},
228
+
]}>
229
+
<Image
230
+
source={{uri: faviconUrl}}
231
+
style={{width: size, height: size}}
232
+
onError={() => setImgError(true)}
233
+
accessibilityIgnoresInvertColors
234
+
/>
235
+
</View>
236
+
)
237
+
}
238
+
239
+
return (
240
+
<View
241
+
style={[
242
+
a.align_center,
243
+
a.justify_center,
244
+
{
245
+
width: size,
246
+
height: size,
247
+
borderRadius: resolvedBorderRadius,
248
+
backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
249
+
},
250
+
]}>
251
+
<FontAwesomeIcon
252
+
icon="database"
253
+
size={Math.round(size * 0.7)}
254
+
style={
255
+
{color: t.atoms.text_contrast_medium.color} as FontAwesomeIconStyle
256
+
}
257
+
/>
258
+
</View>
259
+
)
260
+
}
+12
src/components/ProfileCard.tsx
···
40
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
41
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
42
import {Link as InternalLink, type LinkProps} from '#/components/Link'
0
43
import * as Pills from '#/components/Pills'
44
import {RichText} from '#/components/RichText'
45
import {Text} from '#/components/Typography'
···
259
numberOfLines={1}>
260
{forceLTR(name)}
261
</Text>
0
0
0
0
0
0
0
0
262
{verification.showBadge && (
263
<View
264
style={[
···
318
numberOfLines={1}>
319
{name}
320
</Text>
0
0
0
321
{verification.showBadge && (
322
<View style={[a.pl_xs]}>
323
<VerificationCheck
···
40
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
41
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
42
import {Link as InternalLink, type LinkProps} from '#/components/Link'
43
+
import {PdsBadge} from '#/components/PdsBadge'
44
import * as Pills from '#/components/Pills'
45
import {RichText} from '#/components/RichText'
46
import {Text} from '#/components/Typography'
···
260
numberOfLines={1}>
261
{forceLTR(name)}
262
</Text>
263
+
<View
264
+
style={[
265
+
a.pl_2xs,
266
+
a.self_center,
267
+
{marginTop: platform({default: 0, android: -1})},
268
+
]}>
269
+
<PdsBadge did={profile.did} size="sm" />
270
+
</View>
271
{verification.showBadge && (
272
<View
273
style={[
···
327
numberOfLines={1}>
328
{name}
329
</Text>
330
+
<View style={[a.pl_xs]}>
331
+
<PdsBadge did={profile.did} size="sm" />
332
+
</View>
333
{verification.showBadge && (
334
<View style={[a.pl_xs]}>
335
<VerificationCheck
+13
-5
src/components/ProfileHoverCard/index.web.tsx
···
38
} from '#/components/KnownFollowers'
39
import {InlineLinkText, Link} from '#/components/Link'
40
import {Loader} from '#/components/Loader'
0
41
import * as Pills from '#/components/Pills'
42
import {Portal} from '#/components/Portal'
43
import {RichText} from '#/components/RichText'
···
546
moderation.ui('displayName'),
547
)}
548
</Text>
0
0
0
549
{verification.showBadge && (
550
<View
551
style={[
···
555
},
556
]}>
557
<VerificationCheck
558
-
width={16}
559
verifier={verification.role === 'verifier'}
560
/>
561
</View>
···
581
582
{!isBlockedUser && (
583
<>
584
-
{disableFollowersMetrics && disableFollowingMetrics ? ( null ) :
585
<View style={[a.flex_row, a.flex_wrap, a.gap_md, a.pt_xs]}>
586
{!disableFollowersMetrics ? (
587
<InlineLinkText
···
589
label={`${followers} ${pluralizedFollowers}`}
590
style={[t.atoms.text]}
591
onPress={hide}>
592
-
<Text style={[a.text_md, a.font_semi_bold]}>{followers} </Text>
0
0
593
<Text style={[t.atoms.text_contrast_medium]}>
594
{pluralizedFollowers}
595
</Text>
···
601
label={_(msg`${following} following`)}
602
style={[t.atoms.text]}
603
onPress={hide}>
604
-
<Text style={[a.text_md, a.font_semi_bold]}>{following} </Text>
0
0
605
<Text style={[t.atoms.text_contrast_medium]}>
606
{pluralizedFollowings}
607
</Text>
608
</InlineLinkText>
609
) : null}
610
</View>
611
-
}
612
613
{profile.description?.trim() && !moderation.ui('profileView').blur ? (
614
<View style={[a.pt_md]}>
···
38
} from '#/components/KnownFollowers'
39
import {InlineLinkText, Link} from '#/components/Link'
40
import {Loader} from '#/components/Loader'
41
+
import {PdsBadge} from '#/components/PdsBadge'
42
import * as Pills from '#/components/Pills'
43
import {Portal} from '#/components/Portal'
44
import {RichText} from '#/components/RichText'
···
547
moderation.ui('displayName'),
548
)}
549
</Text>
550
+
<View style={[a.pl_xs, {marginTop: -2}]}>
551
+
<PdsBadge did={profile.did} size="md" interactive={false} />
552
+
</View>
553
{verification.showBadge && (
554
<View
555
style={[
···
559
},
560
]}>
561
<VerificationCheck
562
+
width={14}
563
verifier={verification.role === 'verifier'}
564
/>
565
</View>
···
585
586
{!isBlockedUser && (
587
<>
588
+
{disableFollowersMetrics && disableFollowingMetrics ? null : (
589
<View style={[a.flex_row, a.flex_wrap, a.gap_md, a.pt_xs]}>
590
{!disableFollowersMetrics ? (
591
<InlineLinkText
···
593
label={`${followers} ${pluralizedFollowers}`}
594
style={[t.atoms.text]}
595
onPress={hide}>
596
+
<Text style={[a.text_md, a.font_semi_bold]}>
597
+
{followers}{' '}
598
+
</Text>
599
<Text style={[t.atoms.text_contrast_medium]}>
600
{pluralizedFollowers}
601
</Text>
···
607
label={_(msg`${following} following`)}
608
style={[t.atoms.text]}
609
onPress={hide}>
610
+
<Text style={[a.text_md, a.font_semi_bold]}>
611
+
{following}{' '}
612
+
</Text>
613
<Text style={[t.atoms.text_contrast_medium]}>
614
{pluralizedFollowings}
615
</Text>
616
</InlineLinkText>
617
) : null}
618
</View>
619
+
)}
620
621
{profile.description?.trim() && !moderation.ui('profileView').blur ? (
622
<View style={[a.pt_md]}>
+4
src/components/dms/MessagesListHeader.tsx
···
20
import * as Layout from '#/components/Layout'
21
import {Link} from '#/components/Link'
22
import {PostAlerts} from '#/components/moderation/PostAlerts'
0
23
import {Text} from '#/components/Typography'
24
import {useSimpleVerificationState} from '#/components/verification'
25
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
161
numberOfLines={1}>
162
{displayName}
163
</Text>
0
0
0
164
{verification.showBadge && (
165
<View style={[a.pl_xs]}>
166
<VerificationCheck
···
20
import * as Layout from '#/components/Layout'
21
import {Link} from '#/components/Link'
22
import {PostAlerts} from '#/components/moderation/PostAlerts'
23
+
import {PdsBadge} from '#/components/PdsBadge'
24
import {Text} from '#/components/Typography'
25
import {useSimpleVerificationState} from '#/components/verification'
26
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
162
numberOfLines={1}>
163
{displayName}
164
</Text>
165
+
<View style={[a.pl_xs]}>
166
+
<PdsBadge did={profile.did} size="sm" />
167
+
</View>
168
{verification.showBadge && (
169
<View style={[a.pl_xs]}>
170
<VerificationCheck
+6
src/components/icons/Fediverse.tsx
···
0
0
0
0
0
0
···
1
+
import {createSinglePathSVG} from './TEMPLATE'
2
+
3
+
export const Fediverse = createSinglePathSVG({
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
+
viewBox: '0 0 640 640',
6
+
})
+4
src/screens/Messages/components/ChatListItem.tsx
···
41
import {Link} from '#/components/Link'
42
import {useMenuControl} from '#/components/Menu'
43
import {PostAlerts} from '#/components/moderation/PostAlerts'
0
44
import {createPortalGroup} from '#/components/Portal'
45
import {Text} from '#/components/Typography'
46
import {useSimpleVerificationState} from '#/components/verification'
···
417
]}>
418
{displayName}
419
</Text>
0
0
0
420
</View>
421
{verification.showBadge && (
422
<View style={[a.pl_xs, a.self_center]}>
···
41
import {Link} from '#/components/Link'
42
import {useMenuControl} from '#/components/Menu'
43
import {PostAlerts} from '#/components/moderation/PostAlerts'
44
+
import {PdsBadge} from '#/components/PdsBadge'
45
import {createPortalGroup} from '#/components/Portal'
46
import {Text} from '#/components/Typography'
47
import {useSimpleVerificationState} from '#/components/verification'
···
418
]}>
419
{displayName}
420
</Text>
421
+
</View>
422
+
<View style={[a.pl_xs, a.self_center]}>
423
+
<PdsBadge did={profile.did} size="sm" />
424
</View>
425
{verification.showBadge && (
426
<View style={[a.pl_xs, a.self_center]}>
+4
-1
src/screens/PostThread/components/ThreadItemAnchor.tsx
···
57
import {ContentHider} from '#/components/moderation/ContentHider'
58
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
59
import {PostAlerts} from '#/components/moderation/PostAlerts'
0
60
import {type AppModerationCause} from '#/components/Pills'
61
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
62
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
···
381
)}
382
</Text>
383
384
-
<View style={[a.pl_xs]}>
0
0
385
<VerificationCheckButton profile={authorShadow} size="md" />
386
</View>
387
</View>
···
57
import {ContentHider} from '#/components/moderation/ContentHider'
58
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
59
import {PostAlerts} from '#/components/moderation/PostAlerts'
60
+
import {PdsBadge} from '#/components/PdsBadge'
61
import {type AppModerationCause} from '#/components/Pills'
62
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
63
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
···
382
)}
383
</Text>
384
385
+
<View
386
+
style={[a.pl_xs, a.flex_row, a.gap_2xs, a.align_center]}>
387
+
<PdsBadge did={post.author.did} size="md" />
388
<VerificationCheckButton profile={authorShadow} size="md" />
389
</View>
390
</View>
+5
src/screens/Profile/Header/DisplayName.tsx
···
5
import {sanitizeHandle} from '#/lib/strings/handles'
6
import {type Shadow} from '#/state/cache/types'
7
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
0
8
import {Text} from '#/components/Typography'
9
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
10
···
36
<View
37
style={[
38
a.pl_xs,
0
0
0
39
{
40
marginTop: platform({ios: 2}),
41
},
42
]}>
0
43
<VerificationCheckButton profile={profile} size="lg" />
44
</View>
45
</Text>
···
5
import {sanitizeHandle} from '#/lib/strings/handles'
6
import {type Shadow} from '#/state/cache/types'
7
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
8
+
import {PdsBadge} from '#/components/PdsBadge'
9
import {Text} from '#/components/Typography'
10
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
11
···
37
<View
38
style={[
39
a.pl_xs,
40
+
a.flex_row,
41
+
a.gap_2xs,
42
+
a.align_center,
43
{
44
marginTop: platform({ios: 2}),
45
},
46
]}>
47
+
<PdsBadge did={profile.did} size="lg" />
48
<VerificationCheckButton profile={profile} size="lg" />
49
</View>
50
</Text>
+10
-1
src/screens/Profile/Header/ProfileHeaderStandard.tsx
···
42
shouldShowKnownFollowers,
43
} from '#/components/KnownFollowers'
44
import {Link} from '#/components/Link'
0
45
import * as Prompt from '#/components/Prompt'
46
import {RichText} from '#/components/RichText'
47
import * as Toast from '#/components/Toast'
···
162
profile.displayName || sanitizeHandle(profile.handle),
163
moderation.ui('displayName'),
164
)}
165
-
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
0
0
0
0
0
0
0
0
166
<VerificationCheckButton profile={profile} size="lg" />
167
</View>
168
</Text>
···
42
shouldShowKnownFollowers,
43
} from '#/components/KnownFollowers'
44
import {Link} from '#/components/Link'
45
+
import {PdsBadge} from '#/components/PdsBadge'
46
import * as Prompt from '#/components/Prompt'
47
import {RichText} from '#/components/RichText'
48
import * as Toast from '#/components/Toast'
···
163
profile.displayName || sanitizeHandle(profile.handle),
164
moderation.ui('displayName'),
165
)}
166
+
<View
167
+
style={[
168
+
a.pl_xs,
169
+
a.flex_row,
170
+
a.gap_2xs,
171
+
a.align_center,
172
+
{marginTop: platform({ios: 2})},
173
+
]}>
174
+
<PdsBadge did={profile.did} size="lg" />
175
<VerificationCheckButton profile={profile} size="lg" />
176
</View>
177
</Text>
+45
-4
src/screens/Settings/RunesSettings.tsx
···
5
import {useLingui} from '@lingui/react'
6
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
7
8
-
import { DEFAULT_ALT_TEXT_AI_MODEL } from '#/lib/constants'
9
import {usePalette} from '#/lib/hooks/usePalette'
10
import {type CommonNavigatorParams} from '#/lib/routes/types'
11
import {dynamicActivate} from '#/locale/i18n'
···
114
useSetOpenRouterApiKey,
115
useSetOpenRouterModel,
116
} from '#/state/preferences/openrouter'
0
0
0
0
0
0
117
import {
118
usePostReplacement,
119
useSetPostReplacement,
···
708
const deerVerificationEnabled = useDeerVerificationEnabled()
709
const setDeerVerificationEnabled = useSetDeerVerificationEnabled()
710
0
0
0
0
0
711
const repostCarouselEnabled = useRepostCarouselEnabled()
712
const setRepostCarouselEnabled = useSetRepostCarouselEnabled()
713
···
760
</Toggle.Item>
761
<Toggle.Item
762
name="use_handle_in_links"
763
-
label={_(msg`Use handles in profile links instead of DIDs (requires restart)`)}
0
0
764
value={handleInLinks ?? false}
765
onChange={value => setHandleInLinks(value)}
766
style={[a.w_full]}>
···
909
<Trans>Tweaks</Trans>
910
</SettingsList.ItemText>
911
<Toggle.Item
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
912
name="repost_carousel"
913
label={_(msg`Combine reposts into a horizontal carousel`)}
914
value={repostCarouselEnabled}
···
1186
<SettingsList.Item>
1187
<Admonition type="info" style={[a.flex_1]}>
1188
<Trans>
1189
-
Current model:{' '}
1190
-
{openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '}
1191
<InlineLinkText
1192
to="https://openrouter.ai/models?fmt=cards&input_modalities=image&order=most-popular"
1193
label="openrouter.ai">
···
5
import {useLingui} from '@lingui/react'
6
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
7
8
+
import {DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants'
9
import {usePalette} from '#/lib/hooks/usePalette'
10
import {type CommonNavigatorParams} from '#/lib/routes/types'
11
import {dynamicActivate} from '#/locale/i18n'
···
114
useSetOpenRouterApiKey,
115
useSetOpenRouterModel,
116
} from '#/state/preferences/openrouter'
117
+
import {
118
+
usePdsLabelEnabled,
119
+
usePdsLabelHideBskyPds,
120
+
useSetPdsLabelEnabled,
121
+
useSetPdsLabelHideBskyPds,
122
+
} from '#/state/preferences/pds-label'
123
import {
124
usePostReplacement,
125
useSetPostReplacement,
···
714
const deerVerificationEnabled = useDeerVerificationEnabled()
715
const setDeerVerificationEnabled = useSetDeerVerificationEnabled()
716
717
+
const pdsLabelEnabled = usePdsLabelEnabled()
718
+
const setPdsLabelEnabled = useSetPdsLabelEnabled()
719
+
const pdsLabelHideBskyPds = usePdsLabelHideBskyPds()
720
+
const setPdsLabelHideBskyPds = useSetPdsLabelHideBskyPds()
721
+
722
const repostCarouselEnabled = useRepostCarouselEnabled()
723
const setRepostCarouselEnabled = useSetRepostCarouselEnabled()
724
···
771
</Toggle.Item>
772
<Toggle.Item
773
name="use_handle_in_links"
774
+
label={_(
775
+
msg`Use handles in profile links instead of DIDs (requires restart)`,
776
+
)}
777
value={handleInLinks ?? false}
778
onChange={value => setHandleInLinks(value)}
779
style={[a.w_full]}>
···
922
<Trans>Tweaks</Trans>
923
</SettingsList.ItemText>
924
<Toggle.Item
925
+
name="pds_label_badge"
926
+
label={_(
927
+
msg`Show a PDS badge next to the display name on profiles`,
928
+
)}
929
+
value={pdsLabelEnabled}
930
+
onChange={value => setPdsLabelEnabled(value)}
931
+
style={[a.w_full]}>
932
+
<Toggle.LabelText style={[a.flex_1]}>
933
+
<Trans>
934
+
Show a PDS badge next to the display name on profiles
935
+
</Trans>
936
+
</Toggle.LabelText>
937
+
<Toggle.Platform />
938
+
</Toggle.Item>
939
+
{pdsLabelEnabled && (
940
+
<Toggle.Item
941
+
name="pds_label_hide_bsky"
942
+
label={_(msg`Hide PDS badge for Bluesky-hosted accounts`)}
943
+
value={pdsLabelHideBskyPds}
944
+
onChange={value => setPdsLabelHideBskyPds(value)}
945
+
style={[a.w_full]}>
946
+
<Toggle.LabelText style={[a.flex_1]}>
947
+
<Trans>Hide PDS badge for Bluesky-hosted accounts</Trans>
948
+
</Toggle.LabelText>
949
+
<Toggle.Platform />
950
+
</Toggle.Item>
951
+
)}
952
+
953
+
<Toggle.Item
954
name="repost_carousel"
955
label={_(msg`Combine reposts into a horizontal carousel`)}
956
value={repostCarouselEnabled}
···
1228
<SettingsList.Item>
1229
<Admonition type="info" style={[a.flex_1]}>
1230
<Trans>
1231
+
Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '}
0
1232
<InlineLinkText
1233
to="https://openrouter.ai/models?fmt=cards&input_modalities=image&order=most-popular"
1234
label="openrouter.ai">
+10
-9
src/screens/Settings/Settings.tsx
···
372
]}>
373
{displayName}
374
</Text>
375
-
{shouldShowVerificationCheckButton(verificationState) && (
376
-
<View
377
-
style={[
378
-
{
379
-
marginTop: platform({web: 8, ios: 8, android: 10}),
380
-
},
381
-
]}>
0
382
<VerificationCheckButton profile={shadow} size="lg" />
383
-
</View>
384
-
)}
385
</View>
386
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
387
{sanitizeHandle(profile.handle, '@')}
···
372
]}>
373
{displayName}
374
</Text>
375
+
<View
376
+
style={[
377
+
a.flex_row,
378
+
a.gap_2xs,
379
+
a.align_center,
380
+
{marginTop: platform({web: 8, ios: 8, android: 10})},
381
+
]}>
382
+
{shouldShowVerificationCheckButton(verificationState) && (
383
<VerificationCheckButton profile={shadow} size="lg" />
384
+
)}
385
+
</View>
386
</View>
387
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
388
{sanitizeHandle(profile.handle, '@')}
+10
src/state/persisted/schema.ts
···
173
.optional(),
174
highQualityImages: z.boolean().optional(),
175
hideUnreplyablePosts: z.boolean().optional(),
0
0
0
0
0
0
176
177
postReplacement: z.object({
178
enabled: z.boolean().optional(),
···
304
},
305
highQualityImages: false,
306
hideUnreplyablePosts: false,
0
0
0
0
307
showExternalShareButtons: false,
308
translationServicePreference: 'google',
309
libreTranslateInstance: 'https://libretranslate.com/',
···
173
.optional(),
174
highQualityImages: z.boolean().optional(),
175
hideUnreplyablePosts: z.boolean().optional(),
176
+
pdsLabel: z
177
+
.object({
178
+
enabled: z.boolean(),
179
+
hideBskyPds: z.boolean(),
180
+
})
181
+
.optional(),
182
183
postReplacement: z.object({
184
enabled: z.boolean().optional(),
···
310
},
311
highQualityImages: false,
312
hideUnreplyablePosts: false,
313
+
pdsLabel: {
314
+
enabled: false,
315
+
hideBskyPds: true,
316
+
},
317
showExternalShareButtons: false,
318
translationServicePreference: 'google',
319
libreTranslateInstance: 'https://libretranslate.com/',
+78
-75
src/state/preferences/index.tsx
···
37
import {Provider as NoAppLabelersProvider} from './no-app-labelers'
38
import {Provider as NoDiscoverProvider} from './no-discover-fallback'
39
import {Provider as OpenRouterProvider} from './openrouter'
0
40
import {Provider as PostNameReplacementProvider} from './post-name-replacement.tsx'
41
import {Provider as RepostCarouselProvider} from './repost-carousel-enabled'
42
import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle'
···
96
<ConstellationProvider>
97
<ConstellationInstanceProvider>
98
<DeerVerificationProvider>
99
-
<NoDiscoverProvider>
100
-
<ShowLinkInHandleProvider>
101
-
<UseHandleInLinksProvider>
102
-
<LargeAltBadgeProvider>
103
-
<ExternalEmbedsProvider>
104
-
<HiddenPostsProvider>
105
-
<HighQualityImagesProvider>
106
-
<InAppBrowserProvider>
107
-
<DisableHapticsProvider>
108
-
<AutoplayProvider>
109
-
<UsedStarterPacksProvider>
110
-
<SubtitlesProvider>
111
-
<TrendingSettingsProvider>
112
-
<RepostCarouselProvider>
113
-
<KawaiiProvider>
114
-
<HideFeedsPromoTabProvider>
115
-
<DisableViaRepostNotificationProvider>
116
-
<DisableLikesMetricsProvider>
117
-
<DisableRepostsMetricsProvider>
118
-
<DisableQuotesMetricsProvider>
119
-
<DisableSavesMetricsProvider>
120
-
<DisableReplyMetricsProvider>
121
-
<DisableFollowersMetricsProvider>
122
-
<DisableFollowingMetricsProvider>
123
-
<DisableFollowedByMetricsProvider>
124
-
<DisablePostsMetricsProvider>
125
-
<HideSimilarAccountsRecommProvider>
126
-
<HideUnreplyablePostsProvider>
127
-
<EnableSquareAvatarsProvider>
128
-
<EnableSquareButtonsProvider>
129
-
<PostNameReplacementProvider>
130
-
<DisableVerifyEmailReminderProvider>
131
-
<TranslationServicePreferenceProvider>
132
-
<OpenRouterProvider>
133
-
<DisableComposerPromptProvider>
134
-
<DiscoverContextEnabledProvider>
135
-
{
136
-
children
137
-
}
138
-
</DiscoverContextEnabledProvider>
139
-
</DisableComposerPromptProvider>
140
-
</OpenRouterProvider>
141
-
</TranslationServicePreferenceProvider>
142
-
</DisableVerifyEmailReminderProvider>
143
-
</PostNameReplacementProvider>
144
-
</EnableSquareButtonsProvider>
145
-
</EnableSquareAvatarsProvider>
146
-
</HideUnreplyablePostsProvider>
147
-
</HideSimilarAccountsRecommProvider>
148
-
</DisablePostsMetricsProvider>
149
-
</DisableFollowedByMetricsProvider>
150
-
</DisableFollowingMetricsProvider>
151
-
</DisableFollowersMetricsProvider>
152
-
</DisableReplyMetricsProvider>
153
-
</DisableSavesMetricsProvider>
154
-
</DisableQuotesMetricsProvider>
155
-
</DisableRepostsMetricsProvider>
156
-
</DisableLikesMetricsProvider>
157
-
</DisableViaRepostNotificationProvider>
158
-
</HideFeedsPromoTabProvider>
159
-
</KawaiiProvider>
160
-
</RepostCarouselProvider>
161
-
</TrendingSettingsProvider>
162
-
</SubtitlesProvider>
163
-
</UsedStarterPacksProvider>
164
-
</AutoplayProvider>
165
-
</DisableHapticsProvider>
166
-
</InAppBrowserProvider>
167
-
</HighQualityImagesProvider>
168
-
</HiddenPostsProvider>
169
-
</ExternalEmbedsProvider>
170
-
</LargeAltBadgeProvider>
171
-
</UseHandleInLinksProvider>
172
-
</ShowLinkInHandleProvider>
173
-
</NoDiscoverProvider>
0
0
174
</DeerVerificationProvider>
175
</ConstellationInstanceProvider>
176
</ConstellationProvider>
···
37
import {Provider as NoAppLabelersProvider} from './no-app-labelers'
38
import {Provider as NoDiscoverProvider} from './no-discover-fallback'
39
import {Provider as OpenRouterProvider} from './openrouter'
40
+
import {Provider as PdsLabelProvider} from './pds-label'
41
import {Provider as PostNameReplacementProvider} from './post-name-replacement.tsx'
42
import {Provider as RepostCarouselProvider} from './repost-carousel-enabled'
43
import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle'
···
97
<ConstellationProvider>
98
<ConstellationInstanceProvider>
99
<DeerVerificationProvider>
100
+
<PdsLabelProvider>
101
+
<NoDiscoverProvider>
102
+
<ShowLinkInHandleProvider>
103
+
<UseHandleInLinksProvider>
104
+
<LargeAltBadgeProvider>
105
+
<ExternalEmbedsProvider>
106
+
<HiddenPostsProvider>
107
+
<HighQualityImagesProvider>
108
+
<InAppBrowserProvider>
109
+
<DisableHapticsProvider>
110
+
<AutoplayProvider>
111
+
<UsedStarterPacksProvider>
112
+
<SubtitlesProvider>
113
+
<TrendingSettingsProvider>
114
+
<RepostCarouselProvider>
115
+
<KawaiiProvider>
116
+
<HideFeedsPromoTabProvider>
117
+
<DisableViaRepostNotificationProvider>
118
+
<DisableLikesMetricsProvider>
119
+
<DisableRepostsMetricsProvider>
120
+
<DisableQuotesMetricsProvider>
121
+
<DisableSavesMetricsProvider>
122
+
<DisableReplyMetricsProvider>
123
+
<DisableFollowersMetricsProvider>
124
+
<DisableFollowingMetricsProvider>
125
+
<DisableFollowedByMetricsProvider>
126
+
<DisablePostsMetricsProvider>
127
+
<HideSimilarAccountsRecommProvider>
128
+
<HideUnreplyablePostsProvider>
129
+
<EnableSquareAvatarsProvider>
130
+
<EnableSquareButtonsProvider>
131
+
<PostNameReplacementProvider>
132
+
<DisableVerifyEmailReminderProvider>
133
+
<TranslationServicePreferenceProvider>
134
+
<OpenRouterProvider>
135
+
<DisableComposerPromptProvider>
136
+
<DiscoverContextEnabledProvider>
137
+
{
138
+
children
139
+
}
140
+
</DiscoverContextEnabledProvider>
141
+
</DisableComposerPromptProvider>
142
+
</OpenRouterProvider>
143
+
</TranslationServicePreferenceProvider>
144
+
</DisableVerifyEmailReminderProvider>
145
+
</PostNameReplacementProvider>
146
+
</EnableSquareButtonsProvider>
147
+
</EnableSquareAvatarsProvider>
148
+
</HideUnreplyablePostsProvider>
149
+
</HideSimilarAccountsRecommProvider>
150
+
</DisablePostsMetricsProvider>
151
+
</DisableFollowedByMetricsProvider>
152
+
</DisableFollowingMetricsProvider>
153
+
</DisableFollowersMetricsProvider>
154
+
</DisableReplyMetricsProvider>
155
+
</DisableSavesMetricsProvider>
156
+
</DisableQuotesMetricsProvider>
157
+
</DisableRepostsMetricsProvider>
158
+
</DisableLikesMetricsProvider>
159
+
</DisableViaRepostNotificationProvider>
160
+
</HideFeedsPromoTabProvider>
161
+
</KawaiiProvider>
162
+
</RepostCarouselProvider>
163
+
</TrendingSettingsProvider>
164
+
</SubtitlesProvider>
165
+
</UsedStarterPacksProvider>
166
+
</AutoplayProvider>
167
+
</DisableHapticsProvider>
168
+
</InAppBrowserProvider>
169
+
</HighQualityImagesProvider>
170
+
</HiddenPostsProvider>
171
+
</ExternalEmbedsProvider>
172
+
</LargeAltBadgeProvider>
173
+
</UseHandleInLinksProvider>
174
+
</ShowLinkInHandleProvider>
175
+
</NoDiscoverProvider>
176
+
</PdsLabelProvider>
177
</DeerVerificationProvider>
178
</ConstellationInstanceProvider>
179
</ConstellationProvider>
+75
src/state/preferences/pds-label.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import React from 'react'
2
+
3
+
import * as persisted from '#/state/persisted'
4
+
5
+
type StateContext = persisted.Schema['pdsLabel']
6
+
type SetContext = (v: persisted.Schema['pdsLabel']) => void
7
+
8
+
const stateContext = React.createContext<StateContext>(
9
+
persisted.defaults.pdsLabel,
10
+
)
11
+
const setContext = React.createContext<SetContext>(
12
+
(_: persisted.Schema['pdsLabel']) => {},
13
+
)
14
+
15
+
export function Provider({children}: React.PropsWithChildren<{}>) {
16
+
const [state, setState] = React.useState(persisted.get('pdsLabel'))
17
+
18
+
const setStateWrapped = React.useCallback(
19
+
(pdsLabel: persisted.Schema['pdsLabel']) => {
20
+
setState(pdsLabel)
21
+
persisted.write('pdsLabel', pdsLabel)
22
+
},
23
+
[setState],
24
+
)
25
+
26
+
React.useEffect(() => {
27
+
return persisted.onUpdate('pdsLabel', next => {
28
+
setState(next)
29
+
})
30
+
}, [setStateWrapped])
31
+
32
+
return (
33
+
<stateContext.Provider value={state}>
34
+
<setContext.Provider value={setStateWrapped}>
35
+
{children}
36
+
</setContext.Provider>
37
+
</stateContext.Provider>
38
+
)
39
+
}
40
+
41
+
export function usePdsLabel() {
42
+
return React.useContext(stateContext) ?? persisted.defaults.pdsLabel!
43
+
}
44
+
45
+
export function usePdsLabelEnabled() {
46
+
return usePdsLabel().enabled
47
+
}
48
+
49
+
export function usePdsLabelHideBskyPds() {
50
+
return usePdsLabel().hideBskyPds
51
+
}
52
+
53
+
export function useSetPdsLabel() {
54
+
return React.useContext(setContext)
55
+
}
56
+
57
+
export function useSetPdsLabelEnabled() {
58
+
const pdsLabel = usePdsLabel()
59
+
const setPdsLabel = useSetPdsLabel()
60
+
61
+
return React.useMemo(
62
+
() => (enabled: boolean) => setPdsLabel({...pdsLabel, enabled}),
63
+
[pdsLabel, setPdsLabel],
64
+
)
65
+
}
66
+
67
+
export function useSetPdsLabelHideBskyPds() {
68
+
const pdsLabel = usePdsLabel()
69
+
const setPdsLabel = useSetPdsLabel()
70
+
71
+
return React.useMemo(
72
+
() => (hideBskyPds: boolean) => setPdsLabel({...pdsLabel, hideBskyPds}),
73
+
[pdsLabel, setPdsLabel],
74
+
)
75
+
}
+78
src/state/queries/pds-label.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import {useQuery} from '@tanstack/react-query'
2
+
3
+
import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity'
4
+
5
+
const BSKY_PDS_HOSTNAMES = ['bsky.social', 'staging.bsky.dev']
6
+
const BSKY_PDS_SUFFIX = '.bsky.network'
7
+
const BRIDGY_FED_HOSTNAME = 'atproto.brid.gy'
8
+
9
+
export function isBskyPdsUrl(url: string): boolean {
10
+
try {
11
+
const hostname = new URL(url).hostname
12
+
return (
13
+
BSKY_PDS_HOSTNAMES.includes(hostname) ||
14
+
hostname.endsWith(BSKY_PDS_SUFFIX)
15
+
)
16
+
} catch {
17
+
return false
18
+
}
19
+
}
20
+
21
+
export function isBridgedPdsUrl(url: string): boolean {
22
+
try {
23
+
return new URL(url).hostname === BRIDGY_FED_HOSTNAME
24
+
} catch {
25
+
return false
26
+
}
27
+
}
28
+
29
+
async function fetchFaviconUrl(pdsUrl: string): Promise<string> {
30
+
let origin = ''
31
+
try {
32
+
origin = new URL(pdsUrl).origin
33
+
} catch {
34
+
return ''
35
+
}
36
+
try {
37
+
const res = await fetch(origin, {headers: {Accept: 'text/html'}})
38
+
if (res.ok) {
39
+
const html = await res.text()
40
+
// Match <link rel="icon"> or <link rel="shortcut icon"> in either attribute order
41
+
const match =
42
+
html.match(
43
+
/<link[^>]+rel=["'][^"']*\bicon\b[^"']*["'][^>]*href=["']([^"']+)["']/i,
44
+
) ||
45
+
html.match(
46
+
/<link[^>]+href=["']([^"']+)["'][^>]*rel=["'][^"']*\bicon\b[^"']*["']/i,
47
+
)
48
+
if (match) {
49
+
const href = match[1]
50
+
if (href.startsWith('http')) return href
51
+
if (href.startsWith('//')) return `https:${href}`
52
+
if (href.startsWith('/')) return `${origin}${href}`
53
+
return `${origin}/${href}`
54
+
}
55
+
}
56
+
} catch {}
57
+
return `${origin}/favicon.ico`
58
+
}
59
+
60
+
export const RQKEY_ROOT = 'pds-label'
61
+
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
62
+
63
+
export function usePdsLabelQuery(did: string | undefined) {
64
+
return useQuery({
65
+
queryKey: RQKEY(did ?? ''),
66
+
queryFn: async () => {
67
+
if (!did) return null
68
+
const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}`)
69
+
const isBsky = isBskyPdsUrl(pdsUrl)
70
+
const isBridged = isBridgedPdsUrl(pdsUrl)
71
+
const faviconUrl =
72
+
isBsky || isBridged ? '' : await fetchFaviconUrl(pdsUrl)
73
+
return {pdsUrl, isBsky, isBridged, faviconUrl}
74
+
},
75
+
enabled: !!did,
76
+
staleTime: 1000 * 60 * 60, // 1 hour
77
+
})
78
+
}
+4
src/view/com/composer/ComposerReplyTo.tsx
···
16
import {type ComposerOptsPostRef} from '#/state/shell/composer'
17
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
18
import {atoms as a, useTheme, web} from '#/alf'
0
19
import {QuoteEmbed} from '#/components/Post/Embed'
20
import {Text} from '#/components/Typography'
21
import {useSimpleVerificationState} from '#/components/verification'
···
116
sanitizeHandle(replyTo.author.handle),
117
)}
118
</Text>
0
0
0
119
{verification.showBadge && (
120
<View style={[a.pl_xs]}>
121
<VerificationCheck
···
16
import {type ComposerOptsPostRef} from '#/state/shell/composer'
17
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
18
import {atoms as a, useTheme, web} from '#/alf'
19
+
import {PdsBadge} from '#/components/PdsBadge'
20
import {QuoteEmbed} from '#/components/Post/Embed'
21
import {Text} from '#/components/Typography'
22
import {useSimpleVerificationState} from '#/components/verification'
···
117
sanitizeHandle(replyTo.author.handle),
118
)}
119
</Text>
120
+
<View style={[a.pl_xs]}>
121
+
<PdsBadge did={replyTo.author.did} size="sm" />
122
+
</View>
123
{verification.showBadge && (
124
<View style={[a.pl_xs]}>
125
<VerificationCheck
+4
src/view/com/composer/text-input/mobile/Autocomplete.tsx
···
9
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
10
import {UserAvatar} from '#/view/com/util/UserAvatar'
11
import {atoms as a, platform, useTheme} from '#/alf'
0
12
import {Text} from '#/components/Typography'
13
import {useSimpleVerificationState} from '#/components/verification'
14
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
115
numberOfLines={1}>
116
{displayName}
117
</Text>
0
0
0
118
{state.isVerified && (
119
<View
120
style={[
···
9
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
10
import {UserAvatar} from '#/view/com/util/UserAvatar'
11
import {atoms as a, platform, useTheme} from '#/alf'
12
+
import {PdsBadge} from '#/components/PdsBadge'
13
import {Text} from '#/components/Typography'
14
import {useSimpleVerificationState} from '#/components/verification'
15
import {VerificationCheck} from '#/components/verification/VerificationCheck'
···
116
numberOfLines={1}>
117
{displayName}
118
</Text>
119
+
<View style={[{marginTop: platform({android: -2})}]}>
120
+
<PdsBadge did={profile.did} size="sm" />
121
+
</View>
122
{state.isVerified && (
123
<View
124
style={[
+4
src/view/com/notifications/NotificationFeedItem.tsx
···
65
import {VerifiedCheck} from '#/components/icons/VerifiedCheck'
66
import {InlineLinkText, Link} from '#/components/Link'
67
import * as MediaPreview from '#/components/MediaPreview'
0
68
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
69
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
70
import {SubtleHover} from '#/components/SubtleHover'
···
1060
author.profile.displayName || author.profile.handle,
1061
)}
1062
</Text>
0
0
0
1063
{verification.showBadge && (
1064
<View style={[a.pl_xs, a.self_center]}>
1065
<VerificationCheck
···
65
import {VerifiedCheck} from '#/components/icons/VerifiedCheck'
66
import {InlineLinkText, Link} from '#/components/Link'
67
import * as MediaPreview from '#/components/MediaPreview'
68
+
import {PdsBadge} from '#/components/PdsBadge'
69
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
70
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
71
import {SubtleHover} from '#/components/SubtleHover'
···
1061
author.profile.displayName || author.profile.handle,
1062
)}
1063
</Text>
1064
+
<View style={[a.pl_xs, a.self_center]}>
1065
+
<PdsBadge did={author.profile.did} size="sm" />
1066
+
</View>
1067
{verification.showBadge && (
1068
<View style={[a.pl_xs, a.self_center]}>
1069
<VerificationCheck
+11
src/view/com/util/PostMeta.tsx
···
16
import {unstableCacheProfileView} from '#/state/queries/profile'
17
import {atoms as a, platform, useTheme, web} from '#/alf'
18
import {WebOnlyInlineLinkText} from '#/components/Link'
0
19
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
20
import {Text} from '#/components/Typography'
21
import {useSimpleVerificationState} from '#/components/verification'
···
113
),
114
)}
115
</MaybeLinkText>
0
0
0
0
0
0
0
0
0
0
116
{verification.showBadge && (
117
<View
118
style={[
···
16
import {unstableCacheProfileView} from '#/state/queries/profile'
17
import {atoms as a, platform, useTheme, web} from '#/alf'
18
import {WebOnlyInlineLinkText} from '#/components/Link'
19
+
import {PdsBadge} from '#/components/PdsBadge'
20
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
21
import {Text} from '#/components/Typography'
22
import {useSimpleVerificationState} from '#/components/verification'
···
114
),
115
)}
116
</MaybeLinkText>
117
+
<View
118
+
style={[
119
+
a.pl_2xs,
120
+
a.self_center,
121
+
{
122
+
marginTop: platform({web: 1, ios: 0, android: -1}),
123
+
},
124
+
]}>
125
+
<PdsBadge did={author.did} size="sm" interactive={false} />
126
+
</View>
127
{verification.showBadge && (
128
<View
129
style={[
+3
src/view/icons/index.tsx
···
54
import {faClipboardCheck} from '@fortawesome/free-solid-svg-icons/faClipboardCheck'
55
import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
56
import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash'
0
57
import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload'
58
import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis'
59
import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope'
···
150
faCommentSlash,
151
faComments,
152
faCompass,
0
153
faDownload,
154
faEllipsis,
155
faEnvelope,
···
160
faFire,
161
faFlask,
162
faFloppyDisk,
0
163
faGear,
164
faGlobe,
165
faHand,
···
54
import {faClipboardCheck} from '@fortawesome/free-solid-svg-icons/faClipboardCheck'
55
import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
56
import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash'
57
+
import {faDatabase} from '@fortawesome/free-solid-svg-icons/faDatabase'
58
import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload'
59
import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis'
60
import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope'
···
151
faCommentSlash,
152
faComments,
153
faCompass,
154
+
faDatabase,
155
faDownload,
156
faEllipsis,
157
faEnvelope,
···
162
faFire,
163
faFlask,
164
faFloppyDisk,
165
+
166
faGear,
167
faGlobe,
168
faHand,
+2
src/view/shell/Drawer.tsx
···
57
} from '#/components/icons/UserCircle'
58
import {InlineLinkText} from '#/components/Link'
59
import {Text} from '#/components/Typography'
0
60
import {useSimpleVerificationState} from '#/components/verification'
61
import {VerificationCheck} from '#/components/verification/VerificationCheck'
62
import {IS_WEB} from '#/env'
···
104
numberOfLines={1}>
105
{profile?.displayName || account.handle}
106
</Text>
0
107
{verification.showBadge && (
108
<View
109
style={{
···
57
} from '#/components/icons/UserCircle'
58
import {InlineLinkText} from '#/components/Link'
59
import {Text} from '#/components/Typography'
60
+
import {PdsBadge} from '#/components/PdsBadge'
61
import {useSimpleVerificationState} from '#/components/verification'
62
import {VerificationCheck} from '#/components/verification/VerificationCheck'
63
import {IS_WEB} from '#/env'
···
105
numberOfLines={1}>
106
{profile?.displayName || account.handle}
107
</Text>
108
+
<PdsBadge did={account.did} size="md" />
109
{verification.showBadge && (
110
<View
111
style={{