about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-03-15 12:11:34 +0000
committerSamuel Newman <mozzius@protonmail.com>2024-03-15 12:11:34 +0000
commitf71ec52517fb32d0f3dd1a1a8aa1da1949a752d5 (patch)
tree3c47bf7e5252ac179add92c5b19d3635a0bd3f49 /src
parent8316f97e274504c3fc10dc22017ecbf958727c22 (diff)
parent4f8381678da505737a96b7420c0f1ea8329f3672 (diff)
downloadvoidsky-f71ec52517fb32d0f3dd1a1a8aa1da1949a752d5.tar.zst
Merge remote-tracking branch 'origin/main' into samuel/alf-login
Diffstat (limited to 'src')
-rw-r--r--src/lib/statsig/events.ts44
-rw-r--r--src/lib/statsig/statsig.tsx8
-rw-r--r--src/state/queries/post.ts47
-rw-r--r--src/state/queries/profile.ts17
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollowsItem.tsx7
-rw-r--r--src/view/com/composer/Composer.tsx11
-rw-r--r--src/view/com/notifications/FeedItem.tsx1
-rw-r--r--src/view/com/pager/FixedTouchableHighlight.tsx42
-rw-r--r--src/view/com/post-thread/PostThreadFollowBtn.tsx5
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx5
-rw-r--r--src/view/com/post/Post.tsx3
-rw-r--r--src/view/com/posts/Feed.tsx16
-rw-r--r--src/view/com/posts/FeedItem.tsx2
-rw-r--r--src/view/com/posts/FeedSlice.tsx6
-rw-r--r--src/view/com/profile/FollowButton.tsx7
-rw-r--r--src/view/com/profile/ProfileCard.tsx4
-rw-r--r--src/view/com/profile/ProfileHeader.tsx5
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx5
-rw-r--r--src/view/com/profile/ProfileMenu.tsx28
-rw-r--r--src/view/com/util/Link.tsx32
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx30
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx9
22 files changed, 224 insertions, 110 deletions
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts
index bc647710c..fa7e597fb 100644
--- a/src/lib/statsig/events.ts
+++ b/src/lib/statsig/events.ts
@@ -1,5 +1,47 @@
-export type Events = {
+export type LogEvents = {
   init: {
     initMs: number
   }
+  'feed:endReached': {
+    feedType: string
+    itemCount: number
+  }
+  'post:create': {
+    imageCount: number
+    isReply: boolean
+    hasLink: boolean
+    hasQuote: boolean
+    langs: string
+    logContext: 'Composer'
+  }
+  'post:like': {
+    logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
+  }
+  'post:repost': {
+    logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
+  }
+  'post:unlike': {
+    logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
+  }
+  'post:unrepost': {
+    logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
+  }
+  'profile:follow': {
+    logContext:
+      | 'RecommendedFollowsItem'
+      | 'PostThreadItem'
+      | 'ProfileCard'
+      | 'ProfileHeader'
+      | 'ProfileHeaderSuggestedFollows'
+      | 'ProfileMenu'
+  }
+  'profile:unfollow': {
+    logContext:
+      | 'RecommendedFollowsItem'
+      | 'PostThreadItem'
+      | 'ProfileCard'
+      | 'ProfileHeader'
+      | 'ProfileHeaderSuggestedFollows'
+      | 'ProfileMenu'
+  }
 }
diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx
index a46cef4da..5745d204a 100644
--- a/src/lib/statsig/statsig.tsx
+++ b/src/lib/statsig/statsig.tsx
@@ -6,7 +6,9 @@ import {
 } from 'statsig-react-native-expo'
 import {useSession} from '../../state/session'
 import {sha256} from 'js-sha256'
-import {Events} from './events'
+import {LogEvents} from './events'
+
+export type {LogEvents}
 
 const statsigOptions = {
   environment: {
@@ -31,9 +33,9 @@ export function attachRouteToLogEvents(
   getCurrentRouteName = getRouteName
 }
 
-export function logEvent<E extends keyof Events>(
+export function logEvent<E extends keyof LogEvents>(
   eventName: E & string,
-  rawMetadata?: Events[E] & FlatJSONRecord,
+  rawMetadata: LogEvents[E] & FlatJSONRecord,
 ) {
   const fullMetadata = {
     ...rawMetadata,
diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts
index eb59f7da4..e3682e304 100644
--- a/src/state/queries/post.ts
+++ b/src/state/queries/post.ts
@@ -5,6 +5,7 @@ import {Shadow} from '#/state/cache/types'
 import {getAgent} from '#/state/session'
 import {updatePostShadow} from '#/state/cache/post-shadow'
 import {track} from '#/lib/analytics/analytics'
+import {logEvent, LogEvents} from '#/lib/statsig/statsig'
 import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
 
 export const RQKEY = (postUri: string) => ['post', postUri]
@@ -58,12 +59,14 @@ export function useGetPost() {
 
 export function usePostLikeMutationQueue(
   post: Shadow<AppBskyFeedDefs.PostView>,
+  logContext: LogEvents['post:like']['logContext'] &
+    LogEvents['post:unlike']['logContext'],
 ) {
   const postUri = post.uri
   const postCid = post.cid
   const initialLikeUri = post.viewer?.like
-  const likeMutation = usePostLikeMutation()
-  const unlikeMutation = usePostUnlikeMutation()
+  const likeMutation = usePostLikeMutation(logContext)
+  const unlikeMutation = usePostUnlikeMutation(logContext)
 
   const queueToggle = useToggleMutationQueue({
     initialState: initialLikeUri,
@@ -111,22 +114,30 @@ export function usePostLikeMutationQueue(
   return [queueLike, queueUnlike]
 }
 
-function usePostLikeMutation() {
+function usePostLikeMutation(logContext: LogEvents['post:like']['logContext']) {
   return useMutation<
     {uri: string}, // responds with the uri of the like
     Error,
     {uri: string; cid: string} // the post's uri and cid
   >({
-    mutationFn: post => getAgent().like(post.uri, post.cid),
+    mutationFn: post => {
+      logEvent('post:like', {logContext})
+      return getAgent().like(post.uri, post.cid)
+    },
     onSuccess() {
       track('Post:Like')
     },
   })
 }
 
-function usePostUnlikeMutation() {
+function usePostUnlikeMutation(
+  logContext: LogEvents['post:unlike']['logContext'],
+) {
   return useMutation<void, Error, {postUri: string; likeUri: string}>({
-    mutationFn: ({likeUri}) => getAgent().deleteLike(likeUri),
+    mutationFn: ({likeUri}) => {
+      logEvent('post:unlike', {logContext})
+      return getAgent().deleteLike(likeUri)
+    },
     onSuccess() {
       track('Post:Unlike')
     },
@@ -135,12 +146,14 @@ function usePostUnlikeMutation() {
 
 export function usePostRepostMutationQueue(
   post: Shadow<AppBskyFeedDefs.PostView>,
+  logContext: LogEvents['post:repost']['logContext'] &
+    LogEvents['post:unrepost']['logContext'],
 ) {
   const postUri = post.uri
   const postCid = post.cid
   const initialRepostUri = post.viewer?.repost
-  const repostMutation = usePostRepostMutation()
-  const unrepostMutation = usePostUnrepostMutation()
+  const repostMutation = usePostRepostMutation(logContext)
+  const unrepostMutation = usePostUnrepostMutation(logContext)
 
   const queueToggle = useToggleMutationQueue({
     initialState: initialRepostUri,
@@ -188,22 +201,32 @@ export function usePostRepostMutationQueue(
   return [queueRepost, queueUnrepost]
 }
 
-function usePostRepostMutation() {
+function usePostRepostMutation(
+  logContext: LogEvents['post:repost']['logContext'],
+) {
   return useMutation<
     {uri: string}, // responds with the uri of the repost
     Error,
     {uri: string; cid: string} // the post's uri and cid
   >({
-    mutationFn: post => getAgent().repost(post.uri, post.cid),
+    mutationFn: post => {
+      logEvent('post:repost', {logContext})
+      return getAgent().repost(post.uri, post.cid)
+    },
     onSuccess() {
       track('Post:Repost')
     },
   })
 }
 
-function usePostUnrepostMutation() {
+function usePostUnrepostMutation(
+  logContext: LogEvents['post:unrepost']['logContext'],
+) {
   return useMutation<void, Error, {postUri: string; repostUri: string}>({
-    mutationFn: ({repostUri}) => getAgent().deleteRepost(repostUri),
+    mutationFn: ({repostUri}) => {
+      logEvent('post:unrepost', {logContext})
+      return getAgent().deleteRepost(repostUri)
+    },
     onSuccess() {
       track('Post:Unrepost')
     },
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index e81ea0f3f..3c9e3e41c 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -26,6 +26,7 @@ import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
 import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
 import {STALE} from '#/state/queries'
 import {track} from '#/lib/analytics/analytics'
+import {logEvent, LogEvents} from '#/lib/statsig/statsig'
 import {ThreadNode} from './post-thread'
 
 export const RQKEY = (did: string) => ['profile', did]
@@ -186,11 +187,13 @@ export function useProfileUpdateMutation() {
 
 export function useProfileFollowMutationQueue(
   profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
+  logContext: LogEvents['profile:follow']['logContext'] &
+    LogEvents['profile:unfollow']['logContext'],
 ) {
   const did = profile.did
   const initialFollowingUri = profile.viewer?.following
-  const followMutation = useProfileFollowMutation()
-  const unfollowMutation = useProfileUnfollowMutation()
+  const followMutation = useProfileFollowMutation(logContext)
+  const unfollowMutation = useProfileUnfollowMutation(logContext)
 
   const queueToggle = useToggleMutationQueue({
     initialState: initialFollowingUri,
@@ -237,9 +240,12 @@ export function useProfileFollowMutationQueue(
   return [queueFollow, queueUnfollow]
 }
 
-function useProfileFollowMutation() {
+function useProfileFollowMutation(
+  logContext: LogEvents['profile:follow']['logContext'],
+) {
   return useMutation<{uri: string; cid: string}, Error, {did: string}>({
     mutationFn: async ({did}) => {
+      logEvent('profile:follow', {logContext})
       return await getAgent().follow(did)
     },
     onSuccess(data, variables) {
@@ -248,9 +254,12 @@ function useProfileFollowMutation() {
   })
 }
 
-function useProfileUnfollowMutation() {
+function useProfileUnfollowMutation(
+  logContext: LogEvents['profile:unfollow']['logContext'],
+) {
   return useMutation<void, Error, {did: string; followUri: string}>({
     mutationFn: async ({followUri}) => {
+      logEvent('profile:unfollow', {logContext})
       track('Profile:Unfollow', {username: followUri})
       return await getAgent().deleteFollow(followUri)
     },
diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
index 07001068c..5f81a4d65 100644
--- a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
@@ -56,7 +56,7 @@ export function RecommendedFollowsItem({
   )
 }
 
-export function ProfileCard({
+function ProfileCard({
   profile,
   onFollowStateChange,
   moderation,
@@ -72,7 +72,10 @@ export function ProfileCard({
   const pal = usePalette('default')
   const [addingMoreSuggestions, setAddingMoreSuggestions] =
     React.useState(false)
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'RecommendedFollowsItem',
+  )
 
   const onToggleFollow = React.useCallback(async () => {
     try {
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index ef965b271..97f8e5194 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -65,6 +65,7 @@ import {logger} from '#/logger'
 import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
 import * as Prompt from '#/components/Prompt'
 import {useDialogStateControlContext} from 'state/dialogs'
+import {logEvent} from '#/lib/statsig/statsig'
 
 type Props = ComposerOpts
 export const ComposePost = observer(function ComposePost({
@@ -255,6 +256,16 @@ export const ComposePost = observer(function ComposePost({
       setIsProcessing(false)
       return
     } finally {
+      if (postUri) {
+        logEvent('post:create', {
+          imageCount: gallery.size,
+          isReply: replyTo != null,
+          hasLink: extLink != null,
+          hasQuote: quote != null,
+          langs: langPrefs.postLanguage,
+          logContext: 'Composer',
+        })
+      }
       track('Create Post', {
         imageCount: gallery.size,
       })
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 45166fe3c..a46870265 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -182,7 +182,6 @@ let FeedItem = ({
       testID={`feedItem-by-${item.notification.author.handle}`}
       style={[
         styles.outer,
-        pal.view,
         pal.border,
         item.notification.isRead
           ? undefined
diff --git a/src/view/com/pager/FixedTouchableHighlight.tsx b/src/view/com/pager/FixedTouchableHighlight.tsx
deleted file mode 100644
index d07196975..000000000
--- a/src/view/com/pager/FixedTouchableHighlight.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-// FixedTouchableHighlight.tsx
-import React, {ComponentProps, useRef} from 'react'
-import {GestureResponderEvent, TouchableHighlight} from 'react-native'
-
-type Position = {pageX: number; pageY: number}
-
-export default function FixedTouchableHighlight({
-  onPress,
-  onPressIn,
-  ...props
-}: ComponentProps<typeof TouchableHighlight>) {
-  const _touchActivatePositionRef = useRef<Position | null>(null)
-
-  function _onPressIn(e: GestureResponderEvent) {
-    const {pageX, pageY} = e.nativeEvent
-
-    _touchActivatePositionRef.current = {
-      pageX,
-      pageY,
-    }
-
-    onPressIn?.(e)
-  }
-
-  function _onPress(e: GestureResponderEvent) {
-    const {pageX, pageY} = e.nativeEvent
-
-    const absX = Math.abs(_touchActivatePositionRef.current?.pageX! - pageX)
-    const absY = Math.abs(_touchActivatePositionRef.current?.pageY! - pageY)
-
-    const dragged = absX > 2 || absY > 2
-    if (!dragged) {
-      onPress?.(e)
-    }
-  }
-
-  return (
-    <TouchableHighlight onPressIn={_onPressIn} onPress={_onPress} {...props}>
-      {props.children}
-    </TouchableHighlight>
-  )
-}
diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx
index e5b747cc9..45c3771f5 100644
--- a/src/view/com/post-thread/PostThreadFollowBtn.tsx
+++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx
@@ -42,7 +42,10 @@ function PostThreadFollowBtnLoaded({
   const {isTabletOrDesktop} = useWebMediaQueries()
   const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> =
     useProfileShadow(profileUnshadowed)
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'PostThreadItem',
+  )
   const requireAuth = useRequireAuth()
 
   const isFollowing = !!profile.viewer?.following
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 9522ea6a0..cd746f9a9 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -407,6 +407,7 @@ let PostThreadItemLoaded = ({
                 record={record}
                 richText={richText}
                 onPressReply={onPressReply}
+                logContext="PostThreadItem"
               />
             </View>
           </View>
@@ -431,7 +432,6 @@ let PostThreadItemLoaded = ({
           <PostHider
             testID={`postThreadItem-by-${post.author.handle}`}
             href={postHref}
-            style={[pal.view]}
             moderation={moderation.content}
             iconSize={isThreadedChild ? 26 : 38}
             iconStyles={
@@ -560,6 +560,7 @@ let PostThreadItemLoaded = ({
                   record={record}
                   richText={richText}
                   onPressReply={onPressReply}
+                  logContext="PostThreadItem"
                 />
               </View>
             </View>
@@ -620,7 +621,6 @@ function PostOuterWrapper({
     return (
       <View
         style={[
-          pal.view,
           pal.border,
           styles.cursor,
           {
@@ -648,7 +648,6 @@ function PostOuterWrapper({
     <View
       style={[
         styles.outer,
-        pal.view,
         pal.border,
         showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
         styles.cursor,
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 5fa4da84e..7e53eb271 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -133,7 +133,7 @@ function PostInner({
   }, [setLimitLines])
 
   return (
-    <Link href={itemHref} style={[styles.outer, pal.view, pal.border, style]}>
+    <Link href={itemHref} style={[styles.outer, pal.border, style]}>
       {showReplyLine && <View style={styles.replyLine} />}
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
@@ -220,6 +220,7 @@ function PostInner({
             record={record}
             richText={richText}
             onPressReply={onPressReply}
+            logContext="Post"
           />
         </View>
       </View>
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index cd3e98785..b86646a4d 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -33,6 +33,7 @@ import {useLingui} from '@lingui/react'
 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
 import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {logEvent} from '#/lib/statsig/statsig'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -223,16 +224,29 @@ let Feed = ({
     setIsPTRing(false)
   }, [refetch, track, setIsPTRing, onHasNew])
 
+  const feedType = feed.split('|')[0]
   const onEndReached = React.useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
 
+    logEvent('feed:endReached', {
+      feedType: feedType,
+      itemCount: feedItems.length,
+    })
     track('Feed:onEndReached')
     try {
       await fetchNextPage()
     } catch (err) {
       logger.error('Failed to load more posts', {message: err})
     }
-  }, [isFetching, hasNextPage, isError, fetchNextPage, track])
+  }, [
+    isFetching,
+    hasNextPage,
+    isError,
+    fetchNextPage,
+    track,
+    feedType,
+    feedItems.length,
+  ])
 
   const onPressTryAgain = React.useCallback(() => {
     refetch()
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 7d29703e2..f3911da60 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -144,7 +144,6 @@ let FeedItemInner = ({
 
   const outerStyles = [
     styles.outer,
-    pal.view,
     {
       borderColor: pal.colors.border,
       paddingBottom:
@@ -310,6 +309,7 @@ let FeedItemInner = ({
             showAppealLabelItem={
               post.author.did === currentAccount?.did && isModeratedPost
             }
+            logContext="FeedItem"
           />
         </View>
       </View>
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index 84edee4a1..49e48aa20 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -78,11 +78,7 @@ function ViewFullThread({slice}: {slice: FeedPostSlice}) {
   }, [slice.rootUri])
 
   return (
-    <Link
-      style={[pal.view, styles.viewFullThread]}
-      href={itemHref}
-      asAnchor
-      noFeedback>
+    <Link style={[styles.viewFullThread]} href={itemHref} asAnchor noFeedback>
       <View style={styles.viewFullThreadDots}>
         <Svg width="4" height="40">
           <Line
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 9cc635b66..7b090ffeb 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -13,13 +13,18 @@ export function FollowButton({
   followedType = 'default',
   profile,
   labelStyle,
+  logContext,
 }: {
   unfollowedType?: ButtonType
   followedType?: ButtonType
   profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
   labelStyle?: StyleProp<TextStyle>
+  logContext: 'ProfileCard'
 }) {
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    logContext,
+  )
   const {_} = useLingui()
 
   const onPressFollow = async () => {
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 266adc51d..019e6c10e 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -230,7 +230,9 @@ export function ProfileCardWithFollowBtn({
       renderButton={
         isMe
           ? undefined
-          : profileShadow => <FollowButton profile={profileShadow} />
+          : profileShadow => (
+              <FollowButton profile={profileShadow} logContext="ProfileCard" />
+            )
       }
     />
   )
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 75e06eb9b..17dc5ce1b 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -103,7 +103,10 @@ let ProfileHeader = ({
   const invalidHandle = isInvalidHandle(profile.handle)
   const {isDesktop} = useWebMediaQueries()
   const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'ProfileHeader',
+  )
   const [__, queueUnblock] = useProfileBlockMutationQueue(profile)
   const unblockPromptControl = Prompt.usePromptControl()
   const moderation = useMemo(
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index 6edc61fcf..8f2c89499 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -170,7 +170,10 @@ function SuggestedFollow({
   const pal = usePalette('default')
   const moderationOpts = useModerationOpts()
   const profile = useProfileShadow(profileUnshadowed)
-  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'ProfileHeaderSuggestedFollows',
+  )
 
   const onPressFollow = React.useCallback(async () => {
     try {
diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx
index 4153b819e..0baa4f394 100644
--- a/src/view/com/profile/ProfileMenu.tsx
+++ b/src/view/com/profile/ProfileMenu.tsx
@@ -52,9 +52,17 @@ let ProfileMenu = ({
 
   const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
   const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
-  const [, queueUnfollow] = useProfileFollowMutationQueue(profile)
+  const [, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'ProfileMenu',
+  )
 
   const blockPromptControl = Prompt.usePromptControl()
+  const loggedOutWarningPromptControl = Prompt.usePromptControl()
+
+  const showLoggedOutWarning = React.useMemo(() => {
+    return !!profile.labels?.find(label => label.val === '!no-unauthenticated')
+  }, [profile.labels])
 
   const invalidateProfileQuery = React.useCallback(() => {
     queryClient.invalidateQueries({
@@ -189,7 +197,13 @@ let ProfileMenu = ({
             <Menu.Item
               testID="profileHeaderDropdownShareBtn"
               label={_(msg`Share`)}
-              onPress={onPressShare}>
+              onPress={() => {
+                if (showLoggedOutWarning) {
+                  loggedOutWarningPromptControl.open()
+                } else {
+                  onPressShare()
+                }
+              }}>
               <Menu.ItemText>
                 <Trans>Share</Trans>
               </Menu.ItemText>
@@ -307,6 +321,16 @@ let ProfileMenu = ({
         }
         confirmButtonColor={profile.viewer?.blocking ? undefined : 'negative'}
       />
+
+      <Prompt.Basic
+        control={loggedOutWarningPromptControl}
+        title={_(msg`Note about sharing`)}
+        description={_(
+          msg`This profile is only visible to logged-in users. It won't be visible to people who aren't logged in.`,
+        )}
+        onConfirm={onPressShare}
+        confirmButtonCta={_(msg`Share anyway`)}
+      />
     </EventStopper>
   )
 }
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index f45622488..7468111b5 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -8,7 +8,6 @@ import {
   View,
   ViewStyle,
   Pressable,
-  TouchableWithoutFeedback,
   TouchableOpacity,
 } from 'react-native'
 import {useLinkProps, StackActions} from '@react-navigation/native'
@@ -23,7 +22,6 @@ import {
 import {isAndroid, isWeb} from 'platform/detection'
 import {sanitizeUrl} from '@braintree/sanitize-url'
 import {PressableWithHover} from './PressableWithHover'
-import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
 import {useModalControls} from '#/state/modals'
 import {useOpenLink} from '#/state/preferences/in-app-browser'
 import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper'
@@ -31,6 +29,7 @@ import {
   DebouncedNavigationProp,
   useNavigationDeduped,
 } from 'lib/hooks/useNavigationDeduped'
+import {useTheme} from '#/alf'
 
 type Event =
   | React.MouseEvent<HTMLAnchorElement, MouseEvent>
@@ -63,6 +62,7 @@ export const Link = memo(function Link({
   navigationAction,
   ...props
 }: Props) {
+  const t = useTheme()
   const {closeModal} = useModalControls()
   const navigation = useNavigationDeduped()
   const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
@@ -85,37 +85,23 @@ export const Link = memo(function Link({
   )
 
   if (noFeedback) {
-    if (isAndroid) {
-      // workaround for Android not working well with left/right swipe gestures and TouchableWithoutFeedback
-      // https://github.com/callstack/react-native-pager-view/issues/424
-      return (
-        <FixedTouchableHighlight
-          testID={testID}
-          onPress={onPress}
-          // @ts-ignore web only -prf
-          href={asAnchor ? sanitizeUrl(href) : undefined}
-          accessible={accessible}
-          accessibilityRole="link"
-          {...props}>
-          <View style={style}>
-            {children ? children : <Text>{title || 'link'}</Text>}
-          </View>
-        </FixedTouchableHighlight>
-      )
-    }
     return (
       <WebAuxClickWrapper>
-        <TouchableWithoutFeedback
+        <Pressable
           testID={testID}
           onPress={onPress}
           accessible={accessible}
           accessibilityRole="link"
-          {...props}>
+          {...props}
+          android_ripple={{
+            color: t.atoms.bg_contrast_25.backgroundColor,
+          }}
+          unstable_pressDelay={isAndroid ? 90 : undefined}>
           {/* @ts-ignore web only -prf */}
           <View style={style} href={anchorHref}>
             {children ? children : <Text>{title || 'link'}</Text>}
           </View>
-        </TouchableWithoutFeedback>
+        </Pressable>
       </WebAuxClickWrapper>
     )
   }
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index 84a047c40..8fc3d9ea6 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -85,11 +85,13 @@ let PostDropdownBtn = ({
   const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
   const deletePromptControl = useDialogControl()
   const hidePromptControl = useDialogControl()
+  const loggedOutWarningPromptControl = useDialogControl()
 
   const rootUri = record.reply?.root?.uri || postUri
   const isThreadMuted = mutedThreads.includes(rootUri)
   const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
   const isAuthor = postAuthor.did === currentAccount?.did
+
   const href = React.useMemo(() => {
     const urip = new AtUri(postUri)
     return makeProfileLink(postAuthor, 'post', urip.rkey)
@@ -167,6 +169,17 @@ let PostDropdownBtn = ({
     hidePost({uri: postUri})
   }, [postUri, hidePost])
 
+  const shouldShowLoggedOutWarning = React.useMemo(() => {
+    return !!postAuthor.labels?.find(
+      label => label.val === '!no-unauthenticated',
+    )
+  }, [postAuthor])
+
+  const onSharePost = React.useCallback(() => {
+    const url = toShareUrl(href)
+    shareUrl(url)
+  }, [href])
+
   return (
     <EventStopper onKeyDown={false}>
       <Menu.Root>
@@ -217,8 +230,11 @@ let PostDropdownBtn = ({
               testID="postDropdownShareBtn"
               label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
               onPress={() => {
-                const url = toShareUrl(href)
-                shareUrl(url)
+                if (shouldShowLoggedOutWarning) {
+                  loggedOutWarningPromptControl.open()
+                } else {
+                  onSharePost()
+                }
               }}>
               <Menu.ItemText>
                 {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
@@ -342,6 +358,16 @@ let PostDropdownBtn = ({
         onConfirm={onHidePost}
         confirmButtonCta={_(msg`Hide`)}
       />
+
+      <Prompt.Basic
+        control={loggedOutWarningPromptControl}
+        title={_(msg`Note about sharing`)}
+        description={_(
+          msg`This post is only visible to logged-in users. It won't be visible to people who aren't logged in.`,
+        )}
+        onConfirm={onSharePost}
+        confirmButtonCta={_(msg`Share anyway`)}
+      />
     </EventStopper>
   )
 }
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 1e26eecce..c96954a11 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -44,6 +44,7 @@ let PostCtrls = ({
   showAppealLabelItem,
   style,
   onPressReply,
+  logContext,
 }: {
   big?: boolean
   post: Shadow<AppBskyFeedDefs.PostView>
@@ -52,13 +53,17 @@ let PostCtrls = ({
   showAppealLabelItem?: boolean
   style?: StyleProp<ViewStyle>
   onPressReply: () => void
+  logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
 }): React.ReactNode => {
   const theme = useTheme()
   const {_} = useLingui()
   const {openComposer} = useComposerControls()
   const {closeModal} = useModalControls()
-  const [queueLike, queueUnlike] = usePostLikeMutationQueue(post)
-  const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(post)
+  const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
+  const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
+    post,
+    logContext,
+  )
   const requireAuth = useRequireAuth()
 
   const defaultCtrlColor = React.useMemo(