Compare commits

...

25 Commits

Author SHA1 Message Date
vineyardbovines 53c668fd89 Merge branches 'bot-labels' and 'bot-labels' of github.com:bluesky-social/social-app into bot-labels 2026-03-05 14:37:41 -05:00
vineyardbovines 4742b0b8e1 Merge branch 'main' into bot-labels 2026-03-05 14:32:18 -05:00
vineyardbovines 35d3037dee revert to dialog sheet for now 2026-03-05 14:26:40 -05:00
vineyardbovines fd1e4a0b0b prompt update 2026-03-05 13:24:56 -05:00
vineyardbovines 2a018a4c42 use prompt component 2026-03-05 13:19:30 -05:00
vineyardbovines ab9f2875a0 minor styling fixes 2026-03-05 13:19:30 -05:00
vineyardbovines 4f6f710e95 show separate alert if you're on your own profile 2026-03-05 13:19:30 -05:00
vineyardbovines 39c3d0647e events 2026-03-05 13:19:30 -05:00
vineyardbovines eb5704e70a ui wip 2026-03-05 13:19:30 -05:00
vineyardbovines 73f9f686d1 wip 2026-03-05 13:19:30 -05:00
vineyardbovines d5d3076dda wip 2026-03-05 13:19:30 -05:00
vineyardbovines 423f1a7ad7 styling for alert variant 2026-03-05 13:18:28 -05:00
vineyardbovines 2a779a7008 wrapper fix 2026-03-05 13:10:20 -05:00
vineyardbovines 3e413e151d use prompt component 2026-03-05 12:59:14 -05:00
vineyardbovines 13486b0fca minor styling fixes 2026-03-05 12:57:48 -05:00
vineyardbovines 443cb9859c show separate alert if you're on your own profile 2026-03-05 12:57:48 -05:00
vineyardbovines c4b1561f83 events 2026-03-05 12:57:48 -05:00
vineyardbovines bd05fafa27 ui wip 2026-03-05 12:57:48 -05:00
vineyardbovines 3dbdca5939 wip 2026-03-05 12:57:48 -05:00
vineyardbovines 4ad5a55fa6 wip 2026-03-05 12:57:48 -05:00
vineyardbovines 2a06d67fb4 extend for font sizing, etc 2026-03-05 12:56:04 -05:00
vineyardbovines fbb10d8a74 extend prompt instead 2026-03-05 12:54:34 -05:00
vineyardbovines c97128c106 handle inner components 2026-03-05 12:23:25 -05:00
vineyardbovines 7f4c81a837 dedupe some shared hooks 2026-03-05 12:16:01 -05:00
vineyardbovines aa1fbf4628 add detached variant to modal 2026-03-05 11:57:19 -05:00
34 changed files with 736 additions and 61 deletions
+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>
)
+9
View File
@@ -99,6 +99,7 @@ import {SearchScreen} from '#/screens/Search'
import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'
import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings'
import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings'
import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings'
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
@@ -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': {}
+6
View File
@@ -12,6 +12,7 @@ import {useProfilesQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
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'
@@ -172,6 +173,11 @@ function AccountItem({
/>
</View>
)}
{profile && (
<View style={{marginTop: -2}}>
<BotBadge profile={profile} />
</View>
)}
</View>
<Text
style={[
+73
View File
@@ -0,0 +1,73 @@
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Robot_Filled_Corner2_Rounded as RobotIcon} from '#/components/icons/Robot'
import {Text} from '#/components/Typography'
import {navigate} from '#/Navigation'
export function BotAccountAlert({
control,
isOwn,
}: {
control: Dialog.DialogControlProps
isOwn: boolean
}) {
const {_} = useLingui()
const t = useTheme()
const description = isOwn
? 'You have marked this account as automated. You can remove it at any time from your account settings.'
: 'This account has been marked as automated by its owner'
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.ScrollableInner label={_(msg`Automated account`)}>
<View style={[a.align_center, a.pb_md, a.shadow_sm]}>
<RobotIcon width={48} fill={t.atoms.text_contrast_medium.color} />
</View>
<Text
style={[
a.leading_snug,
a.text_center,
a.pb_xl,
t.atoms.text_contrast_high,
...(isOwn ? [a.text_md] : [a.text_lg]),
...(!isOwn ? [a.font_semi_bold] : []),
]}>
<Trans>{description}</Trans>
</Text>
<View style={[a.w_full, a.gap_sm]}>
<Button
label={_(msg`Okay`)}
onPress={() => control.close()}
color="primary"
size="large">
<ButtonText>
<Trans>Okay</Trans>
</ButtonText>
</Button>
{isOwn ? (
<Button
label={_(msg`Open settings`)}
onPress={() => {
control.close(() => {
navigate('AutomationLabelSettings')
})
}}
color="secondary"
size="large">
<ButtonText>
<Trans>Open settings</Trans>
</ButtonText>
</Button>
) : null}
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+94
View File
@@ -0,0 +1,94 @@
import {View} from 'react-native'
import {type ComAtprotoLabelDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {isBotAccount} from '#/lib/bots'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {BotAccountAlert} from '#/components/BotAccountAlert'
import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {Robot_Filled_Corner2_Rounded as RobotIcon} from '#/components/icons/Robot'
import {useAnalytics} from '#/analytics'
export function BotBadge({
profile,
alwaysShow = false,
size = 14,
}: {
profile: {did: string; labels?: ComAtprotoLabelDefs.Label[]}
alwaysShow?: boolean
size?: number
}) {
const t = useTheme()
if (!isBotAccount(profile) && !alwaysShow) {
return null
}
return (
<View>
<RobotIcon width={size} fill={t.atoms.text_contrast_medium.color} />
</View>
)
}
export function BotBadgeButton({
profile,
size,
isMe,
}: {
profile: {did: string; labels?: ComAtprotoLabelDefs.Label[]}
size: 'lg' | 'md' | 'sm'
isMe?: boolean
}) {
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
const control = useDialogControl()
if (!isBotAccount(profile)) {
return null
}
let dimensions = 12
if (size === 'lg') {
dimensions = gtPhone ? 20 : 18
} else if (size === 'md') {
dimensions = 14
}
return (
<>
<Button
label={_(msg`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: dimensions,
height: dimensions,
transform: [{scale: hovered ? 1.1 : 1}],
},
]}>
<RobotIcon
width={dimensions}
fill={t.atoms.text_contrast_medium.color}
/>
</View>
)}
</Button>
<BotAccountAlert control={control} isOwn={isMe} />
</>
)
}
+1
View File
@@ -23,6 +23,7 @@ export const Context = createContext<DialogContextProps>({
disableDrag: false,
setDisableDrag: () => {},
isWithinDialog: false,
type: 'sheet',
})
Context.displayName = 'DialogContext'
+109 -52
View File
@@ -1,6 +1,7 @@
import React, {useImperativeHandle} from 'react'
import React, {useImperativeHandle, useMemo, useRef, useState} from 'react'
import {
type LayoutChangeEvent,
Modal,
type NativeScrollEvent,
type NativeSyntheticEvent,
Pressable,
@@ -21,9 +22,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {ScrollProvider} from '#/lib/ScrollContext'
import {logger} from '#/logger'
import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs'
import {List, type ListMethods, type ListProps} from '#/view/com/util/List'
import {android, atoms as a, ios, platform, tokens, useTheme} from '#/alf'
import {useThemeName} from '#/alf/util/useColorModeTheme'
@@ -33,6 +32,7 @@ import {
type DialogInnerProps,
type DialogOuterProps,
} from '#/components/Dialog/types'
import {useDialogCallbackQueue} from '#/components/Dialog/utils'
import {createInput} from '#/components/forms/TextField'
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS} from '#/env'
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
@@ -49,7 +49,93 @@ export * from '#/components/Dialog/utils'
export const Input = createInput(TextInput)
export function Outer({
export function Outer(props: React.PropsWithChildren<DialogOuterProps>) {
if (props.type === 'alert') {
return <OuterAlert {...props} />
}
return <OuterSheet {...props} />
}
function OuterAlert({
children,
control,
onClose,
testID,
}: React.PropsWithChildren<DialogOuterProps>) {
const t = useTheme()
const [isOpen, setIsOpen] = useState(false)
const {enqueueCallback, handleOpen, handleClose} = useDialogCallbackQueue(
control,
onClose,
)
const open: DialogControlProps['open'] = () => {
handleOpen()
setIsOpen(true)
}
const close: DialogControlProps['close'] = cb => {
enqueueCallback(cb)
setIsOpen(false)
}
const onDismiss = () => {
handleClose()
}
useImperativeHandle(control.ref, () => ({open, close}), [open, close])
const context = useMemo(
() => ({
close,
isNativeDialog: true,
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
disableDrag: false,
setDisableDrag: () => {},
isWithinDialog: true,
type: 'alert' as const,
}),
[close],
)
return (
<Modal
visible={isOpen}
transparent
animationType="fade"
onRequestClose={() => close()}
onDismiss={onDismiss}>
<Pressable
accessibilityRole="button"
onPress={() => close()}
style={[
a.flex_1,
a.justify_center,
a.align_center,
a.px_xl,
{backgroundColor: 'rgba(0,0,0,0.5)'},
]}>
<Pressable
accessibilityRole="button"
onPress={e => e.stopPropagation()}
style={[
t.atoms.bg,
a.w_full,
{borderRadius: 48, maxWidth: 320, maxHeight: '90%'},
a.overflow_hidden,
]}>
<Context.Provider value={context}>
<View testID={testID} style={[a.flex_1]}>
{children}
</View>
</Context.Provider>
</Pressable>
</Pressable>
</Modal>
)
}
function OuterSheet({
children,
control,
onClose,
@@ -58,56 +144,33 @@ export function Outer({
}: React.PropsWithChildren<DialogOuterProps>) {
const themeName = useThemeName()
const t = useTheme(themeName)
const ref = React.useRef<BottomSheetNativeComponent>(null)
const closeCallbacks = React.useRef<(() => void)[]>([])
const {setDialogIsOpen, setFullyExpandedCount} =
useDialogStateControlContext()
const ref = useRef<BottomSheetNativeComponent>(null)
const {enqueueCallback, handleOpen, handleClose, setFullyExpandedCount} =
useDialogCallbackQueue(control, onClose)
const prevSnapPoint = React.useRef<BottomSheetSnapPoint>(
const prevSnapPoint = useRef<BottomSheetSnapPoint>(
BottomSheetSnapPoint.Hidden,
)
const [disableDrag, setDisableDrag] = React.useState(false)
const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>(
const [disableDrag, setDisableDrag] = useState(false)
const [snapPoint, setSnapPoint] = useState<BottomSheetSnapPoint>(
BottomSheetSnapPoint.Partial,
)
const callQueuedCallbacks = React.useCallback(() => {
for (const cb of closeCallbacks.current) {
try {
cb()
} catch (e: any) {
logger.error(e || 'Error running close callback')
}
}
closeCallbacks.current = []
}, [])
const open = React.useCallback<DialogControlProps['open']>(() => {
// Run any leftover callbacks that might have been queued up before calling `.open()`
callQueuedCallbacks()
setDialogIsOpen(control.id, true)
const open: DialogControlProps['open'] = () => {
handleOpen()
ref.current?.present()
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
}
// This is the function that we call when we want to dismiss the dialog.
const close = React.useCallback<DialogControlProps['close']>(cb => {
if (typeof cb === 'function') {
closeCallbacks.current.push(cb)
}
const close: DialogControlProps['close'] = cb => {
enqueueCallback(cb)
ref.current?.dismiss()
}, [])
}
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
// happen before we run this. It is passed to the `BottomSheet` component.
const onCloseAnimationComplete = React.useCallback(() => {
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
// tells us that we need to toggle the accessibility overlay setting
setDialogIsOpen(control.id, false)
callQueuedCallbacks()
onClose?.()
}, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
// Runs after the bottom sheet close animation completes.
const onCloseAnimationComplete = () => {
handleClose()
}
const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => {
const {snapPoint} = e.nativeEvent
@@ -138,16 +201,9 @@ export function Outer({
}
}
useImperativeHandle(
control.ref,
() => ({
open,
close,
}),
[open, close],
)
useImperativeHandle(control.ref, () => ({open, close}), [open, close])
const context = React.useMemo(
const context = useMemo(
() => ({
close,
isNativeDialog: true,
@@ -155,6 +211,7 @@ export function Outer({
disableDrag,
setDisableDrag,
isWithinDialog: true,
type: 'sheet' as const,
}),
[close, snapPoint, disableDrag, setDisableDrag],
)
+12 -4
View File
@@ -45,6 +45,7 @@ export function Outer({
control,
onClose,
webOptions,
type = 'sheet',
}: React.PropsWithChildren<DialogOuterProps>) {
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
@@ -96,6 +97,8 @@ export function Outer({
[close, open],
)
const isAlert = type === 'alert'
const context = React.useMemo(
() => ({
close,
@@ -104,8 +107,9 @@ export function Outer({
disableDrag: false,
setDisableDrag: () => {},
isWithinDialog: true,
type,
}),
[close],
[close, type],
)
return (
@@ -124,7 +128,9 @@ export function Outer({
a.inset_0,
a.z_10,
a.px_xl,
webOptions?.alignCenter ? a.justify_center : undefined,
webOptions?.alignCenter || isAlert
? a.justify_center
: undefined,
a.align_center,
{
overflowY: 'auto',
@@ -165,9 +171,10 @@ export function Inner({
contentContainerStyle,
}: DialogInnerProps) {
const t = useTheme()
const {close} = React.useContext(Context)
const {close, type} = React.useContext(Context)
const {gtMobile} = useBreakpoints()
const {reduceMotionEnabled} = useA11y()
const isAlert = type === 'alert'
FocusGuards.useFocusGuards()
return (
<FocusScope.FocusScope loop asChild trapped>
@@ -189,12 +196,13 @@ export function Inner({
a.border,
t.atoms.bg,
{
maxWidth: 600,
maxWidth: isAlert ? 320 : 600,
borderColor: t.palette.contrast_200,
shadowColor: t.palette.black,
shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
shadowRadius: 30,
},
isAlert && {borderRadius: 36},
!reduceMotionEnabled && a.zoom_fade_in,
style,
])}>
+7
View File
@@ -45,6 +45,7 @@ export type DialogContextProps = {
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
// in the event that the hook is used outside of a dialog
isWithinDialog: boolean
type: 'sheet' | 'alert'
}
export type DialogControlOpenOptions = {
@@ -65,6 +66,12 @@ export type DialogOuterProps = {
alignCenter?: boolean
onBackgroundPress?: (e: GestureResponderEvent) => void
}
/**
* The presentation style of the dialog.
* - `sheet` (default): Bottom sheet on native, standard modal on web.
* - `alert`: Centered alert modal on all platforms.
*/
type?: 'sheet' | 'alert'
testID?: string
}
+48 -2
View File
@@ -1,9 +1,55 @@
import React from 'react'
import {useEffect, useRef} from 'react'
import {logger} from '#/logger'
import {useDialogStateControlContext} from '#/state/dialogs'
import {type DialogControlProps} from '#/components/Dialog/types'
export function useDialogCallbackQueue(
control: DialogControlProps,
onClose?: () => void,
) {
const closeCallbacks = useRef<(() => void)[]>([])
const {setDialogIsOpen, setFullyExpandedCount} =
useDialogStateControlContext()
const callQueuedCallbacks = () => {
for (const cb of closeCallbacks.current) {
try {
cb()
} catch (e: any) {
logger.error(e || 'Error running close callback')
}
}
closeCallbacks.current = []
}
const enqueueCallback = (cb?: () => void) => {
if (typeof cb === 'function') {
closeCallbacks.current.push(cb)
}
}
const handleOpen = () => {
callQueuedCallbacks()
setDialogIsOpen(control.id, true)
}
const handleClose = () => {
setDialogIsOpen(control.id, false)
callQueuedCallbacks()
onClose?.()
}
return {
enqueueCallback,
handleOpen,
handleClose,
setFullyExpandedCount,
}
}
export function useAutoOpen(control: DialogControlProps, showTimeout?: number) {
React.useEffect(() => {
useEffect(() => {
if (showTimeout) {
const timeout = setTimeout(() => {
control.open()
@@ -15,6 +15,7 @@ import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, tokens, useTheme} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {Button} from '#/components/Button'
import {useDialogContext} from '#/components/Dialog'
import {Text} from '#/components/Typography'
@@ -149,6 +150,7 @@ function RecentChatItem({
/>
</View>
)}
<BotBadge profile={profile} />
</View>
</Button>
)
+12
View File
@@ -30,6 +30,7 @@ import {
useTheme,
type ViewStyleProp,
} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {
Button,
ButtonIcon,
@@ -272,6 +273,14 @@ function InlineNameAndHandle({
/>
</View>
)}
<View
style={[
a.pl_2xs,
a.self_center,
{marginTop: platform({default: 0, android: -1})},
]}>
<BotBadge profile={profile} />
</View>
<Text
emoji
style={[
@@ -326,6 +335,9 @@ export function Name({
/>
</View>
)}
<View style={[a.pl_xs]}>
<BotBadge profile={profile} />
</View>
</View>
)
}
@@ -23,6 +23,7 @@ import {formatCount} from '#/view/com/util/numeric/format'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {ProfileHeaderHandle} from '#/screens/Profile/Header/Handle'
import {atoms as a, useTheme} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useFollowMethods} from '#/components/hooks/useFollowMethods'
import {useRichText} from '#/components/hooks/useRichText'
@@ -541,6 +542,7 @@ function Inner({
/>
</View>
)}
<BotBadge profile={profile} size={16} />
</View>
<ProfileHeaderHandle profile={profileShadow} disableTaps />
+44 -2
View File
@@ -34,6 +34,7 @@ export function Outer({
control,
testID,
nativeOptions,
type = 'sheet',
}: React.PropsWithChildren<{
control: Dialog.DialogControlProps
testID?: string
@@ -41,6 +42,12 @@ export function Outer({
* Native-specific options for the prompt. Extends `BottomSheetViewProps`
*/
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
/**
* The presentation style of the prompt.
* - `sheet` (default): Bottom sheet on native, standard modal on web.
* - `alert`: Centered alert modal on all platforms.
*/
type?: 'sheet' | 'alert'
}>) {
const titleId = useId()
const descriptionId = useId()
@@ -54,9 +61,10 @@ export function Outer({
<Dialog.Outer
control={control}
testID={testID}
type={type}
webOptions={{alignCenter: true}}
nativeOptions={{preventExpansion: true, ...nativeOptions}}>
<Dialog.Handle />
{type !== 'alert' && <Dialog.Handle />}
<Context.Provider value={context}>
<Dialog.ScrollableInner
accessibilityLabelledBy={titleId}
@@ -69,6 +77,35 @@ export function Outer({
)
}
export function Icon({
icon: Comp,
color,
}: {
icon: React.ComponentType<SVGIconProps>
color?: ButtonColor
}) {
const t = useTheme()
let iconColor: string
switch (color) {
case 'negative':
iconColor = t.palette.negative_500
break
case 'primary':
iconColor = t.palette.primary_500
break
default:
iconColor = t.atoms.text.color
break
}
return (
<View style={[a.pb_sm]}>
<Comp size="xl" style={{color: iconColor}} />
</View>
)
}
export function TitleText({
children,
style,
@@ -210,6 +247,8 @@ export function Basic({
onConfirm,
confirmButtonColor,
showCancel = true,
type,
icon,
}: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control']
title: string
@@ -226,10 +265,13 @@ export function Basic({
onConfirm: (e: GestureResponderEvent) => void
confirmButtonColor?: ButtonColor
showCancel?: boolean
type?: 'sheet' | 'alert'
icon?: React.ComponentType<SVGIconProps>
}>) {
return (
<Outer control={control} testID="confirmModal">
<Outer control={control} testID="confirmModal" type={type}>
<Content>
{icon && <Icon icon={icon} color={confirmButtonColor} />}
<TitleText>{title}</TitleText>
{description && <DescriptionText>{description}</DescriptionText>}
</Content>
@@ -15,6 +15,7 @@ import {isConvoActive, useConvo} from '#/state/messages/convo'
import {type ConvoItem} from '#/state/messages/convo/types'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
import * as Layout from '#/components/Layout'
@@ -169,6 +170,7 @@ function HeaderReady({
/>
</View>
)}
<BotBadge profile={profile} />
</View>
{!isDeletedAccount && (
<Text
+16
View File
@@ -0,0 +1,16 @@
import {createMultiPathSVG, createSinglePathSVG} from './TEMPLATE'
export const Robot_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M9 2a1 1 0 0 1 1 1v1.07A7.002 7.002 0 0 1 16.93 11H18a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-.174A7.004 7.004 0 0 1 12 21a7.004 7.004 0 0 1-5.826-4H6a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1.07A7.002 7.002 0 0 1 14 4.07V3a1 1 0 0 1 1-1h-6Zm3 4a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm-2.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm5 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z',
})
export const Robot_Stroke2_Corner2_Rounded = createMultiPathSVG({
paths: [
'M12 2C12.5523 2 13 2.44772 13 3V5H16.2002L17.3125 5.00488C18.2878 5.02021 18.881 5.0819 19.3623 5.32715C19.9265 5.61472 20.3853 6.07347 20.6729 6.6377C20.9998 7.27941 21 8.11978 21 9.7998V10.0869C21.2088 10.1608 21.4009 10.2798 21.5605 10.4395C21.8419 10.7208 22 11.1022 22 11.5C22 11.8978 21.8419 12.2792 21.5605 12.5605C21.3997 12.7213 21.2057 12.8403 20.9951 12.9141C20.9887 15.1183 20.9371 16.3233 20.4551 17.2695L20.3623 17.4443C19.8798 18.3054 19.1516 19.0056 18.2695 19.4551L18.0654 19.5508C17.2988 19.8801 16.3444 19.9678 14.8545 19.9912L13 20H11L9.14551 19.9912C7.6556 19.9678 6.70119 19.8801 5.93457 19.5508L5.73047 19.4551C4.78966 18.9757 4.02429 18.2103 3.54492 17.2695C3.13636 16.4674 3.03433 15.4795 3.00879 13.8545L3.00391 12.9141C2.79363 12.8403 2.60003 12.7211 2.43945 12.5605C2.15815 12.2792 2 11.8978 2 11.5C2 11.1022 2.15815 10.7208 2.43945 10.4395C2.59907 10.2798 2.79119 10.1608 3 10.0869V9.7998C3 8.22469 3.00007 7.38795 3.26953 6.76074L3.32715 6.6377C3.57888 6.14381 3.96166 5.73057 4.43164 5.44238L4.6377 5.32715C5.27941 5.00018 6.11978 5 7.7998 5H11V3C11 2.44772 11.4477 2 12 2ZM7.7998 7C6.92684 7 6.39208 7.00151 5.99219 7.03418C5.80605 7.0494 5.69342 7.06885 5.625 7.08496C5.59247 7.09262 5.57235 7.09868 5.56152 7.10254C5.55113 7.10625 5.5467 7.10896 5.5459 7.10938C5.35793 7.20521 5.20521 7.35793 5.10938 7.5459C5.10896 7.5467 5.10625 7.55113 5.10254 7.56152C5.09868 7.57235 5.09262 7.59247 5.08496 7.625C5.06885 7.69342 5.0494 7.80605 5.03418 7.99219C5.00151 8.39208 5 8.92684 5 9.7998V12C5 13.4331 5.0016 14.3877 5.06152 15.1211C5.11954 15.8312 5.22227 16.1565 5.32715 16.3623C5.61472 16.9265 6.07347 17.3853 6.6377 17.6729C6.84354 17.7777 7.16883 17.8805 7.87891 17.9385C8.61233 17.9984 9.56689 18 11 18H13C14.4331 18 15.3877 17.9984 16.1211 17.9385C16.8312 17.8805 17.1565 17.7777 17.3623 17.6729C17.9265 17.3853 18.3853 16.9265 18.6729 16.3623C18.7777 16.1565 18.8805 15.8312 18.9385 15.1211C18.9984 14.3877 19 13.4331 19 12V9.7998C19 8.92684 18.9985 8.39208 18.9658 7.99219C18.9506 7.80605 18.9312 7.69342 18.915 7.625C18.9074 7.59247 18.9013 7.57235 18.8975 7.56152C18.8938 7.55113 18.891 7.5467 18.8906 7.5459C18.7948 7.35793 18.6421 7.20521 18.4541 7.10938C18.4533 7.10896 18.4489 7.10625 18.4385 7.10254C18.4277 7.09868 18.4075 7.09262 18.375 7.08496C18.3066 7.06885 18.194 7.0494 18.0078 7.03418C17.6079 7.00151 17.0732 7 16.2002 7H7.7998Z',
'M9 10C9.55228 10 10 10.4477 10 11V13C10 13.5523 9.55228 14 9 14C8.44772 14 8 13.5523 8 13V11C8 10.4477 8.44772 10 9 10ZM15 10C15.5523 10 16 10.4477 16 11V13C16 13.5523 15.5523 14 15 14C14.4477 14 14 13.5523 14 13V11C14 10.4477 14.4477 10 15 10Z',
],
})
export const Robot_Filled_Corner2_Rounded = createSinglePathSVG({
path: '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',
})
+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
}
+9
View File
@@ -0,0 +1,9 @@
import {type ComAtprotoLabelDefs} from '@atproto/api'
export function isBotAccount(
profile: {did: string; labels?: ComAtprotoLabelDefs.Label[]},
): boolean {
return (
profile.labels?.some(l => l.val === 'bot' && l.src === profile.did) ?? false
)
}
+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',
@@ -31,6 +31,7 @@ import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import * as tokens from '#/alf/tokens'
import {BotBadge} from '#/components/BotBadge'
import {useDialogControl} from '#/components/Dialog'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
@@ -422,6 +423,7 @@ function ChatListItemReady({
/>
</View>
)}
<BotBadge profile={profile} />
{lastMessageSentAt && (
<View style={[a.pl_xs]}>
<TimeElapsed timestamp={lastMessageSentAt}>
@@ -34,6 +34,7 @@ import {
REPLY_LINE_WIDTH,
} from '#/screens/PostThread/const'
import {atoms as a, useTheme} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {Button} from '#/components/Button'
import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
@@ -364,6 +365,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
<View style={[a.pl_xs]}>
<VerificationCheckButton profile={authorShadow} size="md" />
</View>
<BotBadge profile={authorShadow} size={14} />
</View>
<Text
style={[
@@ -24,6 +24,7 @@ import {useRequireAuth, useSession} from '#/state/session'
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
import {BotBadgeButton} from '#/components/BotBadge'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
import {useDialogControl} from '#/components/Dialog'
@@ -145,6 +146,9 @@ let ProfileHeaderStandard = ({
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
<VerificationCheckButton profile={profile} size="lg" />
</View>
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
<BotBadgeButton profile={profile} size="lg" isMe={isMe} />
</View>
</Text>
</View>
<ProfileHeaderHandle profile={profile} />
@@ -10,6 +10,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import * as Layout from '#/components/Layout'
@@ -173,6 +174,7 @@ function RecentProfileItem({
/>
</View>
)}
<BotBadge profile={profile} />
</View>
</Link>
<Button
+17
View File
@@ -3,7 +3,9 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {isBotAccount} from '#/lib/bots'
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'
@@ -19,6 +21,7 @@ import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/compon
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'
import {Robot_Stroke2_Corner2_Rounded as RobotIcon} from '#/components/icons/Robot'
import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock'
import {PencilLine_Stroke2_Corner2_Rounded as PencilIcon} from '#/components/icons/Pencil'
import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
@@ -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,192 @@
import React from 'react'
import {View} from 'react-native'
import {type $Typed, ComAtprotoLabelDefs} from '@atproto/api'
import {useQueryClient} from '@tanstack/react-query'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useAnalytics} from '#/analytics'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {RQKEY_ROOT as POST_FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
import {postThreadQueryKeyRoot} from '#/state/queries/usePostThread/types'
import {
useProfileQuery,
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import * as Toggle from '#/components/forms/Toggle'
import {Robot_Filled_Corner2_Rounded as RobotIcon} from '#/components/icons/Robot'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import * as bsky from '#/types/bsky'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'AutomationLabelSettings'
>
export function AutomationLabelSettingsScreen({}: Props) {
const t = useTheme()
const ax = useAnalytics()
const {_} = 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 = React.useCallback(() => {
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]})
},
},
)
}, [updateProfile, profile])
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.p_5xl,
a.rounded_lg,
t.atoms.bg_contrast_50,
a.border,
t.atoms.border_contrast_low,
]}>
<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]}
numberOfLines={1}>
{profile.displayName || profile.handle}
</Text>
{verification.isVerified && (
<VerificationCheck
verifier={verification.role === 'verifier'}
size="xs"
/>
)}
<BotBadge profile={profile} alwaysShow size={16} />
</View>
</View>
<Text
style={[a.text_md, 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={_(msg`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>
)
}
+9
View File
@@ -34,6 +34,7 @@ import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf'
import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice'
import {AvatarStackWithFetch} from '#/components/AvatarStack'
import {BotBadgeButton} from '#/components/BotBadge'
import {Button, ButtonText} from '#/components/Button'
import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist'
import {useDialogControl} from '#/components/Dialog'
@@ -374,6 +375,14 @@ function ProfilePreview({
<VerificationCheckButton profile={shadow} size="lg" />
</View>
)}
<View
style={[
{
marginTop: platform({web: 8, ios: 8, android: 10}),
},
]}>
<BotBadgeButton profile={shadow} size="lg" isMe />
</View>
</View>
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
{sanitizeHandle(profile.handle, '@')}
@@ -15,6 +15,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {type ComposerOptsPostRef} from '#/state/shell/composer'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {QuoteEmbed} from '#/components/Post/Embed'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
@@ -117,6 +118,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
/>
</View>
)}
<BotBadge profile={replyTo.author} />
</View>
<View style={[a.flex_row, a.gap_md]}>
<View style={[a.flex_1, a.flex_grow]}>
@@ -9,6 +9,7 @@ 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 {BotBadge} from '#/components/BotBadge'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
@@ -128,6 +129,7 @@ function AutocompleteProfileCard({
/>
</View>
)}
<BotBadge profile={profile} />
</View>
</View>
<Text
@@ -47,6 +47,7 @@ import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import * as Toast from '#/view/com/util/Toast'
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, platform, useTheme} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
@@ -264,6 +265,7 @@ let NotificationFeedItem = ({
/>
</View>
)}
<BotBadge profile={firstAuthor.profile} />
</InlineLinkText>
</ProfileHoverCard>
)
@@ -1063,6 +1065,7 @@ function ExpandedAuthorCard({author}: {author: Author}) {
/>
</View>
)}
<BotBadge profile={author.profile} />
<Text
numberOfLines={1}
style={[
+11
View File
@@ -14,6 +14,7 @@ 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 {BotBadge} from '#/components/BotBadge'
import {WebOnlyInlineLinkText} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Text} from '#/components/Typography'
@@ -124,6 +125,16 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
/>
</View>
)}
<View
style={[
a.pl_xs,
a.self_center,
{
marginTop: -1,
},
]}>
<BotBadge profile={author} />
</View>
<MaybeLinkText
emoji
numberOfLines={1}
+2
View File
@@ -23,6 +23,7 @@ import {formatCount} from '#/view/com/util/numeric/format'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {NavSignupCard} from '#/view/shell/NavSignupCard'
import {atoms as a, tokens, useTheme, web} from '#/alf'
import {BotBadge} from '#/components/BotBadge'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {
@@ -108,6 +109,7 @@ let DrawerProfileCard = ({
/>
</View>
)}
{profile && <BotBadge profile={profile} size={16} />}
</View>
<Text
emoji