about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/PostControls/PostMenu/PostMenuItems.tsx25
-rw-r--r--src/lib/constants.ts2
-rw-r--r--src/screens/PostThread/components/ThreadItemAnchor.tsx2
-rw-r--r--src/screens/PostThread/index.tsx5
-rw-r--r--src/screens/Profile/ProfileFeed/index.tsx2
-rw-r--r--src/screens/VideoFeed/index.tsx5
-rw-r--r--src/state/feed-feedback.tsx98
-rw-r--r--src/state/queries/feed.ts31
-rw-r--r--src/state/unstable-post-source.tsx4
-rw-r--r--src/view/com/feeds/FeedPage.tsx6
-rw-r--r--src/view/com/posts/PostFeedItem.tsx4
11 files changed, 150 insertions, 34 deletions
diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index 3fd919cd3..2ec0c6a4c 100644
--- a/src/components/PostControls/PostMenu/PostMenuItems.tsx
+++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx
@@ -266,7 +266,9 @@ let PostMenuItems = ({
       feedContext: postFeedContext,
       reqId: postReqId,
     })
-    Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
+    Toast.show(
+      _(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
+    )
   }
 
   const onPressShowLess = () => {
@@ -282,7 +284,9 @@ let PostMenuItems = ({
         feedContext: postFeedContext,
       })
     } else {
-      Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
+      Toast.show(
+        _(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
+      )
     }
   }
 
