[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 {Reply as ReplyIcon} from '../icons/Reply'
import {Repost as RepostIcon} from '../icons/Repost'
import {Robot as RobotIcon} from '../icons/Robot'
import {CONTENT_LABELS} from '../labels'
import * as bsky from '../types/bsky'
import {niceDate} from '../util/nice-date'
@@ -43,6 +44,9 @@ export function Post({thread}: Props) {
}
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)}`
@@ -76,6 +80,12 @@ export function Post({thread}: Props) {
size={15}
/>
)}
{isBot && (
<RobotIcon
className="pl-[3px] mt-px shrink-0 text-slate-500 dark:text-slate-400"
size={15}
/>
)}
</div>
<Link
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/appearance", 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/activity", 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 {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'
import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings'
import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings'
import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings'
@@ -403,6 +404,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
requireAuth: true,
}}
/>
<Stack.Screen
name="AutomationLabelSettings"
getComponent={() => AutomationLabelSettingsScreen}
options={{
title: title(msg`Automation Label`),
requireAuth: true,
}}
/>
<Stack.Screen
name="PrivacyAndSecuritySettings"
getComponent={() => PrivacyAndSecuritySettingsScreen}
+3
View File
@@ -732,6 +732,9 @@ export type Events = {
'verification:settings:hideBadges': {}
'verification:settings:unHideBadges': {}
'bot:label:toggle': {state: 'add' | 'remove'}
'bot:badge:click': {}
'live:create': {duration: number}
'live:edit': {}
'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 {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useActorStatus} from '#/features/liveNow'
export function AccountList({
@@ -116,7 +115,6 @@ function AccountItem({
}) {
const t = useTheme()
const {_} = useLingui()
const verification = useSimpleVerificationState({profile})
const {isActive: live} = useActorStatus(profile)
const onPress = useCallback(() => {
@@ -164,13 +162,12 @@ function AccountItem({
profile?.displayName || profile?.handle || account.handle,
)}
</Text>
{verification.showBadge && (
<View>
<VerificationCheck
width={12}
verifier={verification.role === 'verifier'}
/>
</View>
{profile && (
<ProfileBadges
profile={profile}
size="sm"
style={[{marginTop: -2}]}
/>
)}
</View>
<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 {Button} from '#/components/Button'
import {useDialogContext} from '#/components/Dialog'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
@@ -111,7 +110,6 @@ function RecentChatItem({
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
const verification = useSimpleVerificationState({profile})
if (isBlockedOrBlocking(profile) || isMuted(profile)) {
return null
@@ -141,14 +139,7 @@ function RecentChatItem({
numberOfLines={1}>
{name}
</Text>
{verification.showBadge && (
<View style={[a.pl_2xs]}>
<VerificationCheck
width={10}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges profile={profile} size="xs" style={[a.pl_2xs]} />
</View>
</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 {Link as InternalLink, type LinkProps} from '#/components/Link'
import * as Pills from '#/components/Pills'
import {ProfileBadges} from '#/components/ProfileBadges'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {type Metrics} from '#/analytics'
import {useActorStatus} from '#/features/liveNow'
import type * as bsky from '#/types/bsky'
@@ -242,7 +241,6 @@ function InlineNameAndHandle({
moderationOpts: ModerationOpts
}) {
const t = useTheme()
const verification = useSimpleVerificationState({profile})
const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
@@ -262,19 +260,15 @@ function InlineNameAndHandle({
numberOfLines={1}>
{forceLTR(name)}
</Text>
{verification.showBadge && (
<View
style={[
a.pl_2xs,
a.self_center,
{marginTop: platform({default: 0, android: -1})},
]}>
<VerificationCheck
width={platform({android: 13, default: 12})}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges
profile={profile}
size="md"
style={[
a.pl_2xs,
a.self_center,
{marginTop: platform({default: 0, android: -1})},
]}
/>
<Text
emoji
style={[
@@ -305,7 +299,6 @@ export function Name({
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
const verification = useSimpleVerificationState({profile})
return (
<View style={[a.flex_row, a.align_center, a.max_w_full, style]}>
<Text
@@ -321,14 +314,7 @@ export function Name({
numberOfLines={1}>
{name}
</Text>
{verification.showBadge && (
<View style={[a.pl_xs]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
</View>
)
}
+11 -17
View File
@@ -36,10 +36,9 @@ import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import * as Pills from '#/components/Pills'
import {Portal} from '#/components/Portal'
import {ProfileBadges} from '#/components/ProfileBadges'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {IS_WEB_TOUCH_DEVICE} from '#/env'
import {useActorStatus} from '#/features/liveNow'
import {LiveStatus} from '#/features/liveNow/components/LiveStatusDialog'
@@ -459,7 +458,6 @@ function Inner({
[currentAccount, profile],
)
const isLabeler = profile.associated?.labeler
const verification = useSimpleVerificationState({profile})
return (
<View>
@@ -527,20 +525,16 @@ function Inner({
moderation.ui('displayName'),
)}
</Text>
{verification.showBadge && (
<View
style={[
a.pl_xs,
{
marginTop: -2,
},
]}>
<VerificationCheck
width={16}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges
profile={profile}
size="md"
style={[
a.pl_xs,
{
marginTop: -1,
},
]}
/>
</View>
<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 {Link} from '#/components/Link'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {IS_WEB} from '#/env'
const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
@@ -112,9 +111,6 @@ function HeaderReady({
const {_} = useLingui()
const t = useTheme()
const convoState = useConvo()
const verification = useSimpleVerificationState({
profile,
})
const isDeletedAccount = profile?.handle === 'missing.invalid'
const displayName = isDeletedAccount
@@ -161,14 +157,7 @@ function HeaderReady({
numberOfLines={1}>
{displayName}
</Text>
{verification.showBadge && (
<View style={[a.pl_xs]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
</View>
{!isDeletedAccount && (
<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) {
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) {
return null
}
@@ -3,7 +3,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
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 {useDialogControl} from '#/components/Dialog'
import {useFullVerificationState} from '#/components/verification'
@@ -51,43 +51,36 @@ export function shouldShowVerificationCheckButton(
export function VerificationCheckButton({
profile,
size,
width,
}: {
profile: Shadow<bsky.profile.AnyProfileView>
size: 'lg' | 'md' | 'sm'
width: number
}) {
const state = useFullVerificationState({
profile,
})
if (shouldShowVerificationCheckButton(state)) {
return <Badge profile={profile} verificationState={state} size={size} />
return <Badge profile={profile} verificationState={state} width={width} />
}
return null
}
export function Badge({
function Badge({
profile,
verificationState: state,
size,
width,
}: {
profile: Shadow<bsky.profile.AnyProfileView>
verificationState: FullVerificationState
size: 'lg' | 'md' | 'sm'
width: number
}) {
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const verificationsDialogControl = 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
@@ -116,8 +109,8 @@ export function Badge({
a.align_end,
a.transition_transform,
{
width: dimensions,
height: dimensions,
width: width,
height: width,
transform: [
{
scale: hovered ? 1.1 : 1,
@@ -126,7 +119,7 @@ export function Badge({
},
]}>
<VerificationCheck
width={dimensions}
width={width}
fill={
verifiedByHidden
? t.atoms.bg_contrast_100.backgroundColor
+1
View File
@@ -50,6 +50,7 @@ export type CommonNavigatorParams = {
AccessibilitySettings: undefined
AppearanceSettings: undefined
AccountSettings: undefined
AutomationLabelSettings: undefined
PrivacyAndSecuritySettings: undefined
ActivityPrivacySettings: undefined
ContentAndMediaSettings: undefined
+1
View File
@@ -50,6 +50,7 @@ export const router = new Router<AllNavigatableRoutes>({
AppearanceSettings: '/settings/appearance',
SavedFeeds: '/settings/saved-feeds',
AccountSettings: '/settings/account',
AutomationLabelSettings: '/settings/automation-label',
PrivacyAndSecuritySettings: '/settings/privacy-and-security',
ActivityPrivacySettings: '/settings/privacy-and-security/activity',
ContentAndMediaSettings: '/settings/content-and-media',
@@ -41,9 +41,8 @@ import {Link} from '#/components/Link'
import {useMenuControl} from '#/components/Menu'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {createPortalGroup} from '#/components/Portal'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky'
@@ -112,9 +111,6 @@ function ChatListItemReady({
const playHaptic = useHaptics()
const queryClient = useQueryClient()
const isUnread = convo.unreadCount > 0
const verification = useSimpleVerificationState({
profile,
})
const blockInfo = useMemo(() => {
const modui = moderation.ui('profileView')
@@ -414,14 +410,11 @@ function ChatListItemReady({
{displayName}
</Text>
</View>
{verification.showBadge && (
<View style={[a.pl_xs, a.self_center]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges
profile={profile}
size="md"
style={[a.pl_xs, a.self_center]}
/>
{lastMessageSentAt && (
<View style={[a.pl_xs]}>
<TimeElapsed timestamp={lastMessageSentAt}>
@@ -47,12 +47,12 @@ import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
import {TranslatedPost} from '#/components/Post/Translated'
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
import {useFormatPostStatCount} from '#/components/PostControls/util'
import {ProfileBadges} from '#/components/ProfileBadges'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import * as Skele from '#/components/Skeleton'
import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {WhoCanReply} from '#/components/WhoCanReply'
import {useAnalytics} from '#/analytics'
import {useActorStatus} from '#/features/liveNow'
@@ -362,7 +362,11 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
</Text>
<View style={[a.pl_xs]}>
<VerificationCheckButton profile={authorShadow} size="md" />
<ProfileBadges
profile={authorShadow}
size="md"
interactive
/>
</View>
</View>
<Text
@@ -33,11 +33,11 @@ import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
import {ProfileBadges} from '#/components/ProfileBadges'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {IS_IOS} from '#/env'
import {useActorStatus} from '#/features/liveNow'
import {GermButton} from '../components/GermButton'
@@ -143,7 +143,7 @@ let ProfileHeaderStandard = ({
moderation.ui('displayName'),
)}
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
<VerificationCheckButton profile={profile} size="lg" />
<ProfileBadges profile={profile} size="lg" interactive />
</View>
</Text>
</View>
@@ -14,9 +14,8 @@ import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
@@ -139,7 +138,6 @@ function RecentProfileItem({
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
const verification = useSimpleVerificationState({profile})
return (
<View style={[a.relative]}>
@@ -165,14 +163,7 @@ function RecentProfileItem({
<Text emoji style={[a.text_xs, a.leading_snug]} numberOfLines={1}>
{name}
</Text>
{verification.showBadge && (
<View style={[a.pl_2xs]}>
<VerificationCheck
width={10}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges profile={profile} size="xs" style={[a.pl_xs]} />
</View>
</Link>
<Button
+17
View File
@@ -4,10 +4,12 @@ import {Trans} from '@lingui/react/macro'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a, useTheme} from '#/alf'
import {AgeAssuranceAccountCard} from '#/components/ageAssurance/AgeAssuranceAccountCard'
import {isBotAccount} from '#/components/BotBadge'
import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {
@@ -16,6 +18,7 @@ import {
} from '#/components/dialogs/EmailDialog'
import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
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 {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
import {Freeze_Stroke2_Corner2_Rounded as FreezeIcon} from '#/components/icons/Freeze'
@@ -35,6 +38,7 @@ export function AccountSettingsScreen({}: Props) {
const t = useTheme()
const {_} = useLingui()
const {currentAccount} = useSession()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const emailDialogControl = useEmailDialogControl()
const birthdayControl = useDialogControl()
const changeHandleControl = useDialogControl()
@@ -148,6 +152,19 @@ export function AccountSettingsScreen({}: Props) {
/>
</SettingsList.Item>
<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.PressableItem
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 * as Menu from '#/components/Menu'
import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config'
import {ProfileBadges} from '#/components/ProfileBadges'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {useFullVerificationState} from '#/components/verification'
import {
shouldShowVerificationCheckButton,
VerificationCheckButton,
} from '#/components/verification/VerificationCheckButton'
import {useAnalytics} from '#/analytics'
import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env'
import {useActorStatus} from '#/features/liveNow'
@@ -321,9 +317,6 @@ function ProfilePreview({
const {gtMobile} = useBreakpoints()
const shadow = useProfileShadow(profile)
const moderationOpts = useModerationOpts()
const verificationState = useFullVerificationState({
profile: shadow,
})
const {isActive: live} = useActorStatus(profile)
if (!moderationOpts) return null
@@ -364,16 +357,16 @@ function ProfilePreview({
]}>
{displayName}
</Text>
{shouldShowVerificationCheckButton(verificationState) && (
<View
style={[
{
marginTop: platform({web: 8, ios: 8, android: 10}),
},
]}>
<VerificationCheckButton profile={shadow} size="lg" />
</View>
)}
<ProfileBadges
profile={shadow}
size="xl"
interactive
style={[
{
marginTop: platform({web: 8, ios: 8, android: 10}),
},
]}
/>
</View>
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
{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 {atoms as a, useTheme, web} from '#/alf'
import {QuoteEmbed} from '#/components/Post/Embed'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {parseEmbed} from '#/types/bsky/post'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
@@ -70,8 +69,6 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
}
}, [embed])
const verification = useSimpleVerificationState({profile: replyTo.author})
return (
<Pressable
style={[
@@ -109,14 +106,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
sanitizeHandle(replyTo.author.handle),
)}
</Text>
{verification.showBadge && (
<View style={[a.pl_xs]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges profile={replyTo.author} size="sm" style={[a.pl_xs]} />
</View>
<View style={[a.flex_row, a.gap_md]}>
<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 {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, platform, useTheme} from '#/alf'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
export function Autocomplete({
prefix,
@@ -77,7 +76,6 @@ function AutocompleteProfileCard({
onPress: () => void
}) {
const t = useTheme()
const state = useSimpleVerificationState({profile})
const displayName = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
)
@@ -115,19 +113,15 @@ function AutocompleteProfileCard({
numberOfLines={1}>
{displayName}
</Text>
{state.isVerified && (
<View
style={[
{
marginTop: platform({android: -2}),
},
]}>
<VerificationCheck
width={12}
verifier={state.role === 'verifier'}
/>
</View>
)}
<ProfileBadges
profile={profile}
size="sm"
style={[
{
marginTop: platform({android: -2}),
},
]}
/>
</View>
</View>
<Text
@@ -63,12 +63,11 @@ import {StarterPack} from '#/components/icons/StarterPack'
import {VerifiedCheck} from '#/components/icons/VerifiedCheck'
import {InlineLinkText, Link} from '#/components/Link'
import * as MediaPreview from '#/components/MediaPreview'
import {ProfileBadges} from '#/components/ProfileBadges'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import * as bsky from '#/types/bsky'
const MAX_AUTHORS = 5
@@ -173,9 +172,6 @@ let NotificationFeedItem = ({
const niceTimestamp = niceDate(i18n, item.notification.indexedAt)
const firstAuthor = authors[0]
const firstAuthorVerification = useSimpleVerificationState({
profile: firstAuthor.profile,
})
const firstAuthorName = sanitizeDisplayName(
firstAuthor.profile.displayName || firstAuthor.profile.handle,
)
@@ -246,24 +242,21 @@ let NotificationFeedItem = ({
emoji
label={_(msg`Go to ${firstAuthorName}'s profile`)}>
{forceLTR(firstAuthorName)}
{firstAuthorVerification.showBadge && (
<View
style={[
a.relative,
{
paddingTop: platform({android: 2}),
marginBottom: platform({ios: -7}),
top: platform({web: 1}),
paddingLeft: 3,
paddingRight: 2,
},
]}>
<VerificationCheck
width={14}
verifier={firstAuthorVerification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges
profile={firstAuthor.profile}
size="md"
style={[
a.relative,
{
// weird stuff here
paddingTop: platform({android: 2}),
marginBottom: platform({ios: -6}),
top: platform({web: 2}),
paddingLeft: 3,
paddingRight: 2,
},
]}
/>
</InlineLinkText>
</ProfileHoverCard>
)
@@ -1017,9 +1010,6 @@ function ExpandedAuthorsList({
function ExpandedAuthorCard({author}: {author: Author}) {
const t = useTheme()
const {_} = useLingui()
const verification = useSimpleVerificationState({
profile: author.profile,
})
return (
<Link
key={author.profile.did}
@@ -1055,14 +1045,11 @@ function ExpandedAuthorCard({author}: {author: Author}) {
author.profile.displayName || author.profile.handle,
)}
</Text>
{verification.showBadge && (
<View style={[a.pl_xs, a.self_center]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges
profile={author.profile}
size="md"
style={[a.pl_2xs, a.self_center]}
/>
<Text
numberOfLines={1}
style={[
+7 -19
View File
@@ -13,12 +13,11 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {niceDate} from '#/lib/strings/time'
import {useProfileShadow} from '#/state/cache/profile-shadow'
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 {ProfileBadges} from '#/components/ProfileBadges'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {IS_ANDROID} from '#/env'
import {useActorStatus} from '#/features/liveNow'
import {TimeElapsed} from './TimeElapsed'
@@ -55,7 +54,6 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
}, [queryClient, author])
const timestampLabel = niceDate(i18n, opts.timestamp)
const verification = useSimpleVerificationState({profile: author})
const {isActive: live} = useActorStatus(author)
const MaybeLinkText = opts.linkDisabled ? Text : WebOnlyInlineLinkText
@@ -109,21 +107,11 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
),
)}
</MaybeLinkText>
{verification.showBadge && (
<View
style={[
a.pl_2xs,
a.self_center,
{
marginTop: platform({web: 0, ios: 0, android: -1}),
},
]}>
<VerificationCheck
width={platform({android: 13, default: 12})}
verifier={verification.role === 'verifier'}
/>
</View>
)}
<ProfileBadges
profile={author}
size="sm"
style={[a.pl_2xs, a.self_center]}
/>
<MaybeLinkText
emoji
numberOfLines={1}
+2 -14
View File
@@ -53,9 +53,8 @@ import {
UserCircle_Stroke2_Corner0_Rounded as UserCircle,
} from '#/components/icons/UserCircle'
import {InlineLinkText} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {IS_WEB} from '#/env'
import {useActorStatus} from '#/features/liveNow'
@@ -71,7 +70,6 @@ let DrawerProfileCard = ({
const {_, i18n} = useLingui()
const t = useTheme()
const {data: profile} = useProfileQuery({did: account.did})
const verification = useSimpleVerificationState({profile})
const {isActive: live} = useActorStatus(profile)
return (
@@ -97,17 +95,7 @@ let DrawerProfileCard = ({
numberOfLines={1}>
{profile?.displayName || account.handle}
</Text>
{verification.showBadge && (
<View
style={{
top: 0,
}}>
<VerificationCheck
width={16}
verifier={verification.role === 'verifier'}
/>
</View>
)}
{profile && <ProfileBadges profile={profile} size="lg" />}
</View>
<Text
emoji