diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index f0ef9ed055..9fc7cb3c73 100644
--- a/src/components/PostControls/PostMenu/PostMenuItems.tsx
+++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx
@@ -470,6 +470,7 @@ let PostMenuItems = ({
<>
+ {_(msg`Tell feed provider:`)}
- {_(msg`Assign topic for algo`)}
-
-
+ <>
+
+
+ {_(msg`Assign topic for algo`)}
+
+
+ >
)}
{hasSession && (
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 3f0d499894..ac60c75aca 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -91,6 +91,26 @@ export const STAGING_FEEDS = [
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
+export const PASSIVE_INTERACTIONS = [
+ 'app.bsky.feed.defs#requestLess',
+ 'app.bsky.feed.defs#requestMore',
+ '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',
+]
+
+export const DIRECT_INTERACTIONS = [
+ 'app.bsky.feed.defs#requestLess',
+ 'app.bsky.feed.defs#requestMore',
+]
+
+export const ALL_INTERACTIONS = [
+ ...PASSIVE_INTERACTIONS,
+ ...DIRECT_INTERACTIONS,
+]
+
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 481c335dbe..ea6a1537e3 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -178,7 +178,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
const {_, i18n} = useLingui()
const {openComposer} = useOpenComposer()
const {currentAccount, hasSession} = useSession()
- const feedFeedback = useFeedFeedback(postSource?.feed, hasSession)
+ const feedFeedback = useFeedFeedback(postSource?.feedInfo, 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 f6ab554b6b..cdbe3bf55a 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -48,7 +48,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/state/feed-feedback.tsx b/src/state/feed-feedback.tsx
index a718a761d5..8e8a9f799f 100644
--- a/src/state/feed-feedback.tsx
+++ b/src/state/feed-feedback.tsx
@@ -10,9 +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 {
+ ALL_INTERACTIONS,
+ DIRECT_INTERACTIONS,
+ FEEDBACK_FEEDS,
+ STAGING_FEEDS,
+} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {Logger} from '#/logger'
+import {type FeedSourceInfo} from '#/state/queries/feed'
import {
type FeedDescriptor,
type FeedPostSliceItem,
@@ -27,6 +33,7 @@ export type StateContext = {
onItemSeen: (item: any) => void
sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
feedDescriptor: FeedDescriptor | undefined
+ feedSourceInfo: FeedSourceInfo | undefined
}
const stateContext = createContext({
@@ -34,14 +41,32 @@ const stateContext = createContext({
onItemSeen: (_item: any) => {},
sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
feedDescriptor: undefined,
+ feedSourceInfo: undefined,
})
+// All info needed to send feedback to a feed
+type FeedInfo = {
+ feedDescriptor: FeedDescriptor
+ acceptsInteractions: boolean
+ isDiscover: boolean
+ proxyDid: string
+}
+
export function useFeedFeedback(
- feed: FeedDescriptor | undefined,
+ feed: FeedSourceInfo | FeedDescriptor | undefined,
hasSession: boolean,
) {
const agent = useAgent()
- const enabled = isDiscoverFeed(feed) && hasSession
+
+ const feedInfo = feed ? buildFeedInfo(feed) : null
+ const enabled = feedInfo && feedInfo.acceptsInteractions && hasSession
+
+ const enabledInteractions = useMemo(() => {
+ if (!enabled) {
+ return []
+ }
+ return feedInfo.isDiscover ? ALL_INTERACTIONS : DIRECT_INTERACTIONS
+ }, [enabled, feedInfo])
const queue = useRef>(new Set())
const history = useRef<
@@ -64,20 +89,22 @@ 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 =>
+ 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`,
+ 'atproto-proxy': `${feedInfo?.proxyDid}#bsky_fg`,
},
},
)
@@ -89,10 +116,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, feedInfo, enabledInteractions])
const sendToFeed = useMemo(
() =>
@@ -164,9 +194,10 @@ export function useFeedFeedback(
// call on various events
// queues the event to be sent with the throttled sendToFeed call
sendInteraction,
- feedDescriptor: feed,
+ feedDescriptor: feedInfo?.feedDescriptor,
+ feedSourceInfo: typeof feed === 'object' ? feed : undefined,
}
- }, [enabled, onItemSeen, sendInteraction, feed])
+ }, [enabled, onItemSeen, sendInteraction, feedInfo, feed])
}
export const FeedFeedbackProvider = stateContext.Provider
@@ -184,6 +215,50 @@ function isDiscoverFeed(feed?: FeedDescriptor) {
return !!feed && FEEDBACK_FEEDS.includes(feed)
}
+function buildFeedInfo(feed: FeedSourceInfo | FeedDescriptor): FeedInfo | null {
+ // Build FeedInfo object from either a feed source info object or a feed descriptor string
+ // Only discover feeds are supported for feed descriptor strings
+ if (typeof feed === 'object') {
+ if (feed.type !== 'feed') {
+ // Don't send feedback to non-feed sources
+ return null
+ }
+ const feedDescriptor = feed.feedDescriptor
+ const isDiscover = isDiscoverFeed(feed.feedDescriptor)
+ const proxyDid = feed.view?.did
+ if (!proxyDid) {
+ logger.warn(`No proxy did found for feed: ${feedDescriptor}.`)
+ return null
+ }
+ let acceptsInteractions = feed.acceptsInteractions ?? false
+ if (isDiscover) {
+ // Discover feed doesn't have acceptsInteractions: true, so hardcode this for now
+ acceptsInteractions = true
+ }
+ return {
+ feedDescriptor,
+ isDiscover,
+ proxyDid,
+ acceptsInteractions,
+ }
+ } else {
+ const feedDescriptor = feed
+ const isDiscover = isDiscoverFeed(feedDescriptor)
+ if (!isDiscover) {
+ return null
+ }
+ const proxyDid = STAGING_FEEDS.includes(feedDescriptor)
+ ? 'did:web:algo.pop2.bsky.app'
+ : 'did:web:discover.bsky.app'
+ return {
+ feedDescriptor,
+ isDiscover,
+ proxyDid,
+ acceptsInteractions: true,
+ }
+ }
+}
+
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..e79d07778e 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 | undefined
likeUri: string | undefined
contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
}
@@ -115,6 +116,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,
}
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/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 94cc04f542..14dc2d87fe 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -105,8 +105,10 @@ export function PostThread({uri}: {uri: string}) {
)
const headerRef = React.useRef(null)
const anchorPostSource = useUnstablePostSource(uri)
- const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
-
+ const feedFeedback = useFeedFeedback(
+ anchorPostSource?.feedSourceInfo,
+ hasSession,
+ )
const {data: preferences} = usePreferencesQuery()
const {
isFetching,
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 54eea0493c..8121305597 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -216,7 +216,7 @@ let PostThreadItemLoaded = ({
anchorPostSource?: PostSource
}): React.ReactNode => {
const {currentAccount, hasSession} = useSession()
- const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
+ const feedFeedback = useFeedFeedback(anchorPostSource?.feedInfo, hasSession)
const t = useTheme()
const pal = usePalette('default')
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
index 0593ba931a..631a2dd389 100644
--- a/src/view/com/posts/PostFeedItem.tsx
+++ b/src/view/com/posts/PostFeedItem.tsx
@@ -180,7 +180,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({
@@ -237,7 +237,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,