@@ -486,13 +490,16 @@ let PostMenuItems = ({
         )}
 
         {isDiscoverDebugUser && (
-          <Menu.Item
-            testID="postDropdownReportMisclassificationBtn"
-            label={_(msg`Assign topic for algo`)}
-            onPress={onReportMisclassification}>
-            <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
-            <Menu.ItemIcon icon={AtomIcon} position="right" />
-          </Menu.Item>
+          <>
+            <Menu.Divider />
+            <Menu.Item
+              testID="postDropdownReportMisclassificationBtn"
+              label={_(msg`Assign topic for algo`)}
+              onPress={onReportMisclassification}>
+              <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
+              <Menu.ItemIcon icon={AtomIcon} position="right" />
+            </Menu.Item>
+          </>
         )}
 
         {hasSession && (
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 130722b9c..d81b68db6 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -90,8 +90,6 @@ export const STAGING_FEEDS = [
   `feedgen|${STAGING_DEFAULT_FEED('thevids')}`,
 ]
 
-export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
-
 export const POST_IMG_MAX = {
   width: 2000,
   height: 2000,
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index 7b0d567b8..b59397b0b 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -180,7 +180,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
   const {openComposer} = useOpenComposer()
   const {currentAccount, hasSession} = useSession()
   const {gtTablet} = useBreakpoints()
-  const feedFeedback = useFeedFeedback(postSource?.feed, hasSession)
+  const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession)
 
   const post = postShadow
   const record = item.value.post.record
diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx
index 9cb4173e3..c27f2c322 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -49,7 +49,10 @@ export function PostThread({uri}: {uri: string}) {
   const initialNumToRender = useInitialNumToRender()
   const {height: windowHeight} = useWindowDimensions()
   const anchorPostSource = useUnstablePostSource(uri)
-  const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
+  const feedFeedback = useFeedFeedback(
+    anchorPostSource?.feedSourceInfo,
+    hasSession,
+  )
 
   /*
    * One query to rule them all
diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx
index 2f4b87015..b97fc4ed5 100644
--- a/src/screens/Profile/ProfileFeed/index.tsx
+++ b/src/screens/Profile/ProfileFeed/index.tsx
@@ -169,7 +169,7 @@ export function ProfileFeedScreenInner({
   const [hasNew, setHasNew] = React.useState(false)
   const [isScrolledDown, setIsScrolledDown] = React.useState(false)
   const queryClient = useQueryClient()
-  const feedFeedback = useFeedFeedback(feed, hasSession)
+  const feedFeedback = useFeedFeedback(feedInfo, hasSession)
   const scrollElRef = useAnimatedRef() as ListRef
 
   const onScrollToTop = useCallback(() => {
diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx
index 22989e6c2..1d7c2dd53 100644
--- a/src/screens/VideoFeed/index.tsx
+++ b/src/screens/VideoFeed/index.tsx
@@ -70,6 +70,7 @@ import {
   useFeedFeedbackContext,
 } from '#/state/feed-feedback'
 import {useFeedFeedback} from '#/state/feed-feedback'
+import {useFeedInfo} from '#/state/queries/feed'
 import {usePostLikeMutationQueue} from '#/state/queries/post'
 import {
   type AuthorFilter,
@@ -199,7 +200,9 @@ function Feed() {
         throw new Error(`Invalid video feed params ${JSON.stringify(params)}`)
     }
   }, [params])
-  const feedFeedback = useFeedFeedback(feedDesc, hasSession)
+  const feedUri = params.type === 'feedgen' ? params.uri : undefined
+  const {data: feedInfo} = useFeedInfo(feedUri)
+  const feedFeedback = useFeedFeedback(feedInfo, hasSession)
   const {data, error, hasNextPage, isFetchingNextPage, fetchNextPage} =
     usePostFeedQuery(
       feedDesc,
diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx
index 8b235f492..3e9c2bafa 100644
--- a/src/state/feed-feedback.tsx
+++ b/src/state/feed-feedback.tsx
@@ -10,17 +10,58 @@ 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 {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants'
 import {isNetworkError} from '#/lib/hooks/useCleanError'
 import {logEvent} from '#/lib/statsig/statsig'
 import {Logger} from '#/logger'
 import {
+  type FeedSourceFeedInfo,
+  type FeedSourceInfo,
+  isFeedSourceFeedInfo,
+} from '#/state/queries/feed'
+import {
   type FeedDescriptor,
   type FeedPostSliceItem,
 } from '#/state/queries/post-feed'
 import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
 import {useAgent} from './session'
 
+export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
+
+export const PASSIVE_FEEDBACK_INTERACTIONS = [
+  'app.bsky.feed.defs#clickthroughItem',
+  'app.bsky.feed.defs#clickthroughAuthor',
+  'app.bsky.feed.defs#clickthroughReposter',
+  'app.bsky.feed.defs#clickthroughEmbed',
+  'app.bsky.feed.defs#interactionSeen',
+] as const
+
+export type PassiveFeedbackInteraction =
+  (typeof PASSIVE_FEEDBACK_INTERACTIONS)[number]
+
+export const DIRECT_FEEDBACK_INTERACTIONS = [
+  'app.bsky.feed.defs#requestLess',
+  'app.bsky.feed.defs#requestMore',
+] as const
+
+export type DirectFeedbackInteraction =
+  (typeof DIRECT_FEEDBACK_INTERACTIONS)[number]
+
+export const ALL_FEEDBACK_INTERACTIONS = [
+  ...PASSIVE_FEEDBACK_INTERACTIONS,
+  ...DIRECT_FEEDBACK_INTERACTIONS,
+] as const
+
+export type FeedbackInteraction = (typeof ALL_FEEDBACK_INTERACTIONS)[number]
+
+export function isFeedbackInteraction(
+  interactionEvent: string,
+): interactionEvent is FeedbackInteraction {
+  return ALL_FEEDBACK_INTERACTIONS.includes(
+    interactionEvent as FeedbackInteraction,
+  )
+}
+
 const logger = Logger.create(Logger.Context.FeedFeedback)
 
 export type StateContext = {
@@ -28,6 +69,7 @@ export type StateContext = {
   onItemSeen: (item: any) => void
   sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
   feedDescriptor: FeedDescriptor | undefined
+  feedSourceInfo: FeedSourceInfo | undefined
 }
 
 const stateContext = createContext<StateContext>({
@@ -35,15 +77,27 @@ const stateContext = createContext<StateContext>({
   onItemSeen: (_item: any) => {},
   sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
   feedDescriptor: undefined,
+  feedSourceInfo: undefined,
 })
 stateContext.displayName = 'FeedFeedbackContext'
 
 export function useFeedFeedback(
-  feed: FeedDescriptor | undefined,
+  feedSourceInfo: FeedSourceInfo | undefined,
   hasSession: boolean,
 ) {
   const agent = useAgent()
-  const enabled = isDiscoverFeed(feed) && hasSession
+
+  const feed =
+    !!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo)
+      ? feedSourceInfo
+      : undefined
+
+  const isDiscover = isDiscoverFeed(feed?.feedDescriptor)
+  const acceptsInteractions = Boolean(isDiscover || feed?.acceptsInteractions)
+  const proxyDid = feed?.view?.did
+  const enabled =
+    Boolean(feed) && Boolean(proxyDid) && acceptsInteractions && hasSession
+  const enabledInteractions = getEnabledInteractions(enabled, feed, isDiscover)
 
   const queue = useRef<Set<string>>(new Set())
   const history = useRef<
@@ -66,19 +120,24 @@ export function useFeedFeedback(
     const interactions = Array.from(queue.current).map(toInteraction)
     queue.current.clear()
 
-    let proxyDid = 'did:web:discover.bsky.app'
-    if (STAGING_FEEDS.includes(feed ?? '')) {
-      proxyDid = 'did:web:algo.pop2.bsky.app'
+    const interactionsToSend = interactions.filter(
+      interaction =>
+        interaction.event &&
+        isFeedbackInteraction(interaction.event) &&
+        enabledInteractions.includes(interaction.event),
+    )
+
+    if (interactionsToSend.length === 0) {
+      return
     }
 
     // Send to the feed
     agent.app.bsky.feed
       .sendInteractions(
-        {interactions},
+        {interactions: interactionsToSend},
         {
           encoding: 'application/json',
           headers: {
-            // TODO when we start sending to other feeds, we need to grab their DID -prf
             'atproto-proxy': `${proxyDid}#bsky_fg`,
           },
         },
@@ -93,10 +152,13 @@ export function useFeedFeedback(
     if (aggregatedStats.current === null) {
       aggregatedStats.current = createAggregatedStats()
     }
-    sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
+    sendOrAggregateInteractionsForStats(
+      aggregatedStats.current,
+      interactionsToSend,
+    )
     throttledFlushAggregatedStats()
     logger.debug('flushed')
-  }, [agent, throttledFlushAggregatedStats, feed])
+  }, [agent, throttledFlushAggregatedStats, proxyDid, enabledInteractions])
 
   const sendToFeed = useMemo(
     () =>
@@ -168,7 +230,8 @@ export function useFeedFeedback(
       // call on various events
       // queues the event to be sent with the throttled sendToFeed call
       sendInteraction,
-      feedDescriptor: feed,
+      feedDescriptor: feed?.feedDescriptor,
+      feedSourceInfo: typeof feed === 'object' ? feed : undefined,
     }
   }, [enabled, onItemSeen, sendInteraction, feed])
 }
@@ -184,10 +247,21 @@ export function useFeedFeedbackContext() {
 // take advantage of the feed feedback API. Until that's in
 // place, we're hardcoding it to the discover feed.
 // -prf
-function isDiscoverFeed(feed?: FeedDescriptor) {
+export function isDiscoverFeed(feed?: FeedDescriptor) {
   return !!feed && FEEDBACK_FEEDS.includes(feed)
 }
 
+function getEnabledInteractions(
+  enabled: boolean,
+  feed: FeedSourceFeedInfo | undefined,
+  isDiscover: boolean,
+): readonly FeedbackInteraction[] {
+  if (!enabled || !feed) {
+    return []
+  }
+  return isDiscover ? ALL_FEEDBACK_INTERACTIONS : DIRECT_FEEDBACK_INTERACTIONS
+}
+
 function toString(interaction: AppBskyFeedDefs.Interaction): string {
   return `${interaction.item}|${interaction.event}|${
     interaction.feedContext || ''
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index 89023e513..e6e3e82fb 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -48,6 +48,7 @@ export type FeedSourceFeedInfo = {
   creatorDid: string
   creatorHandle: string
   likeCount: number | undefined
+  acceptsInteractions?: boolean
   likeUri: string | undefined
   contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
 }
@@ -73,6 +74,12 @@ export type FeedSourceListInfo = {
 
 export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
 
+export function isFeedSourceFeedInfo(
+  feed: FeedSourceInfo,
+): feed is FeedSourceFeedInfo {
+  return feed.type === 'feed'
+}
+
 const feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo'
 export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [
   feedSourceInfoQueryKeyRoot,
@@ -115,6 +122,7 @@ export function hydrateFeedGenerator(
     creatorDid: view.creator.did,
     creatorHandle: view.creator.handle,
     likeCount: view.likeCount,
+    acceptsInteractions: view.acceptsInteractions,
     likeUri: view.viewer?.like,
     contentMode: view.contentMode,
   }
@@ -619,6 +627,29 @@ export function useSavedFeeds() {
   })
 }
 
+const feedInfoQueryKeyRoot = 'feedInfo'
+
+export function useFeedInfo(feedUri: string | undefined) {
+  const agent = useAgent()
+
+  return useQuery({
+    staleTime: STALE.INFINITY,
+    queryKey: [feedInfoQueryKeyRoot, feedUri],
+    queryFn: async () => {
+      if (!feedUri) {
+        return undefined
+      }
+
+      const res = await agent.app.bsky.feed.getFeedGenerator({
+        feed: feedUri,
+      })
+
+      const feedSourceInfo = hydrateFeedGenerator(res.data.view)
+      return feedSourceInfo
+    },
+  })
+}
+
 function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) {
   precacheResolvedUri(
     queryClient,
diff --git a/src/state/unstable-post-source.tsx b/src/state/unstable-post-source.tsx
index 450f2c120..17fe18840 100644
--- a/src/state/unstable-post-source.tsx
+++ b/src/state/unstable-post-source.tsx
@@ -2,7 +2,7 @@ import {useEffect, useId, useState} from 'react'
 import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
 
 import {Logger} from '#/logger'
-import {type FeedDescriptor} from '#/state/queries/post-feed'
+import {type FeedSourceInfo} from '#/state/queries/feed'
 
 /**
  * Separate logger for better debugging
@@ -11,7 +11,7 @@ const logger = Logger.create(Logger.Context.PostSource)
 
 export type PostSource = {
   post: AppBskyFeedDefs.FeedViewPost
-  feed?: FeedDescriptor
+  feedSourceInfo?: FeedSourceInfo
 }
 
 /**
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index e8a177a8d..9f28743a1 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -17,7 +17,7 @@ import {isNative} from '#/platform/detection'
 import {listenSoftReset} from '#/state/events'
 import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
 import {useSetHomeBadge} from '#/state/home-badge'
-import {type SavedFeedSourceInfo} from '#/state/queries/feed'
+import {type FeedSourceInfo} from '#/state/queries/feed'
 import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
 import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
 import {truncateAndInvalidate} from '#/state/queries/util'
@@ -51,7 +51,7 @@ export function FeedPage({
   renderEmptyState: () => JSX.Element
   renderEndOfFeed?: () => JSX.Element
   savedFeedConfig?: AppBskyActorDefs.SavedFeed
-  feedInfo: SavedFeedSourceInfo
+  feedInfo: FeedSourceInfo
 }) {
   const {hasSession} = useSession()
   const {_} = useLingui()
@@ -61,7 +61,7 @@ export function FeedPage({
   const [isScrolledDown, setIsScrolledDown] = useState(false)
   const setMinimalShellMode = useSetMinimalShellMode()
   const headerOffset = useHeaderOffset()
-  const feedFeedback = useFeedFeedback(feed, hasSession)
+  const feedFeedback = useFeedFeedback(feedInfo, hasSession)
   const scrollElRef = useRef<ListMethods>(null)
   const [hasNew, setHasNew] = useState(false)
   const setHomeBadge = useSetHomeBadge()
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
index c2780a2a5..2f03a168b 100644
--- a/src/view/com/posts/PostFeedItem.tsx
+++ b/src/view/com/posts/PostFeedItem.tsx
@@ -176,7 +176,7 @@ let FeedItemInner = ({
     const urip = new AtUri(post.uri)
     return makeProfileLink(post.author, 'post', urip.rkey)
   }, [post.uri, post.author])
-  const {sendInteraction, feedDescriptor} = useFeedFeedbackContext()
+  const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext()
 
   const onPressReply = () => {
     sendInteraction({
@@ -234,7 +234,7 @@ let FeedItemInner = ({
     })
     unstableCacheProfileView(queryClient, post.author)
     setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), {
-      feed: feedDescriptor,
+      feedSourceInfo,
       post: {
         post,
         reason: AppBskyFeedDefs.isReasonRepost(reason) ? reason : undefined,