diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index 3fd919cd3e..2ec0c6a4cd 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 && (
-
- {_(msg`Assign topic for algo`)}
-
-
+ <>
+
+
+ {_(msg`Assign topic for algo`)}
+
+
+ >
)}
{hasSession && (
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 130722b9c5..d81b68db63 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 7b0d567b80..b59397b0b5 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 9cb4173e39..c27f2c322d 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 2f4b870154..b97fc4ed58 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 22989e6c20..1d7c2dd534 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 8b235f4929..3e9c2bafa4 100644
--- a/src/state/feed-feedback.tsx
+++ b/src/state/feed-feedback.tsx
@@ -10,10 +10,15 @@ 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,
@@ -21,6 +26,42 @@ import {
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({
@@ -35,15 +77,27 @@ const stateContext = createContext({
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>(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 89023e513e..e6e3e82fb5 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 450f2c120b..17fe188409 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 e8a177a8d8..9f28743a17 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(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 c2780a2a5b..2f03a168b5 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,