diff --git a/bskyembed/src/components/post.tsx b/bskyembed/src/components/post.tsx index be3350485d..fb32250464 100644 --- a/bskyembed/src/components/post.tsx +++ b/bskyembed/src/components/post.tsx @@ -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 && ( + + )} ( + + + +) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 03ff43d739..7ef150e9e5 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -103,6 +103,7 @@ import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacyS import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' +import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings' import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings' @@ -403,6 +404,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { requireAuth: true, }} /> + AutomationLabelSettingsScreen} + options={{ + title: title(msg`Automation Label`), + requireAuth: true, + }} + /> PrivacyAndSecuritySettingsScreen} diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 28c3150c26..d3d5d88f92 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -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': {} diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx index ad0f44809d..2e5e39aaa4 100644 --- a/src/components/AccountList.tsx +++ b/src/components/AccountList.tsx @@ -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({ /> )} + {profile && ( + + + + )} + + + + + + {description} + + + + {isOwn ? ( + + ) : null} + + + + ) +} diff --git a/src/components/BotBadge.tsx b/src/components/BotBadge.tsx new file mode 100644 index 0000000000..8d8783e5ec --- /dev/null +++ b/src/components/BotBadge.tsx @@ -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 ( + + + + ) +} + +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 ( + <> + + + + ) +} diff --git a/src/components/PostControls/ShareMenu/RecentChats.tsx b/src/components/PostControls/ShareMenu/RecentChats.tsx index 3ba2682239..07943d073c 100644 --- a/src/components/PostControls/ShareMenu/RecentChats.tsx +++ b/src/components/PostControls/ShareMenu/RecentChats.tsx @@ -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({ /> )} + ) diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 1cbfc2b50d..0e489c5aac 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -30,6 +30,7 @@ import { useTheme, type ViewStyleProp, } from '#/alf' +import {BotBadge} from '#/components/BotBadge' import { Button, ButtonIcon, @@ -272,6 +273,14 @@ function InlineNameAndHandle({ /> )} + + + )} + + + ) } diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 8586607e52..938bdf330c 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -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({ /> )} + diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index afdeee545e..1f87c77371 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -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({ /> )} + {!isDeletedAccount && ( !l.val.startsWith('!')) + labels = labels.filter( + l => + !l.val.startsWith('!') && + !(l.val === 'bot' && l.src === currentAccount.did), + ) if (!labels.length) { return null } diff --git a/src/lib/bots.ts b/src/lib/bots.ts new file mode 100644 index 0000000000..0f9a24baae --- /dev/null +++ b/src/lib/bots.ts @@ -0,0 +1,10 @@ +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 + ) +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 0a3a0d5458..5bb7265709 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -50,6 +50,7 @@ export type CommonNavigatorParams = { AccessibilitySettings: undefined AppearanceSettings: undefined AccountSettings: undefined + AutomationLabelSettings: undefined PrivacyAndSecuritySettings: undefined ActivityPrivacySettings: undefined ContentAndMediaSettings: undefined diff --git a/src/routes.ts b/src/routes.ts index f325539c71..75eda461c8 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -50,6 +50,7 @@ export const router = new Router({ 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', diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index f09cf23007..30985b21f4 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -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({ /> )} + {lastMessageSentAt && ( diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 1531784aee..80ebec2b8c 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -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({ + + + + diff --git a/src/screens/Search/components/SearchHistory.tsx b/src/screens/Search/components/SearchHistory.tsx index 5b463ae75d..6cae572c93 100644 --- a/src/screens/Search/components/SearchHistory.tsx +++ b/src/screens/Search/components/SearchHistory.tsx @@ -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({ /> )} +