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,