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 #0
+2
src/components/AccountList.tsx
··· 17 17 import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 18 18 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron' 19 19 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 20 + import {PdsBadge} from '#/components/PdsBadge' 20 21 import {Text} from '#/components/Typography' 21 22 import {useSimpleVerificationState} from '#/components/verification' 22 23 import {VerificationCheck} from '#/components/verification/VerificationCheck' ··· 167 168 profile?.displayName || profile?.handle || account.handle, 168 169 )} 169 170 </Text> 171 + <PdsBadge did={account.did} size="sm" /> 170 172 {verification.showBadge && ( 171 173 <View> 172 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 40 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 41 41 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 42 42 import {Link as InternalLink, type LinkProps} from '#/components/Link' 43 + import {PdsBadge} from '#/components/PdsBadge' 43 44 import * as Pills from '#/components/Pills' 44 45 import {RichText} from '#/components/RichText' 45 46 import {Text} from '#/components/Typography' ··· 259 260 numberOfLines={1}> 260 261 {forceLTR(name)} 261 262 </Text> 263 + <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> 262 271 {verification.showBadge && ( 263 272 <View 264 273 style={[ ··· 318 327 numberOfLines={1}> 319 328 {name} 320 329 </Text> 330 + <View style={[a.pl_xs]}> 331 + <PdsBadge did={profile.did} size="sm" /> 332 + </View> 321 333 {verification.showBadge && ( 322 334 <View style={[a.pl_xs]}> 323 335 <VerificationCheck
+13 -5
src/components/ProfileHoverCard/index.web.tsx
··· 38 38 } from '#/components/KnownFollowers' 39 39 import {InlineLinkText, Link} from '#/components/Link' 40 40 import {Loader} from '#/components/Loader' 41 + import {PdsBadge} from '#/components/PdsBadge' 41 42 import * as Pills from '#/components/Pills' 42 43 import {Portal} from '#/components/Portal' 43 44 import {RichText} from '#/components/RichText' ··· 546 547 moderation.ui('displayName'), 547 548 )} 548 549 </Text> 550 + <View style={[a.pl_xs, {marginTop: -2}]}> 551 + <PdsBadge did={profile.did} size="md" interactive={false} /> 552 + </View> 549 553 {verification.showBadge && ( 550 554 <View 551 555 style={[ ··· 555 559 }, 556 560 ]}> 557 561 <VerificationCheck 558 - width={16} 562 + width={14} 559 563 verifier={verification.role === 'verifier'} 560 564 /> 561 565 </View> ··· 581 585 582 586 {!isBlockedUser && ( 583 587 <> 584 - {disableFollowersMetrics && disableFollowingMetrics ? ( null ) : 588 + {disableFollowersMetrics && disableFollowingMetrics ? null : ( 585 589 <View style={[a.flex_row, a.flex_wrap, a.gap_md, a.pt_xs]}> 586 590 {!disableFollowersMetrics ? ( 587 591 <InlineLinkText ··· 589 593 label={`${followers} ${pluralizedFollowers}`} 590 594 style={[t.atoms.text]} 591 595 onPress={hide}> 592 - <Text style={[a.text_md, a.font_semi_bold]}>{followers} </Text> 596 + <Text style={[a.text_md, a.font_semi_bold]}> 597 + {followers}{' '} 598 + </Text> 593 599 <Text style={[t.atoms.text_contrast_medium]}> 594 600 {pluralizedFollowers} 595 601 </Text> ··· 601 607 label={_(msg`${following} following`)} 602 608 style={[t.atoms.text]} 603 609 onPress={hide}> 604 - <Text style={[a.text_md, a.font_semi_bold]}>{following} </Text> 610 + <Text style={[a.text_md, a.font_semi_bold]}> 611 + {following}{' '} 612 + </Text> 605 613 <Text style={[t.atoms.text_contrast_medium]}> 606 614 {pluralizedFollowings} 607 615 </Text> 608 616 </InlineLinkText> 609 617 ) : null} 610 618 </View> 611 - } 619 + )} 612 620 613 621 {profile.description?.trim() && !moderation.ui('profileView').blur ? ( 614 622 <View style={[a.pt_md]}>
+4
src/components/dms/MessagesListHeader.tsx
··· 20 20 import * as Layout from '#/components/Layout' 21 21 import {Link} from '#/components/Link' 22 22 import {PostAlerts} from '#/components/moderation/PostAlerts' 23 + import {PdsBadge} from '#/components/PdsBadge' 23 24 import {Text} from '#/components/Typography' 24 25 import {useSimpleVerificationState} from '#/components/verification' 25 26 import {VerificationCheck} from '#/components/verification/VerificationCheck' ··· 161 162 numberOfLines={1}> 162 163 {displayName} 163 164 </Text> 165 + <View style={[a.pl_xs]}> 166 + <PdsBadge did={profile.did} size="sm" /> 167 + </View> 164 168 {verification.showBadge && ( 165 169 <View style={[a.pl_xs]}> 166 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 41 import {Link} from '#/components/Link' 42 42 import {useMenuControl} from '#/components/Menu' 43 43 import {PostAlerts} from '#/components/moderation/PostAlerts' 44 + import {PdsBadge} from '#/components/PdsBadge' 44 45 import {createPortalGroup} from '#/components/Portal' 45 46 import {Text} from '#/components/Typography' 46 47 import {useSimpleVerificationState} from '#/components/verification' ··· 417 418 ]}> 418 419 {displayName} 419 420 </Text> 421 + </View> 422 + <View style={[a.pl_xs, a.self_center]}> 423 + <PdsBadge did={profile.did} size="sm" /> 420 424 </View> 421 425 {verification.showBadge && ( 422 426 <View style={[a.pl_xs, a.self_center]}>
+4 -1
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 57 57 import {ContentHider} from '#/components/moderation/ContentHider' 58 58 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 59 59 import {PostAlerts} from '#/components/moderation/PostAlerts' 60 + import {PdsBadge} from '#/components/PdsBadge' 60 61 import {type AppModerationCause} from '#/components/Pills' 61 62 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 62 63 import {PostControls, PostControlsSkeleton} from '#/components/PostControls' ··· 381 382 )} 382 383 </Text> 383 384 384 - <View style={[a.pl_xs]}> 385 + <View 386 + style={[a.pl_xs, a.flex_row, a.gap_2xs, a.align_center]}> 387 + <PdsBadge did={post.author.did} size="md" /> 385 388 <VerificationCheckButton profile={authorShadow} size="md" /> 386 389 </View> 387 390 </View>
+5
src/screens/Profile/Header/DisplayName.tsx
··· 5 5 import {sanitizeHandle} from '#/lib/strings/handles' 6 6 import {type Shadow} from '#/state/cache/types' 7 7 import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 8 + import {PdsBadge} from '#/components/PdsBadge' 8 9 import {Text} from '#/components/Typography' 9 10 import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 10 11 ··· 36 37 <View 37 38 style={[ 38 39 a.pl_xs, 40 + a.flex_row, 41 + a.gap_2xs, 42 + a.align_center, 39 43 { 40 44 marginTop: platform({ios: 2}), 41 45 }, 42 46 ]}> 47 + <PdsBadge did={profile.did} size="lg" /> 43 48 <VerificationCheckButton profile={profile} size="lg" /> 44 49 </View> 45 50 </Text>
+10 -1
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 42 42 shouldShowKnownFollowers, 43 43 } from '#/components/KnownFollowers' 44 44 import {Link} from '#/components/Link' 45 + import {PdsBadge} from '#/components/PdsBadge' 45 46 import * as Prompt from '#/components/Prompt' 46 47 import {RichText} from '#/components/RichText' 47 48 import * as Toast from '#/components/Toast' ··· 162 163 profile.displayName || sanitizeHandle(profile.handle), 163 164 moderation.ui('displayName'), 164 165 )} 165 - <View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}> 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" /> 166 175 <VerificationCheckButton profile={profile} size="lg" /> 167 176 </View> 168 177 </Text>
+45 -4
src/screens/Settings/RunesSettings.tsx
··· 5 5 import {useLingui} from '@lingui/react' 6 6 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 7 7 8 - import { DEFAULT_ALT_TEXT_AI_MODEL } from '#/lib/constants' 8 + import {DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants' 9 9 import {usePalette} from '#/lib/hooks/usePalette' 10 10 import {type CommonNavigatorParams} from '#/lib/routes/types' 11 11 import {dynamicActivate} from '#/locale/i18n' ··· 114 114 useSetOpenRouterApiKey, 115 115 useSetOpenRouterModel, 116 116 } from '#/state/preferences/openrouter' 117 + import { 118 + usePdsLabelEnabled, 119 + usePdsLabelHideBskyPds, 120 + useSetPdsLabelEnabled, 121 + useSetPdsLabelHideBskyPds, 122 + } from '#/state/preferences/pds-label' 117 123 import { 118 124 usePostReplacement, 119 125 useSetPostReplacement, ··· 708 714 const deerVerificationEnabled = useDeerVerificationEnabled() 709 715 const setDeerVerificationEnabled = useSetDeerVerificationEnabled() 710 716 717 + const pdsLabelEnabled = usePdsLabelEnabled() 718 + const setPdsLabelEnabled = useSetPdsLabelEnabled() 719 + const pdsLabelHideBskyPds = usePdsLabelHideBskyPds() 720 + const setPdsLabelHideBskyPds = useSetPdsLabelHideBskyPds() 721 + 711 722 const repostCarouselEnabled = useRepostCarouselEnabled() 712 723 const setRepostCarouselEnabled = useSetRepostCarouselEnabled() 713 724 ··· 760 771 </Toggle.Item> 761 772 <Toggle.Item 762 773 name="use_handle_in_links" 763 - label={_(msg`Use handles in profile links instead of DIDs (requires restart)`)} 774 + label={_( 775 + msg`Use handles in profile links instead of DIDs (requires restart)`, 776 + )} 764 777 value={handleInLinks ?? false} 765 778 onChange={value => setHandleInLinks(value)} 766 779 style={[a.w_full]}> ··· 909 922 <Trans>Tweaks</Trans> 910 923 </SettingsList.ItemText> 911 924 <Toggle.Item 925 + 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 912 954 name="repost_carousel" 913 955 label={_(msg`Combine reposts into a horizontal carousel`)} 914 956 value={repostCarouselEnabled} ··· 1186 1228 <SettingsList.Item> 1187 1229 <Admonition type="info" style={[a.flex_1]}> 1188 1230 <Trans> 1189 - Current model:{' '} 1190 - {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '} 1231 + Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '} 1191 1232 <InlineLinkText 1192 1233 to="https://openrouter.ai/models?fmt=cards&input_modalities=image&order=most-popular" 1193 1234 label="openrouter.ai">
+10 -9
src/screens/Settings/Settings.tsx
··· 372 372 ]}> 373 373 {displayName} 374 374 </Text> 375 - {shouldShowVerificationCheckButton(verificationState) && ( 376 - <View 377 - style={[ 378 - { 379 - marginTop: platform({web: 8, ios: 8, android: 10}), 380 - }, 381 - ]}> 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) && ( 382 383 <VerificationCheckButton profile={shadow} size="lg" /> 383 - </View> 384 - )} 384 + )} 385 + </View> 385 386 </View> 386 387 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}> 387 388 {sanitizeHandle(profile.handle, '@')}
+10
src/state/persisted/schema.ts
··· 173 173 .optional(), 174 174 highQualityImages: z.boolean().optional(), 175 175 hideUnreplyablePosts: z.boolean().optional(), 176 + pdsLabel: z 177 + .object({ 178 + enabled: z.boolean(), 179 + hideBskyPds: z.boolean(), 180 + }) 181 + .optional(), 176 182 177 183 postReplacement: z.object({ 178 184 enabled: z.boolean().optional(), ··· 304 310 }, 305 311 highQualityImages: false, 306 312 hideUnreplyablePosts: false, 313 + pdsLabel: { 314 + enabled: false, 315 + hideBskyPds: true, 316 + }, 307 317 showExternalShareButtons: false, 308 318 translationServicePreference: 'google', 309 319 libreTranslateInstance: 'https://libretranslate.com/',
+78 -75
src/state/preferences/index.tsx
··· 37 37 import {Provider as NoAppLabelersProvider} from './no-app-labelers' 38 38 import {Provider as NoDiscoverProvider} from './no-discover-fallback' 39 39 import {Provider as OpenRouterProvider} from './openrouter' 40 + import {Provider as PdsLabelProvider} from './pds-label' 40 41 import {Provider as PostNameReplacementProvider} from './post-name-replacement.tsx' 41 42 import {Provider as RepostCarouselProvider} from './repost-carousel-enabled' 42 43 import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle' ··· 96 97 <ConstellationProvider> 97 98 <ConstellationInstanceProvider> 98 99 <DeerVerificationProvider> 99 - <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> 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> 174 177 </DeerVerificationProvider> 175 178 </ConstellationInstanceProvider> 176 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 16 import {type ComposerOptsPostRef} from '#/state/shell/composer' 17 17 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 18 18 import {atoms as a, useTheme, web} from '#/alf' 19 + import {PdsBadge} from '#/components/PdsBadge' 19 20 import {QuoteEmbed} from '#/components/Post/Embed' 20 21 import {Text} from '#/components/Typography' 21 22 import {useSimpleVerificationState} from '#/components/verification' ··· 116 117 sanitizeHandle(replyTo.author.handle), 117 118 )} 118 119 </Text> 120 + <View style={[a.pl_xs]}> 121 + <PdsBadge did={replyTo.author.did} size="sm" /> 122 + </View> 119 123 {verification.showBadge && ( 120 124 <View style={[a.pl_xs]}> 121 125 <VerificationCheck
+4
src/view/com/composer/text-input/mobile/Autocomplete.tsx
··· 9 9 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 10 10 import {UserAvatar} from '#/view/com/util/UserAvatar' 11 11 import {atoms as a, platform, useTheme} from '#/alf' 12 + import {PdsBadge} from '#/components/PdsBadge' 12 13 import {Text} from '#/components/Typography' 13 14 import {useSimpleVerificationState} from '#/components/verification' 14 15 import {VerificationCheck} from '#/components/verification/VerificationCheck' ··· 115 116 numberOfLines={1}> 116 117 {displayName} 117 118 </Text> 119 + <View style={[{marginTop: platform({android: -2})}]}> 120 + <PdsBadge did={profile.did} size="sm" /> 121 + </View> 118 122 {state.isVerified && ( 119 123 <View 120 124 style={[
+4
src/view/com/notifications/NotificationFeedItem.tsx
··· 65 65 import {VerifiedCheck} from '#/components/icons/VerifiedCheck' 66 66 import {InlineLinkText, Link} from '#/components/Link' 67 67 import * as MediaPreview from '#/components/MediaPreview' 68 + import {PdsBadge} from '#/components/PdsBadge' 68 69 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 69 70 import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 70 71 import {SubtleHover} from '#/components/SubtleHover' ··· 1060 1061 author.profile.displayName || author.profile.handle, 1061 1062 )} 1062 1063 </Text> 1064 + <View style={[a.pl_xs, a.self_center]}> 1065 + <PdsBadge did={author.profile.did} size="sm" /> 1066 + </View> 1063 1067 {verification.showBadge && ( 1064 1068 <View style={[a.pl_xs, a.self_center]}> 1065 1069 <VerificationCheck
+11
src/view/com/util/PostMeta.tsx
··· 16 16 import {unstableCacheProfileView} from '#/state/queries/profile' 17 17 import {atoms as a, platform, useTheme, web} from '#/alf' 18 18 import {WebOnlyInlineLinkText} from '#/components/Link' 19 + import {PdsBadge} from '#/components/PdsBadge' 19 20 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 20 21 import {Text} from '#/components/Typography' 21 22 import {useSimpleVerificationState} from '#/components/verification' ··· 113 114 ), 114 115 )} 115 116 </MaybeLinkText> 117 + <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> 116 127 {verification.showBadge && ( 117 128 <View 118 129 style={[
+3
src/view/icons/index.tsx
··· 54 54 import {faClipboardCheck} from '@fortawesome/free-solid-svg-icons/faClipboardCheck' 55 55 import {faClone} from '@fortawesome/free-solid-svg-icons/faClone' 56 56 import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash' 57 + import {faDatabase} from '@fortawesome/free-solid-svg-icons/faDatabase' 57 58 import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload' 58 59 import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis' 59 60 import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope' ··· 150 151 faCommentSlash, 151 152 faComments, 152 153 faCompass, 154 + faDatabase, 153 155 faDownload, 154 156 faEllipsis, 155 157 faEnvelope, ··· 160 162 faFire, 161 163 faFlask, 162 164 faFloppyDisk, 165 + 163 166 faGear, 164 167 faGlobe, 165 168 faHand,
+2
src/view/shell/Drawer.tsx
··· 57 57 } from '#/components/icons/UserCircle' 58 58 import {InlineLinkText} from '#/components/Link' 59 59 import {Text} from '#/components/Typography' 60 + import {PdsBadge} from '#/components/PdsBadge' 60 61 import {useSimpleVerificationState} from '#/components/verification' 61 62 import {VerificationCheck} from '#/components/verification/VerificationCheck' 62 63 import {IS_WEB} from '#/env' ··· 104 105 numberOfLines={1}> 105 106 {profile?.displayName || account.handle} 106 107 </Text> 108 + <PdsBadge did={account.did} size="md" /> 107 109 {verification.showBadge && ( 108 110 <View 109 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
jeanmachine.dev submitted #0
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