Enable show less / more buttons for third party feeds (#8672)

Co-authored-by: hailey <hailey@blueskyweb.xyz>
Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
kindgracekind
2025-08-26 14:16:50 -05:00
committed by GitHub
parent 98d96bd28b
commit 88e6dff484
11 changed files with 150 additions and 34 deletions
@@ -266,7 +266,9 @@ let PostMenuItems = ({
feedContext: postFeedContext, feedContext: postFeedContext,
reqId: postReqId, reqId: postReqId,
}) })
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) Toast.show(
_(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
)
} }
const onPressShowLess = () => { const onPressShowLess = () => {
@@ -282,7 +284,9 @@ let PostMenuItems = ({
feedContext: postFeedContext, feedContext: postFeedContext,
}) })
} else { } 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 && ( {isDiscoverDebugUser && (
<Menu.Item <>
testID="postDropdownReportMisclassificationBtn" <Menu.Divider />
label={_(msg`Assign topic for algo`)} <Menu.Item
onPress={onReportMisclassification}> testID="postDropdownReportMisclassificationBtn"
<Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText> label={_(msg`Assign topic for algo`)}
<Menu.ItemIcon icon={AtomIcon} position="right" /> onPress={onReportMisclassification}>
</Menu.Item> <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
<Menu.ItemIcon icon={AtomIcon} position="right" />
</Menu.Item>
</>
)} )}
{hasSession && ( {hasSession && (
-2
View File
@@ -90,8 +90,6 @@ export const STAGING_FEEDS = [
`feedgen|${STAGING_DEFAULT_FEED('thevids')}`, `feedgen|${STAGING_DEFAULT_FEED('thevids')}`,
] ]
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
export const POST_IMG_MAX = { export const POST_IMG_MAX = {
width: 2000, width: 2000,
height: 2000, height: 2000,
@@ -180,7 +180,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const {currentAccount, hasSession} = useSession() const {currentAccount, hasSession} = useSession()
const {gtTablet} = useBreakpoints() const {gtTablet} = useBreakpoints()
const feedFeedback = useFeedFeedback(postSource?.feed, hasSession) const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession)
const post = postShadow const post = postShadow
const record = item.value.post.record const record = item.value.post.record
+4 -1
View File
@@ -49,7 +49,10 @@ export function PostThread({uri}: {uri: string}) {
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
const {height: windowHeight} = useWindowDimensions() const {height: windowHeight} = useWindowDimensions()
const anchorPostSource = useUnstablePostSource(uri) const anchorPostSource = useUnstablePostSource(uri)
const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) const feedFeedback = useFeedFeedback(
anchorPostSource?.feedSourceInfo,
hasSession,
)
/* /*
* One query to rule them all * One query to rule them all
+1 -1
View File
@@ -169,7 +169,7 @@ export function ProfileFeedScreenInner({
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const feedFeedback = useFeedFeedback(feed, hasSession) const feedFeedback = useFeedFeedback(feedInfo, hasSession)
const scrollElRef = useAnimatedRef() as ListRef const scrollElRef = useAnimatedRef() as ListRef
const onScrollToTop = useCallback(() => { const onScrollToTop = useCallback(() => {
+4 -1
View File
@@ -70,6 +70,7 @@ import {
useFeedFeedbackContext, useFeedFeedbackContext,
} from '#/state/feed-feedback' } from '#/state/feed-feedback'
import {useFeedFeedback} from '#/state/feed-feedback' import {useFeedFeedback} from '#/state/feed-feedback'
import {useFeedInfo} from '#/state/queries/feed'
import {usePostLikeMutationQueue} from '#/state/queries/post' import {usePostLikeMutationQueue} from '#/state/queries/post'
import { import {
type AuthorFilter, type AuthorFilter,
@@ -199,7 +200,9 @@ function Feed() {
throw new Error(`Invalid video feed params ${JSON.stringify(params)}`) throw new Error(`Invalid video feed params ${JSON.stringify(params)}`)
} }
}, [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} = const {data, error, hasNextPage, isFetchingNextPage, fetchNextPage} =
usePostFeedQuery( usePostFeedQuery(
feedDesc, feedDesc,
+86 -12
View File
@@ -10,10 +10,15 @@ import {AppState, type AppStateStatus} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api' import {type AppBskyFeedDefs} from '@atproto/api'
import throttle from 'lodash.throttle' 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 {isNetworkError} from '#/lib/hooks/useCleanError'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {Logger} from '#/logger' import {Logger} from '#/logger'
import {
type FeedSourceFeedInfo,
type FeedSourceInfo,
isFeedSourceFeedInfo,
} from '#/state/queries/feed'
import { import {
type FeedDescriptor, type FeedDescriptor,
type FeedPostSliceItem, type FeedPostSliceItem,
@@ -21,6 +26,42 @@ import {
import {getItemsForFeedback} from '#/view/com/posts/PostFeed' import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAgent} from './session' 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) const logger = Logger.create(Logger.Context.FeedFeedback)
export type StateContext = { export type StateContext = {
@@ -28,6 +69,7 @@ export type StateContext = {
onItemSeen: (item: any) => void onItemSeen: (item: any) => void
sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
feedDescriptor: FeedDescriptor | undefined feedDescriptor: FeedDescriptor | undefined
feedSourceInfo: FeedSourceInfo | undefined
} }
const stateContext = createContext<StateContext>({ const stateContext = createContext<StateContext>({
@@ -35,15 +77,27 @@ const stateContext = createContext<StateContext>({
onItemSeen: (_item: any) => {}, onItemSeen: (_item: any) => {},
sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {}, sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
feedDescriptor: undefined, feedDescriptor: undefined,
feedSourceInfo: undefined,
}) })
stateContext.displayName = 'FeedFeedbackContext' stateContext.displayName = 'FeedFeedbackContext'
export function useFeedFeedback( export function useFeedFeedback(
feed: FeedDescriptor | undefined, feedSourceInfo: FeedSourceInfo | undefined,
hasSession: boolean, hasSession: boolean,
) { ) {
const agent = useAgent() 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 queue = useRef<Set<string>>(new Set())
const history = useRef< const history = useRef<
@@ -66,19 +120,24 @@ export function useFeedFeedback(
const interactions = Array.from(queue.current).map(toInteraction) const interactions = Array.from(queue.current).map(toInteraction)
queue.current.clear() queue.current.clear()
let proxyDid = 'did:web:discover.bsky.app' const interactionsToSend = interactions.filter(
if (STAGING_FEEDS.includes(feed ?? '')) { interaction =>
proxyDid = 'did:web:algo.pop2.bsky.app' interaction.event &&
isFeedbackInteraction(interaction.event) &&
enabledInteractions.includes(interaction.event),
)
if (interactionsToSend.length === 0) {
return
} }
// Send to the feed // Send to the feed
agent.app.bsky.feed agent.app.bsky.feed
.sendInteractions( .sendInteractions(
{interactions}, {interactions: interactionsToSend},
{ {
encoding: 'application/json', encoding: 'application/json',
headers: { headers: {
// TODO when we start sending to other feeds, we need to grab their DID -prf
'atproto-proxy': `${proxyDid}#bsky_fg`, 'atproto-proxy': `${proxyDid}#bsky_fg`,
}, },
}, },
@@ -93,10 +152,13 @@ export function useFeedFeedback(
if (aggregatedStats.current === null) { if (aggregatedStats.current === null) {
aggregatedStats.current = createAggregatedStats() aggregatedStats.current = createAggregatedStats()
} }
sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) sendOrAggregateInteractionsForStats(
aggregatedStats.current,
interactionsToSend,
)
throttledFlushAggregatedStats() throttledFlushAggregatedStats()
logger.debug('flushed') logger.debug('flushed')
}, [agent, throttledFlushAggregatedStats, feed]) }, [agent, throttledFlushAggregatedStats, proxyDid, enabledInteractions])
const sendToFeed = useMemo( const sendToFeed = useMemo(
() => () =>
@@ -168,7 +230,8 @@ export function useFeedFeedback(
// call on various events // call on various events
// queues the event to be sent with the throttled sendToFeed call // queues the event to be sent with the throttled sendToFeed call
sendInteraction, sendInteraction,
feedDescriptor: feed, feedDescriptor: feed?.feedDescriptor,
feedSourceInfo: typeof feed === 'object' ? feed : undefined,
} }
}, [enabled, onItemSeen, sendInteraction, feed]) }, [enabled, onItemSeen, sendInteraction, feed])
} }
@@ -184,10 +247,21 @@ export function useFeedFeedbackContext() {
// take advantage of the feed feedback API. Until that's in // take advantage of the feed feedback API. Until that's in
// place, we're hardcoding it to the discover feed. // place, we're hardcoding it to the discover feed.
// -prf // -prf
function isDiscoverFeed(feed?: FeedDescriptor) { export function isDiscoverFeed(feed?: FeedDescriptor) {
return !!feed && FEEDBACK_FEEDS.includes(feed) 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 { function toString(interaction: AppBskyFeedDefs.Interaction): string {
return `${interaction.item}|${interaction.event}|${ return `${interaction.item}|${interaction.event}|${
interaction.feedContext || '' interaction.feedContext || ''
+31
View File
@@ -48,6 +48,7 @@ export type FeedSourceFeedInfo = {
creatorDid: string creatorDid: string
creatorHandle: string creatorHandle: string
likeCount: number | undefined likeCount: number | undefined
acceptsInteractions?: boolean
likeUri: string | undefined likeUri: string | undefined
contentMode: AppBskyFeedDefs.GeneratorView['contentMode'] contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
} }
@@ -73,6 +74,12 @@ export type FeedSourceListInfo = {
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
export function isFeedSourceFeedInfo(
feed: FeedSourceInfo,
): feed is FeedSourceFeedInfo {
return feed.type === 'feed'
}
const feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo' const feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo'
export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [ export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [
feedSourceInfoQueryKeyRoot, feedSourceInfoQueryKeyRoot,
@@ -115,6 +122,7 @@ export function hydrateFeedGenerator(
creatorDid: view.creator.did, creatorDid: view.creator.did,
creatorHandle: view.creator.handle, creatorHandle: view.creator.handle,
likeCount: view.likeCount, likeCount: view.likeCount,
acceptsInteractions: view.acceptsInteractions,
likeUri: view.viewer?.like, likeUri: view.viewer?.like,
contentMode: view.contentMode, 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) { function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) {
precacheResolvedUri( precacheResolvedUri(
queryClient, queryClient,
+2 -2
View File
@@ -2,7 +2,7 @@ import {useEffect, useId, useState} from 'react'
import {type AppBskyFeedDefs, AtUri} from '@atproto/api' import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {Logger} from '#/logger' import {Logger} from '#/logger'
import {type FeedDescriptor} from '#/state/queries/post-feed' import {type FeedSourceInfo} from '#/state/queries/feed'
/** /**
* Separate logger for better debugging * Separate logger for better debugging
@@ -11,7 +11,7 @@ const logger = Logger.create(Logger.Context.PostSource)
export type PostSource = { export type PostSource = {
post: AppBskyFeedDefs.FeedViewPost post: AppBskyFeedDefs.FeedViewPost
feed?: FeedDescriptor feedSourceInfo?: FeedSourceInfo
} }
/** /**
+3 -3
View File
@@ -17,7 +17,7 @@ import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
import {useSetHomeBadge} from '#/state/home-badge' 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 {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed' import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
@@ -51,7 +51,7 @@ export function FeedPage({
renderEmptyState: () => JSX.Element renderEmptyState: () => JSX.Element
renderEndOfFeed?: () => JSX.Element renderEndOfFeed?: () => JSX.Element
savedFeedConfig?: AppBskyActorDefs.SavedFeed savedFeedConfig?: AppBskyActorDefs.SavedFeed
feedInfo: SavedFeedSourceInfo feedInfo: FeedSourceInfo
}) { }) {
const {hasSession} = useSession() const {hasSession} = useSession()
const {_} = useLingui() const {_} = useLingui()
@@ -61,7 +61,7 @@ export function FeedPage({
const [isScrolledDown, setIsScrolledDown] = useState(false) const [isScrolledDown, setIsScrolledDown] = useState(false)
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const headerOffset = useHeaderOffset() const headerOffset = useHeaderOffset()
const feedFeedback = useFeedFeedback(feed, hasSession) const feedFeedback = useFeedFeedback(feedInfo, hasSession)
const scrollElRef = useRef<ListMethods>(null) const scrollElRef = useRef<ListMethods>(null)
const [hasNew, setHasNew] = useState(false) const [hasNew, setHasNew] = useState(false)
const setHomeBadge = useSetHomeBadge() const setHomeBadge = useSetHomeBadge()
+2 -2
View File
@@ -176,7 +176,7 @@ let FeedItemInner = ({
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author]) }, [post.uri, post.author])
const {sendInteraction, feedDescriptor} = useFeedFeedbackContext() const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext()
const onPressReply = () => { const onPressReply = () => {
sendInteraction({ sendInteraction({
@@ -234,7 +234,7 @@ let FeedItemInner = ({
}) })
unstableCacheProfileView(queryClient, post.author) unstableCacheProfileView(queryClient, post.author)
setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), { setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), {
feed: feedDescriptor, feedSourceInfo,
post: { post: {
post, post,
reason: AppBskyFeedDefs.isReasonRepost(reason) ? reason : undefined, reason: AppBskyFeedDefs.isReasonRepost(reason) ? reason : undefined,