this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[APP-1928] add bot/automated account badge and self-labeling settings (#10008)

Co-authored-by: Eric Bailey <git@esb.lol>

authored by

Spence Pope
Eric Bailey
and committed by
GitHub
2bd98116 8bdb1c30

+641 -235
+1
assets/icons/bot_filled.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 0a2 2 0 0 1 1 3.73V5h4.2c1.68 0 2.52 0 3.162.327a3 3 0 0 1 1.31 1.31C22 7.28 22 8.12 22 9.8v.25a2.501 2.501 0 0 1 0 4.9V15c0 2.8 0 4.2-.545 5.27a5 5 0 0 1-2.185 2.185C18.2 23 16.8 23 14 23h-4c-2.8 0-4.2 0-5.27-.545a5 5 0 0 1-2.185-2.185C2 19.2 2 17.8 2 15v-.05a2.5 2.5 0 0 1 0-4.9V9.8c0-1.68 0-2.52.327-3.162a3 3 0 0 1 1.31-1.31C4.28 5 5.12 5 6.8 5H11V3.73A2 2 0 0 1 12 0M8 10a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0v-2a2 2 0 0 0-2-2m8 0a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0v-2a2 2 0 0 0-2-2" clip-rule="evenodd"/></svg>
+1
assets/icons/bot_stroke.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2a1 1 0 0 1 1 1v2h3.2l1.113.005c.975.015 1.568.077 2.05.322a3 3 0 0 1 1.31 1.31C21 7.28 21 8.12 21 9.8v.287a1.498 1.498 0 0 1-.005 2.827c-.006 2.204-.058 3.41-.54 4.356l-.093.174a5 5 0 0 1-2.092 2.011l-.205.096c-.766.33-1.72.417-3.21.44L13 20h-2l-1.854-.009c-1.49-.023-2.445-.11-3.211-.44l-.205-.096a5 5 0 0 1-2.185-2.185c-.409-.803-.51-1.79-.536-3.415l-.005-.94A1.498 1.498 0 0 1 3 10.086V9.8c0-1.575 0-2.412.27-3.04l.057-.122a3 3 0 0 1 1.105-1.196l.206-.115C5.279 5 6.12 5 7.8 5H11V3a1 1 0 0 1 1-1M7.8 7c-.873 0-1.408.002-1.808.034a3 3 0 0 0-.367.051l-.063.018-.016.006a1 1 0 0 0-.437.437l-.006.016-.018.063a3 3 0 0 0-.05.367C5.001 8.392 5 8.927 5 9.8V12c0 1.433.002 2.388.062 3.121.058.71.16 1.036.265 1.241a3 3 0 0 0 1.31 1.31c.207.106.532.209 1.242.267.733.06 1.688.061 3.121.061h2c1.433 0 2.388-.002 3.121-.061.71-.058 1.036-.161 1.241-.266a3 3 0 0 0 1.31-1.31c.106-.206.209-.532.267-1.242.06-.733.061-1.688.061-3.121V9.8c0-.873-.002-1.408-.034-1.808a2.5 2.5 0 0 0-.051-.367l-.017-.063-.007-.016a1 1 0 0 0-.437-.437l-.015-.006-.064-.018a3 3 0 0 0-.367-.05C17.608 7.001 17.073 7 16.2 7zM9 10a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1m6 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1"/></svg>
+10
bskyembed/src/components/post.tsx
··· 10 10 import {Like as LikeIcon} from '../icons/Like' 11 11 import {Reply as ReplyIcon} from '../icons/Reply' 12 12 import {Repost as RepostIcon} from '../icons/Repost' 13 + import {Robot as RobotIcon} from '../icons/Robot' 13 14 import {CONTENT_LABELS} from '../labels' 14 15 import * as bsky from '../types/bsky' 15 16 import {niceDate} from '../util/nice-date' ··· 43 44 } 44 45 45 46 const verification = getVerificationState({profile: post.author}) 47 + const isBot = post.author.labels?.some( 48 + l => l.val === 'bot' && l.src === post.author.did, 49 + ) 46 50 47 51 const href = `/profile/${post.author.did}/post/${getRkey(post)}` 48 52 ··· 73 77 <VerificationCheck 74 78 className="pl-[3px] mt-px shrink-0" 75 79 verifier={verification.role === 'verifier'} 80 + size={15} 81 + /> 82 + )} 83 + {isBot && ( 84 + <RobotIcon 85 + className="pl-[3px] mt-px shrink-0 text-slate-500 dark:text-slate-400" 76 86 size={15} 77 87 /> 78 88 )}
+22
bskyembed/src/icons/Robot.tsx
··· 1 + import {h} from 'preact' 2 + 3 + export const Robot = ({ 4 + size = 14, 5 + className, 6 + }: { 7 + size?: number 8 + className?: string 9 + }) => ( 10 + <svg 11 + className={className} 12 + width={size} 13 + height={size} 14 + viewBox="0 0 24 24" 15 + fill="none" 16 + xmlns="http://www.w3.org/2000/svg"> 17 + <path 18 + d="M12 0C13.1046 0 14 0.89543 14 2C14 2.73976 13.5971 3.3835 13 3.72949V5H17.2002C18.8802 5 19.7206 5.00018 20.3623 5.32715C20.9265 5.61472 21.3853 6.07347 21.6729 6.6377C21.9998 7.27941 22 8.11978 22 9.7998V10.0498C23.1411 10.2814 24 11.2905 24 12.5C24 13.7094 23.141 14.7175 22 14.9492V15C22 17.8 21.9999 19.2 21.4551 20.2695C20.9757 21.2103 20.2103 21.9757 19.2695 22.4551C18.2 22.9999 16.8 23 14 23H10C7.20005 23 5.79998 22.9999 4.73047 22.4551C3.78966 21.9757 3.02429 21.2103 2.54492 20.2695C2.00013 19.2 2 17.8 2 15V14.9492C0.858955 14.7175 0 13.7094 0 12.5C0 11.2905 0.85886 10.2814 2 10.0498V9.7998C2 8.11978 2.00018 7.27941 2.32715 6.6377C2.61472 6.07347 3.07347 5.61472 3.6377 5.32715C4.27941 5.00018 5.11978 5 6.7998 5H11V3.72949C10.4029 3.3835 10 2.73976 10 2C10 0.89543 10.8954 0 12 0ZM8 10C6.89543 10 6 10.8954 6 12V14C6 15.1046 6.89543 16 8 16C9.10457 16 10 15.1046 10 14V12C10 10.8954 9.10457 10 8 10ZM16 10C14.8954 10 14 10.8954 14 12V14C14 15.1046 14.8954 16 16 16C17.1046 16 18 15.1046 18 14V12C18 10.8954 17.1046 10 16 10Z" 19 + fill="currentColor" 20 + /> 21 + </svg> 22 + )
+1
bskyweb/cmd/bskyweb/server.go
··· 292 292 e.GET("/settings/accessibility", server.WebGeneric) 293 293 e.GET("/settings/appearance", server.WebGeneric) 294 294 e.GET("/settings/account", server.WebGeneric) 295 + e.GET("/settings/automation-label", server.WebGeneric) 295 296 e.GET("/settings/privacy-and-security", server.WebGeneric) 296 297 e.GET("/settings/privacy-and-security/activity", server.WebGeneric) 297 298 e.GET("/settings/content-and-media", server.WebGeneric)
+9
src/Navigation.tsx
··· 103 103 import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' 104 104 import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' 105 105 import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' 106 + import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings' 106 107 import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' 107 108 import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' 108 109 import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings' ··· 400 401 getComponent={() => AccountSettingsScreen} 401 402 options={{ 402 403 title: title(msg`Account`), 404 + requireAuth: true, 405 + }} 406 + /> 407 + <Stack.Screen 408 + name="AutomationLabelSettings" 409 + getComponent={() => AutomationLabelSettingsScreen} 410 + options={{ 411 + title: title(msg`Automation Label`), 403 412 requireAuth: true, 404 413 }} 405 414 />
+3
src/analytics/metrics/types.ts
··· 732 732 'verification:settings:hideBadges': {} 733 733 'verification:settings:unHideBadges': {} 734 734 735 + 'bot:label:toggle': {state: 'add' | 'remove'} 736 + 'bot:badge:click': {} 737 + 735 738 'live:create': {duration: number} 736 739 'live:edit': {} 737 740 'live:remove': {}
+7 -10
src/components/AccountList.tsx
··· 16 16 import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 17 17 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron' 18 18 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 19 + import {ProfileBadges} from '#/components/ProfileBadges' 19 20 import {Text} from '#/components/Typography' 20 - import {useSimpleVerificationState} from '#/components/verification' 21 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 22 21 import {useActorStatus} from '#/features/liveNow' 23 22 24 23 export function AccountList({ ··· 116 115 }) { 117 116 const t = useTheme() 118 117 const {_} = useLingui() 119 - const verification = useSimpleVerificationState({profile}) 120 118 const {isActive: live} = useActorStatus(profile) 121 119 122 120 const onPress = useCallback(() => { ··· 164 162 profile?.displayName || profile?.handle || account.handle, 165 163 )} 166 164 </Text> 167 - {verification.showBadge && ( 168 - <View> 169 - <VerificationCheck 170 - width={12} 171 - verifier={verification.role === 'verifier'} 172 - /> 173 - </View> 165 + {profile && ( 166 + <ProfileBadges 167 + profile={profile} 168 + size="sm" 169 + style={[{marginTop: -2}]} 170 + /> 174 171 )} 175 172 </View> 176 173 <Text
+79
src/components/BotAccountAlert.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans, useLingui} from '@lingui/react/macro' 3 + 4 + import {useSession} from '#/state/session' 5 + import {atoms as a, useTheme, web} from '#/alf' 6 + import {Button, ButtonText} from '#/components/Button' 7 + import * as Dialog from '#/components/Dialog' 8 + import {Bot_Filled as RobotIcon} from '#/components/icons/Bot' 9 + import {Text} from '#/components/Typography' 10 + import {navigate} from '#/Navigation' 11 + import type * as bsky from '#/types/bsky' 12 + 13 + export function BotAccountAlert({ 14 + control, 15 + profile, 16 + }: { 17 + control: Dialog.DialogControlProps 18 + profile: bsky.profile.AnyProfileView 19 + }) { 20 + const {t: l} = useLingui() 21 + const t = useTheme() 22 + const {currentAccount} = useSession() 23 + 24 + const isSelf = profile.did === currentAccount?.did 25 + const description = isSelf 26 + ? l`You have marked this account as automated. You can remove it at any time from your account settings.` 27 + : l`This account has been marked as automated by its owner.` 28 + 29 + return ( 30 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 31 + <Dialog.ScrollableInner 32 + label={l`Automated account`} 33 + style={[web({maxWidth: 320})]}> 34 + <View style={[a.align_center, a.pb_md, a.shadow_sm]}> 35 + <RobotIcon width={48} fill={t.atoms.text_contrast_medium.color} /> 36 + </View> 37 + <View style={[a.align_center]}> 38 + <Text 39 + style={[ 40 + a.leading_snug, 41 + a.text_center, 42 + a.pb_xl, 43 + a.text_md, 44 + t.atoms.text_contrast_high, 45 + {maxWidth: 300}, 46 + ]}> 47 + {description} 48 + </Text> 49 + </View> 50 + <View style={[a.w_full, a.gap_sm]}> 51 + <Button 52 + label={l`Okay`} 53 + onPress={() => control.close()} 54 + color="primary" 55 + size="large"> 56 + <ButtonText> 57 + <Trans>Okay</Trans> 58 + </ButtonText> 59 + </Button> 60 + {isSelf ? ( 61 + <Button 62 + label={l`Open settings`} 63 + onPress={() => { 64 + control.close(() => { 65 + navigate('AutomationLabelSettings') 66 + }) 67 + }} 68 + color="secondary" 69 + size="large"> 70 + <ButtonText> 71 + <Trans>Open settings</Trans> 72 + </ButtonText> 73 + </Button> 74 + ) : null} 75 + </View> 76 + </Dialog.ScrollableInner> 77 + </Dialog.Outer> 78 + ) 79 + }
+92
src/components/BotBadge.tsx
··· 1 + import {View} from 'react-native' 2 + import {type ComAtprotoLabelDefs} from '@atproto/api' 3 + import {useLingui} from '@lingui/react/macro' 4 + 5 + import {atoms as a, useTheme} from '#/alf' 6 + import {BotAccountAlert} from '#/components/BotAccountAlert' 7 + import {Button} from '#/components/Button' 8 + import {useDialogControl} from '#/components/Dialog' 9 + import {Bot_Filled as RobotIcon} from '#/components/icons/Bot' 10 + import {useAnalytics} from '#/analytics' 11 + import type * as bsky from '#/types/bsky' 12 + 13 + export function isBotAccount(profile: { 14 + did: string 15 + labels?: ComAtprotoLabelDefs.Label[] 16 + }): boolean { 17 + return ( 18 + profile.labels?.some(l => l.val === 'bot' && l.src === profile.did) ?? false 19 + ) 20 + } 21 + 22 + export function BotBadge({ 23 + profile, 24 + alwaysShow = false, 25 + width, 26 + }: { 27 + profile: bsky.profile.AnyProfileView 28 + alwaysShow?: boolean 29 + width: number 30 + }) { 31 + const t = useTheme() 32 + 33 + if (!isBotAccount(profile) && !alwaysShow) { 34 + return null 35 + } 36 + 37 + return ( 38 + <View> 39 + <RobotIcon width={width} fill={t.atoms.text_contrast_medium.color} /> 40 + </View> 41 + ) 42 + } 43 + 44 + export function BotBadgeButton({ 45 + profile, 46 + width, 47 + }: { 48 + profile: bsky.profile.AnyProfileView 49 + width: number 50 + }) { 51 + const t = useTheme() 52 + const ax = useAnalytics() 53 + const {t: l} = useLingui() 54 + const control = useDialogControl() 55 + 56 + if (!isBotAccount(profile)) { 57 + return null 58 + } 59 + 60 + return ( 61 + <> 62 + <Button 63 + label={l`Automated account`} 64 + hitSlop={20} 65 + onPress={evt => { 66 + evt.preventDefault() 67 + ax.metric('bot:badge:click', {}) 68 + control.open() 69 + }}> 70 + {({hovered}) => ( 71 + <View 72 + style={[ 73 + a.justify_end, 74 + a.align_end, 75 + a.transition_transform, 76 + { 77 + width: width, 78 + height: width, 79 + transform: [{scale: hovered ? 1.1 : 1}], 80 + }, 81 + ]}> 82 + <RobotIcon 83 + width={width} 84 + fill={t.atoms.text_contrast_medium.color} 85 + /> 86 + </View> 87 + )} 88 + </Button> 89 + <BotAccountAlert control={control} profile={profile} /> 90 + </> 91 + ) 92 + }
+2 -11
src/components/PostControls/ShareMenu/RecentChats.tsx
··· 17 17 import {atoms as a, tokens, useTheme} from '#/alf' 18 18 import {Button} from '#/components/Button' 19 19 import {useDialogContext} from '#/components/Dialog' 20 + import {ProfileBadges} from '#/components/ProfileBadges' 20 21 import {Text} from '#/components/Typography' 21 - import {useSimpleVerificationState} from '#/components/verification' 22 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 23 22 import {useAnalytics} from '#/analytics' 24 23 import type * as bsky from '#/types/bsky' 25 24 ··· 111 110 profile.displayName || sanitizeHandle(profile.handle), 112 111 moderation.ui('displayName'), 113 112 ) 114 - const verification = useSimpleVerificationState({profile}) 115 113 116 114 if (isBlockedOrBlocking(profile) || isMuted(profile)) { 117 115 return null ··· 141 139 numberOfLines={1}> 142 140 {name} 143 141 </Text> 144 - {verification.showBadge && ( 145 - <View style={[a.pl_2xs]}> 146 - <VerificationCheck 147 - width={10} 148 - verifier={verification.role === 'verifier'} 149 - /> 150 - </View> 151 - )} 142 + <ProfileBadges profile={profile} size="xs" style={[a.pl_2xs]} /> 152 143 </View> 153 144 </Button> 154 145 )
+76
src/components/ProfileBadges.tsx
··· 1 + import {View} from 'react-native' 2 + 3 + import {useProfileShadow} from '#/state/cache/profile-shadow' 4 + import {atoms as a, type ViewStyleProp} from '#/alf' 5 + import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge' 6 + import {useSimpleVerificationState} from '#/components/verification' 7 + import {VerificationCheck} from '#/components/verification/VerificationCheck' 8 + import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 9 + import type * as bsky from '#/types/bsky' 10 + 11 + export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' 12 + 13 + const verificationIconSizes: Record<Size, number> = { 14 + xs: 10, 15 + sm: 12, 16 + md: 14, 17 + lg: 18, 18 + xl: 22, 19 + } as const 20 + 21 + const botIconSizes: Record<Size, number> = { 22 + xs: 11, 23 + sm: 13, 24 + md: 15, 25 + lg: 19, 26 + xl: 23, 27 + } as const 28 + 29 + export function ProfileBadges({ 30 + profile, 31 + interactive = false, 32 + size, 33 + style, 34 + }: ViewStyleProp & { 35 + profile: bsky.profile.AnyProfileView 36 + interactive?: boolean 37 + size: Size 38 + }) { 39 + const shadowed = useProfileShadow(profile) 40 + const verification = useSimpleVerificationState({profile}) 41 + 42 + // if nothing to show, don't render the container at all 43 + if (!verification.showBadge && !isBotAccount(shadowed)) return null 44 + 45 + const isOnTheSmallSide = size === 'xs' || size === 'sm' 46 + 47 + return ( 48 + <View 49 + style={[ 50 + a.flex_row, 51 + a.align_center, 52 + isOnTheSmallSide ? a.gap_2xs : a.gap_xs, 53 + style, 54 + ]}> 55 + {interactive ? ( 56 + <> 57 + <VerificationCheckButton 58 + profile={shadowed} 59 + width={verificationIconSizes[size]} 60 + /> 61 + <BotBadgeButton profile={shadowed} width={botIconSizes[size]} /> 62 + </> 63 + ) : ( 64 + <> 65 + {verification.showBadge && ( 66 + <VerificationCheck 67 + verifier={verification.role === 'verifier'} 68 + width={verificationIconSizes[size]} 69 + /> 70 + )} 71 + <BotBadge profile={shadowed} width={botIconSizes[size]} /> 72 + </> 73 + )} 74 + </View> 75 + ) 76 + }
+11 -25
src/components/ProfileCard.tsx
··· 41 41 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 42 42 import {Link as InternalLink, type LinkProps} from '#/components/Link' 43 43 import * as Pills from '#/components/Pills' 44 + import {ProfileBadges} from '#/components/ProfileBadges' 44 45 import {RichText} from '#/components/RichText' 45 46 import {Text} from '#/components/Typography' 46 - import {useSimpleVerificationState} from '#/components/verification' 47 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 48 47 import {type Metrics} from '#/analytics' 49 48 import {useActorStatus} from '#/features/liveNow' 50 49 import type * as bsky from '#/types/bsky' ··· 242 241 moderationOpts: ModerationOpts 243 242 }) { 244 243 const t = useTheme() 245 - const verification = useSimpleVerificationState({profile}) 246 244 const moderation = moderateProfile(profile, moderationOpts) 247 245 const name = sanitizeDisplayName( 248 246 profile.displayName || sanitizeHandle(profile.handle), ··· 262 260 numberOfLines={1}> 263 261 {forceLTR(name)} 264 262 </Text> 265 - {verification.showBadge && ( 266 - <View 267 - style={[ 268 - a.pl_2xs, 269 - a.self_center, 270 - {marginTop: platform({default: 0, android: -1})}, 271 - ]}> 272 - <VerificationCheck 273 - width={platform({android: 13, default: 12})} 274 - verifier={verification.role === 'verifier'} 275 - /> 276 - </View> 277 - )} 263 + <ProfileBadges 264 + profile={profile} 265 + size="md" 266 + style={[ 267 + a.pl_2xs, 268 + a.self_center, 269 + {marginTop: platform({default: 0, android: -1})}, 270 + ]} 271 + /> 278 272 <Text 279 273 emoji 280 274 style={[ ··· 305 299 profile.displayName || sanitizeHandle(profile.handle), 306 300 moderation.ui('displayName'), 307 301 ) 308 - const verification = useSimpleVerificationState({profile}) 309 302 return ( 310 303 <View style={[a.flex_row, a.align_center, a.max_w_full, style]}> 311 304 <Text ··· 321 314 numberOfLines={1}> 322 315 {name} 323 316 </Text> 324 - {verification.showBadge && ( 325 - <View style={[a.pl_xs]}> 326 - <VerificationCheck 327 - width={14} 328 - verifier={verification.role === 'verifier'} 329 - /> 330 - </View> 331 - )} 317 + <ProfileBadges profile={profile} size="md" style={[a.pl_xs]} /> 332 318 </View> 333 319 ) 334 320 }
+11 -17
src/components/ProfileHoverCard/index.web.tsx
··· 36 36 import {Loader} from '#/components/Loader' 37 37 import * as Pills from '#/components/Pills' 38 38 import {Portal} from '#/components/Portal' 39 + import {ProfileBadges} from '#/components/ProfileBadges' 39 40 import {RichText} from '#/components/RichText' 40 41 import {Text} from '#/components/Typography' 41 - import {useSimpleVerificationState} from '#/components/verification' 42 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 43 42 import {IS_WEB_TOUCH_DEVICE} from '#/env' 44 43 import {useActorStatus} from '#/features/liveNow' 45 44 import {LiveStatus} from '#/features/liveNow/components/LiveStatusDialog' ··· 459 458 [currentAccount, profile], 460 459 ) 461 460 const isLabeler = profile.associated?.labeler 462 - const verification = useSimpleVerificationState({profile}) 463 461 464 462 return ( 465 463 <View> ··· 527 525 moderation.ui('displayName'), 528 526 )} 529 527 </Text> 530 - {verification.showBadge && ( 531 - <View 532 - style={[ 533 - a.pl_xs, 534 - { 535 - marginTop: -2, 536 - }, 537 - ]}> 538 - <VerificationCheck 539 - width={16} 540 - verifier={verification.role === 'verifier'} 541 - /> 542 - </View> 543 - )} 528 + <ProfileBadges 529 + profile={profile} 530 + size="md" 531 + style={[ 532 + a.pl_xs, 533 + { 534 + marginTop: -1, 535 + }, 536 + ]} 537 + /> 544 538 </View> 545 539 546 540 <ProfileHeaderHandle profile={profileShadow} disableTaps />
+2 -13
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 {ProfileBadges} from '#/components/ProfileBadges' 23 24 import {Text} from '#/components/Typography' 24 - import {useSimpleVerificationState} from '#/components/verification' 25 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 26 25 import {IS_WEB} from '#/env' 27 26 28 27 const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE ··· 112 111 const {_} = useLingui() 113 112 const t = useTheme() 114 113 const convoState = useConvo() 115 - const verification = useSimpleVerificationState({ 116 - profile, 117 - }) 118 114 119 115 const isDeletedAccount = profile?.handle === 'missing.invalid' 120 116 const displayName = isDeletedAccount ··· 161 157 numberOfLines={1}> 162 158 {displayName} 163 159 </Text> 164 - {verification.showBadge && ( 165 - <View style={[a.pl_xs]}> 166 - <VerificationCheck 167 - width={14} 168 - verifier={verification.role === 'verifier'} 169 - /> 170 - </View> 171 - )} 160 + <ProfileBadges profile={profile} size="md" style={[a.pl_xs]} /> 172 161 </View> 173 162 {!isDeletedAccount && ( 174 163 <Text
+9
src/components/icons/Bot.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Bot_Stroke = createSinglePathSVG({ 4 + path: 'M12 2a1 1 0 0 1 1 1v2h3.2l1.113.005c.975.015 1.568.077 2.05.322a3 3 0 0 1 1.31 1.31C21 7.28 21 8.12 21 9.8v.287a1.498 1.498 0 0 1-.005 2.827c-.006 2.204-.058 3.41-.54 4.356l-.093.174a5 5 0 0 1-2.092 2.011l-.205.096c-.766.33-1.72.417-3.21.44L13 20h-2l-1.854-.009c-1.49-.023-2.445-.11-3.211-.44l-.205-.096a5 5 0 0 1-2.185-2.185c-.409-.803-.51-1.79-.536-3.415l-.005-.94A1.498 1.498 0 0 1 3 10.086V9.8c0-1.575 0-2.412.27-3.04l.057-.122a3 3 0 0 1 1.105-1.196l.206-.115C5.279 5 6.12 5 7.8 5H11V3a1 1 0 0 1 1-1M7.8 7c-.873 0-1.408.002-1.808.034a2.6 2.6 0 0 0-.367.051l-.063.018-.016.006a1 1 0 0 0-.437.437q0 0-.006.016l-.018.063a2.6 2.6 0 0 0-.05.367C5.001 8.392 5 8.927 5 9.8V12c0 1.433.002 2.388.062 3.121.058.71.16 1.036.265 1.241a3 3 0 0 0 1.31 1.31c.207.106.532.209 1.242.267.733.06 1.688.061 3.121.061h2c1.433 0 2.388-.002 3.121-.061.71-.058 1.036-.161 1.241-.266a3 3 0 0 0 1.31-1.31c.106-.206.209-.532.267-1.242.06-.733.061-1.688.061-3.121V9.8c0-.873-.002-1.408-.034-1.808a2.5 2.5 0 0 0-.051-.367l-.017-.063-.007-.016a1 1 0 0 0-.437-.437l-.015-.006-.064-.018a2.6 2.6 0 0 0-.367-.05C17.608 7.001 17.073 7 16.2 7zM9 10a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1m6 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1', 5 + }) 6 + 7 + export const Bot_Filled = createSinglePathSVG({ 8 + path: 'M12 0a2 2 0 0 1 1 3.73V5h4.2c1.68 0 2.52 0 3.162.327a3 3 0 0 1 1.31 1.31C22 7.28 22 8.12 22 9.8v.25a2.501 2.501 0 0 1 0 4.9V15c0 2.8 0 4.2-.545 5.27a5 5 0 0 1-2.185 2.185C18.2 23 16.8 23 14 23h-4c-2.8 0-4.2 0-5.27-.545a5 5 0 0 1-2.185-2.185C2 19.2 2 17.8 2 15v-.05a2.5 2.5 0 0 1 0-4.9V9.8c0-1.68 0-2.52.327-3.162a3 3 0 0 1 1.31-1.31C4.28 5 5.12 5 6.8 5H11V3.73A2 2 0 0 1 12 0M8 10a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0v-2a2 2 0 0 0-2-2m8 0a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0v-2a2 2 0 0 0-2-2', 9 + })
+5 -1
src/components/moderation/LabelsOnMe.tsx
··· 36 36 if (!labels || !currentAccount) { 37 37 return null 38 38 } 39 - labels = labels.filter(l => !l.val.startsWith('!')) 39 + labels = labels.filter( 40 + l => 41 + !l.val.startsWith('!') && 42 + !(l.val === 'bot' && l.src === currentAccount.did), 43 + ) 40 44 if (!labels.length) { 41 45 return null 42 46 }
+10 -17
src/components/verification/VerificationCheckButton.tsx
··· 3 3 import {useLingui} from '@lingui/react' 4 4 5 5 import {type Shadow} from '#/state/cache/types' 6 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 6 + import {atoms as a, useTheme} from '#/alf' 7 7 import {Button} from '#/components/Button' 8 8 import {useDialogControl} from '#/components/Dialog' 9 9 import {useFullVerificationState} from '#/components/verification' ··· 51 51 52 52 export function VerificationCheckButton({ 53 53 profile, 54 - size, 54 + width, 55 55 }: { 56 56 profile: Shadow<bsky.profile.AnyProfileView> 57 - size: 'lg' | 'md' | 'sm' 57 + width: number 58 58 }) { 59 59 const state = useFullVerificationState({ 60 60 profile, 61 61 }) 62 62 63 63 if (shouldShowVerificationCheckButton(state)) { 64 - return <Badge profile={profile} verificationState={state} size={size} /> 64 + return <Badge profile={profile} verificationState={state} width={width} /> 65 65 } 66 66 67 67 return null 68 68 } 69 69 70 - export function Badge({ 70 + function Badge({ 71 71 profile, 72 72 verificationState: state, 73 - size, 73 + width, 74 74 }: { 75 75 profile: Shadow<bsky.profile.AnyProfileView> 76 76 verificationState: FullVerificationState 77 - size: 'lg' | 'md' | 'sm' 77 + width: number 78 78 }) { 79 79 const t = useTheme() 80 80 const ax = useAnalytics() 81 81 const {_} = useLingui() 82 82 const verificationsDialogControl = useDialogControl() 83 83 const verifierDialogControl = useDialogControl() 84 - const {gtPhone} = useBreakpoints() 85 - let dimensions = 12 86 - if (size === 'lg') { 87 - dimensions = gtPhone ? 20 : 18 88 - } else if (size === 'md') { 89 - dimensions = 14 90 - } 91 84 92 85 const verifiedByHidden = !state.profile.showBadge && state.profile.isViewer 93 86 ··· 116 109 a.align_end, 117 110 a.transition_transform, 118 111 { 119 - width: dimensions, 120 - height: dimensions, 112 + width: width, 113 + height: width, 121 114 transform: [ 122 115 { 123 116 scale: hovered ? 1.1 : 1, ··· 126 119 }, 127 120 ]}> 128 121 <VerificationCheck 129 - width={dimensions} 122 + width={width} 130 123 fill={ 131 124 verifiedByHidden 132 125 ? t.atoms.bg_contrast_100.backgroundColor
+1
src/lib/routes/types.ts
··· 50 50 AccessibilitySettings: undefined 51 51 AppearanceSettings: undefined 52 52 AccountSettings: undefined 53 + AutomationLabelSettings: undefined 53 54 PrivacyAndSecuritySettings: undefined 54 55 ActivityPrivacySettings: undefined 55 56 ContentAndMediaSettings: undefined
+1
src/routes.ts
··· 50 50 AppearanceSettings: '/settings/appearance', 51 51 SavedFeeds: '/settings/saved-feeds', 52 52 AccountSettings: '/settings/account', 53 + AutomationLabelSettings: '/settings/automation-label', 53 54 PrivacyAndSecuritySettings: '/settings/privacy-and-security', 54 55 ActivityPrivacySettings: '/settings/privacy-and-security/activity', 55 56 ContentAndMediaSettings: '/settings/content-and-media',
+6 -13
src/screens/Messages/components/ChatListItem.tsx
··· 41 41 import {useMenuControl} from '#/components/Menu' 42 42 import {PostAlerts} from '#/components/moderation/PostAlerts' 43 43 import {createPortalGroup} from '#/components/Portal' 44 + import {ProfileBadges} from '#/components/ProfileBadges' 44 45 import {Text} from '#/components/Typography' 45 - import {useSimpleVerificationState} from '#/components/verification' 46 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 47 46 import {useAnalytics} from '#/analytics' 48 47 import {IS_NATIVE} from '#/env' 49 48 import type * as bsky from '#/types/bsky' ··· 112 111 const playHaptic = useHaptics() 113 112 const queryClient = useQueryClient() 114 113 const isUnread = convo.unreadCount > 0 115 - const verification = useSimpleVerificationState({ 116 - profile, 117 - }) 118 114 119 115 const blockInfo = useMemo(() => { 120 116 const modui = moderation.ui('profileView') ··· 414 410 {displayName} 415 411 </Text> 416 412 </View> 417 - {verification.showBadge && ( 418 - <View style={[a.pl_xs, a.self_center]}> 419 - <VerificationCheck 420 - width={14} 421 - verifier={verification.role === 'verifier'} 422 - /> 423 - </View> 424 - )} 413 + <ProfileBadges 414 + profile={profile} 415 + size="md" 416 + style={[a.pl_xs, a.self_center]} 417 + /> 425 418 {lastMessageSentAt && ( 426 419 <View style={[a.pl_xs]}> 427 420 <TimeElapsed timestamp={lastMessageSentAt}>
+6 -2
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 47 47 import {TranslatedPost} from '#/components/Post/Translated' 48 48 import {PostControls, PostControlsSkeleton} from '#/components/PostControls' 49 49 import {useFormatPostStatCount} from '#/components/PostControls/util' 50 + import {ProfileBadges} from '#/components/ProfileBadges' 50 51 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 51 52 import * as Prompt from '#/components/Prompt' 52 53 import {RichText} from '#/components/RichText' 53 54 import * as Skele from '#/components/Skeleton' 54 55 import {Text} from '#/components/Typography' 55 - import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 56 56 import {WhoCanReply} from '#/components/WhoCanReply' 57 57 import {useAnalytics} from '#/analytics' 58 58 import {useActorStatus} from '#/features/liveNow' ··· 362 362 </Text> 363 363 364 364 <View style={[a.pl_xs]}> 365 - <VerificationCheckButton profile={authorShadow} size="md" /> 365 + <ProfileBadges 366 + profile={authorShadow} 367 + size="md" 368 + interactive 369 + /> 366 370 </View> 367 371 </View> 368 372 <Text
+2 -2
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 33 33 KnownFollowers, 34 34 shouldShowKnownFollowers, 35 35 } from '#/components/KnownFollowers' 36 + import {ProfileBadges} from '#/components/ProfileBadges' 36 37 import * as Prompt from '#/components/Prompt' 37 38 import {RichText} from '#/components/RichText' 38 39 import * as Toast from '#/components/Toast' 39 40 import {Text} from '#/components/Typography' 40 - import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 41 41 import {IS_IOS} from '#/env' 42 42 import {useActorStatus} from '#/features/liveNow' 43 43 import {GermButton} from '../components/GermButton' ··· 143 143 moderation.ui('displayName'), 144 144 )} 145 145 <View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}> 146 - <VerificationCheckButton profile={profile} size="lg" /> 146 + <ProfileBadges profile={profile} size="lg" interactive /> 147 147 </View> 148 148 </Text> 149 149 </View>
+2 -11
src/screens/Search/components/SearchHistory.tsx
··· 14 14 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 15 15 import * as Layout from '#/components/Layout' 16 16 import {Link} from '#/components/Link' 17 + import {ProfileBadges} from '#/components/ProfileBadges' 17 18 import {Text} from '#/components/Typography' 18 - import {useSimpleVerificationState} from '#/components/verification' 19 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 20 19 import {useAnalytics} from '#/analytics' 21 20 import type * as bsky from '#/types/bsky' 22 21 ··· 139 138 profile.displayName || sanitizeHandle(profile.handle), 140 139 moderation.ui('displayName'), 141 140 ) 142 - const verification = useSimpleVerificationState({profile}) 143 141 144 142 return ( 145 143 <View style={[a.relative]}> ··· 165 163 <Text emoji style={[a.text_xs, a.leading_snug]} numberOfLines={1}> 166 164 {name} 167 165 </Text> 168 - {verification.showBadge && ( 169 - <View style={[a.pl_2xs]}> 170 - <VerificationCheck 171 - width={10} 172 - verifier={verification.role === 'verifier'} 173 - /> 174 - </View> 175 - )} 166 + <ProfileBadges profile={profile} size="xs" style={[a.pl_xs]} /> 176 167 </View> 177 168 </Link> 178 169 <Button
+17
src/screens/Settings/AccountSettings.tsx
··· 4 4 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 5 5 6 6 import {type CommonNavigatorParams} from '#/lib/routes/types' 7 + import {useProfileQuery} from '#/state/queries/profile' 7 8 import {useSession} from '#/state/session' 8 9 import * as SettingsList from '#/screens/Settings/components/SettingsList' 9 10 import {atoms as a, useTheme} from '#/alf' 10 11 import {AgeAssuranceAccountCard} from '#/components/ageAssurance/AgeAssuranceAccountCard' 12 + import {isBotAccount} from '#/components/BotBadge' 11 13 import {useDialogControl} from '#/components/Dialog' 12 14 import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' 13 15 import { ··· 16 18 } from '#/components/dialogs/EmailDialog' 17 19 import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' 18 20 import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake' 21 + import {Bot_Stroke as RobotIcon} from '#/components/icons/Bot' 19 22 import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car' 20 23 import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' 21 24 import {Freeze_Stroke2_Corner2_Rounded as FreezeIcon} from '#/components/icons/Freeze' ··· 35 38 const t = useTheme() 36 39 const {_} = useLingui() 37 40 const {currentAccount} = useSession() 41 + const {data: profile} = useProfileQuery({did: currentAccount?.did}) 38 42 const emailDialogControl = useEmailDialogControl() 39 43 const birthdayControl = useDialogControl() 40 44 const changeHandleControl = useDialogControl() ··· 148 152 /> 149 153 </SettingsList.Item> 150 154 <AgeAssuranceAccountCard style={[a.px_xl, a.pt_xs, a.pb_md]} /> 155 + <SettingsList.LinkItem 156 + to="/settings/automation-label" 157 + label={_(msg`Automation label`)}> 158 + <SettingsList.ItemIcon icon={RobotIcon} /> 159 + <SettingsList.ItemText> 160 + <Trans>Automation label</Trans> 161 + </SettingsList.ItemText> 162 + {profile && ( 163 + <SettingsList.BadgeText> 164 + {isBotAccount(profile) ? _(msg`On`) : _(msg`Off`)} 165 + </SettingsList.BadgeText> 166 + )} 167 + </SettingsList.LinkItem> 151 168 <SettingsList.Divider /> 152 169 <SettingsList.PressableItem 153 170 label={_(msg`Export my data`)}
+202
src/screens/Settings/AutomationLabelSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {type $Typed, ComAtprotoLabelDefs} from '@atproto/api' 3 + import {Trans, useLingui} from '@lingui/react/macro' 4 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 5 + import {useQueryClient} from '@tanstack/react-query' 6 + 7 + import {type CommonNavigatorParams} from '#/lib/routes/types' 8 + import {RQKEY_ROOT as POST_FEED_RQKEY_ROOT} from '#/state/queries/post-feed' 9 + import { 10 + useProfileQuery, 11 + useProfileUpdateMutation, 12 + } from '#/state/queries/profile' 13 + import {postThreadQueryKeyRoot} from '#/state/queries/usePostThread/types' 14 + import {useSession} from '#/state/session' 15 + import {UserAvatar} from '#/view/com/util/UserAvatar' 16 + import {atoms as a, platform, useTheme} from '#/alf' 17 + import {BotBadge} from '#/components/BotBadge' 18 + import * as Toggle from '#/components/forms/Toggle' 19 + import {Bot_Filled as RobotIcon} from '#/components/icons/Bot' 20 + import * as Layout from '#/components/Layout' 21 + import {Text} from '#/components/Typography' 22 + import {useSimpleVerificationState} from '#/components/verification' 23 + import {VerificationCheck} from '#/components/verification/VerificationCheck' 24 + import {useAnalytics} from '#/analytics' 25 + import * as bsky from '#/types/bsky' 26 + 27 + type Props = NativeStackScreenProps< 28 + CommonNavigatorParams, 29 + 'AutomationLabelSettings' 30 + > 31 + export function AutomationLabelSettingsScreen({}: Props) { 32 + const t = useTheme() 33 + const ax = useAnalytics() 34 + const {t: l} = useLingui() 35 + const queryClient = useQueryClient() 36 + const {currentAccount} = useSession() 37 + const {data: profile} = useProfileQuery({did: currentAccount?.did}) 38 + const updateProfile = useProfileUpdateMutation() 39 + const verification = useSimpleVerificationState({profile}) 40 + 41 + const isBotLabeled = 42 + profile?.labels?.some(l => l.val === 'bot' && l.src === profile.did) ?? 43 + false 44 + const canToggle = profile && !updateProfile.isPending 45 + 46 + const onToggle = () => { 47 + if (!profile) { 48 + return 49 + } 50 + let wasAdded = false 51 + ax.metric('bot:label:toggle', {state: isBotLabeled ? 'remove' : 'add'}) 52 + updateProfile.mutate( 53 + { 54 + profile, 55 + updates: existing => { 56 + const labels: $Typed<ComAtprotoLabelDefs.SelfLabels> = bsky.validate( 57 + existing.labels, 58 + ComAtprotoLabelDefs.validateSelfLabels, 59 + ) 60 + ? existing.labels 61 + : { 62 + $type: 'com.atproto.label.defs#selfLabels', 63 + values: [], 64 + } 65 + 66 + const hasLabel = labels.values.some(l => l.val === 'bot') 67 + if (hasLabel) { 68 + wasAdded = false 69 + labels.values = labels.values.filter(l => l.val !== 'bot') 70 + } else { 71 + wasAdded = true 72 + labels.values.push({val: 'bot'}) 73 + } 74 + 75 + if (labels.values.length === 0) { 76 + delete existing.labels 77 + } else { 78 + existing.labels = labels 79 + } 80 + 81 + return existing 82 + }, 83 + checkCommitted: res => { 84 + const exists = !!res.data.labels?.some(l => l.val === 'bot') 85 + return exists === wasAdded 86 + }, 87 + }, 88 + { 89 + onSuccess() { 90 + queryClient.invalidateQueries({queryKey: [POST_FEED_RQKEY_ROOT]}) 91 + queryClient.invalidateQueries({queryKey: [postThreadQueryKeyRoot]}) 92 + }, 93 + }, 94 + ) 95 + } 96 + 97 + return ( 98 + <Layout.Screen> 99 + <Layout.Header.Outer> 100 + <Layout.Header.BackButton /> 101 + <Layout.Header.Content> 102 + <Layout.Header.TitleText> 103 + <Trans>Automation Label</Trans> 104 + </Layout.Header.TitleText> 105 + </Layout.Header.Content> 106 + <Layout.Header.Slot /> 107 + </Layout.Header.Outer> 108 + <Layout.Content> 109 + <View style={[a.p_xl, a.gap_xl]}> 110 + {profile && ( 111 + <View 112 + style={[ 113 + a.flex_row, 114 + a.justify_center, 115 + a.align_center, 116 + a.gap_sm, 117 + a.rounded_lg, 118 + a.border, 119 + t.atoms.bg_contrast_50, 120 + t.atoms.border_contrast_low, 121 + { 122 + height: 160, 123 + paddingRight: 20, // helps visually center 124 + }, 125 + ]}> 126 + <UserAvatar size={42} avatar={profile.avatar} type="user" /> 127 + <View> 128 + <View style={[a.flex_row, a.align_baseline]}> 129 + <View style={[a.flex_row, a.align_center, a.gap_xs]}> 130 + <Text 131 + emoji 132 + style={[ 133 + a.text_xl, 134 + a.font_semi_bold, 135 + a.flex_shrink, 136 + a.leading_tight, 137 + ]} 138 + numberOfLines={1}> 139 + {profile.displayName || profile.handle} 140 + </Text> 141 + {verification.isVerified && ( 142 + <VerificationCheck 143 + verifier={verification.role === 'verifier'} 144 + size="sm" 145 + /> 146 + )} 147 + <View style={{top: platform({ios: -1})}}> 148 + <BotBadge profile={profile} alwaysShow width={17} /> 149 + </View> 150 + </View> 151 + </View> 152 + <Text 153 + style={[ 154 + a.text_md, 155 + a.leading_snug, 156 + t.atoms.text_contrast_medium, 157 + ]} 158 + numberOfLines={1}> 159 + @{profile.handle} 160 + </Text> 161 + </View> 162 + </View> 163 + )} 164 + <View style={[a.gap_sm]}> 165 + <Text style={[a.text_2xl, a.font_bold]}> 166 + <Trans>Add automation label to account</Trans> 167 + </Text> 168 + <Text style={[a.text_md, a.leading_snug]}> 169 + <Trans> 170 + This label lets the world know that this account is automated. 171 + If turned on, this label appears next to the account's name on 172 + their profile and posts. It can be turned on or off at any time. 173 + </Trans> 174 + </Text> 175 + </View> 176 + <Toggle.Item 177 + name="automation_label" 178 + disabled={!canToggle || updateProfile.isPending} 179 + value={isBotLabeled} 180 + onChange={onToggle} 181 + label={l`Show automation label`} 182 + style={[ 183 + a.w_full, 184 + a.p_md, 185 + a.rounded_lg, 186 + a.border, 187 + t.atoms.border_contrast_low, 188 + t.atoms.bg_contrast_50, 189 + ]}> 190 + <View style={[a.pr_xs]}> 191 + <RobotIcon width={24} fill={t.atoms.text_contrast_medium.color} /> 192 + </View> 193 + <Toggle.LabelText style={[a.flex_1, a.text_md, a.font_medium]}> 194 + <Trans>Show automation label</Trans> 195 + </Toggle.LabelText> 196 + <Toggle.Platform /> 197 + </Toggle.Item> 198 + </View> 199 + </Layout.Content> 200 + </Layout.Screen> 201 + ) 202 + }
+11 -18
src/screens/Settings/Settings.tsx
··· 61 61 import {Loader} from '#/components/Loader' 62 62 import * as Menu from '#/components/Menu' 63 63 import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' 64 + import {ProfileBadges} from '#/components/ProfileBadges' 64 65 import * as Prompt from '#/components/Prompt' 65 66 import {Text} from '#/components/Typography' 66 - import {useFullVerificationState} from '#/components/verification' 67 - import { 68 - shouldShowVerificationCheckButton, 69 - VerificationCheckButton, 70 - } from '#/components/verification/VerificationCheckButton' 71 67 import {useAnalytics} from '#/analytics' 72 68 import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env' 73 69 import {useActorStatus} from '#/features/liveNow' ··· 321 317 const {gtMobile} = useBreakpoints() 322 318 const shadow = useProfileShadow(profile) 323 319 const moderationOpts = useModerationOpts() 324 - const verificationState = useFullVerificationState({ 325 - profile: shadow, 326 - }) 327 320 const {isActive: live} = useActorStatus(profile) 328 321 329 322 if (!moderationOpts) return null ··· 364 357 ]}> 365 358 {displayName} 366 359 </Text> 367 - {shouldShowVerificationCheckButton(verificationState) && ( 368 - <View 369 - style={[ 370 - { 371 - marginTop: platform({web: 8, ios: 8, android: 10}), 372 - }, 373 - ]}> 374 - <VerificationCheckButton profile={shadow} size="lg" /> 375 - </View> 376 - )} 360 + <ProfileBadges 361 + profile={shadow} 362 + size="xl" 363 + interactive 364 + style={[ 365 + { 366 + marginTop: platform({web: 8, ios: 8, android: 10}), 367 + }, 368 + ]} 369 + /> 377 370 </View> 378 371 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}> 379 372 {sanitizeHandle(profile.handle, '@')}
+2 -12
src/view/com/composer/ComposerReplyTo.tsx
··· 16 16 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 17 17 import {atoms as a, useTheme, web} from '#/alf' 18 18 import {QuoteEmbed} from '#/components/Post/Embed' 19 + import {ProfileBadges} from '#/components/ProfileBadges' 19 20 import {Text} from '#/components/Typography' 20 - import {useSimpleVerificationState} from '#/components/verification' 21 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 22 21 import {parseEmbed} from '#/types/bsky/post' 23 22 24 23 export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { ··· 70 69 } 71 70 }, [embed]) 72 71 73 - const verification = useSimpleVerificationState({profile: replyTo.author}) 74 - 75 72 return ( 76 73 <Pressable 77 74 style={[ ··· 109 106 sanitizeHandle(replyTo.author.handle), 110 107 )} 111 108 </Text> 112 - {verification.showBadge && ( 113 - <View style={[a.pl_xs]}> 114 - <VerificationCheck 115 - width={14} 116 - verifier={verification.role === 'verifier'} 117 - /> 118 - </View> 119 - )} 109 + <ProfileBadges profile={replyTo.author} size="sm" style={[a.pl_xs]} /> 120 110 </View> 121 111 <View style={[a.flex_row, a.gap_md]}> 122 112 <View style={[a.flex_1, a.flex_grow]}>
+10 -16
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 {ProfileBadges} from '#/components/ProfileBadges' 12 13 import {Text} from '#/components/Typography' 13 - import {useSimpleVerificationState} from '#/components/verification' 14 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 15 14 16 15 export function Autocomplete({ 17 16 prefix, ··· 77 76 onPress: () => void 78 77 }) { 79 78 const t = useTheme() 80 - const state = useSimpleVerificationState({profile}) 81 79 const displayName = sanitizeDisplayName( 82 80 profile.displayName || sanitizeHandle(profile.handle), 83 81 ) ··· 115 113 numberOfLines={1}> 116 114 {displayName} 117 115 </Text> 118 - {state.isVerified && ( 119 - <View 120 - style={[ 121 - { 122 - marginTop: platform({android: -2}), 123 - }, 124 - ]}> 125 - <VerificationCheck 126 - width={12} 127 - verifier={state.role === 'verifier'} 128 - /> 129 - </View> 130 - )} 116 + <ProfileBadges 117 + profile={profile} 118 + size="sm" 119 + style={[ 120 + { 121 + marginTop: platform({android: -2}), 122 + }, 123 + ]} 124 + /> 131 125 </View> 132 126 </View> 133 127 <Text
+21 -34
src/view/com/notifications/NotificationFeedItem.tsx
··· 63 63 import {VerifiedCheck} from '#/components/icons/VerifiedCheck' 64 64 import {InlineLinkText, Link} from '#/components/Link' 65 65 import * as MediaPreview from '#/components/MediaPreview' 66 + import {ProfileBadges} from '#/components/ProfileBadges' 66 67 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 67 68 import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 68 69 import {SubtleHover} from '#/components/SubtleHover' 69 70 import {Text} from '#/components/Typography' 70 - import {useSimpleVerificationState} from '#/components/verification' 71 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 72 71 import * as bsky from '#/types/bsky' 73 72 74 73 const MAX_AUTHORS = 5 ··· 173 172 174 173 const niceTimestamp = niceDate(i18n, item.notification.indexedAt) 175 174 const firstAuthor = authors[0] 176 - const firstAuthorVerification = useSimpleVerificationState({ 177 - profile: firstAuthor.profile, 178 - }) 179 175 const firstAuthorName = sanitizeDisplayName( 180 176 firstAuthor.profile.displayName || firstAuthor.profile.handle, 181 177 ) ··· 246 242 emoji 247 243 label={_(msg`Go to ${firstAuthorName}'s profile`)}> 248 244 {forceLTR(firstAuthorName)} 249 - {firstAuthorVerification.showBadge && ( 250 - <View 251 - style={[ 252 - a.relative, 253 - { 254 - paddingTop: platform({android: 2}), 255 - marginBottom: platform({ios: -7}), 256 - top: platform({web: 1}), 257 - paddingLeft: 3, 258 - paddingRight: 2, 259 - }, 260 - ]}> 261 - <VerificationCheck 262 - width={14} 263 - verifier={firstAuthorVerification.role === 'verifier'} 264 - /> 265 - </View> 266 - )} 245 + <ProfileBadges 246 + profile={firstAuthor.profile} 247 + size="md" 248 + style={[ 249 + a.relative, 250 + { 251 + // weird stuff here 252 + paddingTop: platform({android: 2}), 253 + marginBottom: platform({ios: -6}), 254 + top: platform({web: 2}), 255 + paddingLeft: 3, 256 + paddingRight: 2, 257 + }, 258 + ]} 259 + /> 267 260 </InlineLinkText> 268 261 </ProfileHoverCard> 269 262 ) ··· 1017 1010 function ExpandedAuthorCard({author}: {author: Author}) { 1018 1011 const t = useTheme() 1019 1012 const {_} = useLingui() 1020 - const verification = useSimpleVerificationState({ 1021 - profile: author.profile, 1022 - }) 1023 1013 return ( 1024 1014 <Link 1025 1015 key={author.profile.did} ··· 1055 1045 author.profile.displayName || author.profile.handle, 1056 1046 )} 1057 1047 </Text> 1058 - {verification.showBadge && ( 1059 - <View style={[a.pl_xs, a.self_center]}> 1060 - <VerificationCheck 1061 - width={14} 1062 - verifier={verification.role === 'verifier'} 1063 - /> 1064 - </View> 1065 - )} 1048 + <ProfileBadges 1049 + profile={author.profile} 1050 + size="md" 1051 + style={[a.pl_2xs, a.self_center]} 1052 + /> 1066 1053 <Text 1067 1054 numberOfLines={1} 1068 1055 style={[
+7 -19
src/view/com/util/PostMeta.tsx
··· 13 13 import {niceDate} from '#/lib/strings/time' 14 14 import {useProfileShadow} from '#/state/cache/profile-shadow' 15 15 import {unstableCacheProfileView} from '#/state/queries/profile' 16 - import {atoms as a, platform, useTheme, web} from '#/alf' 16 + import {atoms as a, useTheme, web} from '#/alf' 17 17 import {WebOnlyInlineLinkText} from '#/components/Link' 18 + import {ProfileBadges} from '#/components/ProfileBadges' 18 19 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 19 20 import {Text} from '#/components/Typography' 20 - import {useSimpleVerificationState} from '#/components/verification' 21 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 22 21 import {IS_ANDROID} from '#/env' 23 22 import {useActorStatus} from '#/features/liveNow' 24 23 import {TimeElapsed} from './TimeElapsed' ··· 55 54 }, [queryClient, author]) 56 55 57 56 const timestampLabel = niceDate(i18n, opts.timestamp) 58 - const verification = useSimpleVerificationState({profile: author}) 59 57 const {isActive: live} = useActorStatus(author) 60 58 61 59 const MaybeLinkText = opts.linkDisabled ? Text : WebOnlyInlineLinkText ··· 109 107 ), 110 108 )} 111 109 </MaybeLinkText> 112 - {verification.showBadge && ( 113 - <View 114 - style={[ 115 - a.pl_2xs, 116 - a.self_center, 117 - { 118 - marginTop: platform({web: 0, ios: 0, android: -1}), 119 - }, 120 - ]}> 121 - <VerificationCheck 122 - width={platform({android: 13, default: 12})} 123 - verifier={verification.role === 'verifier'} 124 - /> 125 - </View> 126 - )} 110 + <ProfileBadges 111 + profile={author} 112 + size="sm" 113 + style={[a.pl_2xs, a.self_center]} 114 + /> 127 115 <MaybeLinkText 128 116 emoji 129 117 numberOfLines={1}
+2 -14
src/view/shell/Drawer.tsx
··· 53 53 UserCircle_Stroke2_Corner0_Rounded as UserCircle, 54 54 } from '#/components/icons/UserCircle' 55 55 import {InlineLinkText} from '#/components/Link' 56 + import {ProfileBadges} from '#/components/ProfileBadges' 56 57 import {Text} from '#/components/Typography' 57 - import {useSimpleVerificationState} from '#/components/verification' 58 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 59 58 import {IS_WEB} from '#/env' 60 59 import {useActorStatus} from '#/features/liveNow' 61 60 ··· 71 70 const {_, i18n} = useLingui() 72 71 const t = useTheme() 73 72 const {data: profile} = useProfileQuery({did: account.did}) 74 - const verification = useSimpleVerificationState({profile}) 75 73 const {isActive: live} = useActorStatus(profile) 76 74 77 75 return ( ··· 97 95 numberOfLines={1}> 98 96 {profile?.displayName || account.handle} 99 97 </Text> 100 - {verification.showBadge && ( 101 - <View 102 - style={{ 103 - top: 0, 104 - }}> 105 - <VerificationCheck 106 - width={16} 107 - verifier={verification.role === 'verifier'} 108 - /> 109 - </View> 110 - )} 98 + {profile && <ProfileBadges profile={profile} size="lg" />} 111 99 </View> 112 100 <Text 113 101 emoji