diff --git a/assets/icons/personPlus_stroke2_corner0_rounded.svg b/assets/icons/personPlus_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..118268bf97
--- /dev/null
+++ b/assets/icons/personPlus_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index e22faa060c..982f422134 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -64,7 +64,7 @@ type NonTextElements =
export type ButtonProps = Pick<
PressableProps,
- 'disabled' | 'onPress' | 'testID' | 'onLongPress'
+ 'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop'
> &
AccessibilityProps &
VariantProps & {
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index 79ca34f17e..3f680120b8 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -26,9 +26,11 @@ import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
-import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
-import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
-import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
+import {
+ Person_Stroke2_Corner0_Rounded as Person,
+ PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
+ PersonX_Stroke2_Corner0_Rounded as PersonX,
+} from '#/components/icons/Person'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
diff --git a/src/components/icons/Person.tsx b/src/components/icons/Person.tsx
index 6d09148c9d..7fceada533 100644
--- a/src/components/icons/Person.tsx
+++ b/src/components/icons/Person.tsx
@@ -3,3 +3,15 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Person_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C3.917 15.521 7.242 12 12 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 19.5 21h-15a1 1 0 0 1-.996-1.094Z',
})
+
+export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z',
+})
+
+export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
+})
+
+export const PersonPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19c.71-2.909 3.092-5 6.322-5 .621 0 1.206.077 1.748.218a1 1 0 1 0 .504-1.936A8.931 8.931 0 0 0 12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H11a1 1 0 1 0 0-2H5.678ZM18 14a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z',
+})
diff --git a/src/components/icons/PersonCheck.tsx b/src/components/icons/PersonCheck.tsx
deleted file mode 100644
index 097271d89a..0000000000
--- a/src/components/icons/PersonCheck.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import {createSinglePathSVG} from './TEMPLATE'
-
-export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
- path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z',
-})
diff --git a/src/components/icons/PersonX.tsx b/src/components/icons/PersonX.tsx
deleted file mode 100644
index a015e13765..0000000000
--- a/src/components/icons/PersonX.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import {createSinglePathSVG} from './TEMPLATE'
-
-export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
- path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
-})
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts
index 42f4737f82..00444c18c4 100644
--- a/src/lib/statsig/events.ts
+++ b/src/lib/statsig/events.ts
@@ -115,6 +115,7 @@ export type LogEvents = {
| 'ProfileHeaderSuggestedFollows'
| 'ProfileMenu'
| 'ProfileHoverCard'
+ | 'AvatarButton'
}
'profile:unfollow': {
logContext:
@@ -126,6 +127,7 @@ export type LogEvents = {
| 'ProfileMenu'
| 'ProfileHoverCard'
| 'Chat'
+ | 'AvatarButton'
}
'chat:create': {
logContext: 'ProfileHeader' | 'NewChatDialog'
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index 2721871f35..4481935f77 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -1,4 +1,5 @@
export type Gate =
// Keep this alphabetic please.
| 'request_notifications_permission_after_onboarding_v2'
+ | 'show_avi_follow_button'
| 'show_follow_back_label_v2'
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 9d2985f155..981b4e72f5 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -40,6 +40,7 @@ import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {PostAlerts} from '../../../components/moderation/PostAlerts'
import {PostHider} from '../../../components/moderation/PostHider'
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
+import {AviFollowButton} from '../posts/AviFollowButton'
import {WhoCanReply} from '../threadgate/WhoCanReply'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Link, TextLink} from '../util/Link'
@@ -470,12 +471,16 @@ let PostThreadItemLoaded = ({
{/* If we are in threaded mode, the avatar is rendered in PostMeta */}
{!isThreadedChild && (
-
+
+
+
{showChildReplyLine && (
}
-
+
+
+
(null)
+ const navigation = useNavigation()
+
+ const name = sanitizeDisplayName(
+ profile.displayName || profile.handle,
+ moderation.ui('displayName'),
+ )
+ const isFollowing =
+ profile.viewer?.following ||
+ profile.did === followed ||
+ profile.did === currentAccount?.did
+
+ function onPress() {
+ follow()
+ setFollowed(profile.did)
+ Toast.show(_(msg`Following ${name}`))
+ }
+
+ const items: DropdownItem[] = [
+ {
+ label: _(msg`View profile`),
+ onPress: () => {
+ navigation.navigate('Profile', {name: profile.did})
+ },
+ icon: {
+ ios: {
+ name: 'arrow.up.right.square',
+ },
+ android: '',
+ web: ['far', 'arrow-up-right-from-square'],
+ },
+ },
+ {
+ label: _(msg`Follow ${name}`),
+ onPress: onPress,
+ icon: {
+ ios: {
+ name: 'person.badge.plus',
+ },
+ android: '',
+ web: ['far', 'user-plus'],
+ },
+ },
+ ]
+
+ return hasSession && gate('show_avi_follow_button') ? (
+
+ {children}
+
+ {!isFollowing && (
+
+ )}
+
+ ) : (
+ children
+ )
+}
diff --git a/src/view/com/posts/AviFollowButton.web.tsx b/src/view/com/posts/AviFollowButton.web.tsx
new file mode 100644
index 0000000000..6ad3c9f1fd
--- /dev/null
+++ b/src/view/com/posts/AviFollowButton.web.tsx
@@ -0,0 +1 @@
+export {Fragment as AviFollowButton} from 'react'
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 8077c29683..b10ffe19fa 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -41,6 +41,7 @@ import {PostEmbeds} from '../util/post-embeds'
import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
+import {AviFollowButton} from './AviFollowButton'
interface FeedItemProps {
record: AppBskyFeedPost.Record
@@ -284,13 +285,15 @@ let FeedItemInner = ({
-
+
+
+
{isThreadParent && (