Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53c668fd89 | |||
| 4742b0b8e1 | |||
| 35d3037dee | |||
| fd1e4a0b0b | |||
| 2a018a4c42 | |||
| ab9f2875a0 | |||
| 4f6f710e95 | |||
| 39c3d0647e | |||
| eb5704e70a | |||
| 73f9f686d1 | |||
| d5d3076dda | |||
| 423f1a7ad7 | |||
| 2a779a7008 | |||
| 3e413e151d | |||
| 13486b0fca | |||
| 443cb9859c | |||
| c4b1561f83 | |||
| bd05fafa27 | |||
| 3dbdca5939 | |||
| 4ad5a55fa6 | |||
| 2a06d67fb4 | |||
| fbb10d8a74 | |||
| c97128c106 | |||
| 7f4c81a837 | |||
| aa1fbf4628 |
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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}
|
||||
|
||||
@@ -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': {}
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export const Context = createContext<DialogContextProps>({
|
||||
disableDrag: false,
|
||||
setDisableDrag: () => {},
|
||||
isWithinDialog: false,
|
||||
type: 'sheet',
|
||||
})
|
||||
Context.displayName = 'DialogContext'
|
||||
|
||||
|
||||
+109
-52
@@ -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],
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
])}>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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={[
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user