From 6f696e4b737be1ceed45a43ee17a17c1aae6b1e5 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:17 +0300 Subject: [PATCH] [SDK] Migrate the feed api classes to the appview client (#11361) Co-authored-by: Claude Fable 5 --- src/lib/api/feed/author.ts | 45 ++++---- src/lib/api/feed/custom.ts | 88 ++++++++-------- src/lib/api/feed/demo.ts | 9 +- src/lib/api/feed/following.ts | 31 +++--- src/lib/api/feed/home.ts | 24 +++-- src/lib/api/feed/likes.ts | 49 +++++---- src/lib/api/feed/list.ts | 45 ++++---- src/lib/api/feed/merge.ts | 111 ++++++++++++-------- src/lib/api/feed/posts.ts | 39 +++---- src/state/queries/explore-feed-previews.tsx | 9 +- src/state/queries/post-feed.ts | 47 ++++++--- src/state/queries/post-liked-by.ts | 30 +++--- src/state/queries/post-quotes.ts | 21 ++-- src/state/queries/post-reposted-by.ts | 23 ++-- 14 files changed, 314 insertions(+), 257 deletions(-) diff --git a/src/lib/api/feed/author.ts b/src/lib/api/feed/author.ts index 3b97b8ef73..92f8d327b4 100644 --- a/src/lib/api/feed/author.ts +++ b/src/lib/api/feed/author.ts @@ -1,23 +1,25 @@ -import { - AppBskyFeedDefs, - type AppBskyFeedGetAuthorFeed as GetAuthorFeed, - type AtpAgent, -} from '@atproto/api' +import {AppBskyFeedDefs} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' +type GetAuthorFeedParams = XrpcRequestParams< + typeof app.bsky.feed.getAuthorFeed.main +> + export class AuthorFeedAPI implements FeedAPI { - agent: AtpAgent - _params: GetAuthorFeed.QueryParams + client: Client + _params: GetAuthorFeedParams constructor({ - agent, + client, feedParams, }: { - agent: AtpAgent - feedParams: GetAuthorFeed.QueryParams + client: Client + feedParams: GetAuthorFeedParams }) { - this.agent = agent + this.client = client this._params = feedParams } @@ -28,11 +30,11 @@ export class AuthorFeedAPI implements FeedAPI { } async peekLatest(): Promise { - const res = await this.agent.getAuthorFeed({ + const data = await this.client.call(app.bsky.feed.getAuthorFeed, { ...this.params, limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -42,19 +44,20 @@ export class AuthorFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getAuthorFeed({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI (blocked actor, rate limit). + * The agent behaved the same way - its `success` flag was only ever true - + * so the empty-page branch this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getAuthorFeed, { ...this.params, cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: this._filter(res.data.feed), - } - } return { - feed: [], + cursor: data.cursor, + feed: this._filter(data.feed), } } diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 54d9dc9067..3b1ec3bec3 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -1,46 +1,46 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedGetFeed as GetCustomFeed, - AtpAgent, - jsonStringToLex, -} from '@atproto/api' +import {type AppBskyFeedDefs, AtpAgent, jsonStringToLex} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' import { getAppLanguageAsContentLanguage, getContentLanguages, } from '#/state/preferences/languages' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' +type GetCustomFeedParams = XrpcRequestParams + export class CustomFeedAPI implements FeedAPI { - agent: AtpAgent - params: GetCustomFeed.QueryParams + client: Client + params: GetCustomFeedParams userInterests?: string constructor({ - agent, + client, feedParams, userInterests, }: { - agent: AtpAgent - feedParams: GetCustomFeed.QueryParams + client: Client + feedParams: GetCustomFeedParams userInterests?: string }) { - this.agent = agent + this.client = client this.params = feedParams this.userInterests = userInterests } async peekLatest(): Promise { const contentLangs = getContentLanguages().join(',') - const res = await this.agent.app.bsky.feed.getFeed( + const data = await this.client.call( + app.bsky.feed.getFeed, { ...this.params, limit: 1, }, {headers: {'Accept-Language': contentLangs}}, ) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -51,11 +51,17 @@ export class CustomFeedAPI implements FeedAPI { limit: number }): Promise { const contentLangs = getContentLanguages().join(',') - const agent = this.agent const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed) - const res = agent.did - ? await this.agent.app.bsky.feed.getFeed( + /* + * The authed branch rejects on failure, so the error propagates to the + * query and drives the feed error UI (feedgen offline, misconfigured, rate + * limited). Only the logged-out branch can resolve without data, and it + * signals that with a null body. + */ + const data = this.client.did + ? await this.client.call( + app.bsky.feed.getFeed, { ...this.params, cursor, @@ -71,21 +77,22 @@ export class CustomFeedAPI implements FeedAPI { }, ) : await loggedOutFetch({...this.params, cursor, limit}) - if (res.success) { - // NOTE - // some custom feeds fail to enforce the pagination limit - // so we manually truncate here - // -prf - if (res.data.feed.length > limit) { - res.data.feed = res.data.feed.slice(0, limit) - } + + if (!data) { return { - cursor: res.data.feed.length ? res.data.cursor : undefined, - feed: res.data.feed, + feed: [], } } + + // NOTE + // some custom feeds fail to enforce the pagination limit + // so we manually truncate here + // -prf + const feed = + data.feed.length > limit ? data.feed.slice(0, limit) : data.feed return { - feed: [], + cursor: feed.length ? data.cursor : undefined, + feed, } } } @@ -105,7 +112,7 @@ async function loggedOutFetch({ feed: string limit: number cursor?: string -}) { +}): Promise { let contentLangs = getAppLanguageAsContentLanguage() /** @@ -128,14 +135,15 @@ async function loggedOutFetch({ headers: {'Accept-Language': contentLangs, ...labelersHeader}, }, ) + /* + * The response is hand-decoded rather than validated, so the lex output shape + * is asserted here just as the old-world one was. + */ let data = res.ok - ? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema) + ? (jsonStringToLex(await res.text()) as app.bsky.feed.getFeed.$OutputBody) : null if (data?.feed?.length) { - return { - success: true, - data, - } + return data } // no data, try again with language headers removed @@ -146,17 +154,11 @@ async function loggedOutFetch({ {method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}}, ) data = res.ok - ? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema) + ? (jsonStringToLex(await res.text()) as app.bsky.feed.getFeed.$OutputBody) : null if (data?.feed?.length) { - return { - success: true, - data, - } + return data } - return { - success: false, - data: {feed: []}, - } + return null } diff --git a/src/lib/api/feed/demo.ts b/src/lib/api/feed/demo.ts index 42d1046bdc..a4c1c360e9 100644 --- a/src/lib/api/feed/demo.ts +++ b/src/lib/api/feed/demo.ts @@ -1,13 +1,14 @@ -import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' import {DEMO_FEED} from '#/lib/demo' import {type FeedAPI, type FeedAPIResponse} from './types' export class DemoFeedAPI implements FeedAPI { - agent: AtpAgent + client: Client - constructor({agent}: {agent: AtpAgent}) { - this.agent = agent + constructor({client}: {client: Client}) { + this.client = client } async peekLatest(): Promise { diff --git a/src/lib/api/feed/following.ts b/src/lib/api/feed/following.ts index 17e96d8e1b..596483577a 100644 --- a/src/lib/api/feed/following.ts +++ b/src/lib/api/feed/following.ts @@ -1,19 +1,21 @@ -import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' export class FollowingFeedAPI implements FeedAPI { - agent: AtpAgent + client: Client - constructor({agent}: {agent: AtpAgent}) { - this.agent = agent + constructor({client}: {client: Client}) { + this.client = client } async peekLatest(): Promise { - const res = await this.agent.getTimeline({ + const data = await this.client.call(app.bsky.feed.getTimeline, { limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -23,18 +25,19 @@ export class FollowingFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getTimeline({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI. The agent behaved the same + * way - its `success` flag was only ever true - so the empty-page branch + * this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getTimeline, { cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: res.data.feed, - } - } return { - feed: [], + cursor: data.cursor, + feed: data.feed, } } } diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts index aa13c70bf0..a5ff0f3b93 100644 --- a/src/lib/api/feed/home.ts +++ b/src/lib/api/feed/home.ts @@ -1,4 +1,6 @@ -import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString} from '@atproto/syntax' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {CustomFeedAPI} from './custom' @@ -27,7 +29,7 @@ export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = { } export class HomeFeedAPI implements FeedAPI { - agent: AtpAgent + client: Client following: FollowingFeedAPI discover: CustomFeedAPI usingDiscover = false @@ -36,25 +38,25 @@ export class HomeFeedAPI implements FeedAPI { constructor({ userInterests, - agent, + client, }: { userInterests?: string - agent: AtpAgent + client: Client }) { - this.agent = agent - this.following = new FollowingFeedAPI({agent}) + this.client = client + this.following = new FollowingFeedAPI({client}) this.discover = new CustomFeedAPI({ - agent, - feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, + client, + feedParams: {feed: PROD_DEFAULT_FEED('whats-hot') as AtUriString}, }) this.userInterests = userInterests } reset() { - this.following = new FollowingFeedAPI({agent: this.agent}) + this.following = new FollowingFeedAPI({client: this.client}) this.discover = new CustomFeedAPI({ - agent: this.agent, - feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, + client: this.client, + feedParams: {feed: PROD_DEFAULT_FEED('whats-hot') as AtUriString}, userInterests: this.userInterests, }) this.usingDiscover = false diff --git a/src/lib/api/feed/likes.ts b/src/lib/api/feed/likes.ts index 1511dc833a..d7d63fc4db 100644 --- a/src/lib/api/feed/likes.ts +++ b/src/lib/api/feed/likes.ts @@ -1,32 +1,34 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedGetActorLikes as GetActorLikes, - type AtpAgent, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' +type GetActorLikesParams = XrpcRequestParams< + typeof app.bsky.feed.getActorLikes.main +> + export class LikesFeedAPI implements FeedAPI { - agent: AtpAgent - params: GetActorLikes.QueryParams + client: Client + params: GetActorLikesParams constructor({ - agent, + client, feedParams, }: { - agent: AtpAgent - feedParams: GetActorLikes.QueryParams + client: Client + feedParams: GetActorLikesParams }) { - this.agent = agent + this.client = client this.params = feedParams } async peekLatest(): Promise { - const res = await this.agent.getActorLikes({ + const data = await this.client.call(app.bsky.feed.getActorLikes, { ...this.params, limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -36,21 +38,22 @@ export class LikesFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getActorLikes({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI. The agent behaved the same + * way - its `success` flag was only ever true - so the empty-page branch + * this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getActorLikes, { ...this.params, cursor, limit, }) - if (res.success) { - // HACKFIX: the API incorrectly returns a cursor when there are no items -sfn - const isEmptyPage = res.data.feed.length === 0 - return { - cursor: isEmptyPage ? undefined : res.data.cursor, - feed: res.data.feed, - } - } + // HACKFIX: the API incorrectly returns a cursor when there are no items -sfn + const isEmptyPage = data.feed.length === 0 return { - feed: [], + cursor: isEmptyPage ? undefined : data.cursor, + feed: data.feed, } } } diff --git a/src/lib/api/feed/list.ts b/src/lib/api/feed/list.ts index 9697b0aaf3..e4d07d73d7 100644 --- a/src/lib/api/feed/list.ts +++ b/src/lib/api/feed/list.ts @@ -1,32 +1,34 @@ -import { - type Agent, - type AppBskyFeedDefs, - type AppBskyFeedGetListFeed as GetListFeed, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' +type GetListFeedParams = XrpcRequestParams< + typeof app.bsky.feed.getListFeed.main +> + export class ListFeedAPI implements FeedAPI { - agent: Agent - params: GetListFeed.QueryParams + client: Client + params: GetListFeedParams constructor({ - agent, + client, feedParams, }: { - agent: Agent - feedParams: GetListFeed.QueryParams + client: Client + feedParams: GetListFeedParams }) { - this.agent = agent + this.client = client this.params = feedParams } async peekLatest(): Promise { - const res = await this.agent.app.bsky.feed.getListFeed({ + const data = await this.client.call(app.bsky.feed.getListFeed, { ...this.params, limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -36,19 +38,20 @@ export class ListFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.app.bsky.feed.getListFeed({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI. The agent behaved the same + * way - its `success` flag was only ever true - so the empty-page branch + * this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getListFeed, { ...this.params, cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: res.data.feed, - } - } return { - feed: [], + cursor: data.cursor, + feed: data.feed, } } } diff --git a/src/lib/api/feed/merge.ts b/src/lib/api/feed/merge.ts index c341dd53a0..2e3abcb8e3 100644 --- a/src/lib/api/feed/merge.ts +++ b/src/lib/api/feed/merge.ts @@ -1,8 +1,6 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedGetTimeline, - type AtpAgent, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString} from '@atproto/syntax' import shuffle from 'lodash.shuffle' import {bundleAsync} from '#/lib/async/bundle' @@ -10,6 +8,7 @@ import {timeout} from '#/lib/async/timeout' import {feedUriToHref} from '#/lib/strings/url-helpers' import {getContentLanguages} from '#/state/preferences/languages' import {type FeedParams} from '#/state/queries/post-feed' +import {app} from '#/lexicons' import {FeedTuner} from '../feed-manip' import {type FeedTunerFn} from '../feed-manip' import { @@ -22,9 +21,19 @@ import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' const REQUEST_WAIT_MS = 500 // 500ms const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours +/** + * A page of feed items, or `null` when the source could not produce one. Only + * sources that deliberately swallow their own errors return `null`; the rest + * reject so the error reaches the caller. + */ +type MergeFeedPage = { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] +} | null + export class MergeFeedAPI implements FeedAPI { userInterests?: string - agent: AtpAgent + client: Client params: FeedParams feedTuners: FeedTunerFn[] following: MergeFeedSource_Following @@ -34,29 +43,29 @@ export class MergeFeedAPI implements FeedAPI { sampleCursor = 0 constructor({ - agent, + client, feedParams, feedTuners, userInterests, }: { - agent: AtpAgent + client: Client feedParams: FeedParams feedTuners: FeedTunerFn[] userInterests?: string }) { - this.agent = agent + this.client = client this.params = feedParams this.feedTuners = feedTuners this.userInterests = userInterests this.following = new MergeFeedSource_Following({ - agent: this.agent, + client: this.client, feedTuners: this.feedTuners, }) } reset() { this.following = new MergeFeedSource_Following({ - agent: this.agent, + client: this.client, feedTuners: this.feedTuners, }) this.customFeeds = [] @@ -68,7 +77,7 @@ export class MergeFeedAPI implements FeedAPI { this.params.mergeFeedSources.map( feedUri => new MergeFeedSource_Custom({ - agent: this.agent, + client: this.client, feedUri, feedTuners: this.feedTuners, userInterests: this.userInterests, @@ -81,10 +90,10 @@ export class MergeFeedAPI implements FeedAPI { } async peekLatest(): Promise { - const res = await this.agent.getTimeline({ + const data = await this.client.call(app.bsky.feed.getTimeline, { limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -175,7 +184,7 @@ export class MergeFeedAPI implements FeedAPI { } class MergeFeedSource { - agent: AtpAgent + client: Client feedTuners: FeedTunerFn[] sourceInfo: ReasonFeedSource | undefined cursor: string | undefined = undefined @@ -183,13 +192,13 @@ class MergeFeedSource { hasMore = true constructor({ - agent, + client, feedTuners, }: { - agent: AtpAgent + client: Client feedTuners: FeedTunerFn[] }) { - this.agent = agent + this.client = client this.feedTuners = feedTuners } @@ -210,11 +219,11 @@ class MergeFeedSource { } _fetchNextInner = bundleAsync(async (n: number) => { - const res = await this._getFeed(this.cursor, n) - if (res.success) { - this.cursor = res.data.cursor - if (res.data.feed.length) { - this.queue = this.queue.concat(res.data.feed) + const page = await this._getFeed(this.cursor, n) + if (page) { + this.cursor = page.cursor + if (page.feed.length) { + this.queue = this.queue.concat(page.feed) } else { this.hasMore = false } @@ -226,7 +235,7 @@ class MergeFeedSource { protected _getFeed( _cursor: string | undefined, _limit: number, - ): Promise { + ): Promise { throw new Error('Must be overridden') } } @@ -238,42 +247,49 @@ class MergeFeedSource_Following extends MergeFeedSource { return this._fetchNextInner(n) } + /* + * No error handling: a failed timeline read rejects, which is what the agent + * did too, so the error still reaches `MergeFeedAPI.fetch` and the query. + */ protected async _getFeed( cursor: string | undefined, limit: number, - ): Promise { - const res = await this.agent.getTimeline({cursor, limit}) + ): Promise { + const data = await this.client.call(app.bsky.feed.getTimeline, { + cursor, + limit, + }) // run the tuner pre-emptively to ensure better mixing - const slices = this.tuner.tune(res.data.feed, { + const slices = this.tuner.tune(data.feed, { dryRun: false, }) - res.data.feed = slices.map(slice => slice._feedPost) - return res + return { + cursor: data.cursor, + feed: slices.map(slice => slice._feedPost), + } } } class MergeFeedSource_Custom extends MergeFeedSource { - agent: AtpAgent minDate: Date feedUri: string userInterests?: string constructor({ - agent, + client, feedUri, feedTuners, userInterests, }: { - agent: AtpAgent + client: Client feedUri: string feedTuners: FeedTunerFn[] userInterests?: string }) { super({ - agent, + client, feedTuners, }) - this.agent = agent this.feedUri = feedUri this.userInterests = userInterests this.sourceInfo = { @@ -287,15 +303,16 @@ class MergeFeedSource_Custom extends MergeFeedSource { protected async _getFeed( cursor: string | undefined, limit: number, - ): Promise { + ): Promise { try { const contentLangs = getContentLanguages().join(',') const isBlueskyOwned = isBlueskyOwnedFeed(this.feedUri) - const res = await this.agent.app.bsky.feed.getFeed( + const data = await this.client.call( + app.bsky.feed.getFeed, { cursor, limit, - feed: this.feedUri, + feed: this.feedUri as AtUriString, }, { headers: { @@ -310,22 +327,24 @@ class MergeFeedSource_Custom extends MergeFeedSource { // some custom feeds fail to enforce the pagination limit // so we manually truncate here // -prf - if (limit && res.data.feed.length > limit) { - res.data.feed = res.data.feed.slice(0, limit) - } + let feed: AppBskyFeedDefs.FeedViewPost[] = + limit && data.feed.length > limit + ? data.feed.slice(0, limit) + : data.feed // filter out older posts - res.data.feed = res.data.feed.filter( - post => new Date(post.post.indexedAt) > this.minDate, - ) + feed = feed.filter(post => new Date(post.post.indexedAt) > this.minDate) // attach source info - for (const post of res.data.feed) { + for (const post of feed) { // @ts-ignore post.__source = this.sourceInfo } - return res + return { + cursor: data.cursor, + feed, + } } catch { // dont bubble custom-feed errors - return {success: false, headers: {}, data: {feed: []}} + return null } } } diff --git a/src/lib/api/feed/posts.ts b/src/lib/api/feed/posts.ts index 33eff50997..d2bcfc4317 100644 --- a/src/lib/api/feed/posts.ts +++ b/src/lib/api/feed/posts.ts @@ -1,25 +1,25 @@ -import { - type Agent, - type AppBskyFeedDefs, - type AppBskyFeedGetPosts, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' import {logger} from '#/logger' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' +type GetPostsParams = XrpcRequestParams + export class PostListFeedAPI implements FeedAPI { - agent: Agent - params: AppBskyFeedGetPosts.QueryParams + client: Client + params: GetPostsParams peek: AppBskyFeedDefs.FeedViewPost | null = null constructor({ - agent, + client, feedParams, }: { - agent: Agent - feedParams: AppBskyFeedGetPosts.QueryParams + client: Client + feedParams: GetPostsParams }) { - this.agent = agent + this.client = client if (feedParams.uris.length > 25) { logger.warn( `Too many URIs provided - expected 25, got ${feedParams.uris.length}`, @@ -36,17 +36,18 @@ export class PostListFeedAPI implements FeedAPI { } async fetch({}: {}): Promise { - const res = await this.agent.app.bsky.feed.getPosts({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI. The agent behaved the same + * way - its `success` flag was only ever true - so the empty-page branch + * this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getPosts, { ...this.params, }) - if (res.success) { - this.peek = {post: res.data.posts[0]} - return { - feed: res.data.posts.map(post => ({post})), - } - } + this.peek = {post: data.posts[0]} return { - feed: [], + feed: data.posts.map(post => ({post})), } } } diff --git a/src/state/queries/explore-feed-previews.tsx b/src/state/queries/explore-feed-previews.tsx index acbc4ee7e1..c340078603 100644 --- a/src/state/queries/explore-feed-previews.tsx +++ b/src/state/queries/explore-feed-previews.tsx @@ -5,6 +5,7 @@ import { AtUri, moderatePost, } from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import { @@ -28,7 +29,7 @@ import { embedViewRecordToPostView, getEmbeddedPost, } from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' const RQKEY_ROOT = 'feed-previews' const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds] @@ -121,7 +122,7 @@ export function useFeedPreviews( const uris = feeds.map(feed => feed.uri) const {_} = useLingui() - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() const userInterests = aggregateUserInterests(preferences) const moderationOpts = useModerationOpts() @@ -143,8 +144,8 @@ export function useFeedPreviews( queryFn: async ({pageParam}) => { const feed = feeds[pageParam] const api = new CustomFeedAPI({ - agent, - feedParams: {feed: feed.uri}, + client, + feedParams: {feed: feed.uri as AtUriString}, userInterests, }) const data = await api.fetch({cursor: undefined, limit: LIMIT}) diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 959ed81c28..0d5b48acde 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -4,12 +4,13 @@ import { type AppBskyActorDefs, AppBskyFeedDefs, type AppBskyFeedPost, - type AtpAgent, AtUri, moderatePost, type ModerationDecision, type ModerationPrefs, } from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtIdentifierString, type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -33,7 +34,7 @@ import {DISCOVER_FEED_URI} from '#/lib/constants' import {logger} from '#/logger' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' -import {useAgent} from '#/state/session' +import {useAgent, useAppviewClient} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' import {KnownError} from '#/view/com/posts/PostFeedErrorMessage' import {useFeedTuners} from '../preferences/feed-tuners' @@ -149,6 +150,7 @@ export function usePostFeedQuery( ) ?? -1 const enableFollowingToDiscoverFallback = followingPinnedIndex === 0 const agent = useAgent() + const client = useAppviewClient() const lastRun = useRef<{ data: InfiniteData args: typeof selectArgs @@ -193,7 +195,7 @@ export function usePostFeedQuery( feedDesc, feedParams: params || {}, feedTuners, - agent, + client, // Not in the query key because they don't change: userInterests, // Not in the query key. Reacting to it switching isn't important: @@ -443,55 +445,68 @@ function createApi({ feedParams, feedTuners, userInterests, - agent, + client, enableFollowingToDiscoverFallback, }: { feedDesc: FeedDescriptor feedParams: FeedParams feedTuners: FeedTunerFn[] userInterests?: string - agent: AtpAgent + client: Client enableFollowingToDiscoverFallback: boolean }) { if (feedDesc === 'following') { if (feedParams.mergeFeedEnabled) { return new MergeFeedAPI({ - agent, + client, feedParams, feedTuners, userInterests, }) } else { if (enableFollowingToDiscoverFallback) { - return new HomeFeedAPI({agent, userInterests}) + return new HomeFeedAPI({client, userInterests}) } else { - return new FollowingFeedAPI({agent}) + return new FollowingFeedAPI({client}) } } } else if (feedDesc.startsWith('author')) { const [__, actor, filter] = feedDesc.split('|') - return new AuthorFeedAPI({agent, feedParams: {actor, filter}}) + /* + * The descriptor is split out of an internally-built string, so neither the + * actor identifier nor the filter token is narrowed by the compiler here. + */ + return new AuthorFeedAPI({ + client, + feedParams: {actor: actor as AtIdentifierString, filter}, + }) } else if (feedDesc.startsWith('likes')) { const [__, actor] = feedDesc.split('|') - return new LikesFeedAPI({agent, feedParams: {actor}}) + return new LikesFeedAPI({ + client, + feedParams: {actor: actor as AtIdentifierString}, + }) } else if (feedDesc.startsWith('feedgen')) { const [__, feed] = feedDesc.split('|') return new CustomFeedAPI({ - agent, - feedParams: {feed}, + client, + feedParams: {feed: feed as AtUriString}, userInterests, }) } else if (feedDesc.startsWith('list')) { const [__, list] = feedDesc.split('|') - return new ListFeedAPI({agent, feedParams: {list}}) + return new ListFeedAPI({client, feedParams: {list: list as AtUriString}}) } else if (feedDesc.startsWith('posts')) { const [__, uriList] = feedDesc.split('|') - return new PostListFeedAPI({agent, feedParams: {uris: uriList.split(',')}}) + return new PostListFeedAPI({ + client, + feedParams: {uris: uriList.split(',') as AtUriString[]}, + }) } else if (feedDesc === 'demo') { - return new DemoFeedAPI({agent}) + return new DemoFeedAPI({client}) } else { // shouldnt happen - return new FollowingFeedAPI({agent}) + return new FollowingFeedAPI({client}) } } diff --git a/src/state/queries/post-liked-by.ts b/src/state/queries/post-liked-by.ts index e4f37c14ca..cc18a46426 100644 --- a/src/state/queries/post-liked-by.ts +++ b/src/state/queries/post-liked-by.ts @@ -1,4 +1,5 @@ -import {type AppBskyActorDefs, type AppBskyFeedGetLikes} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,7 +10,8 @@ import { import {STALE} from '#/state/queries' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 30 type RQPageParam = string | undefined @@ -19,22 +21,22 @@ const RQKEY_ROOT = 'liked-by' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function useLikedByQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetLikes.OutputSchema, + app.bsky.feed.getLikes.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.getLikes({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getLikes, { + // the enabled flag prevents this from running until resolvedUri is set + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -59,12 +61,14 @@ export const createLikedBySampleQueryKey = (args: {uri: string}) => * perturb the liked-by screen's pagination. */ export function useLikedBySampleQuery({uri}: {uri: string | undefined}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: createLikedBySampleQueryKey({uri: uri ?? ''}), queryFn: async () => { - const res = await agent.getLikes({uri: uri ?? '', limit: SAMPLE_SIZE}) - return res.data + return await client.call(app.bsky.feed.getLikes, { + uri: (uri ?? '') as AtUriString, + limit: SAMPLE_SIZE, + }) }, staleTime: STALE.MINUTES.FIVE, enabled: !!uri, @@ -81,7 +85,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) @@ -98,7 +102,7 @@ export function* findAllProfilesInQueryData( } } const sampleQueryDatas = - queryClient.getQueriesData({ + queryClient.getQueriesData({ queryKey: [likedBySampleQueryKeyRoot], }) for (const [_queryKey, queryData] of sampleQueryDatas) { diff --git a/src/state/queries/post-quotes.ts b/src/state/queries/post-quotes.ts index 1d0fa07e8e..77b5dabcf8 100644 --- a/src/state/queries/post-quotes.ts +++ b/src/state/queries/post-quotes.ts @@ -2,9 +2,9 @@ import { type AppBskyActorDefs, AppBskyEmbedRecord, type AppBskyFeedDefs, - type AppBskyFeedGetQuotes, AtUri, } from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -12,7 +12,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import { didOrHandleUriMatches, embedViewRecordToPostView, @@ -26,22 +27,22 @@ const RQKEY_ROOT = 'post-quotes' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function usePostQuotesQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetQuotes.OutputSchema, + app.bsky.feed.getQuotes.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.api.app.bsky.feed.getQuotes({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getQuotes, { + // the enabled flag prevents this from running until resolvedUri is set + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -72,7 +73,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) @@ -99,7 +100,7 @@ export function* findAllPostsInQueryData( uri: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/post-reposted-by.ts b/src/state/queries/post-reposted-by.ts index 814a815aae..d2c5e62385 100644 --- a/src/state/queries/post-reposted-by.ts +++ b/src/state/queries/post-reposted-by.ts @@ -1,7 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyFeedGetRepostedBy, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,7 +7,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 30 type RQPageParam = string | undefined @@ -19,22 +18,22 @@ const RQKEY_ROOT = 'post-reposted-by' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function usePostRepostedByQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetRepostedBy.OutputSchema, + app.bsky.feed.getRepostedBy.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.getRepostedBy({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getRepostedBy, { + // the enabled flag prevents this from running until resolvedUri is set + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -47,7 +46,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], })