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

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Spence Pope
2026-03-10 14:28:49 -04:00
committed by GitHub
parent 8bdb1c30c8
commit 2bd9811652
32 changed files with 641 additions and 235 deletions
+1
View File
@@ -0,0 +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>

After

Width:  |  Height:  |  Size: 621 B

+1
View File
@@ -0,0 +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>

After

Width:  |  Height:  |  Size: 1.3 KiB

+10
View File
@@ -10,6 +10,7 @@ import logo from '../../assets/logo_full_name.svg'
import {Like as LikeIcon} from '../icons/Like' import {Like as LikeIcon} from '../icons/Like'
import {Reply as ReplyIcon} from '../icons/Reply' import {Reply as ReplyIcon} from '../icons/Reply'
import {Repost as RepostIcon} from '../icons/Repost' import {Repost as RepostIcon} from '../icons/Repost'
import {Robot as RobotIcon} from '../icons/Robot'
import {CONTENT_LABELS} from '../labels' import {CONTENT_LABELS} from '../labels'
import * as bsky from '../types/bsky' import * as bsky from '../types/bsky'
import {niceDate} from '../util/nice-date' import {niceDate} from '../util/nice-date'
@@ -43,6 +44,9 @@ export function Post({thread}: Props) {
} }
const verification = getVerificationState({profile: post.author}) const verification = getVerificationState({profile: post.author})
const isBot = post.author.labels?.some(
l => l.val === 'bot' && l.src === post.author.did,
)
const href = `/profile/${post.author.did}/post/${getRkey(post)}` const href = `/profile/${post.author.did}/post/${getRkey(post)}`
@@ -76,6 +80,12 @@ export function Post({thread}: Props) {
size={15} size={15}
/> />
)} )}
{isBot && (
<RobotIcon
className="pl-[3px] mt-px shrink-0 text-slate-500 dark:text-slate-400"
size={15}
/>
)}
</div> </div>
<Link <Link
href={`/profile/${post.author.did}`} href={`/profile/${post.author.did}`}
+22
View File
@@ -0,0 +1,22 @@
import {h} from 'preact'
export const Robot = ({
size = 14,
className,
}: {
size?: number
className?: string
}) => (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
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"
fill="currentColor"
/>
</svg>
)
+1
View File
@@ -292,6 +292,7 @@ func serve(cctx *cli.Context) error {
e.GET("/settings/accessibility", server.WebGeneric) e.GET("/settings/accessibility", server.WebGeneric)
e.GET("/settings/appearance", server.WebGeneric) e.GET("/settings/appearance", server.WebGeneric)
e.GET("/settings/account", server.WebGeneric) e.GET("/settings/account", server.WebGeneric)
e.GET("/settings/automation-label", server.WebGeneric)
e.GET("/settings/privacy-and-security", server.WebGeneric) e.GET("/settings/privacy-and-security", server.WebGeneric)
e.GET("/settings/privacy-and-security/activity", server.WebGeneric) e.GET("/settings/privacy-and-security/activity", server.WebGeneric)
e.GET("/settings/content-and-media", server.WebGeneric) e.GET("/settings/content-and-media", server.WebGeneric)
+9
View File
@@ -103,6 +103,7 @@ import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacyS
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'
import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings'
import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings'
import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings' import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings'
@@ -403,6 +404,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
requireAuth: true, requireAuth: true,
}} }}
/> />
<Stack.Screen
name="AutomationLabelSettings"
getComponent={() => AutomationLabelSettingsScreen}
options={{
title: title(msg`Automation Label`),
requireAuth: true,
}}
/>
<Stack.Screen <Stack.Screen
name="PrivacyAndSecuritySettings" name="PrivacyAndSecuritySettings"
getComponent={() => PrivacyAndSecuritySettingsScreen} getComponent={() => PrivacyAndSecuritySettingsScreen}
+3
View File
@@ -732,6 +732,9 @@ export type Events = {
'verification:settings:hideBadges': {} 'verification:settings:hideBadges': {}
'verification:settings:unHideBadges': {} 'verification:settings:unHideBadges': {}
'bot:label:toggle': {state: 'add' | 'remove'}
'bot:badge:click': {}
'live:create': {duration: number} 'live:create': {duration: number}
'live:edit': {} 'live:edit': {}
'live:remove': {} 'live:remove': {}
+7 -10
View File
@@ -16,9 +16,8 @@ import {Button} from '#/components/Button'
import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
export function AccountList({ export function AccountList({
@@ -116,7 +115,6 @@ function AccountItem({
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const verification = useSimpleVerificationState({profile})
const {isActive: live} = useActorStatus(profile) const {isActive: live} = useActorStatus(profile)
const onPress = useCallback(() => { const onPress = useCallback(() => {
@@ -164,13 +162,12 @@ function AccountItem({
profile?.displayName || profile?.handle || account.handle, profile?.displayName || profile?.handle || account.handle,
)} )}
</Text> </Text>
{verification.showBadge && ( {profile && (
<View> <ProfileBadges
<VerificationCheck profile={profile}
width={12} size="sm"
verifier={verification.role === 'verifier'} style={[{marginTop: -2}]}
/> />
</View>
)} )}
</View> </View>
<Text <Text
+79
View File
@@ -0,0 +1,79 @@
import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {useSession} from '#/state/session'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Bot_Filled as RobotIcon} from '#/components/icons/Bot'
import {Text} from '#/components/Typography'
import {navigate} from '#/Navigation'
import type * as bsky from '#/types/bsky'
export function BotAccountAlert({
control,
profile,
}: {
control: Dialog.DialogControlProps
profile: bsky.profile.AnyProfileView
}) {
const {t: l} = useLingui()
const t = useTheme()
const {currentAccount} = useSession()
const isSelf = profile.did === currentAccount?.did
const description = isSelf
? l`You have marked this account as automated. You can remove it at any time from your account settings.`
: l`This account has been marked as automated by its owner.`
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.ScrollableInner
label={l`Automated account`}
style={[web({maxWidth: 320})]}>
<View style={[a.align_center, a.pb_md, a.shadow_sm]}>
<RobotIcon width={48} fill={t.atoms.text_contrast_medium.color} />
</View>
<View style={[a.align_center]}>
<Text
style={[
a.leading_snug,
a.text_center,
a.pb_xl,
a.text_md,
t.atoms.text_contrast_high,
{maxWidth: 300},
]}>
{description}
</Text>
</View>
<View style={[a.w_full, a.gap_sm]}>
<Button
label={l`Okay`}
onPress={() => control.close()}
color="primary"
size="large">
<ButtonText>
<Trans>Okay</Trans>
</ButtonText>
</Button>
{isSelf ? (
<Button
label={l`Open settings`}
onPress={() => {
control.close(() => {
navigate('AutomationLabelSettings')
})
}}
color="secondary"
size="large">
<ButtonText>
<Trans>Open settings</Trans>
</ButtonText>
</Button>
) : null}
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+92
View File
@@ -0,0 +1,92 @@
import {View} from 'react-native'
import {type ComAtprotoLabelDefs} from '@atproto/api'
import {useLingui} from '@lingui/react/macro'
import {atoms as a, useTheme} from '#/alf'
import {BotAccountAlert} from '#/components/BotAccountAlert'
import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {Bot_Filled as RobotIcon} from '#/components/icons/Bot'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
export function isBotAccount(profile: {
did: string
labels?: ComAtprotoLabelDefs.Label[]
}): boolean {
return (
profile.labels?.some(l => l.val === 'bot' && l.src === profile.did) ?? false
)
}
export function BotBadge({
profile,
alwaysShow = false,
width,
}: {
profile: bsky.profile.AnyProfileView
alwaysShow?: boolean
width: number
}) {
const t = useTheme()
if (!isBotAccount(profile) && !alwaysShow) {
return null
}
return (
<View>
<RobotIcon width={width} fill={t.atoms.text_contrast_medium.color} />
</View>
)
}
export function BotBadgeButton({
profile,
width,
}: {
profile: bsky.profile.AnyProfileView
width: number
}) {
const t = useTheme()
const ax = useAnalytics()
const {t: l} = useLingui()
const control = useDialogControl()
if (!isBotAccount(profile)) {
return null
}
return (
<>
<Button
label={l`Automated account`}
hitSlop={20}
onPress={evt => {
evt.preventDefault()
ax.metric('bot:badge:click', {})
control.open()
}}>
{({hovered}) => (
<View
style={[
a.justify_end,
a.align_end,
a.transition_transform,
{
width: width,
height: width,
transform: [{scale: hovered ? 1.1 : 1}],
},
]}>
<RobotIcon
width={width}
fill={t.atoms.text_contrast_medium.color}
/>
</View>
)}
</Button>
<BotAccountAlert control={control} profile={profile} />
</>
)
}
@@ -17,9 +17,8 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, tokens, useTheme} from '#/alf' import {atoms as a, tokens, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {useDialogContext} from '#/components/Dialog' import {useDialogContext} from '#/components/Dialog'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
@@ -111,7 +110,6 @@ function RecentChatItem({
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'), moderation.ui('displayName'),
) )
const verification = useSimpleVerificationState({profile})
if (isBlockedOrBlocking(profile) || isMuted(profile)) { if (isBlockedOrBlocking(profile) || isMuted(profile)) {
return null return null
@@ -141,14 +139,7 @@ function RecentChatItem({
numberOfLines={1}> numberOfLines={1}>
{name} {name}
</Text> </Text>
{verification.showBadge && ( <ProfileBadges profile={profile} size="xs" style={[a.pl_2xs]} />
<View style={[a.pl_2xs]}>
<VerificationCheck
width={10}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View> </View>
</Button> </Button>
) )
+76
View File
@@ -0,0 +1,76 @@
import {View} from 'react-native'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {atoms as a, type ViewStyleProp} from '#/alf'
import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import type * as bsky from '#/types/bsky'
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
const verificationIconSizes: Record<Size, number> = {
xs: 10,
sm: 12,
md: 14,
lg: 18,
xl: 22,
} as const
const botIconSizes: Record<Size, number> = {
xs: 11,
sm: 13,
md: 15,
lg: 19,
xl: 23,
} as const
export function ProfileBadges({
profile,
interactive = false,
size,
style,
}: ViewStyleProp & {
profile: bsky.profile.AnyProfileView
interactive?: boolean
size: Size
}) {
const shadowed = useProfileShadow(profile)
const verification = useSimpleVerificationState({profile})
// if nothing to show, don't render the container at all
if (!verification.showBadge && !isBotAccount(shadowed)) return null
const isOnTheSmallSide = size === 'xs' || size === 'sm'
return (
<View
style={[
a.flex_row,
a.align_center,
isOnTheSmallSide ? a.gap_2xs : a.gap_xs,
style,
]}>
{interactive ? (
<>
<VerificationCheckButton
profile={shadowed}
width={verificationIconSizes[size]}
/>
<BotBadgeButton profile={shadowed} width={botIconSizes[size]} />
</>
) : (
<>
{verification.showBadge && (
<VerificationCheck
verifier={verification.role === 'verifier'}
width={verificationIconSizes[size]}
/>
)}
<BotBadge profile={shadowed} width={botIconSizes[size]} />
</>
)}
</View>
)
}
+11 -25
View File
@@ -41,10 +41,9 @@ import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Link as InternalLink, type LinkProps} from '#/components/Link' import {Link as InternalLink, type LinkProps} from '#/components/Link'
import * as Pills from '#/components/Pills' import * as Pills from '#/components/Pills'
import {ProfileBadges} from '#/components/ProfileBadges'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {type Metrics} from '#/analytics' import {type Metrics} from '#/analytics'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
@@ -242,7 +241,6 @@ function InlineNameAndHandle({
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}) { }) {
const t = useTheme() const t = useTheme()
const verification = useSimpleVerificationState({profile})
const moderation = moderateProfile(profile, moderationOpts) const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName( const name = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
@@ -262,19 +260,15 @@ function InlineNameAndHandle({
numberOfLines={1}> numberOfLines={1}>
{forceLTR(name)} {forceLTR(name)}
</Text> </Text>
{verification.showBadge && ( <ProfileBadges
<View profile={profile}
style={[ size="md"
a.pl_2xs, style={[
a.self_center, a.pl_2xs,
{marginTop: platform({default: 0, android: -1})}, a.self_center,
]}> {marginTop: platform({default: 0, android: -1})},
<VerificationCheck ]}
width={platform({android: 13, default: 12})} />
verifier={verification.role === 'verifier'}
/>
</View>
)}
<Text <Text
emoji emoji
style={[ style={[
@@ -305,7 +299,6 @@ export function Name({
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'), moderation.ui('displayName'),
) )
const verification = useSimpleVerificationState({profile})
return ( return (
<View style={[a.flex_row, a.align_center, a.max_w_full, style]}> <View style={[a.flex_row, a.align_center, a.max_w_full, style]}>
<Text <Text
@@ -321,14 +314,7 @@ export function Name({
numberOfLines={1}> numberOfLines={1}>
{name} {name}
</Text> </Text>
{verification.showBadge && ( <ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
<View style={[a.pl_xs]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View> </View>
) )
} }
+11 -17
View File
@@ -36,10 +36,9 @@ import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Pills from '#/components/Pills' import * as Pills from '#/components/Pills'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {ProfileBadges} from '#/components/ProfileBadges'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {IS_WEB_TOUCH_DEVICE} from '#/env' import {IS_WEB_TOUCH_DEVICE} from '#/env'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
import {LiveStatus} from '#/features/liveNow/components/LiveStatusDialog' import {LiveStatus} from '#/features/liveNow/components/LiveStatusDialog'
@@ -459,7 +458,6 @@ function Inner({
[currentAccount, profile], [currentAccount, profile],
) )
const isLabeler = profile.associated?.labeler const isLabeler = profile.associated?.labeler
const verification = useSimpleVerificationState({profile})
return ( return (
<View> <View>
@@ -527,20 +525,16 @@ function Inner({
moderation.ui('displayName'), moderation.ui('displayName'),
)} )}
</Text> </Text>
{verification.showBadge && ( <ProfileBadges
<View profile={profile}
style={[ size="md"
a.pl_xs, style={[
{ a.pl_xs,
marginTop: -2, {
}, marginTop: -1,
]}> },
<VerificationCheck ]}
width={16} />
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View> </View>
<ProfileHeaderHandle profile={profileShadow} disableTaps /> <ProfileHeaderHandle profile={profileShadow} disableTaps />
+2 -13
View File
@@ -20,9 +20,8 @@ import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link' import {Link} from '#/components/Link'
import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostAlerts} from '#/components/moderation/PostAlerts'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
@@ -112,9 +111,6 @@ function HeaderReady({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const convoState = useConvo() const convoState = useConvo()
const verification = useSimpleVerificationState({
profile,
})
const isDeletedAccount = profile?.handle === 'missing.invalid' const isDeletedAccount = profile?.handle === 'missing.invalid'
const displayName = isDeletedAccount const displayName = isDeletedAccount
@@ -161,14 +157,7 @@ function HeaderReady({
numberOfLines={1}> numberOfLines={1}>
{displayName} {displayName}
</Text> </Text>
{verification.showBadge && ( <ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
<View style={[a.pl_xs]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View> </View>
{!isDeletedAccount && ( {!isDeletedAccount && (
<Text <Text
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Bot_Stroke = createSinglePathSVG({
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',
})
export const Bot_Filled = createSinglePathSVG({
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',
})
+5 -1
View File
@@ -36,7 +36,11 @@ export function LabelsOnMe({
if (!labels || !currentAccount) { if (!labels || !currentAccount) {
return null return null
} }
labels = labels.filter(l => !l.val.startsWith('!')) labels = labels.filter(
l =>
!l.val.startsWith('!') &&
!(l.val === 'bot' && l.src === currentAccount.did),
)
if (!labels.length) { if (!labels.length) {
return null return null
} }
@@ -3,7 +3,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {type Shadow} from '#/state/cache/types' import {type Shadow} from '#/state/cache/types'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {useFullVerificationState} from '#/components/verification' import {useFullVerificationState} from '#/components/verification'
@@ -51,43 +51,36 @@ export function shouldShowVerificationCheckButton(
export function VerificationCheckButton({ export function VerificationCheckButton({
profile, profile,
size, width,
}: { }: {
profile: Shadow<bsky.profile.AnyProfileView> profile: Shadow<bsky.profile.AnyProfileView>
size: 'lg' | 'md' | 'sm' width: number
}) { }) {
const state = useFullVerificationState({ const state = useFullVerificationState({
profile, profile,
}) })
if (shouldShowVerificationCheckButton(state)) { if (shouldShowVerificationCheckButton(state)) {
return <Badge profile={profile} verificationState={state} size={size} /> return <Badge profile={profile} verificationState={state} width={width} />
} }
return null return null
} }
export function Badge({ function Badge({
profile, profile,
verificationState: state, verificationState: state,
size, width,
}: { }: {
profile: Shadow<bsky.profile.AnyProfileView> profile: Shadow<bsky.profile.AnyProfileView>
verificationState: FullVerificationState verificationState: FullVerificationState
size: 'lg' | 'md' | 'sm' width: number
}) { }) {
const t = useTheme() const t = useTheme()
const ax = useAnalytics() const ax = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const verificationsDialogControl = useDialogControl() const verificationsDialogControl = useDialogControl()
const verifierDialogControl = useDialogControl() const verifierDialogControl = useDialogControl()
const {gtPhone} = useBreakpoints()
let dimensions = 12
if (size === 'lg') {
dimensions = gtPhone ? 20 : 18
} else if (size === 'md') {
dimensions = 14
}
const verifiedByHidden = !state.profile.showBadge && state.profile.isViewer const verifiedByHidden = !state.profile.showBadge && state.profile.isViewer
@@ -116,8 +109,8 @@ export function Badge({
a.align_end, a.align_end,
a.transition_transform, a.transition_transform,
{ {
width: dimensions, width: width,
height: dimensions, height: width,
transform: [ transform: [
{ {
scale: hovered ? 1.1 : 1, scale: hovered ? 1.1 : 1,
@@ -126,7 +119,7 @@ export function Badge({
}, },
]}> ]}>
<VerificationCheck <VerificationCheck
width={dimensions} width={width}
fill={ fill={
verifiedByHidden verifiedByHidden
? t.atoms.bg_contrast_100.backgroundColor ? t.atoms.bg_contrast_100.backgroundColor
+1
View File
@@ -50,6 +50,7 @@ export type CommonNavigatorParams = {
AccessibilitySettings: undefined AccessibilitySettings: undefined
AppearanceSettings: undefined AppearanceSettings: undefined
AccountSettings: undefined AccountSettings: undefined
AutomationLabelSettings: undefined
PrivacyAndSecuritySettings: undefined PrivacyAndSecuritySettings: undefined
ActivityPrivacySettings: undefined ActivityPrivacySettings: undefined
ContentAndMediaSettings: undefined ContentAndMediaSettings: undefined
+1
View File
@@ -50,6 +50,7 @@ export const router = new Router<AllNavigatableRoutes>({
AppearanceSettings: '/settings/appearance', AppearanceSettings: '/settings/appearance',
SavedFeeds: '/settings/saved-feeds', SavedFeeds: '/settings/saved-feeds',
AccountSettings: '/settings/account', AccountSettings: '/settings/account',
AutomationLabelSettings: '/settings/automation-label',
PrivacyAndSecuritySettings: '/settings/privacy-and-security', PrivacyAndSecuritySettings: '/settings/privacy-and-security',
ActivityPrivacySettings: '/settings/privacy-and-security/activity', ActivityPrivacySettings: '/settings/privacy-and-security/activity',
ContentAndMediaSettings: '/settings/content-and-media', ContentAndMediaSettings: '/settings/content-and-media',
@@ -41,9 +41,8 @@ import {Link} from '#/components/Link'
import {useMenuControl} from '#/components/Menu' import {useMenuControl} from '#/components/Menu'
import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostAlerts} from '#/components/moderation/PostAlerts'
import {createPortalGroup} from '#/components/Portal' import {createPortalGroup} from '#/components/Portal'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
@@ -112,9 +111,6 @@ function ChatListItemReady({
const playHaptic = useHaptics() const playHaptic = useHaptics()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const isUnread = convo.unreadCount > 0 const isUnread = convo.unreadCount > 0
const verification = useSimpleVerificationState({
profile,
})
const blockInfo = useMemo(() => { const blockInfo = useMemo(() => {
const modui = moderation.ui('profileView') const modui = moderation.ui('profileView')
@@ -414,14 +410,11 @@ function ChatListItemReady({
{displayName} {displayName}
</Text> </Text>
</View> </View>
{verification.showBadge && ( <ProfileBadges
<View style={[a.pl_xs, a.self_center]}> profile={profile}
<VerificationCheck size="md"
width={14} style={[a.pl_xs, a.self_center]}
verifier={verification.role === 'verifier'} />
/>
</View>
)}
{lastMessageSentAt && ( {lastMessageSentAt && (
<View style={[a.pl_xs]}> <View style={[a.pl_xs]}>
<TimeElapsed timestamp={lastMessageSentAt}> <TimeElapsed timestamp={lastMessageSentAt}>
@@ -47,12 +47,12 @@ import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
import {TranslatedPost} from '#/components/Post/Translated' import {TranslatedPost} from '#/components/Post/Translated'
import {PostControls, PostControlsSkeleton} from '#/components/PostControls' import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
import {useFormatPostStatCount} from '#/components/PostControls/util' import {useFormatPostStatCount} from '#/components/PostControls/util'
import {ProfileBadges} from '#/components/ProfileBadges'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import * as Skele from '#/components/Skeleton' import * as Skele from '#/components/Skeleton'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {WhoCanReply} from '#/components/WhoCanReply' import {WhoCanReply} from '#/components/WhoCanReply'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
@@ -362,7 +362,11 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
</Text> </Text>
<View style={[a.pl_xs]}> <View style={[a.pl_xs]}>
<VerificationCheckButton profile={authorShadow} size="md" /> <ProfileBadges
profile={authorShadow}
size="md"
interactive
/>
</View> </View>
</View> </View>
<Text <Text
@@ -33,11 +33,11 @@ import {
KnownFollowers, KnownFollowers,
shouldShowKnownFollowers, shouldShowKnownFollowers,
} from '#/components/KnownFollowers' } from '#/components/KnownFollowers'
import {ProfileBadges} from '#/components/ProfileBadges'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {IS_IOS} from '#/env' import {IS_IOS} from '#/env'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
import {GermButton} from '../components/GermButton' import {GermButton} from '../components/GermButton'
@@ -143,7 +143,7 @@ let ProfileHeaderStandard = ({
moderation.ui('displayName'), moderation.ui('displayName'),
)} )}
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}> <View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
<VerificationCheckButton profile={profile} size="lg" /> <ProfileBadges profile={profile} size="lg" interactive />
</View> </View>
</Text> </Text>
</View> </View>
@@ -14,9 +14,8 @@ import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link' import {Link} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
@@ -139,7 +138,6 @@ function RecentProfileItem({
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'), moderation.ui('displayName'),
) )
const verification = useSimpleVerificationState({profile})
return ( return (
<View style={[a.relative]}> <View style={[a.relative]}>
@@ -165,14 +163,7 @@ function RecentProfileItem({
<Text emoji style={[a.text_xs, a.leading_snug]} numberOfLines={1}> <Text emoji style={[a.text_xs, a.leading_snug]} numberOfLines={1}>
{name} {name}
</Text> </Text>
{verification.showBadge && ( <ProfileBadges profile={profile} size="xs" style={[a.pl_xs]} />
<View style={[a.pl_2xs]}>
<VerificationCheck
width={10}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View> </View>
</Link> </Link>
<Button <Button
+17
View File
@@ -4,10 +4,12 @@ import {Trans} from '@lingui/react/macro'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {type CommonNavigatorParams} from '#/lib/routes/types' import {type CommonNavigatorParams} from '#/lib/routes/types'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import * as SettingsList from '#/screens/Settings/components/SettingsList' import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {AgeAssuranceAccountCard} from '#/components/ageAssurance/AgeAssuranceAccountCard' import {AgeAssuranceAccountCard} from '#/components/ageAssurance/AgeAssuranceAccountCard'
import {isBotAccount} from '#/components/BotBadge'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import { import {
@@ -16,6 +18,7 @@ import {
} from '#/components/dialogs/EmailDialog' } from '#/components/dialogs/EmailDialog'
import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake' import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake'
import {Bot_Stroke as RobotIcon} from '#/components/icons/Bot'
import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car' import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car'
import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
import {Freeze_Stroke2_Corner2_Rounded as FreezeIcon} from '#/components/icons/Freeze' import {Freeze_Stroke2_Corner2_Rounded as FreezeIcon} from '#/components/icons/Freeze'
@@ -35,6 +38,7 @@ export function AccountSettingsScreen({}: Props) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const emailDialogControl = useEmailDialogControl() const emailDialogControl = useEmailDialogControl()
const birthdayControl = useDialogControl() const birthdayControl = useDialogControl()
const changeHandleControl = useDialogControl() const changeHandleControl = useDialogControl()
@@ -148,6 +152,19 @@ export function AccountSettingsScreen({}: Props) {
/> />
</SettingsList.Item> </SettingsList.Item>
<AgeAssuranceAccountCard style={[a.px_xl, a.pt_xs, a.pb_md]} /> <AgeAssuranceAccountCard style={[a.px_xl, a.pt_xs, a.pb_md]} />
<SettingsList.LinkItem
to="/settings/automation-label"
label={_(msg`Automation label`)}>
<SettingsList.ItemIcon icon={RobotIcon} />
<SettingsList.ItemText>
<Trans>Automation label</Trans>
</SettingsList.ItemText>
{profile && (
<SettingsList.BadgeText>
{isBotAccount(profile) ? _(msg`On`) : _(msg`Off`)}
</SettingsList.BadgeText>
)}
</SettingsList.LinkItem>
<SettingsList.Divider /> <SettingsList.Divider />
<SettingsList.PressableItem <SettingsList.PressableItem
label={_(msg`Export my data`)} label={_(msg`Export my data`)}
@@ -0,0 +1,202 @@
import {View} from 'react-native'
import {type $Typed, ComAtprotoLabelDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {RQKEY_ROOT as POST_FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
import {
useProfileQuery,
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {postThreadQueryKeyRoot} from '#/state/queries/usePostThread/types'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, platform, useTheme} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import * as Toggle from '#/components/forms/Toggle'
import {Bot_Filled as RobotIcon} from '#/components/icons/Bot'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useAnalytics} from '#/analytics'
import * as bsky from '#/types/bsky'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'AutomationLabelSettings'
>
export function AutomationLabelSettingsScreen({}: Props) {
const t = useTheme()
const ax = useAnalytics()
const {t: l} = useLingui()
const queryClient = useQueryClient()
const {currentAccount} = useSession()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const updateProfile = useProfileUpdateMutation()
const verification = useSimpleVerificationState({profile})
const isBotLabeled =
profile?.labels?.some(l => l.val === 'bot' && l.src === profile.did) ??
false
const canToggle = profile && !updateProfile.isPending
const onToggle = () => {
if (!profile) {
return
}
let wasAdded = false
ax.metric('bot:label:toggle', {state: isBotLabeled ? 'remove' : 'add'})
updateProfile.mutate(
{
profile,
updates: existing => {
const labels: $Typed<ComAtprotoLabelDefs.SelfLabels> = bsky.validate(
existing.labels,
ComAtprotoLabelDefs.validateSelfLabels,
)
? existing.labels
: {
$type: 'com.atproto.label.defs#selfLabels',
values: [],
}
const hasLabel = labels.values.some(l => l.val === 'bot')
if (hasLabel) {
wasAdded = false
labels.values = labels.values.filter(l => l.val !== 'bot')
} else {
wasAdded = true
labels.values.push({val: 'bot'})
}
if (labels.values.length === 0) {
delete existing.labels
} else {
existing.labels = labels
}
return existing
},
checkCommitted: res => {
const exists = !!res.data.labels?.some(l => l.val === 'bot')
return exists === wasAdded
},
},
{
onSuccess() {
queryClient.invalidateQueries({queryKey: [POST_FEED_RQKEY_ROOT]})
queryClient.invalidateQueries({queryKey: [postThreadQueryKeyRoot]})
},
},
)
}
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Automation Label</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<View style={[a.p_xl, a.gap_xl]}>
{profile && (
<View
style={[
a.flex_row,
a.justify_center,
a.align_center,
a.gap_sm,
a.rounded_lg,
a.border,
t.atoms.bg_contrast_50,
t.atoms.border_contrast_low,
{
height: 160,
paddingRight: 20, // helps visually center
},
]}>
<UserAvatar size={42} avatar={profile.avatar} type="user" />
<View>
<View style={[a.flex_row, a.align_baseline]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Text
emoji
style={[
a.text_xl,
a.font_semi_bold,
a.flex_shrink,
a.leading_tight,
]}
numberOfLines={1}>
{profile.displayName || profile.handle}
</Text>
{verification.isVerified && (
<VerificationCheck
verifier={verification.role === 'verifier'}
size="sm"
/>
)}
<View style={{top: platform({ios: -1})}}>
<BotBadge profile={profile} alwaysShow width={17} />
</View>
</View>
</View>
<Text
style={[
a.text_md,
a.leading_snug,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
@{profile.handle}
</Text>
</View>
</View>
)}
<View style={[a.gap_sm]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Add automation label to account</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
This label lets the world know that this account is automated.
If turned on, this label appears next to the account's name on
their profile and posts. It can be turned on or off at any time.
</Trans>
</Text>
</View>
<Toggle.Item
name="automation_label"
disabled={!canToggle || updateProfile.isPending}
value={isBotLabeled}
onChange={onToggle}
label={l`Show automation label`}
style={[
a.w_full,
a.p_md,
a.rounded_lg,
a.border,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_50,
]}>
<View style={[a.pr_xs]}>
<RobotIcon width={24} fill={t.atoms.text_contrast_medium.color} />
</View>
<Toggle.LabelText style={[a.flex_1, a.text_md, a.font_medium]}>
<Trans>Show automation label</Trans>
</Toggle.LabelText>
<Toggle.Platform />
</Toggle.Item>
</View>
</Layout.Content>
</Layout.Screen>
)
}
+11 -18
View File
@@ -61,13 +61,9 @@ import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config'
import {ProfileBadges} from '#/components/ProfileBadges'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useFullVerificationState} from '#/components/verification'
import {
shouldShowVerificationCheckButton,
VerificationCheckButton,
} from '#/components/verification/VerificationCheckButton'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env' import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
@@ -321,9 +317,6 @@ function ProfilePreview({
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const shadow = useProfileShadow(profile) const shadow = useProfileShadow(profile)
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const verificationState = useFullVerificationState({
profile: shadow,
})
const {isActive: live} = useActorStatus(profile) const {isActive: live} = useActorStatus(profile)
if (!moderationOpts) return null if (!moderationOpts) return null
@@ -364,16 +357,16 @@ function ProfilePreview({
]}> ]}>
{displayName} {displayName}
</Text> </Text>
{shouldShowVerificationCheckButton(verificationState) && ( <ProfileBadges
<View profile={shadow}
style={[ size="xl"
{ interactive
marginTop: platform({web: 8, ios: 8, android: 10}), style={[
}, {
]}> marginTop: platform({web: 8, ios: 8, android: 10}),
<VerificationCheckButton profile={shadow} size="lg" /> },
</View> ]}
)} />
</View> </View>
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}> <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
{sanitizeHandle(profile.handle, '@')} {sanitizeHandle(profile.handle, '@')}
+2 -12
View File
@@ -16,9 +16,8 @@ import {type ComposerOptsPostRef} from '#/state/shell/composer'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf' import {atoms as a, useTheme, web} from '#/alf'
import {QuoteEmbed} from '#/components/Post/Embed' import {QuoteEmbed} from '#/components/Post/Embed'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {parseEmbed} from '#/types/bsky/post' import {parseEmbed} from '#/types/bsky/post'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
@@ -70,8 +69,6 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
} }
}, [embed]) }, [embed])
const verification = useSimpleVerificationState({profile: replyTo.author})
return ( return (
<Pressable <Pressable
style={[ style={[
@@ -109,14 +106,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
sanitizeHandle(replyTo.author.handle), sanitizeHandle(replyTo.author.handle),
)} )}
</Text> </Text>
{verification.showBadge && ( <ProfileBadges profile={replyTo.author} size="sm" style={[a.pl_xs]} />
<View style={[a.pl_xs]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View> </View>
<View style={[a.flex_row, a.gap_md]}> <View style={[a.flex_row, a.gap_md]}>
<View style={[a.flex_1, a.flex_grow]}> <View style={[a.flex_1, a.flex_grow]}>
@@ -9,9 +9,8 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, platform, useTheme} from '#/alf' import {atoms as a, platform, useTheme} from '#/alf'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
export function Autocomplete({ export function Autocomplete({
prefix, prefix,
@@ -77,7 +76,6 @@ function AutocompleteProfileCard({
onPress: () => void onPress: () => void
}) { }) {
const t = useTheme() const t = useTheme()
const state = useSimpleVerificationState({profile})
const displayName = sanitizeDisplayName( const displayName = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
) )
@@ -115,19 +113,15 @@ function AutocompleteProfileCard({
numberOfLines={1}> numberOfLines={1}>
{displayName} {displayName}
</Text> </Text>
{state.isVerified && ( <ProfileBadges
<View profile={profile}
style={[ size="sm"
{ style={[
marginTop: platform({android: -2}), {
}, marginTop: platform({android: -2}),
]}> },
<VerificationCheck ]}
width={12} />
verifier={state.role === 'verifier'}
/>
</View>
)}
</View> </View>
</View> </View>
<Text <Text
@@ -63,12 +63,11 @@ import {StarterPack} from '#/components/icons/StarterPack'
import {VerifiedCheck} from '#/components/icons/VerifiedCheck' import {VerifiedCheck} from '#/components/icons/VerifiedCheck'
import {InlineLinkText, Link} from '#/components/Link' import {InlineLinkText, Link} from '#/components/Link'
import * as MediaPreview from '#/components/MediaPreview' import * as MediaPreview from '#/components/MediaPreview'
import {ProfileBadges} from '#/components/ProfileBadges'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard' import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {SubtleHover} from '#/components/SubtleHover' import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
const MAX_AUTHORS = 5 const MAX_AUTHORS = 5
@@ -173,9 +172,6 @@ let NotificationFeedItem = ({
const niceTimestamp = niceDate(i18n, item.notification.indexedAt) const niceTimestamp = niceDate(i18n, item.notification.indexedAt)
const firstAuthor = authors[0] const firstAuthor = authors[0]
const firstAuthorVerification = useSimpleVerificationState({
profile: firstAuthor.profile,
})
const firstAuthorName = sanitizeDisplayName( const firstAuthorName = sanitizeDisplayName(
firstAuthor.profile.displayName || firstAuthor.profile.handle, firstAuthor.profile.displayName || firstAuthor.profile.handle,
) )
@@ -246,24 +242,21 @@ let NotificationFeedItem = ({
emoji emoji
label={_(msg`Go to ${firstAuthorName}'s profile`)}> label={_(msg`Go to ${firstAuthorName}'s profile`)}>
{forceLTR(firstAuthorName)} {forceLTR(firstAuthorName)}
{firstAuthorVerification.showBadge && ( <ProfileBadges
<View profile={firstAuthor.profile}
style={[ size="md"
a.relative, style={[
{ a.relative,
paddingTop: platform({android: 2}), {
marginBottom: platform({ios: -7}), // weird stuff here
top: platform({web: 1}), paddingTop: platform({android: 2}),
paddingLeft: 3, marginBottom: platform({ios: -6}),
paddingRight: 2, top: platform({web: 2}),
}, paddingLeft: 3,
]}> paddingRight: 2,
<VerificationCheck },
width={14} ]}
verifier={firstAuthorVerification.role === 'verifier'} />
/>
</View>
)}
</InlineLinkText> </InlineLinkText>
</ProfileHoverCard> </ProfileHoverCard>
) )
@@ -1017,9 +1010,6 @@ function ExpandedAuthorsList({
function ExpandedAuthorCard({author}: {author: Author}) { function ExpandedAuthorCard({author}: {author: Author}) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const verification = useSimpleVerificationState({
profile: author.profile,
})
return ( return (
<Link <Link
key={author.profile.did} key={author.profile.did}
@@ -1055,14 +1045,11 @@ function ExpandedAuthorCard({author}: {author: Author}) {
author.profile.displayName || author.profile.handle, author.profile.displayName || author.profile.handle,
)} )}
</Text> </Text>
{verification.showBadge && ( <ProfileBadges
<View style={[a.pl_xs, a.self_center]}> profile={author.profile}
<VerificationCheck size="md"
width={14} style={[a.pl_2xs, a.self_center]}
verifier={verification.role === 'verifier'} />
/>
</View>
)}
<Text <Text
numberOfLines={1} numberOfLines={1}
style={[ style={[
+7 -19
View File
@@ -13,12 +13,11 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {niceDate} from '#/lib/strings/time' import {niceDate} from '#/lib/strings/time'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {unstableCacheProfileView} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/profile'
import {atoms as a, platform, useTheme, web} from '#/alf' import {atoms as a, useTheme, web} from '#/alf'
import {WebOnlyInlineLinkText} from '#/components/Link' import {WebOnlyInlineLinkText} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {IS_ANDROID} from '#/env' import {IS_ANDROID} from '#/env'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
import {TimeElapsed} from './TimeElapsed' import {TimeElapsed} from './TimeElapsed'
@@ -55,7 +54,6 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
}, [queryClient, author]) }, [queryClient, author])
const timestampLabel = niceDate(i18n, opts.timestamp) const timestampLabel = niceDate(i18n, opts.timestamp)
const verification = useSimpleVerificationState({profile: author})
const {isActive: live} = useActorStatus(author) const {isActive: live} = useActorStatus(author)
const MaybeLinkText = opts.linkDisabled ? Text : WebOnlyInlineLinkText const MaybeLinkText = opts.linkDisabled ? Text : WebOnlyInlineLinkText
@@ -109,21 +107,11 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
), ),
)} )}
</MaybeLinkText> </MaybeLinkText>
{verification.showBadge && ( <ProfileBadges
<View profile={author}
style={[ size="sm"
a.pl_2xs, style={[a.pl_2xs, a.self_center]}
a.self_center, />
{
marginTop: platform({web: 0, ios: 0, android: -1}),
},
]}>
<VerificationCheck
width={platform({android: 13, default: 12})}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<MaybeLinkText <MaybeLinkText
emoji emoji
numberOfLines={1} numberOfLines={1}
+2 -14
View File
@@ -53,9 +53,8 @@ import {
UserCircle_Stroke2_Corner0_Rounded as UserCircle, UserCircle_Stroke2_Corner0_Rounded as UserCircle,
} from '#/components/icons/UserCircle' } from '#/components/icons/UserCircle'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
@@ -71,7 +70,6 @@ let DrawerProfileCard = ({
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
const t = useTheme() const t = useTheme()
const {data: profile} = useProfileQuery({did: account.did}) const {data: profile} = useProfileQuery({did: account.did})
const verification = useSimpleVerificationState({profile})
const {isActive: live} = useActorStatus(profile) const {isActive: live} = useActorStatus(profile)
return ( return (
@@ -97,17 +95,7 @@ let DrawerProfileCard = ({
numberOfLines={1}> numberOfLines={1}>
{profile?.displayName || account.handle} {profile?.displayName || account.handle}
</Text> </Text>
{verification.showBadge && ( {profile && <ProfileBadges profile={profile} size="lg" />}
<View
style={{
top: 0,
}}>
<VerificationCheck
width={16}
verifier={verification.role === 'verifier'}
/>
</View>
)}
</View> </View>
<Text <Text
emoji emoji