migrate the feed source and feed feedback calls to the appview client

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-03 17:57:58 +03:00
parent 1a6bfd8a6a
commit 6e59eeb9a6
2 changed files with 90 additions and 69 deletions
+22 -13
View File
@@ -8,6 +8,7 @@ import {
} from 'react' } from 'react'
import {AppState, type AppStateStatus} from 'react-native' import {AppState, type AppStateStatus} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api' import {type AppBskyFeedDefs} from '@atproto/api'
import {type AtUriString, type DidString} from '@atproto/syntax'
import throttle from 'lodash.throttle' import throttle from 'lodash.throttle'
import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants' import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants'
@@ -22,7 +23,8 @@ import {
} from '#/state/queries/post-feed' } from '#/state/queries/post-feed'
import {getItemsForFeedback} from '#/view/com/posts/PostFeed' import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {useAgent} from './session' import {app} from '#/lexicons'
import {useAppviewClient} from './session'
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS] export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
@@ -65,7 +67,7 @@ export function useFeedFeedback(
) { ) {
const ax = useAnalytics() const ax = useAnalytics()
const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback) const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback)
const agent = useAgent() const client = useAppviewClient()
const feed = const feed =
!!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo) !!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo)
@@ -150,16 +152,19 @@ export function useFeedFeedback(
return return
} }
// Send to the feed /*
agent.app.bsky.feed * Send to the feed. Interactions go to the feed generator rather than the
.sendInteractions( * appview, which the agent did by setting `atproto-proxy` by hand; the
{interactions: interactionsToSend, feed: feed?.uri}, * client's per-call `service` option writes that same header.
*/
client
.call(
app.bsky.feed.sendInteractions,
{ {
encoding: 'application/json', interactions: interactionsToSend,
headers: { feed: feed?.uri as AtUriString | undefined,
'atproto-proxy': `${proxyDid}#bsky_fg`,
},
}, },
{service: `${proxyDid as DidString}#bsky_fg`},
) )
.catch(() => {}) // ignore upstream errors .catch(() => {}) // ignore upstream errors
@@ -172,7 +177,7 @@ export function useFeedFeedback(
) )
throttledFlushAggregatedStats() throttledFlushAggregatedStats()
logger.debug('flushed') logger.debug('flushed')
}, [agent, throttledFlushAggregatedStats, proxyDid, enabled, feed]) }, [client, throttledFlushAggregatedStats, proxyDid, enabled, feed])
const sendToFeed = useMemo( const sendToFeed = useMemo(
() => () =>
@@ -283,9 +288,13 @@ function toString(interaction: AppBskyFeedDefs.Interaction): string {
}|${interaction.reqId || ''}` }|${interaction.reqId || ''}`
} }
function toInteraction(str: string): AppBskyFeedDefs.Interaction { function toInteraction(str: string): app.bsky.feed.defs.Interaction {
const [item, event, feedContext, reqId] = str.split('|') const [item, event, feedContext, reqId] = str.split('|')
return {item, event, feedContext, reqId} /*
* The fields come from splitting an internally-built key, so neither the
* at-uri nor the event token is narrowed by the compiler here.
*/
return {item, event, feedContext, reqId} as app.bsky.feed.defs.Interaction
} }
type AggregatedStats = { type AggregatedStats = {
+62 -50
View File
@@ -3,11 +3,11 @@ import {
type AppBskyActorDefs, type AppBskyActorDefs,
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyGraphDefs, type AppBskyGraphDefs,
type AppBskyUnspeccedGetPopularFeedGenerators,
AtUri, AtUri,
moderateFeedGenerator, moderateFeedGenerator,
RichText, RichText,
} from '@atproto/api' } from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {t} from '@lingui/core/macro' import {t} from '@lingui/core/macro'
import { import {
type InfiniteData, type InfiniteData,
@@ -26,7 +26,8 @@ import {GCTIME, STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list' import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util' import {createQueryKey} from '#/state/queries/util'
import {useAgent, useSession} from '#/state/session' import {useAppviewClient, useSession} from '#/state/session'
import {app} from '#/lexicons'
import {router} from '#/routes' import {router} from '#/routes'
import {useModerationOpts} from '../preferences/moderation-opts' import {useModerationOpts} from '../preferences/moderation-opts'
import {type FeedDescriptor} from './post-feed' import {type FeedDescriptor} from './post-feed'
@@ -184,7 +185,7 @@ export function getAvatarTypeFromUri(uri: string) {
export function useFeedSourceInfoQuery({uri}: {uri: string}) { export function useFeedSourceInfoQuery({uri}: {uri: string}) {
const type = getFeedTypeFromUri(uri) const type = getFeedTypeFromUri(uri)
const agent = useAgent() const client = useAppviewClient()
return useQuery({ return useQuery({
staleTime: STALE.INFINITY, staleTime: STALE.INFINITY,
@@ -193,14 +194,16 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
let view: FeedSourceInfo let view: FeedSourceInfo
if (type === 'feed') { if (type === 'feed') {
const res = await agent.app.bsky.feed.getFeedGenerator({feed: uri}) const data = await client.call(app.bsky.feed.getFeedGenerator, {
view = hydrateFeedGenerator(res.data.view) feed: uri as AtUriString,
})
view = hydrateFeedGenerator(data.view)
} else { } else {
const res = await agent.app.bsky.graph.getList({ const data = await client.call(app.bsky.graph.getList, {
list: uri, list: uri as AtUriString,
limit: 1, limit: 1,
}) })
view = hydrateList(res.data.list) view = hydrateList(data.list)
} }
return view return view
@@ -234,7 +237,7 @@ export function createGetPopularFeedsQueryKey(
export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
const {hasSession} = useSession() const {hasSession} = useSession()
const agent = useAgent() const client = useAppviewClient()
const limit = options?.limit || 10 const limit = options?.limit || 10
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -255,24 +258,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
enabled: Boolean(moderationOpts) && options?.enabled !== false, enabled: Boolean(moderationOpts) && options?.enabled !== false,
queryKey: createGetPopularFeedsQueryKey(options), queryKey: createGetPopularFeedsQueryKey(options),
queryFn: async ({pageParam}) => { queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ const data = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit, limit,
cursor: pageParam, cursor: pageParam,
}) },
)
// precache feeds // precache feeds
for (const feed of res.data.feeds) { for (const feed of data.feeds) {
const hydratedFeed = hydrateFeedGenerator(feed) const hydratedFeed = hydrateFeedGenerator(feed)
precacheFeed(queryClient, hydratedFeed) precacheFeed(queryClient, hydratedFeed)
} }
return res.data return data
}, },
initialPageParam: undefined as string | undefined, initialPageParam: undefined as string | undefined,
getNextPageParam: lastPage => lastPage.cursor, getNextPageParam: lastPage => lastPage.cursor,
select: useCallback( select: useCallback(
( (
data: InfiniteData<AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema>, data: InfiniteData<app.bsky.unspecced.getPopularFeedGenerators.$OutputBody>,
) => { ) => {
const { const {
savedFeeds, savedFeeds,
@@ -336,24 +342,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
} }
export function useSearchPopularFeedsMutation() { export function useSearchPopularFeedsMutation() {
const agent = useAgent() const client = useAppviewClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
return useMutation({ return useMutation({
mutationFn: async (query: string) => { mutationFn: async (query: string) => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ const data = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit: 10, limit: 10,
query: query, query: query,
}) },
)
if (moderationOpts) { if (moderationOpts) {
return res.data.feeds.filter(feed => { return data.feeds.filter(feed => {
const decision = moderateFeedGenerator(feed, moderationOpts) const decision = moderateFeedGenerator(feed, moderationOpts)
return !decision.ui('contentMedia').blur return !decision.ui('contentMedia').blur
}) })
} }
return res.data.feeds return data.feeds
}, },
}) })
} }
@@ -371,7 +380,7 @@ export function usePopularFeedsSearch({
query: string query: string
enabled?: boolean enabled?: boolean
}) { }) {
const agent = useAgent() const client = useAppviewClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const enabledInner = enabled ?? Boolean(moderationOpts) const enabledInner = enabled ?? Boolean(moderationOpts)
@@ -379,12 +388,15 @@ export function usePopularFeedsSearch({
enabled: enabledInner, enabled: enabledInner,
queryKey: createPopularFeedsSearchQueryKey(query), queryKey: createPopularFeedsSearchQueryKey(query),
queryFn: async () => { queryFn: async () => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ const data = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit: 15, limit: 15,
query: query, query: query,
}) },
)
return res.data.feeds return data.feeds
}, },
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
select(data) { select(data) {
@@ -444,7 +456,7 @@ const createPinnedFeedInfosQueryKey = (
export function usePinnedFeedsInfos() { export function usePinnedFeedsInfos() {
const {hasSession} = useSession() const {hasSession} = useSession()
const agent = useAgent() const client = useAppviewClient()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? [] const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
@@ -467,13 +479,13 @@ export function usePinnedFeedsInfos() {
const pinnedFeeds = pinnedItems.filter(feed => feed.type === 'feed') const pinnedFeeds = pinnedItems.filter(feed => feed.type === 'feed')
let feedsPromise = Promise.resolve() let feedsPromise = Promise.resolve()
if (pinnedFeeds.length > 0) { if (pinnedFeeds.length > 0) {
feedsPromise = agent.app.bsky.feed feedsPromise = client
.getFeedGenerators({ .call(app.bsky.feed.getFeedGenerators, {
feeds: pinnedFeeds.map(f => f.value), feeds: pinnedFeeds.map(f => f.value as AtUriString),
}) })
.then(res => { .then(data => {
for (let i = 0; i < res.data.feeds.length; i++) { for (let i = 0; i < data.feeds.length; i++) {
const feedView = res.data.feeds[i] const feedView = data.feeds[i]
resolved.set(feedView.uri, hydrateFeedGenerator(feedView)) resolved.set(feedView.uri, hydrateFeedGenerator(feedView))
} }
}) })
@@ -482,13 +494,13 @@ export function usePinnedFeedsInfos() {
// Get all lists. This currently has to be done individually. // Get all lists. This currently has to be done individually.
const pinnedLists = pinnedItems.filter(feed => feed.type === 'list') const pinnedLists = pinnedItems.filter(feed => feed.type === 'list')
const listsPromises = pinnedLists.map(list => const listsPromises = pinnedLists.map(list =>
agent.app.bsky.graph client
.getList({ .call(app.bsky.graph.getList, {
list: list.value, list: list.value as AtUriString,
limit: 1, limit: 1,
}) })
.then(res => { .then(data => {
const listView = res.data.list const listView = data.list
resolved.set(listView.uri, hydrateList(listView)) resolved.set(listView.uri, hydrateList(listView))
}), }),
) )
@@ -551,7 +563,7 @@ export type SavedFeedItem =
} }
export function useSavedFeeds() { export function useSavedFeeds() {
const agent = useAgent() const client = useAppviewClient()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const savedItems = preferences?.savedFeeds ?? [] const savedItems = preferences?.savedFeeds ?? []
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -582,25 +594,25 @@ export function useSavedFeeds() {
let feedsPromise = Promise.resolve() let feedsPromise = Promise.resolve()
if (savedFeeds.length > 0) { if (savedFeeds.length > 0) {
feedsPromise = agent.app.bsky.feed feedsPromise = client
.getFeedGenerators({ .call(app.bsky.feed.getFeedGenerators, {
feeds: savedFeeds.map(f => f.value), feeds: savedFeeds.map(f => f.value as AtUriString),
}) })
.then(res => { .then(data => {
res.data.feeds.forEach(f => { data.feeds.forEach(f => {
resolvedFeeds.set(f.uri, f) resolvedFeeds.set(f.uri, f)
}) })
}) })
} }
const listsPromises = savedLists.map(list => const listsPromises = savedLists.map(list =>
agent.app.bsky.graph client
.getList({ .call(app.bsky.graph.getList, {
list: list.value, list: list.value as AtUriString,
limit: 1, limit: 1,
}) })
.then(res => { .then(data => {
const listView = res.data.list const listView = data.list
resolvedLists.set(listView.uri, listView) resolvedLists.set(listView.uri, listView)
}), }),
) )
@@ -656,7 +668,7 @@ export function useSavedFeeds() {
const feedInfoQueryKeyRoot = 'feedInfo' const feedInfoQueryKeyRoot = 'feedInfo'
export function useFeedInfo(feedUri: string | undefined) { export function useFeedInfo(feedUri: string | undefined) {
const agent = useAgent() const client = useAppviewClient()
return useQuery({ return useQuery({
staleTime: STALE.INFINITY, staleTime: STALE.INFINITY,
@@ -666,11 +678,11 @@ export function useFeedInfo(feedUri: string | undefined) {
return null return null
} }
const res = await agent.app.bsky.feed.getFeedGenerator({ const data = await client.call(app.bsky.feed.getFeedGenerator, {
feed: feedUri, feed: feedUri as AtUriString,
}) })
const feedSourceInfo = hydrateFeedGenerator(res.data.view) const feedSourceInfo = hydrateFeedGenerator(data.view)
return feedSourceInfo return feedSourceInfo
}, },
}) })