[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:
@@ -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}
|
||||
|
||||
@@ -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': {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -50,6 +50,7 @@ export type CommonNavigatorParams = {
|
||||
AccessibilitySettings: undefined
|
||||
AppearanceSettings: undefined
|
||||
AccountSettings: undefined
|
||||
AutomationLabelSettings: undefined
|
||||
PrivacyAndSecuritySettings: undefined
|
||||
ActivityPrivacySettings: undefined
|
||||
ContentAndMediaSettings: undefined
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, '@')}
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user