[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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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, '@')}
|
||||
|
||||
Reference in New Issue
Block a user