diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/models/content/feed-source.ts | 231 | ||||
-rw-r--r-- | src/state/models/discovery/feeds.ts | 148 | ||||
-rw-r--r-- | src/state/models/lists/actor-feeds.ts | 123 | ||||
-rw-r--r-- | src/state/models/ui/preferences.ts | 27 | ||||
-rw-r--r-- | src/state/models/ui/profile.ts | 255 | ||||
-rw-r--r-- | src/state/queries/feed.ts | 68 | ||||
-rw-r--r-- | src/state/queries/like.ts | 24 | ||||
-rw-r--r-- | src/state/queries/suggested-feeds.ts | 29 |
8 files changed, 88 insertions, 817 deletions
diff --git a/src/state/models/content/feed-source.ts b/src/state/models/content/feed-source.ts deleted file mode 100644 index cd8c08b56..000000000 --- a/src/state/models/content/feed-source.ts +++ /dev/null @@ -1,231 +0,0 @@ -import {AtUri, RichText, AppBskyFeedDefs, AppBskyGraphDefs} from '@atproto/api' -import {makeAutoObservable, runInAction} from 'mobx' -import {RootStoreModel} from 'state/models/root-store' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {bundleAsync} from 'lib/async/bundle' -import {cleanError} from 'lib/strings/errors' -import {track} from 'lib/analytics/analytics' -import {logger} from '#/logger' - -export class FeedSourceModel { - // state - _reactKey: string - hasLoaded = false - error: string | undefined - - // data - uri: string - cid: string = '' - type: 'feed-generator' | 'list' | 'unsupported' = 'unsupported' - avatar: string | undefined = '' - displayName: string = '' - descriptionRT: RichText | null = null - creatorDid: string = '' - creatorHandle: string = '' - likeCount: number | undefined = 0 - likeUri: string | undefined = '' - - constructor(public rootStore: RootStoreModel, uri: string) { - this._reactKey = uri - this.uri = uri - - try { - const urip = new AtUri(uri) - if (urip.collection === 'app.bsky.feed.generator') { - this.type = 'feed-generator' - } else if (urip.collection === 'app.bsky.graph.list') { - this.type = 'list' - } - } catch {} - this.displayName = uri.split('/').pop() || '' - - makeAutoObservable( - this, - { - rootStore: false, - }, - {autoBind: true}, - ) - } - - get href() { - const urip = new AtUri(this.uri) - const collection = - urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists' - return `/profile/${urip.hostname}/${collection}/${urip.rkey}` - } - - get isSaved() { - return this.rootStore.preferences.savedFeeds.includes(this.uri) - } - - get isPinned() { - return false - } - - get isLiked() { - return !!this.likeUri - } - - get isOwner() { - return this.creatorDid === this.rootStore.me.did - } - - setup = bundleAsync(async () => { - try { - if (this.type === 'feed-generator') { - const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ - feed: this.uri, - }) - this.hydrateFeedGenerator(res.data.view) - } else if (this.type === 'list') { - const res = await this.rootStore.agent.app.bsky.graph.getList({ - list: this.uri, - limit: 1, - }) - this.hydrateList(res.data.list) - } - } catch (e) { - runInAction(() => { - this.error = cleanError(e) - }) - } - }) - - hydrateFeedGenerator(view: AppBskyFeedDefs.GeneratorView) { - this.uri = view.uri - this.cid = view.cid - this.avatar = view.avatar - this.displayName = view.displayName - ? sanitizeDisplayName(view.displayName) - : `Feed by ${sanitizeHandle(view.creator.handle, '@')}` - this.descriptionRT = new RichText({ - text: view.description || '', - facets: (view.descriptionFacets || [])?.slice(), - }) - this.creatorDid = view.creator.did - this.creatorHandle = view.creator.handle - this.likeCount = view.likeCount - this.likeUri = view.viewer?.like - this.hasLoaded = true - } - - hydrateList(view: AppBskyGraphDefs.ListView) { - this.uri = view.uri - this.cid = view.cid - this.avatar = view.avatar - this.displayName = view.name - ? sanitizeDisplayName(view.name) - : `User List by ${sanitizeHandle(view.creator.handle, '@')}` - this.descriptionRT = new RichText({ - text: view.description || '', - facets: (view.descriptionFacets || [])?.slice(), - }) - this.creatorDid = view.creator.did - this.creatorHandle = view.creator.handle - this.likeCount = undefined - this.hasLoaded = true - } - - async save() { - if (this.type !== 'feed-generator') { - return - } - try { - await this.rootStore.preferences.addSavedFeed(this.uri) - } catch (error) { - logger.error('Failed to save feed', {error}) - } finally { - track('CustomFeed:Save') - } - } - - async unsave() { - // TODO TEMPORARY — see PRF's comment in content/list.ts togglePin - if (this.type !== 'feed-generator' && this.type !== 'list') { - return - } - try { - await this.rootStore.preferences.removeSavedFeed(this.uri) - } catch (error) { - logger.error('Failed to unsave feed', {error}) - } finally { - track('CustomFeed:Unsave') - } - } - - async pin() { - try { - await this.rootStore.preferences.addPinnedFeed(this.uri) - } catch (error) { - logger.error('Failed to pin feed', {error}) - } finally { - track('CustomFeed:Pin', { - name: this.displayName, - uri: this.uri, - }) - } - } - - async togglePin() { - if (!this.isPinned) { - track('CustomFeed:Pin', { - name: this.displayName, - uri: this.uri, - }) - return this.rootStore.preferences.addPinnedFeed(this.uri) - } else { - track('CustomFeed:Unpin', { - name: this.displayName, - uri: this.uri, - }) - - if (this.type === 'list') { - // TODO TEMPORARY — see PRF's comment in content/list.ts togglePin - return this.unsave() - } else { - return this.rootStore.preferences.removePinnedFeed(this.uri) - } - } - } - - async like() { - if (this.type !== 'feed-generator') { - return - } - try { - this.likeUri = 'pending' - this.likeCount = (this.likeCount || 0) + 1 - const res = await this.rootStore.agent.like(this.uri, this.cid) - this.likeUri = res.uri - } catch (e: any) { - this.likeUri = undefined - this.likeCount = (this.likeCount || 1) - 1 - logger.error('Failed to like feed', {error: e}) - } finally { - track('CustomFeed:Like') - } - } - - async unlike() { - if (this.type !== 'feed-generator') { - return - } - if (!this.likeUri) { - return - } - const uri = this.likeUri - try { - this.likeUri = undefined - this.likeCount = (this.likeCount || 1) - 1 - await this.rootStore.agent.deleteLike(uri!) - } catch (e: any) { - this.likeUri = uri - this.likeCount = (this.likeCount || 0) + 1 - logger.error('Failed to unlike feed', {error: e}) - } finally { - track('CustomFeed:Unlike') - } - } -} diff --git a/src/state/models/discovery/feeds.ts b/src/state/models/discovery/feeds.ts deleted file mode 100644 index a7c94e40d..000000000 --- a/src/state/models/discovery/feeds.ts +++ /dev/null @@ -1,148 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import {AppBskyUnspeccedGetPopularFeedGenerators} from '@atproto/api' -import {RootStoreModel} from '../root-store' -import {bundleAsync} from 'lib/async/bundle' -import {cleanError} from 'lib/strings/errors' -import {FeedSourceModel} from '../content/feed-source' -import {logger} from '#/logger' - -const DEFAULT_LIMIT = 50 - -export class FeedsDiscoveryModel { - // state - isLoading = false - isRefreshing = false - hasLoaded = false - error = '' - loadMoreCursor: string | undefined = undefined - - // data - feeds: FeedSourceModel[] = [] - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - { - rootStore: false, - }, - {autoBind: true}, - ) - } - - get hasMore() { - if (this.loadMoreCursor) { - return true - } - return false - } - - get hasContent() { - return this.feeds.length > 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - // public api - // = - - refresh = bundleAsync(async () => { - this._xLoading() - try { - const res = - await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit: DEFAULT_LIMIT, - }) - this._replaceAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - }) - - loadMore = bundleAsync(async () => { - if (!this.hasMore) { - return - } - this._xLoading() - try { - const res = - await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit: DEFAULT_LIMIT, - cursor: this.loadMoreCursor, - }) - this._append(res) - } catch (e: any) { - this._xIdle(e) - } - this._xIdle() - }) - - search = async (query: string) => { - this._xLoading(false) - try { - const results = - await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit: DEFAULT_LIMIT, - query: query, - }) - this._replaceAll(results) - } catch (e: any) { - this._xIdle(e) - } - this._xIdle() - } - - clear() { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = false - this.error = '' - this.feeds = [] - } - - // state transitions - // = - - _xLoading(isRefreshing = true) { - this.isLoading = true - this.isRefreshing = isRefreshing - this.error = '' - } - - _xIdle(err?: any) { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = true - this.error = cleanError(err) - if (err) { - logger.error('Failed to fetch popular feeds', {error: err}) - } - } - - // helper functions - // = - - _replaceAll(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) { - // 1. set feeds data to empty array - this.feeds = [] - // 2. call this._append() - this._append(res) - } - - _append(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) { - // 1. push data into feeds array - for (const f of res.data.feeds) { - const model = new FeedSourceModel(this.rootStore, f.uri) - model.hydrateFeedGenerator(f) - this.feeds.push(model) - } - // 2. set loadMoreCursor - this.loadMoreCursor = res.data.cursor - } -} diff --git a/src/state/models/lists/actor-feeds.ts b/src/state/models/lists/actor-feeds.ts deleted file mode 100644 index 29c01e536..000000000 --- a/src/state/models/lists/actor-feeds.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import {AppBskyFeedGetActorFeeds as GetActorFeeds} from '@atproto/api' -import {RootStoreModel} from '../root-store' -import {bundleAsync} from 'lib/async/bundle' -import {cleanError} from 'lib/strings/errors' -import {FeedSourceModel} from '../content/feed-source' -import {logger} from '#/logger' - -const PAGE_SIZE = 30 - -export class ActorFeedsModel { - // state - isLoading = false - isRefreshing = false - hasLoaded = false - error = '' - hasMore = true - loadMoreCursor?: string - - // data - feeds: FeedSourceModel[] = [] - - constructor( - public rootStore: RootStoreModel, - public params: GetActorFeeds.QueryParams, - ) { - makeAutoObservable( - this, - { - rootStore: false, - }, - {autoBind: true}, - ) - } - - get hasContent() { - return this.feeds.length > 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - // public api - // = - - async refresh() { - return this.loadMore(true) - } - - clear() { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = false - this.error = '' - this.hasMore = true - this.loadMoreCursor = undefined - this.feeds = [] - } - - loadMore = bundleAsync(async (replace: boolean = false) => { - if (!replace && !this.hasMore) { - return - } - this._xLoading(replace) - try { - const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({ - actor: this.params.actor, - limit: PAGE_SIZE, - cursor: replace ? undefined : this.loadMoreCursor, - }) - if (replace) { - this._replaceAll(res) - } else { - this._appendAll(res) - } - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - }) - - // state transitions - // = - - _xLoading(isRefreshing = false) { - this.isLoading = true - this.isRefreshing = isRefreshing - this.error = '' - } - - _xIdle(err?: any) { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = true - this.error = cleanError(err) - if (err) { - logger.error('Failed to fetch user followers', {error: err}) - } - } - - // helper functions - // = - - _replaceAll(res: GetActorFeeds.Response) { - this.feeds = [] - this._appendAll(res) - } - - _appendAll(res: GetActorFeeds.Response) { - this.loadMoreCursor = res.data.cursor - this.hasMore = !!this.loadMoreCursor - for (const f of res.data.feeds) { - const model = new FeedSourceModel(this.rootStore, f.uri) - model.hydrateFeedGenerator(f) - this.feeds.push(model) - } - } -} diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 1068ac651..a4c3517cc 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -126,33 +126,6 @@ export class PreferencesModel { ], } } - - // feeds - // = - - isPinnedFeed(uri: string) { - return this.pinnedFeeds.includes(uri) - } - - /** - * @deprecated use `useAddSavedFeedMutation` from `#/state/queries/preferences` instead - */ - async addSavedFeed(_v: string) {} - - /** - * @deprecated use `useRemoveSavedFeedMutation` from `#/state/queries/preferences` instead - */ - async removeSavedFeed(_v: string) {} - - /** - * @deprecated use `usePinFeedMutation` from `#/state/queries/preferences` instead - */ - async addPinnedFeed(_v: string) {} - - /** - * @deprecated use `useUnpinFeedMutation` from `#/state/queries/preferences` instead - */ - async removePinnedFeed(_v: string) {} } // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts deleted file mode 100644 index d6ea0c084..000000000 --- a/src/state/models/ui/profile.ts +++ /dev/null @@ -1,255 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import {RootStoreModel} from '../root-store' -import {ProfileModel} from '../content/profile' -import {ActorFeedsModel} from '../lists/actor-feeds' -import {logger} from '#/logger' - -export enum Sections { - PostsNoReplies = 'Posts', - PostsWithReplies = 'Posts & replies', - PostsWithMedia = 'Media', - Likes = 'Likes', - CustomAlgorithms = 'Feeds', - Lists = 'Lists', -} - -export interface ProfileUiParams { - user: string -} - -export class ProfileUiModel { - static LOADING_ITEM = {_reactKey: '__loading__'} - static END_ITEM = {_reactKey: '__end__'} - static EMPTY_ITEM = {_reactKey: '__empty__'} - - isAuthenticatedUser = false - - // data - profile: ProfileModel - feed: PostsFeedModel - algos: ActorFeedsModel - lists: ListsListModel - - // ui state - selectedViewIndex = 0 - - constructor( - public rootStore: RootStoreModel, - public params: ProfileUiParams, - ) { - makeAutoObservable( - this, - { - rootStore: false, - params: false, - }, - {autoBind: true}, - ) - this.profile = new ProfileModel(rootStore, {actor: params.user}) - this.feed = new PostsFeedModel(rootStore, 'author', { - actor: params.user, - limit: 10, - filter: 'posts_no_replies', - }) - this.algos = new ActorFeedsModel(rootStore, {actor: params.user}) - this.lists = new ListsListModel(rootStore, params.user) - } - - get currentView(): PostsFeedModel | ActorFeedsModel | ListsListModel { - if ( - this.selectedView === Sections.PostsNoReplies || - this.selectedView === Sections.PostsWithReplies || - this.selectedView === Sections.PostsWithMedia || - this.selectedView === Sections.Likes - ) { - return this.feed - } else if (this.selectedView === Sections.Lists) { - return this.lists - } - if (this.selectedView === Sections.CustomAlgorithms) { - return this.algos - } - throw new Error(`Invalid selector value: ${this.selectedViewIndex}`) - } - - get isInitialLoading() { - const view = this.currentView - return view.isLoading && !view.isRefreshing && !view.hasContent - } - - get isRefreshing() { - return this.profile.isRefreshing || this.currentView.isRefreshing - } - - get selectorItems() { - const items = [ - Sections.PostsNoReplies, - Sections.PostsWithReplies, - Sections.PostsWithMedia, - this.isAuthenticatedUser && Sections.Likes, - ].filter(Boolean) as string[] - if (this.algos.hasLoaded && !this.algos.isEmpty) { - items.push(Sections.CustomAlgorithms) - } - if (this.lists.hasLoaded && !this.lists.isEmpty) { - items.push(Sections.Lists) - } - return items - } - - get selectedView() { - // If, for whatever reason, the selected view index is not available, default back to posts - // This can happen when the user was focused on a view but performed an action that caused - // the view to disappear (e.g. deleting the last list in their list of lists https://imgflip.com/i/7txu1y) - return this.selectorItems[this.selectedViewIndex] || Sections.PostsNoReplies - } - - get uiItems() { - let arr: any[] = [] - // if loading, return loading item to show loading spinner - if (this.isInitialLoading) { - arr = arr.concat([ProfileUiModel.LOADING_ITEM]) - } else if (this.currentView.hasError) { - // if error, return error item to show error message - arr = arr.concat([ - { - _reactKey: '__error__', - error: this.currentView.error, - }, - ]) - } else { - if ( - this.selectedView === Sections.PostsNoReplies || - this.selectedView === Sections.PostsWithReplies || - this.selectedView === Sections.PostsWithMedia || - this.selectedView === Sections.Likes - ) { - if (this.feed.hasContent) { - arr = this.feed.slices.slice() - if (!this.feed.hasMore) { - arr = arr.concat([ProfileUiModel.END_ITEM]) - } - } else if (this.feed.isEmpty) { - arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) - } - } else if (this.selectedView === Sections.CustomAlgorithms) { - if (this.algos.hasContent) { - arr = this.algos.feeds - } else if (this.algos.isEmpty) { - arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) - } - } else if (this.selectedView === Sections.Lists) { - if (this.lists.hasContent) { - arr = this.lists.lists - } else if (this.lists.isEmpty) { - arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) - } - } else { - // fallback, add empty item, to show empty message - arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) - } - } - return arr - } - - get showLoadingMoreFooter() { - if ( - this.selectedView === Sections.PostsNoReplies || - this.selectedView === Sections.PostsWithReplies || - this.selectedView === Sections.PostsWithMedia || - this.selectedView === Sections.Likes - ) { - return this.feed.hasContent && this.feed.hasMore && this.feed.isLoading - } else if (this.selectedView === Sections.Lists) { - return this.lists.hasContent && this.lists.hasMore && this.lists.isLoading - } - return false - } - - // public api - // = - - setSelectedViewIndex(index: number) { - // ViewSelector fires onSelectView on mount - if (index === this.selectedViewIndex) return - - this.selectedViewIndex = index - - if ( - this.selectedView === Sections.PostsNoReplies || - this.selectedView === Sections.PostsWithReplies || - this.selectedView === Sections.PostsWithMedia - ) { - let filter = 'posts_no_replies' - if (this.selectedView === Sections.PostsWithReplies) { - filter = 'posts_with_replies' - } else if (this.selectedView === Sections.PostsWithMedia) { - filter = 'posts_with_media' - } - - this.feed = new PostsFeedModel( - this.rootStore, - 'author', - { - actor: this.params.user, - limit: 10, - filter, - }, - { - isSimpleFeed: ['posts_with_media'].includes(filter), - }, - ) - - this.feed.setup() - } else if (this.selectedView === Sections.Likes) { - this.feed = new PostsFeedModel( - this.rootStore, - 'likes', - { - actor: this.params.user, - limit: 10, - }, - { - isSimpleFeed: true, - }, - ) - - this.feed.setup() - } - } - - async setup() { - await Promise.all([ - this.profile - .setup() - .catch(err => logger.error('Failed to fetch profile', {error: err})), - this.feed - .setup() - .catch(err => logger.error('Failed to fetch feed', {error: err})), - ]) - runInAction(() => { - this.isAuthenticatedUser = - this.profile.did === this.rootStore.session.currentSession?.did - }) - this.algos.refresh() - // HACK: need to use the DID as a param, not the username -prf - this.lists.source = this.profile.did - this.lists - .loadMore() - .catch(err => logger.error('Failed to fetch lists', {error: err})) - } - - async refresh() { - await Promise.all([this.profile.refresh(), this.currentView.refresh()]) - } - - async loadMore() { - if ( - !this.currentView.isLoading && - !this.currentView.hasError && - !this.currentView.isEmpty - ) { - await this.currentView.loadMore() - } - } -} diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index dde37315d..4ec82c6fb 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -21,39 +21,41 @@ import {sanitizeHandle} from '#/lib/strings/handles' import {useSession} from '#/state/session' import {usePreferencesQuery} from '#/state/queries/preferences' -export type FeedSourceInfo = - | { - type: 'feed' - uri: string - route: { - href: string - name: string - params: Record<string, string> - } - cid: string - avatar: string | undefined - displayName: string - description: RichText - creatorDid: string - creatorHandle: string - likeCount: number | undefined - likeUri: string | undefined - } - | { - type: 'list' - uri: string - route: { - href: string - name: string - params: Record<string, string> - } - cid: string - avatar: string | undefined - displayName: string - description: RichText - creatorDid: string - creatorHandle: string - } +export type FeedSourceFeedInfo = { + type: 'feed' + uri: string + route: { + href: string + name: string + params: Record<string, string> + } + cid: string + avatar: string | undefined + displayName: string + description: RichText + creatorDid: string + creatorHandle: string + likeCount: number | undefined + likeUri: string | undefined +} + +export type FeedSourceListInfo = { + type: 'list' + uri: string + route: { + href: string + name: string + params: Record<string, string> + } + cid: string + avatar: string | undefined + displayName: string + description: RichText + creatorDid: string + creatorHandle: string +} + +export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [ 'getFeedSourceInfo', diff --git a/src/state/queries/like.ts b/src/state/queries/like.ts new file mode 100644 index 000000000..187d8fb82 --- /dev/null +++ b/src/state/queries/like.ts @@ -0,0 +1,24 @@ +import {useMutation} from '@tanstack/react-query' + +import {useSession} from '#/state/session' + +export function useLikeMutation() { + const {agent} = useSession() + + return useMutation({ + mutationFn: async ({uri, cid}: {uri: string; cid: string}) => { + const res = await agent.like(uri, cid) + return {uri: res.uri} + }, + }) +} + +export function useUnlikeMutation() { + const {agent} = useSession() + + return useMutation({ + mutationFn: async ({uri}: {uri: string}) => { + await agent.deleteLike(uri) + }, + }) +} diff --git a/src/state/queries/suggested-feeds.ts b/src/state/queries/suggested-feeds.ts new file mode 100644 index 000000000..e148c97c3 --- /dev/null +++ b/src/state/queries/suggested-feeds.ts @@ -0,0 +1,29 @@ +import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {AppBskyFeedGetSuggestedFeeds} from '@atproto/api' + +import {useSession} from '#/state/session' + +export const suggestedFeedsQueryKey = ['suggestedFeeds'] + +export function useSuggestedFeedsQuery() { + const {agent} = useSession() + + return useInfiniteQuery< + AppBskyFeedGetSuggestedFeeds.OutputSchema, + Error, + InfiniteData<AppBskyFeedGetSuggestedFeeds.OutputSchema>, + QueryKey, + string | undefined + >({ + queryKey: suggestedFeedsQueryKey, + queryFn: async ({pageParam}) => { + const res = await agent.app.bsky.feed.getSuggestedFeeds({ + limit: 10, + cursor: pageParam, + }) + return res.data + }, + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.cursor, + }) +} |