about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package.json4
-rw-r--r--src/lib/api/feed-manip.ts8
-rw-r--r--src/screens/Search/Explore.tsx1
-rw-r--r--src/screens/VideoFeed/index.tsx16
-rw-r--r--src/state/feed-feedback.tsx18
-rw-r--r--src/state/queries/explore-feed-previews.tsx1
-rw-r--r--src/state/queries/post-feed.ts3
-rw-r--r--src/view/com/posts/PostFeed.tsx20
-rw-r--r--src/view/com/posts/PostFeedItem.tsx30
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx3
-rw-r--r--src/view/com/util/forms/PostDropdownBtnMenuItems.tsx111
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx53
-rw-r--r--src/view/screens/DebugMod.tsx1
-rw-r--r--yarn.lock116
14 files changed, 201 insertions, 184 deletions
diff --git a/package.json b/package.json
index 62762bfc7..fed0d4fc1 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
     "icons:optimize": "svgo -f ./assets/icons"
   },
   "dependencies": {
-    "@atproto/api": "^0.15.7",
+    "@atproto/api": "^0.15.8",
     "@bitdrift/react-native": "^0.6.8",
     "@braintree/sanitize-url": "^6.0.2",
     "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -219,7 +219,7 @@
     "zod": "^3.20.2"
   },
   "devDependencies": {
-    "@atproto/dev-env": "^0.3.131",
+    "@atproto/dev-env": "^0.3.132",
     "@babel/core": "^7.26.0",
     "@babel/preset-env": "^7.26.0",
     "@babel/runtime": "^7.26.0",
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index a1b2e2bc9..3309191f3 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -1,5 +1,5 @@
 import {
-  AppBskyActorDefs,
+  type AppBskyActorDefs,
   AppBskyEmbedRecord,
   AppBskyEmbedRecordWithMedia,
   AppBskyFeedDefs,
@@ -9,7 +9,7 @@ import {
 import * as bsky from '#/types/bsky'
 import {isPostInLanguage} from '../../locale/helpers'
 import {FALLBACK_MARKER_POST} from './feed/home'
-import {ReasonFeedSource} from './feed/types'
+import {type ReasonFeedSource} from './feed/types'
 
 type FeedViewPost = AppBskyFeedDefs.FeedViewPost
 
@@ -187,6 +187,10 @@ export class FeedViewPostsSlice {
     return this._feedPost.feedContext
   }
 
+  get reqId() {
+    return this._feedPost.reqId
+  }
+
   get isRepost() {
     const reason = this._feedPost.reason
     return AppBskyFeedDefs.isReasonRepost(reason)
diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx
index 8050d7f73..1aac68c43 100644
--- a/src/screens/Search/Explore.tsx
+++ b/src/screens/Search/Explore.tsx
@@ -882,6 +882,7 @@ export function Explore({
               record={subItem.record}
               reason={indexInSlice === 0 ? slice.reason : undefined}
               feedContext={slice.feedContext}
+              reqId={slice.reqId}
               moderation={subItem.moderation}
               parentAuthor={subItem.parentAuthor}
               showReplyTo={item.showReplyTo}
diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx
index aabfe4b20..047961766 100644
--- a/src/screens/VideoFeed/index.tsx
+++ b/src/screens/VideoFeed/index.tsx
@@ -178,6 +178,7 @@ type VideoItem = {
   post: AppBskyFeedDefs.PostView
   video: AppBskyEmbedVideo.View
   feedContext: string | undefined
+  reqId: string | undefined
 }
 
 function Feed() {
@@ -216,6 +217,7 @@ function Feed() {
           post: AppBskyFeedDefs.PostView
           video: AppBskyEmbedVideo.View
           feedContext: string | undefined
+          reqId: string | undefined
         }[] = []
         for (const slice of page.slices) {
           const feedPost = slice.items.find(
@@ -228,6 +230,7 @@ function Feed() {
               post: feedPost.post,
               video: feedPost.post.embed,
               feedContext: slice.feedContext,
+              reqId: slice.reqId,
             })
           }
         }
@@ -274,6 +277,7 @@ function Feed() {
           moderation={item.moderation}
           scrollGesture={scrollGesture}
           feedContext={item.feedContext}
+          reqId={item.reqId}
         />
       )
     },
@@ -470,6 +474,7 @@ let VideoItem = ({
   scrollGesture,
   moderation,
   feedContext,
+  reqId,
 }: {
   player?: VideoPlayer
   post: AppBskyFeedDefs.PostView
@@ -479,6 +484,7 @@ let VideoItem = ({
   scrollGesture: NativeGesture
   moderation?: ModerationDecision
   feedContext: string | undefined
+  reqId: string | undefined
 }): React.ReactNode => {
   const postShadow = usePostShadow(post)
   const {width, height} = useSafeAreaFrame()
@@ -490,9 +496,10 @@ let VideoItem = ({
         item: post.uri,
         event: 'app.bsky.feed.defs#interactionSeen',
         feedContext,
+        reqId,
       })
     }
-  }, [active, post.uri, feedContext, sendInteraction])
+  }, [active, post.uri, feedContext, reqId, sendInteraction])
 
   // TODO: high-performance android phones should also
   // be capable of rendering 3 video players, but currently
@@ -537,6 +544,7 @@ let VideoItem = ({
               scrollGesture={scrollGesture}
               moderation={moderation}
               feedContext={feedContext}
+              reqId={reqId}
             />
           )}
         </>
@@ -682,6 +690,7 @@ function Overlay({
   scrollGesture,
   moderation,
   feedContext,
+  reqId,
 }: {
   player?: VideoPlayer
   post: Shadow<AppBskyFeedDefs.PostView>
@@ -690,6 +699,7 @@ function Overlay({
   scrollGesture: NativeGesture
   moderation: ModerationDecision
   feedContext: string | undefined
+  reqId: string | undefined
 }) {
   const {_} = useLingui()
   const t = useTheme()
@@ -760,6 +770,7 @@ function Overlay({
                 player={player}
                 post={post}
                 feedContext={feedContext}
+                reqId={reqId}
               />
             )}
           </View>
@@ -1002,10 +1013,12 @@ function PlayPauseTapArea({
   player,
   post,
   feedContext,
+  reqId,
 }: {
   player: VideoPlayer
   post: Shadow<AppBskyFeedDefs.PostView>
   feedContext: string | undefined
+  reqId: string | undefined
 }) {
   const {_} = useLingui()
   const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -1036,6 +1049,7 @@ function PlayPauseTapArea({
         item: post.uri,
         event: 'app.bsky.feed.defs#interactionLike',
         feedContext,
+        reqId,
       })
     } else {
       doubleTapRef.current = setTimeout(togglePlayPause, 200)
diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx
index 2ad5ff91a..8880cb6b3 100644
--- a/src/state/feed-feedback.tsx
+++ b/src/state/feed-feedback.tsx
@@ -1,12 +1,15 @@
 import React from 'react'
-import {AppState, AppStateStatus} from 'react-native'
-import {AppBskyFeedDefs} from '@atproto/api'
+import {AppState, type AppStateStatus} from 'react-native'
+import {type AppBskyFeedDefs} from '@atproto/api'
 import throttle from 'lodash.throttle'
 
 import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants'
 import {logEvent} from '#/lib/statsig/statsig'
 import {logger} from '#/logger'
-import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
+import {
+  type FeedDescriptor,
+  type FeedPostSliceItem,
+} from '#/state/queries/post-feed'
 import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
 import {useAgent} from './session'
 
@@ -103,7 +106,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
         return
       }
       const items = getItemsForFeedback(feedItem)
-      for (const {item: postItem, feedContext} of items) {
+      for (const {item: postItem, feedContext, reqId} of items) {
         if (!history.current.has(postItem)) {
           history.current.add(postItem)
           queue.current.add(
@@ -111,6 +114,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
               item: postItem.uri,
               event: 'app.bsky.feed.defs#interactionSeen',
               feedContext,
+              reqId,
             }),
           )
           sendToFeed()
@@ -164,12 +168,12 @@ function isDiscoverFeed(feed: FeedDescriptor) {
 function toString(interaction: AppBskyFeedDefs.Interaction): string {
   return `${interaction.item}|${interaction.event}|${
     interaction.feedContext || ''
-  }`
+  }|${interaction.reqId || ''}`
 }
 
 function toInteraction(str: string): AppBskyFeedDefs.Interaction {
-  const [item, event, feedContext] = str.split('|')
-  return {item, event, feedContext}
+  const [item, event, feedContext, reqId] = str.split('|')
+  return {item, event, feedContext, reqId}
 }
 
 type AggregatedStats = {
diff --git a/src/state/queries/explore-feed-previews.tsx b/src/state/queries/explore-feed-previews.tsx
index 4cd7336c0..45bfc5c48 100644
--- a/src/state/queries/explore-feed-previews.tsx
+++ b/src/state/queries/explore-feed-previews.tsx
@@ -215,6 +215,7 @@ export function useFeedPreviews(
                 isFallbackMarker: false,
                 isIncompleteThread: item.isIncompleteThread,
                 feedContext: item.feedContext,
+                reqId: item.reqId,
                 reason: item.reason,
                 feedPostUri: item.feedPostUri,
                 items: item.items
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index f3fa13cfb..920892924 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -92,6 +92,7 @@ export interface FeedPostSlice {
   isIncompleteThread: boolean
   isFallbackMarker: boolean
   feedContext: string | undefined
+  reqId: string | undefined
   feedPostUri: string
   reason?:
     | AppBskyFeedDefs.ReasonRepost
@@ -316,6 +317,7 @@ export function usePostFeedQuery(
                     userActionHistory.seen(
                       slice.items.map(item => ({
                         feedContext: slice.feedContext,
+                        reqId: slice.reqId,
                         likeCount: item.post.likeCount ?? 0,
                         repostCount: item.post.repostCount ?? 0,
                         replyCount: item.post.replyCount ?? 0,
@@ -333,6 +335,7 @@ export function usePostFeedQuery(
                     isIncompleteThread: slice.isIncompleteThread,
                     isFallbackMarker: slice.isFallbackMarker,
                     feedContext: slice.feedContext,
+                    reqId: slice.reqId,
                     reason: slice.reason,
                     feedPostUri: slice.feedPostUri,
                     items: slice.items.map((item, i) => {
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index 732d0fcab..9aa4512a4 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -103,6 +103,7 @@ type FeedRow =
       items: FeedPostSliceItem[]
       sourceFeedUri: string
       feedContexts: (string | undefined)[]
+      reqIds: (string | undefined)[]
     }
   | {
       type: 'sliceViewFullThread'
@@ -134,16 +135,19 @@ export function getItemsForFeedback(feedRow: FeedRow):
   | {
       item: FeedPostSliceItem
       feedContext: string | undefined
+      reqId: string | undefined
     }[] {
   if (feedRow.type === 'sliceItem') {
     return feedRow.slice.items.map(item => ({
       item,
       feedContext: feedRow.slice.feedContext,
+      reqId: feedRow.slice.reqId,
     }))
   } else if (feedRow.type === 'videoGridRow') {
     return feedRow.items.map((item, i) => ({
       item,
       feedContext: feedRow.feedContexts[i],
+      reqId: feedRow.reqIds[i],
     }))
   } else {
     return []
@@ -398,6 +402,7 @@ let PostFeed = ({
           const videos: {
             item: FeedPostSliceItem
             feedContext: string | undefined
+            reqId: string | undefined
           }[] = []
           for (const page of data.pages) {
             for (const slice of page.slices) {
@@ -405,7 +410,11 @@ let PostFeed = ({
                 item => item.uri === slice.feedPostUri,
               )
               if (item && AppBskyEmbedVideo.isView(item.post.embed)) {
-                videos.push({item, feedContext: slice.feedContext})
+                videos.push({
+                  item,
+                  feedContext: slice.feedContext,
+                  reqId: slice.reqId,
+                })
               }
             }
           }
@@ -413,12 +422,17 @@ let PostFeed = ({
           const rows: {
             item: FeedPostSliceItem
             feedContext: string | undefined
+            reqId: string | undefined
           }[][] = []
           for (let i = 0; i < videos.length; i++) {
             const video = videos[i]
             const item = video.item
             const cols = gtMobile ? 3 : 2
-            const rowItem = {item, feedContext: video.feedContext}
+            const rowItem = {
+              item,
+              feedContext: video.feedContext,
+              reqId: video.reqId,
+            }
             if (i % cols === 0) {
               rows.push([rowItem])
             } else {
@@ -434,6 +448,7 @@ let PostFeed = ({
               items: row.map(r => r.item),
               sourceFeedUri: feedUriOrActorDid,
               feedContexts: row.map(r => r.feedContext),
+              reqIds: row.map(r => r.reqId),
             })
           }
         } else {
@@ -685,6 +700,7 @@ let PostFeed = ({
             record={item.record}
             reason={indexInSlice === 0 ? slice.reason : undefined}
             feedContext={slice.feedContext}
+            reqId={slice.reqId}
             moderation={item.moderation}
             parentAuthor={item.parentAuthor}
             showReplyTo={row.showReplyTo}
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
index ceb653b9c..2cc749404 100644
--- a/src/view/com/posts/PostFeedItem.tsx
+++ b/src/view/com/posts/PostFeedItem.tsx
@@ -70,6 +70,7 @@ interface FeedItemProps {
   isThreadLastChild?: boolean
   isThreadParent?: boolean
   feedContext: string | undefined
+  reqId: string | undefined
   hideTopBorder?: boolean
   isParentBlocked?: boolean
   isParentNotFound?: boolean
@@ -80,6 +81,7 @@ export function PostFeedItem({
   record,
   reason,
   feedContext,
+  reqId,
   moderation,
   parentAuthor,
   showReplyTo,
@@ -117,6 +119,7 @@ export function PostFeedItem({
         record={record}
         reason={reason}
         feedContext={feedContext}
+        reqId={reqId}
         richText={richText}
         parentAuthor={parentAuthor}
         showReplyTo={showReplyTo}
@@ -140,6 +143,7 @@ let FeedItemInner = ({
   record,
   reason,
   feedContext,
+  reqId,
   richText,
   moderation,
   parentAuthor,
@@ -171,11 +175,12 @@ let FeedItemInner = ({
   }, [post.uri, post.author])
   const {sendInteraction} = useFeedFeedbackContext()
 
-  const onPressReply = useCallback(() => {
+  const onPressReply = () => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#interactionReply',
       feedContext,
+      reqId,
     })
     openComposer({
       replyTo: {
@@ -187,40 +192,44 @@ let FeedItemInner = ({
         moderation,
       },
     })
-  }, [post, record, openComposer, moderation, sendInteraction, feedContext])
+  }
 
-  const onOpenAuthor = useCallback(() => {
+  const onOpenAuthor = () => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#clickthroughAuthor',
       feedContext,
+      reqId,
     })
-  }, [sendInteraction, post, feedContext])
+  }
 
-  const onOpenReposter = useCallback(() => {
+  const onOpenReposter = () => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#clickthroughReposter',
       feedContext,
+      reqId,
     })
-  }, [sendInteraction, post, feedContext])
+  }
 
-  const onOpenEmbed = useCallback(() => {
+  const onOpenEmbed = () => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#clickthroughEmbed',
       feedContext,
+      reqId,
     })
-  }, [sendInteraction, post, feedContext])
+  }
 
-  const onBeforePress = useCallback(() => {
+  const onBeforePress = () => {
     sendInteraction({
       item: post.uri,
       event: 'app.bsky.feed.defs#clickthroughItem',
       feedContext,
+      reqId,
     })
     precacheProfile(queryClient, post.author)
-  }, [queryClient, post, sendInteraction, feedContext])
+  }
 
   const outerStyles = [
     styles.outer,
@@ -437,6 +446,7 @@ let FeedItemInner = ({
             onPressReply={onPressReply}
             logContext="FeedItem"
             feedContext={feedContext}
+            reqId={reqId}
             threadgateRecord={threadgateRecord}
             onShowLess={onShowLess}
           />
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index c50b36640..57ee95e31 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -28,6 +28,7 @@ let PostDropdownBtn = ({
   testID,
   post,
   postFeedContext,
+  postReqId,
   record,
   richText,
   style,
@@ -40,6 +41,7 @@ let PostDropdownBtn = ({
   testID: string
   post: Shadow<AppBskyFeedDefs.PostView>
   postFeedContext: string | undefined
+  postReqId: string | undefined
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
   style?: StyleProp<ViewStyle>
@@ -99,6 +101,7 @@ let PostDropdownBtn = ({
             testID={testID}
             post={post}
             postFeedContext={postFeedContext}
+            postReqId={postReqId}
             record={record}
             richText={richText}
             timestamp={timestamp}
diff --git a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx
index 7958968b4..a5f41ea7a 100644
--- a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx
+++ b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx
@@ -1,4 +1,4 @@
-import React, {memo, useCallback} from 'react'
+import React, {memo} from 'react'
 import {
   Platform,
   type PressableProps,
@@ -97,6 +97,7 @@ import * as Toast from '../Toast'
 let PostDropdownMenuItems = ({
   post,
   postFeedContext,
+  postReqId,
   record,
   richText,
   timestamp,
@@ -106,6 +107,7 @@ let PostDropdownMenuItems = ({
   testID: string
   post: Shadow<AppBskyFeedDefs.PostView>
   postFeedContext: string | undefined
+  postReqId: string | undefined
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
   style?: StyleProp<ViewStyle>
@@ -189,7 +191,7 @@ let PostDropdownMenuItems = ({
     langPrefs.primaryLanguage,
   )
 
-  const onDeletePost = React.useCallback(() => {
+  const onDeletePost = () => {
     deletePostMutate({uri: postUri}).then(
       () => {
         Toast.show(_(msg({message: 'Post deleted', context: 'toast'})))
@@ -215,18 +217,9 @@ let PostDropdownMenuItems = ({
         Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
       },
     )
-  }, [
-    navigation,
-    postUri,
-    deletePostMutate,
-    postAuthor,
-    currentAccount,
-    isAuthor,
-    href,
-    _,
-  ])
-
-  const onToggleThreadMute = React.useCallback(() => {
+  }
+
+  const onToggleThreadMute = () => {
     try {
       if (isThreadMuted) {
         unmuteThread()
@@ -246,16 +239,16 @@ let PostDropdownMenuItems = ({
         )
       }
     }
-  }, [isThreadMuted, unmuteThread, _, muteThread])
+  }
 
-  const onCopyPostText = React.useCallback(() => {
+  const onCopyPostText = () => {
     const str = richTextToString(richText, true)
 
     Clipboard.setStringAsync(str)
     Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
-  }, [_, richText])
+  }
 
-  const onPressTranslate = React.useCallback(async () => {
+  const onPressTranslate = async () => {
     await openLink(translatorUrl, true)
 
     if (
@@ -270,40 +263,40 @@ let PostDropdownMenuItems = ({
         textLength: post.record.text.length,
       })
     }
-  }, [openLink, translatorUrl, langPrefs, post])
+  }
 
-  const onHidePost = React.useCallback(() => {
+  const onHidePost = () => {
     hidePost({uri: postUri})
-  }, [postUri, hidePost])
+  }
 
-  const hideInPWI = React.useMemo(() => {
-    return !!postAuthor.labels?.find(
-      label => label.val === '!no-unauthenticated',
-    )
-  }, [postAuthor])
+  const hideInPWI = !!postAuthor.labels?.find(
+    label => label.val === '!no-unauthenticated',
+  )
 
   const showLoggedOutWarning =
     postAuthor.did !== currentAccount?.did && hideInPWI
 
-  const onSharePost = React.useCallback(() => {
+  const onSharePost = () => {
     const url = toShareUrl(href)
     shareUrl(url)
-  }, [href])
+  }
 
-  const onPressShowMore = React.useCallback(() => {
+  const onPressShowMore = () => {
     feedFeedback.sendInteraction({
       event: 'app.bsky.feed.defs#requestMore',
       item: postUri,
       feedContext: postFeedContext,
+      reqId: postReqId,
     })
     Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
-  }, [feedFeedback, postUri, postFeedContext, _])
+  }
 
-  const onPressShowLess = React.useCallback(() => {
+  const onPressShowLess = () => {
     feedFeedback.sendInteraction({
       event: 'app.bsky.feed.defs#requestLess',
       item: postUri,
       feedContext: postFeedContext,
+      reqId: postReqId,
     })
     if (onShowLess) {
       onShowLess({
@@ -313,19 +306,16 @@ let PostDropdownMenuItems = ({
     } else {
       Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
     }
-  }, [feedFeedback, postUri, postFeedContext, _, onShowLess])
+  }
 
-  const onSelectChatToShareTo = React.useCallback(
-    (conversation: string) => {
-      navigation.navigate('MessagesConversation', {
-        conversation,
-        embed: postUri,
-      })
-    },
-    [navigation, postUri],
-  )
+  const onSelectChatToShareTo = (conversation: string) => {
+    navigation.navigate('MessagesConversation', {
+      conversation,
+      embed: postUri,
+    })
+  }
 
-  const onToggleQuotePostAttachment = React.useCallback(async () => {
+  const onToggleQuotePostAttachment = async () => {
     if (!quoteEmbed) return
 
     const action = quoteEmbed.isDetached ? 'reattach' : 'detach'
@@ -348,7 +338,7 @@ let PostDropdownMenuItems = ({
       )
       logger.error(`Failed to ${action} quote`, {safeMessage: e.message})
     }
-  }, [_, quoteEmbed, post, toggleQuoteDetachment])
+  }
 
   const canHidePostForMe = !isAuthor && !isPostHidden
   const canEmbed = isWeb && gtMobile && !hideInPWI
@@ -356,7 +346,7 @@ let PostDropdownMenuItems = ({
     !isAuthor && isRootPostAuthor && !isPostHidden && isReply
   const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
 
-  const onToggleReplyVisibility = React.useCallback(async () => {
+  const onToggleReplyVisibility = async () => {
     // TODO no threadgate?
     if (!canHideReplyForEveryone) return
 
@@ -380,25 +370,18 @@ let PostDropdownMenuItems = ({
       )
       logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
     }
-  }, [
-    _,
-    isReplyHiddenByThreadgate,
-    rootUri,
-    postUri,
-    canHideReplyForEveryone,
-    toggleReplyVisibility,
-  ])
+  }
 
-  const onPressPin = useCallback(() => {
+  const onPressPin = () => {
     logEvent(isPinned ? 'post:unpin' : 'post:pin', {})
     pinPostMutate({
       postUri,
       postCid,
       action: isPinned ? 'unpin' : 'pin',
     })
-  }, [isPinned, pinPostMutate, postCid, postUri])
+  }
 
-  const onBlockAuthor = useCallback(async () => {
+  const onBlockAuthor = async () => {
     try {
       await queueBlock()
       Toast.show(_(msg({message: 'Account blocked', context: 'toast'})))
@@ -408,9 +391,9 @@ let PostDropdownMenuItems = ({
         Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
       }
     }
-  }, [_, queueBlock])
+  }
 
-  const onMuteAuthor = useCallback(async () => {
+  const onMuteAuthor = async () => {
     if (postAuthor.viewer?.muted) {
       try {
         await queueUnmute()
@@ -432,22 +415,22 @@ let PostDropdownMenuItems = ({
         }
       }
     }
-  }, [_, queueMute, queueUnmute, postAuthor.viewer?.muted])
+  }
 
-  const onShareATURI = useCallback(() => {
+  const onShareATURI = () => {
     shareText(postUri)
-  }, [postUri])
+  }
 
-  const onShareAuthorDID = useCallback(() => {
+  const onShareAuthorDID = () => {
     shareText(postAuthor.did)
-  }, [postAuthor.did])
+  }
 
-  const onReportMisclassification = useCallback(() => {
+  const onReportMisclassification = () => {
     const url = `https://docs.google.com/forms/d/e/1FAIpQLSd0QPqhNFksDQf1YyOos7r1ofCLvmrKAH1lU042TaS3GAZaWQ/viewform?entry.1756031717=${toShareUrl(
       href,
     )}`
     openLink(url)
-  }, [href, openLink])
+  }
 
   return (
     <>
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index a9cae8886..3f82eb294 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -1,4 +1,4 @@
-import React, {memo, useCallback} from 'react'
+import React, {memo} from 'react'
 import {
   Pressable,
   type PressableStateCallbackType,
@@ -55,6 +55,7 @@ let PostCtrls = ({
   record,
   richText,
   feedContext,
+  reqId,
   style,
   onPressReply,
   onPostReply,
@@ -67,6 +68,7 @@ let PostCtrls = ({
   record: AppBskyFeedPost.Record
   richText: RichTextAPI
   feedContext?: string | undefined
+  reqId?: string | undefined
   style?: StyleProp<ViewStyle>
   onPressReply: () => void
   onPostReply?: (postUri: string | undefined) => void
@@ -117,7 +119,7 @@ let PostCtrls = ({
   const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] =
     React.useState(false)
 
-  const onPressToggleLike = React.useCallback(async () => {
+  const onPressToggleLike = async () => {
     if (isBlocked) {
       Toast.show(
         _(msg`Cannot interact with a blocked user`),
@@ -134,6 +136,7 @@ let PostCtrls = ({
           item: post.uri,
           event: 'app.bsky.feed.defs#interactionLike',
           feedContext,
+          reqId,
         })
         captureAction(ProgressGuideAction.Like)
         await queueLike()
@@ -145,20 +148,9 @@ let PostCtrls = ({
         throw e
       }
     }
-  }, [
-    _,
-    playHaptic,
-    post.uri,
-    post.viewer?.like,
-    queueLike,
-    queueUnlike,
-    sendInteraction,
-    captureAction,
-    feedContext,
-    isBlocked,
-  ])
+  }
 
-  const onRepost = useCallback(async () => {
+  const onRepost = async () => {
     if (isBlocked) {
       Toast.show(
         _(msg`Cannot interact with a blocked user`),
@@ -173,6 +165,7 @@ let PostCtrls = ({
           item: post.uri,
           event: 'app.bsky.feed.defs#interactionRepost',
           feedContext,
+          reqId,
         })
         await queueRepost()
       } else {
@@ -183,18 +176,9 @@ let PostCtrls = ({
         throw e
       }
     }
-  }, [
-    _,
-    post.uri,
-    post.viewer?.repost,
-    queueRepost,
-    queueUnrepost,
-    sendInteraction,
-    feedContext,
-    isBlocked,
-  ])
+  }
 
-  const onQuote = useCallback(() => {
+  const onQuote = () => {
     if (isBlocked) {
       Toast.show(
         _(msg`Cannot interact with a blocked user`),
@@ -207,22 +191,15 @@ let PostCtrls = ({
       item: post.uri,
       event: 'app.bsky.feed.defs#interactionQuote',
       feedContext,
+      reqId,
     })
     openComposer({
       quote: post,
       onPost: onPostReply,
     })
-  }, [
-    _,
-    sendInteraction,
-    post,
-    feedContext,
-    openComposer,
-    onPostReply,
-    isBlocked,
-  ])
+  }
 
-  const onShare = useCallback(() => {
+  const onShare = () => {
     const urip = new AtUri(post.uri)
     const href = makeProfileLink(post.author, 'post', urip.rkey)
     const url = toShareUrl(href)
@@ -231,8 +208,9 @@ let PostCtrls = ({
       item: post.uri,
       event: 'app.bsky.feed.defs#interactionShare',
       feedContext,
+      reqId,
     })
-  }, [post.uri, post.author, sendInteraction, feedContext])
+  }
 
   const btnStyle = React.useCallback(
     ({pressed, hovered}: PressableStateCallbackType) => [
@@ -374,6 +352,7 @@ let PostCtrls = ({
           testID="postDropdownBtn"
           post={post}
           postFeedContext={feedContext}
+          postReqId={reqId}
           record={record}
           richText={richText}
           style={{padding: 5}}
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
index c3a82ac8e..0ccf9b67a 100644
--- a/src/view/screens/DebugMod.tsx
+++ b/src/view/screens/DebugMod.tsx
@@ -829,6 +829,7 @@ function MockPostFeedItem({
       showReplyTo={false}
       reason={undefined}
       feedContext={''}
+      reqId={undefined}
       rootPost={post}
     />
   )
diff --git a/yarn.lock b/yarn.lock
index 54b18d8cc..776f5ef7e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -20,28 +20,27 @@
     "@jridgewell/gen-mapping" "^0.3.0"
     "@jridgewell/trace-mapping" "^0.3.9"
 
-"@atproto-labs/fetch-node@0.1.8":
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.8.tgz#687fc8be6107f10a4247c17989792862affd838b"
-  integrity sha512-OOTIhZNPEDDm7kaYU8iYRgzM+D5n3mP2iiBSyKuLakKTaZBL5WwYlUsJVsqX26SnUXtGEroOJEVJ6f66OcG80w==
+"@atproto-labs/fetch-node@0.1.9":
+  version "0.1.9"
+  resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.9.tgz#5df902413cc2ebfff914999ad3fbbc13b20e1dd0"
+  integrity sha512-8sHDDXZEzQptLu8ddUU/8U+THS6dumgPynVX0/1PjUYd4S/FWyPcz6yMIiVChTfzKnZvYRRz47+qvOKhydrHQw==
   dependencies:
-    "@atproto-labs/fetch" "0.2.2"
-    "@atproto-labs/pipe" "0.1.0"
+    "@atproto-labs/fetch" "0.2.3"
+    "@atproto-labs/pipe" "0.1.1"
     ipaddr.js "^2.1.0"
-    psl "^1.9.0"
     undici "^6.14.1"
 
-"@atproto-labs/fetch@0.2.2":
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.2.2.tgz#c65acfd7b2265a8fe7d4ba3997126cce07bafe26"
-  integrity sha512-QyafkedbFeVaN20DYUpnY2hcArYxjdThPXbYMqOSoZhcvkrUqaw4xDND4wZB5TBD9cq2yqe9V6mcw9P4XQKQuQ==
+"@atproto-labs/fetch@0.2.3":
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.2.3.tgz#d47afec078f630c50e291c56264cc0ff13d0c6cc"
+  integrity sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==
   dependencies:
-    "@atproto-labs/pipe" "0.1.0"
+    "@atproto-labs/pipe" "0.1.1"
 
-"@atproto-labs/pipe@0.1.0":
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/@atproto-labs/pipe/-/pipe-0.1.0.tgz#c8d86923b6d8e900d39efe6fdcdf0d897c434086"
-  integrity sha512-ghOqHFyJlQVFPESzlVHjKroP0tPzbmG5Jms0dNI9yLDEfL8xp4OFPWLX4f6T8mRq69wWs4nIDM3sSsFbFqLa1w==
+"@atproto-labs/pipe@0.1.1":
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/@atproto-labs/pipe/-/pipe-0.1.1.tgz#1c4232d16bf95f251e993cb6ee440f9aa4e87ce6"
+  integrity sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==
 
 "@atproto-labs/simple-store-memory@0.1.3":
   version "0.1.3"
@@ -64,10 +63,10 @@
     "@atproto/xrpc" "^0.7.0"
     "@atproto/xrpc-server" "^0.7.18"
 
-"@atproto/api@^0.15.7":
-  version "0.15.7"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.7.tgz#8436162d9fa5dac627bdd5c0f5c9598309ec1383"
-  integrity sha512-YRETLcOwDCYfGs7Sl9ObqPwhOlVWrPkw4f1AYGIrXLQS58WHe/vz1lZbqOqMsC6gvCnyZnOuKlhsRHZ14rBLzg==
+"@atproto/api@^0.15.8":
+  version "0.15.8"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.8.tgz#f284a9c225191ebd35b46f5695932ab649c04a61"
+  integrity sha512-PsCgmV4zPjN8VuJMruxqauhn88PuS0b8t2Xsjl4617+bCPpY513jVlxgNH/XExxO7TSVvJM7EzdLY4o3fqh/xQ==
   dependencies:
     "@atproto/common-web" "^0.4.2"
     "@atproto/lexicon" "^0.4.11"
@@ -95,14 +94,14 @@
     multiformats "^9.9.0"
     uint8arrays "3.0.0"
 
-"@atproto/bsky@^0.0.149":
-  version "0.0.149"
-  resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.149.tgz#3e9cfb999b9958e9a61776eddb72d424905ec3be"
-  integrity sha512-7j2KgWHm1nOTQDmtEcNwtldTArS9WwZS3M+aw7OmGH8wCa8vEljNxP6HETjtktDMNTrSipHmmyqh25+Rc5+Ziw==
+"@atproto/bsky@^0.0.150":
+  version "0.0.150"
+  resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.150.tgz#6626095875d805d0d3f38fa4e184b9f7d274c80f"
+  integrity sha512-dn1jzP1EId842+g78Q6EMdOmgEZxa9bSq20HMdd5/R8uu559mPs8zigFuddqCoT1fRaJXFC8ZP7Jk5asvBQhrA==
   dependencies:
-    "@atproto-labs/fetch-node" "0.1.8"
+    "@atproto-labs/fetch-node" "0.1.9"
     "@atproto-labs/xrpc-utils" "0.0.14"
-    "@atproto/api" "^0.15.7"
+    "@atproto/api" "^0.15.8"
     "@atproto/common" "^0.4.11"
     "@atproto/crypto" "^0.4.4"
     "@atproto/did" "^0.1.5"
@@ -219,20 +218,20 @@
     "@noble/hashes" "^1.6.1"
     uint8arrays "3.0.0"
 
-"@atproto/dev-env@^0.3.131":
-  version "0.3.131"
-  resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.131.tgz#b3b4cee5f367766d542515b1713523423ecb5a71"
-  integrity sha512-Tijqc/vq7qKGTpgoKm1BwyvP2QfoOQRjNm9Ro5CDAMXsKqHfXxPiytxYqxj6QR/PptC27aDUqgmexluZN6XbWg==
+"@atproto/dev-env@^0.3.132":
+  version "0.3.132"
+  resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.132.tgz#78d55ef08a368a752c55b1ee7b7c08a41f27b5ac"
+  integrity sha512-RFd/9kgvmbP859N6NLu/FxCzLsj01iq22P9jNpL+dQNXbWXHYwGMUa6edf/ZrljNi3dFBNxabdDZJ2q+8uvBJQ==
   dependencies:
-    "@atproto/api" "^0.15.7"
-    "@atproto/bsky" "^0.0.149"
+    "@atproto/api" "^0.15.8"
+    "@atproto/bsky" "^0.0.150"
     "@atproto/bsync" "^0.0.19"
     "@atproto/common-web" "^0.4.2"
     "@atproto/crypto" "^0.4.4"
     "@atproto/identity" "^0.4.8"
     "@atproto/lexicon" "^0.4.11"
-    "@atproto/ozone" "^0.1.110"
-    "@atproto/pds" "^0.4.137"
+    "@atproto/ozone" "^0.1.111"
+    "@atproto/pds" "^0.4.138"
     "@atproto/sync" "^0.1.23"
     "@atproto/syntax" "^0.4.0"
     "@atproto/xrpc-server" "^0.7.18"
@@ -302,21 +301,21 @@
   optionalDependencies:
     "@atproto/oauth-provider-api" "0.1.2"
 
-"@atproto/oauth-provider-ui@0.1.4":
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.4.tgz#5e092d30afa583fdab54fc78371aecb1cbfa017d"
-  integrity sha512-GTQnB7OUBFSeXcdRseAGYzKe9UUFB/kGjRcIA8+pO5pCMD7JdXI+WliUhsbdmQ2I+OK78aAlCrmygNWpLtpZgg==
+"@atproto/oauth-provider-ui@0.1.5":
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.5.tgz#b080c5e814975821689c5976c27ac1081211106f"
+  integrity sha512-pW0Vx3kvIWH1Mu3SOImNHP9JbmhSj2e3ChDvtfXCWI1oC03fiaMlJfdxrx9Plq5Z+DajnCzPzrf1Lvbopjf94Q==
   optionalDependencies:
     "@atproto/oauth-provider-api" "0.1.2"
 
-"@atproto/oauth-provider@^0.7.6":
-  version "0.7.6"
-  resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.6.tgz#68bc37303611d548bae9f653d41bc89bd8890152"
-  integrity sha512-4YcnddACznmpuRmHlt9G+kccdv2Gct5qQOF9Yyjse8cl2Td+Rg1gkchpRdWUnyr9fgZzmCsSBYzEfVXge3eUiQ==
+"@atproto/oauth-provider@^0.7.7":
+  version "0.7.7"
+  resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.7.tgz#dbbdeb405ab1d239fd926340f83fb41e13455011"
+  integrity sha512-ElphzmOjw1hr42HN4dD6sMAQFtpTkaJ8bBDAsbL9YBVJDEGhmHsF3Ye8mDUO4nhEdg7PUTWiCzXyqnaorAjiTA==
   dependencies:
-    "@atproto-labs/fetch" "0.2.2"
-    "@atproto-labs/fetch-node" "0.1.8"
-    "@atproto-labs/pipe" "0.1.0"
+    "@atproto-labs/fetch" "0.2.3"
+    "@atproto-labs/fetch-node" "0.1.9"
+    "@atproto-labs/pipe" "0.1.1"
     "@atproto-labs/simple-store" "0.2.0"
     "@atproto-labs/simple-store-memory" "0.1.3"
     "@atproto/common" "^0.4.11"
@@ -324,7 +323,7 @@
     "@atproto/jwk-jose" "0.1.6"
     "@atproto/oauth-provider-api" "0.1.2"
     "@atproto/oauth-provider-frontend" "0.1.4"
-    "@atproto/oauth-provider-ui" "0.1.4"
+    "@atproto/oauth-provider-ui" "0.1.5"
     "@atproto/oauth-types" "0.2.7"
     "@atproto/syntax" "0.4.0"
     "@hapi/accept" "^6.0.3"
@@ -337,7 +336,6 @@
     http-errors "^2.0.0"
     ioredis "^5.3.2"
     jose "^5.2.0"
-    psl "^1.9.0"
     zod "^3.23.8"
 
 "@atproto/oauth-types@0.2.7":
@@ -348,12 +346,12 @@
     "@atproto/jwk" "0.1.5"
     zod "^3.23.8"
 
-"@atproto/ozone@^0.1.110":
-  version "0.1.110"
-  resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.110.tgz#78ad57961b4699c8aa3e6f7d5b6f215d7760a723"
-  integrity sha512-X7VU7QAkwJrwpgmAuAHqvVDX9CEW0Ts5R4ovATgEt2lbxyxtJtYIm1dG346fAlOfC9f3RGN+HI8vBMWrrrLKAQ==
+"@atproto/ozone@^0.1.111":
+  version "0.1.111"
+  resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.111.tgz#7ef4a02f1af045ab44254fb9d44ab0e50fd94ba9"
+  integrity sha512-NY+Cn/3dY4tPFkMUoJR1KMZN/v9ZIxjx6EQBMwn/nqTiHk0E3rtGEbyL2jLQ7x+FxpPTjDgpnn3K725+8XUaAg==
   dependencies:
-    "@atproto/api" "^0.15.7"
+    "@atproto/api" "^0.15.8"
     "@atproto/common" "^0.4.11"
     "@atproto/crypto" "^0.4.4"
     "@atproto/identity" "^0.4.8"
@@ -378,20 +376,20 @@
     undici "^6.14.1"
     ws "^8.12.0"
 
-"@atproto/pds@^0.4.137":
-  version "0.4.137"
-  resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.137.tgz#87468703b02bf42681ddd50049ee906331655731"
-  integrity sha512-DRUck9CgOdK0cP6B6/1Cku2gb5t31Vhh9su2TcqF9eymZP1dNSI6nfTIEp+cuwpW/VpDeu7AfHCSgYfnJeZ5yg==
+"@atproto/pds@^0.4.138":
+  version "0.4.138"
+  resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.138.tgz#437d785c83f710bf37bef8baf687b0a46ce9dc68"
+  integrity sha512-WLzDhmguTgs2wQNKoGxCbpKNegDnRiemSslenMbPrB7kSiXYj+XZobLyoIXHv1EnAd2pbThwNEL8z8EfkM0mDg==
   dependencies:
-    "@atproto-labs/fetch-node" "0.1.8"
+    "@atproto-labs/fetch-node" "0.1.9"
     "@atproto-labs/xrpc-utils" "0.0.14"
-    "@atproto/api" "^0.15.7"
+    "@atproto/api" "^0.15.8"
     "@atproto/aws" "^0.2.21"
     "@atproto/common" "^0.4.11"
     "@atproto/crypto" "^0.4.4"
     "@atproto/identity" "^0.4.8"
     "@atproto/lexicon" "^0.4.11"
-    "@atproto/oauth-provider" "^0.7.6"
+    "@atproto/oauth-provider" "^0.7.7"
     "@atproto/repo" "^0.8.1"
     "@atproto/syntax" "^0.4.0"
     "@atproto/xrpc" "^0.7.0"