diff options
-rw-r--r-- | src/components/PostControls/PostMenu/PostMenuItems.tsx | 25 | ||||
-rw-r--r-- | src/lib/constants.ts | 2 | ||||
-rw-r--r-- | src/screens/PostThread/components/ThreadItemAnchor.tsx | 2 | ||||
-rw-r--r-- | src/screens/PostThread/index.tsx | 5 | ||||
-rw-r--r-- | src/screens/Profile/ProfileFeed/index.tsx | 2 | ||||
-rw-r--r-- | src/screens/VideoFeed/index.tsx | 5 | ||||
-rw-r--r-- | src/state/feed-feedback.tsx | 98 | ||||
-rw-r--r-- | src/state/queries/feed.ts | 31 | ||||
-rw-r--r-- | src/state/unstable-post-source.tsx | 4 | ||||
-rw-r--r-- | src/view/com/feeds/FeedPage.tsx | 6 | ||||
-rw-r--r-- | src/view/com/posts/PostFeedItem.tsx | 4 |
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, |