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

feat: pds badge #67

merged opened by jeanmachine.dev targeting main from jeanmachine.dev/witchsky.app: jean/pds-label

Adds a new "PDS badge" feature, that displays similarly to the verification checkmark & (almost) everywhere the verification checkmark does - but displays uniquely if they are running off of a Bluesky PDS, a self-hosted PDS, or if they're a bridged account.

For 3rd party PDSes, if they have a favicon set in the PDS's root html, it will render that instead of a default database icon.

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:6vxtya3serxcwvcdk5e7psvv/sh.tangled.repo.pull/3mg3zr5wt4m22
+796 -95
Diff #4
+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' 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> 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
···
··· 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
···
··· 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' 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> 262 {verification.showBadge && ( 263 <View 264 style={[ ··· 318 numberOfLines={1}> 319 {name} 320 </Text> 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' 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> 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> 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> 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' 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> 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
···
··· 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' 44 import {createPortalGroup} from '#/components/Portal' 45 import {Text} from '#/components/Typography' 46 import {useSimpleVerificationState} from '#/components/verification' ··· 418 {displayName} 419 </Text> 420 </View> 421 {verification.showBadge && ( 422 <View style={[a.pl_xs, a.self_center]}> 423 <VerificationCheck
··· 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' ··· 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]}> 427 <VerificationCheck
+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' 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]}> 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' 8 import {Text} from '#/components/Typography' 9 import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 10 ··· 36 <View 37 style={[ 38 a.pl_xs, 39 { 40 marginTop: platform({ios: 2}), 41 }, 42 ]}> 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' 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})}]}> 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' 117 import { 118 usePostReplacement, 119 useSetPostReplacement, ··· 708 const deerVerificationEnabled = useDeerVerificationEnabled() 709 const setDeerVerificationEnabled = useSetDeerVerificationEnabled() 710 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)`)} 764 value={handleInLinks ?? false} 765 onChange={value => setHandleInLinks(value)} 766 style={[a.w_full]}> ··· 908 <SettingsList.ItemText> 909 <Trans>Tweaks</Trans> 910 </SettingsList.ItemText> 911 <Toggle.Item 912 name="repost_carousel" 913 label={_(msg`Combine reposts into a horizontal carousel`)} ··· 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]}> ··· 921 <SettingsList.ItemText> 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`)} ··· 1228 <SettingsList.Item> 1229 <Admonition type="info" style={[a.flex_1]}> 1230 <Trans> 1231 + Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '} 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 - ]}> 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(), 176 177 postReplacement: z.object({ 178 enabled: z.boolean().optional(), ··· 304 }, 305 highQualityImages: false, 306 hideUnreplyablePosts: false, 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' 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> 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
···
··· 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
···
··· 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' 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> 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' 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> 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' 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> 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' 19 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 20 import {Text} from '#/components/Typography' 21 import {useSimpleVerificationState} from '#/components/verification' ··· 113 ), 114 )} 115 </MaybeLinkText> 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' 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, 153 faDownload, 154 faEllipsis, 155 faEnvelope, ··· 160 faFire, 161 faFlask, 162 faFloppyDisk, 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' 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> 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={{

History

5 rounds 2 comments
sign up or login to add to the discussion
1 commit
expand
feat: pds badge for identifying service providers.
expand 1 comment

67 LGTM ✅✅✅🚀🚀

pull request successfully merged
7 commits
expand
feat: pds badge for identifying service providers.
import fix
small focus fix
slight sizing adjustements
removed pds badge from settings page.
added interactivity for the pds badge based upon context; fixed pixel weirdness via rounding.
slight size adjustement and interactivity change for hover embed
expand 1 comment

LGTM 💖💫

7 commits
expand
feat: pds badge for identifying service providers.
import fix
small focus fix
slight sizing adjustements
removed pds badge from settings page.
added interactivity for the pds badge based upon context; fixed pixel weirdness via rounding.
slight size adjustement and interactivity change for hover embed
expand 0 comments
14 commits
expand
feat: pds badge for identifying service providers.
import fix
small focus fix
slight sizing adjustements
removed pds badge from settings page.
added interactivity for the pds badge based upon context; fixed pixel weirdness via rounding.
slight size adjustement and interactivity change for hover embed
feat: pds badge for identifying service providers.
import fix
small focus fix
slight sizing adjustements
removed pds badge from settings page.
added interactivity for the pds badge based upon context; fixed pixel weirdness via rounding.
slight size adjustement and interactivity change for hover embed
expand 0 comments
7 commits
expand
feat: pds badge for identifying service providers.
import fix
small focus fix
slight sizing adjustements
removed pds badge from settings page.
added interactivity for the pds badge based upon context; fixed pixel weirdness via rounding.
slight size adjustement and interactivity change for hover embed
expand 0 comments