diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/models/feeds/posts-slice.ts | 91 | ||||
-rw-r--r-- | src/state/models/feeds/posts.ts | 429 | ||||
-rw-r--r-- | src/state/models/me.ts | 10 | ||||
-rw-r--r-- | src/state/models/ui/profile.ts | 1 | ||||
-rw-r--r-- | src/state/preferences/feed-tuners.tsx | 48 | ||||
-rw-r--r-- | src/state/queries/post-feed.ts | 176 | ||||
-rw-r--r-- | src/state/queries/post-thread.ts | 10 | ||||
-rw-r--r-- | src/state/queries/post.ts | 98 | ||||
-rw-r--r-- | src/state/queries/resolve-uri.ts | 17 |
9 files changed, 285 insertions, 595 deletions
diff --git a/src/state/models/feeds/posts-slice.ts b/src/state/models/feeds/posts-slice.ts deleted file mode 100644 index 2501cef6f..000000000 --- a/src/state/models/feeds/posts-slice.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import {RootStoreModel} from '../root-store' -import {FeedViewPostsSlice} from 'lib/api/feed-manip' -import {PostsFeedItemModel} from './post' -import {FeedSourceInfo} from 'lib/api/feed/types' - -export class PostsFeedSliceModel { - // ui state - _reactKey: string = '' - - // data - items: PostsFeedItemModel[] = [] - source: FeedSourceInfo | undefined - - constructor(public rootStore: RootStoreModel, slice: FeedViewPostsSlice) { - this._reactKey = slice._reactKey - this.source = slice.source - for (let i = 0; i < slice.items.length; i++) { - this.items.push( - new PostsFeedItemModel( - rootStore, - `${this._reactKey} - ${i}`, - slice.items[i], - ), - ) - } - makeAutoObservable(this, {rootStore: false}) - } - - get uri() { - if (this.isReply) { - return this.items[1].post.uri - } - return this.items[0].post.uri - } - - get isThread() { - return ( - this.items.length > 1 && - this.items.every( - item => item.post.author.did === this.items[0].post.author.did, - ) - ) - } - - get isReply() { - return this.items.length > 1 && !this.isThread - } - - get rootItem() { - if (this.isReply) { - return this.items[1] - } - return this.items[0] - } - - get moderation() { - // prefer the most stringent item - const topItem = this.items.find(item => item.moderation.content.filter) - if (topItem) { - return topItem.moderation - } - // otherwise just use the first one - return this.items[0].moderation - } - - shouldFilter(ignoreFilterForDid: string | undefined): boolean { - const mods = this.items - .filter(item => item.post.author.did !== ignoreFilterForDid) - .map(item => item.moderation) - return !!mods.find(mod => mod.content.filter) - } - - containsUri(uri: string) { - return !!this.items.find(item => item.post.uri === uri) - } - - isThreadParentAt(i: number) { - if (this.items.length === 1) { - return false - } - return i < this.items.length - 1 - } - - isThreadChildAt(i: number) { - if (this.items.length === 1) { - return false - } - return i > 0 - } -} diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts deleted file mode 100644 index 0a06c581c..000000000 --- a/src/state/models/feeds/posts.ts +++ /dev/null @@ -1,429 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import { - AppBskyFeedGetTimeline as GetTimeline, - AppBskyFeedGetAuthorFeed as GetAuthorFeed, - AppBskyFeedGetFeed as GetCustomFeed, - AppBskyFeedGetActorLikes as GetActorLikes, - AppBskyFeedGetListFeed as GetListFeed, -} from '@atproto/api' -import AwaitLock from 'await-lock' -import {bundleAsync} from 'lib/async/bundle' -import {RootStoreModel} from '../root-store' -import {cleanError} from 'lib/strings/errors' -import {FeedTuner} from 'lib/api/feed-manip' -import {PostsFeedSliceModel} from './posts-slice' -import {track} from 'lib/analytics/analytics' -import {FeedViewPostsSlice} from 'lib/api/feed-manip' - -import {FeedAPI, FeedAPIResponse} from 'lib/api/feed/types' -import {FollowingFeedAPI} from 'lib/api/feed/following' -import {AuthorFeedAPI} from 'lib/api/feed/author' -import {LikesFeedAPI} from 'lib/api/feed/likes' -import {CustomFeedAPI} from 'lib/api/feed/custom' -import {ListFeedAPI} from 'lib/api/feed/list' -import {MergeFeedAPI} from 'lib/api/feed/merge' -import {logger} from '#/logger' - -const PAGE_SIZE = 30 - -type FeedType = 'home' | 'following' | 'author' | 'custom' | 'likes' | 'list' - -export enum KnownError { - FeedgenDoesNotExist, - FeedgenMisconfigured, - FeedgenBadResponse, - FeedgenOffline, - FeedgenUnknown, - Unknown, -} - -type Options = { - /** - * Formats the feed in a flat array with no threading of replies, just - * top-level posts. - */ - isSimpleFeed?: boolean -} - -type QueryParams = - | GetTimeline.QueryParams - | GetAuthorFeed.QueryParams - | GetActorLikes.QueryParams - | GetCustomFeed.QueryParams - | GetListFeed.QueryParams - -export class PostsFeedModel { - // state - isLoading = false - isRefreshing = false - hasNewLatest = false - hasLoaded = false - isBlocking = false - isBlockedBy = false - error = '' - knownError: KnownError | undefined - loadMoreError = '' - params: QueryParams - hasMore = true - pollCursor: string | undefined - api: FeedAPI - tuner = new FeedTuner() - pageSize = PAGE_SIZE - options: Options = {} - - // used to linearize async modifications to state - lock = new AwaitLock() - - // used to track if a feed is coming up empty - emptyFetches = 0 - - // data - slices: PostsFeedSliceModel[] = [] - - constructor( - public rootStore: RootStoreModel, - public feedType: FeedType, - params: QueryParams, - options?: Options, - ) { - makeAutoObservable( - this, - { - rootStore: false, - params: false, - }, - {autoBind: true}, - ) - this.params = params - this.options = options || {} - if (feedType === 'home') { - this.api = new MergeFeedAPI(rootStore) - } else if (feedType === 'following') { - this.api = new FollowingFeedAPI(rootStore) - } else if (feedType === 'author') { - this.api = new AuthorFeedAPI( - rootStore, - params as GetAuthorFeed.QueryParams, - ) - } else if (feedType === 'likes') { - this.api = new LikesFeedAPI( - rootStore, - params as GetActorLikes.QueryParams, - ) - } else if (feedType === 'custom') { - this.api = new CustomFeedAPI( - rootStore, - params as GetCustomFeed.QueryParams, - ) - } else if (feedType === 'list') { - this.api = new ListFeedAPI(rootStore, params as GetListFeed.QueryParams) - } else { - this.api = new FollowingFeedAPI(rootStore) - } - } - - get reactKey() { - if (this.feedType === 'author') { - return (this.params as GetAuthorFeed.QueryParams).actor - } - if (this.feedType === 'custom') { - return (this.params as GetCustomFeed.QueryParams).feed - } - if (this.feedType === 'list') { - return (this.params as GetListFeed.QueryParams).list - } - return this.feedType - } - - get hasContent() { - return this.slices.length !== 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - get isLoadingMore() { - return this.isLoading && !this.isRefreshing && this.hasContent - } - - setHasNewLatest(v: boolean) { - this.hasNewLatest = v - } - - // public api - // = - - /** - * Nuke all data - */ - clear() { - logger.debug('FeedModel:clear') - this.isLoading = false - this.isRefreshing = false - this.hasNewLatest = false - this.hasLoaded = false - this.error = '' - this.hasMore = true - this.pollCursor = undefined - this.slices = [] - this.tuner.reset() - } - - /** - * Load for first render - */ - setup = bundleAsync(async (isRefreshing: boolean = false) => { - logger.debug('FeedModel:setup', {isRefreshing}) - if (isRefreshing) { - this.isRefreshing = true // set optimistically for UI - } - await this.lock.acquireAsync() - try { - this.setHasNewLatest(false) - this.api.reset() - this.tuner.reset() - this._xLoading(isRefreshing) - try { - const res = await this.api.fetchNext({limit: this.pageSize}) - await this._replaceAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } finally { - this.lock.release() - } - }) - - /** - * Register any event listeners. Returns a cleanup function. - */ - registerListeners() { - const sub = this.rootStore.onPostDeleted(this.onPostDeleted.bind(this)) - return () => sub.remove() - } - - /** - * Reset and load - */ - async refresh() { - await this.setup(true) - } - - /** - * Load more posts to the end of the feed - */ - loadMore = bundleAsync(async () => { - await this.lock.acquireAsync() - try { - if (!this.hasMore || this.hasError) { - return - } - this._xLoading() - try { - const res = await this.api.fetchNext({ - limit: this.pageSize, - }) - await this._appendAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(undefined, e) - runInAction(() => { - this.hasMore = false - }) - } - } finally { - this.lock.release() - if (this.feedType === 'custom') { - track('CustomFeed:LoadMore') - } - } - }) - - /** - * Attempt to load more again after a failure - */ - async retryLoadMore() { - this.loadMoreError = '' - this.hasMore = true - return this.loadMore() - } - - /** - * Check if new posts are available - */ - async checkForLatest() { - if (!this.hasLoaded || this.hasNewLatest || this.isLoading) { - return - } - const post = await this.api.peekLatest() - if (post) { - const slices = this.tuner.tune( - [post], - this.rootStore.preferences.getFeedTuners(this.feedType), - { - dryRun: true, - maintainOrder: true, - }, - ) - if (slices[0]) { - const sliceModel = new PostsFeedSliceModel(this.rootStore, slices[0]) - if (sliceModel.moderation.content.filter) { - return - } - this.setHasNewLatest(sliceModel.uri !== this.pollCursor) - } - } - } - - /** - * Updates the UI after the user has created a post - */ - onPostCreated() { - if (!this.slices.length) { - return this.refresh() - } else { - this.setHasNewLatest(true) - } - } - - /** - * Removes posts from the feed upon deletion. - */ - onPostDeleted(uri: string) { - let i - do { - i = this.slices.findIndex(slice => slice.containsUri(uri)) - if (i !== -1) { - this.slices.splice(i, 1) - } - } while (i !== -1) - } - - // state transitions - // = - - _xLoading(isRefreshing = false) { - this.isLoading = true - this.isRefreshing = isRefreshing - this.error = '' - this.knownError = undefined - } - - _xIdle(error?: any, loadMoreError?: any) { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = true - this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError - this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError - this.error = cleanError(error) - this.knownError = detectKnownError(this.feedType, error) - this.loadMoreError = cleanError(loadMoreError) - if (error) { - logger.error('Posts feed request failed', {error}) - } - if (loadMoreError) { - logger.error('Posts feed load-more request failed', { - error: loadMoreError, - }) - } - } - - // helper functions - // = - - async _replaceAll(res: FeedAPIResponse) { - this.pollCursor = res.feed[0]?.post.uri - return this._appendAll(res, true) - } - - async _appendAll(res: FeedAPIResponse, replace = false) { - this.hasMore = !!res.cursor && res.feed.length > 0 - if (replace) { - this.emptyFetches = 0 - } - - this.rootStore.me.follows.hydrateMany( - res.feed.map(item => item.post.author), - ) - for (const item of res.feed) { - this.rootStore.posts.fromFeedItem(item) - } - - const slices = this.options.isSimpleFeed - ? res.feed.map(item => new FeedViewPostsSlice([item])) - : this.tuner.tune( - res.feed, - this.rootStore.preferences.getFeedTuners(this.feedType), - ) - - const toAppend: PostsFeedSliceModel[] = [] - for (const slice of slices) { - const sliceModel = new PostsFeedSliceModel(this.rootStore, slice) - const dupTest = (item: PostsFeedSliceModel) => - item._reactKey === sliceModel._reactKey - // sanity check - // if a duplicate _reactKey passes through, the UI breaks hard - if (!replace) { - if (this.slices.find(dupTest) || toAppend.find(dupTest)) { - continue - } - } - toAppend.push(sliceModel) - } - runInAction(() => { - if (replace) { - this.slices = toAppend - } else { - this.slices = this.slices.concat(toAppend) - } - if (toAppend.length === 0) { - this.emptyFetches++ - if (this.emptyFetches >= 10) { - this.hasMore = false - } - } - }) - } -} - -function detectKnownError( - feedType: FeedType, - error: any, -): KnownError | undefined { - if (!error) { - return undefined - } - if (typeof error !== 'string') { - error = error.toString() - } - if (feedType !== 'custom') { - return KnownError.Unknown - } - if (error.includes('could not find feed')) { - return KnownError.FeedgenDoesNotExist - } - if (error.includes('feed unavailable')) { - return KnownError.FeedgenOffline - } - if (error.includes('invalid did document')) { - return KnownError.FeedgenMisconfigured - } - if (error.includes('could not resolve did document')) { - return KnownError.FeedgenMisconfigured - } - if ( - error.includes('invalid feed generator service details in did document') - ) { - return KnownError.FeedgenMisconfigured - } - if (error.includes('feed provided an invalid response')) { - return KnownError.FeedgenBadResponse - } - return KnownError.FeedgenUnknown -} diff --git a/src/state/models/me.ts b/src/state/models/me.ts index d3061f166..4bbb5a04b 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -4,7 +4,6 @@ import { ComAtprotoServerListAppPasswords, } from '@atproto/api' import {RootStoreModel} from './root-store' -import {PostsFeedModel} from './feeds/posts' import {NotificationsFeedModel} from './feeds/notifications' import {MyFeedsUIModel} from './ui/my-feeds' import {MyFollowsCache} from './cache/my-follows' @@ -22,7 +21,6 @@ export class MeModel { avatar: string = '' followsCount: number | undefined followersCount: number | undefined - mainFeed: PostsFeedModel notifications: NotificationsFeedModel myFeeds: MyFeedsUIModel follows: MyFollowsCache @@ -41,16 +39,12 @@ export class MeModel { {rootStore: false, serialize: false, hydrate: false}, {autoBind: true}, ) - this.mainFeed = new PostsFeedModel(this.rootStore, 'home', { - algorithm: 'reverse-chronological', - }) this.notifications = new NotificationsFeedModel(this.rootStore) this.myFeeds = new MyFeedsUIModel(this.rootStore) this.follows = new MyFollowsCache(this.rootStore) } clear() { - this.mainFeed.clear() this.notifications.clear() this.myFeeds.clear() this.follows.clear() @@ -109,10 +103,6 @@ export class MeModel { if (sess.hasSession) { this.did = sess.currentSession?.did || '' await this.fetchProfile() - this.mainFeed.clear() - /* dont await */ this.mainFeed.setup().catch(e => { - logger.error('Failed to setup main feed model', {error: e}) - }) /* dont await */ this.notifications.setup().catch(e => { logger.error('Failed to setup notifications model', { error: e, diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index f96340c65..0ef592928 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -1,7 +1,6 @@ import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from '../root-store' import {ProfileModel} from '../content/profile' -import {PostsFeedModel} from '../feeds/posts' import {ActorFeedsModel} from '../lists/actor-feeds' import {ListsListModel} from '../lists/lists-list' import {logger} from '#/logger' diff --git a/src/state/preferences/feed-tuners.tsx b/src/state/preferences/feed-tuners.tsx new file mode 100644 index 000000000..96770055c --- /dev/null +++ b/src/state/preferences/feed-tuners.tsx @@ -0,0 +1,48 @@ +import {useMemo} from 'react' +import {FeedTuner} from '#/lib/api/feed-manip' +import {FeedDescriptor} from '../queries/post-feed' +import {useLanguagePrefs} from './languages' + +export function useFeedTuners(feedDesc: FeedDescriptor) { + const langPrefs = useLanguagePrefs() + + return useMemo(() => { + if (feedDesc.startsWith('feedgen')) { + return [ + FeedTuner.dedupReposts, + FeedTuner.preferredLangOnly(langPrefs.contentLanguages), + ] + } + if (feedDesc.startsWith('list')) { + return [FeedTuner.dedupReposts] + } + if (feedDesc === 'home' || feedDesc === 'following') { + const feedTuners = [] + + if (false /*TODOthis.homeFeed.hideReposts*/) { + feedTuners.push(FeedTuner.removeReposts) + } else { + feedTuners.push(FeedTuner.dedupReposts) + } + + if (true /*TODOthis.homeFeed.hideReplies*/) { + feedTuners.push(FeedTuner.removeReplies) + } /* TODO else { + feedTuners.push( + FeedTuner.thresholdRepliesOnly({ + userDid: this.rootStore.session.data?.did || '', + minLikes: this.homeFeed.hideRepliesByLikeCount, + followedOnly: !!this.homeFeed.hideRepliesByUnfollowed, + }), + ) + }*/ + + if (false /*TODOthis.homeFeed.hideQuotePosts*/) { + feedTuners.push(FeedTuner.removeQuotePosts) + } + + return feedTuners + } + return [] + }, [feedDesc, langPrefs]) +} diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts new file mode 100644 index 000000000..1a391d5c3 --- /dev/null +++ b/src/state/queries/post-feed.ts @@ -0,0 +1,176 @@ +import {useCallback, useMemo} from 'react' +import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api' +import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query' +import {useSession} from '../session' +import {useFeedTuners} from '../preferences/feed-tuners' +import {FeedTuner, NoopFeedTuner} from 'lib/api/feed-manip' +import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types' +import {FollowingFeedAPI} from 'lib/api/feed/following' +import {AuthorFeedAPI} from 'lib/api/feed/author' +import {LikesFeedAPI} from 'lib/api/feed/likes' +import {CustomFeedAPI} from 'lib/api/feed/custom' +import {ListFeedAPI} from 'lib/api/feed/list' +import {MergeFeedAPI} from 'lib/api/feed/merge' +import {useStores} from '../models/root-store' + +type ActorDid = string +type AuthorFilter = + | 'posts_with_replies' + | 'posts_no_replies' + | 'posts_with_media' +type FeedUri = string +type ListUri = string +export type FeedDescriptor = + | 'home' + | 'following' + | `author|${ActorDid}|${AuthorFilter}` + | `feedgen|${FeedUri}` + | `likes|${ActorDid}` + | `list|${ListUri}` +export interface FeedParams { + disableTuner?: boolean + mergeFeedEnabled?: boolean + mergeFeedSources?: string[] +} + +type RQPageParam = string | undefined + +export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { + return ['post-feed', feedDesc, params || {}] +} + +export interface FeedPostSliceItem { + _reactKey: string + uri: string + post: AppBskyFeedDefs.PostView + record: AppBskyFeedPost.Record + reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource +} + +export interface FeedPostSlice { + _reactKey: string + rootUri: string + isThread: boolean + items: FeedPostSliceItem[] +} + +export interface FeedPage { + cursor: string | undefined + slices: FeedPostSlice[] +} + +export function usePostFeedQuery( + feedDesc: FeedDescriptor, + params?: FeedParams, + opts?: {enabled?: boolean}, +) { + const {agent} = useSession() + const feedTuners = useFeedTuners(feedDesc) + const store = useStores() + const enabled = opts?.enabled !== false + + const api: FeedAPI = useMemo(() => { + if (feedDesc === 'home') { + return new MergeFeedAPI(agent, params || {}, feedTuners) + } else if (feedDesc === 'following') { + return new FollowingFeedAPI(agent) + } else if (feedDesc.startsWith('author')) { + const [_, actor, filter] = feedDesc.split('|') + return new AuthorFeedAPI(agent, {actor, filter}) + } else if (feedDesc.startsWith('likes')) { + const [_, actor] = feedDesc.split('|') + return new LikesFeedAPI(agent, {actor}) + } else if (feedDesc.startsWith('feedgen')) { + const [_, feed] = feedDesc.split('|') + return new CustomFeedAPI(agent, {feed}) + } else if (feedDesc.startsWith('list')) { + const [_, list] = feedDesc.split('|') + return new ListFeedAPI(agent, {list}) + } else { + // shouldnt happen + return new FollowingFeedAPI(agent) + } + }, [feedDesc, params, feedTuners, agent]) + const tuner = useMemo( + () => (params?.disableTuner ? new NoopFeedTuner() : new FeedTuner()), + [params], + ) + + const pollLatest = useCallback(async () => { + if (!enabled) { + return false + } + console.log('poll') + const post = await api.peekLatest() + if (post) { + const slices = tuner.tune([post], feedTuners, { + dryRun: true, + maintainOrder: true, + }) + if (slices[0]) { + if ( + !moderatePost( + slices[0].items[0].post, + store.preferences.moderationOpts, + ).content.filter + ) { + return true + } + } + } + return false + }, [api, tuner, feedTuners, store.preferences.moderationOpts, enabled]) + + const out = useInfiniteQuery< + FeedPage, + Error, + InfiniteData<FeedPage>, + QueryKey, + RQPageParam + >({ + queryKey: RQKEY(feedDesc, params), + async queryFn({pageParam}: {pageParam: RQPageParam}) { + console.log('fetch', feedDesc, pageParam) + if (!pageParam) { + tuner.reset() + } + const res = await api.fetch({cursor: pageParam, limit: 30}) + const slices = tuner.tune(res.feed, feedTuners) + return { + cursor: res.cursor, + slices: slices.map(slice => ({ + _reactKey: slice._reactKey, + rootUri: slice.rootItem.post.uri, + isThread: + slice.items.length > 1 && + slice.items.every( + item => item.post.author.did === slice.items[0].post.author.did, + ), + source: undefined, // TODO + items: slice.items + .map((item, i) => { + if ( + AppBskyFeedPost.isRecord(item.post.record) && + AppBskyFeedPost.validateRecord(item.post.record).success + ) { + return { + _reactKey: `${slice._reactKey}-${i}`, + uri: item.post.uri, + post: item.post, + record: item.post.record, + reason: i === 0 && slice.source ? slice.source : item.reason, + } + } + return undefined + }) + .filter(Boolean) as FeedPostSliceItem[], + })), + } + }, + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.cursor, + enabled, + }) + + return {...out, pollLatest} +} diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index 4dea8aaf1..386c70483 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -57,17 +57,17 @@ export type ThreadNode = export function usePostThreadQuery(uri: string | undefined) { const {agent} = useSession() - return useQuery<ThreadNode, Error>( - RQKEY(uri || ''), - async () => { + return useQuery<ThreadNode, Error>({ + queryKey: RQKEY(uri || ''), + async queryFn() { const res = await agent.getPostThread({uri: uri!}) if (res.success) { return responseToThreadNodes(res.data.thread) } return {type: 'unknown', uri: uri!} }, - {enabled: !!uri}, - ) + enabled: !!uri, + }) } export function sortThread( diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index f62190c67..ffff7f967 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -7,9 +7,9 @@ export const RQKEY = (postUri: string) => ['post', postUri] export function usePostQuery(uri: string | undefined) { const {agent} = useSession() - return useQuery<AppBskyFeedDefs.PostView>( - RQKEY(uri || ''), - async () => { + return useQuery<AppBskyFeedDefs.PostView>({ + queryKey: RQKEY(uri || ''), + async queryFn() { const res = await agent.getPosts({uris: [uri!]}) if (res.success && res.data.posts[0]) { return res.data.posts[0] @@ -17,10 +17,8 @@ export function usePostQuery(uri: string | undefined) { throw new Error('No data') }, - { - enabled: !!uri, - }, - ) + enabled: !!uri, + }) } export function usePostLikeMutation() { @@ -29,7 +27,8 @@ export function usePostLikeMutation() { {uri: string}, // responds with the uri of the like Error, {uri: string; cid: string; likeCount: number} // the post's uri, cid, and likes - >(post => agent.like(post.uri, post.cid), { + >({ + mutationFn: post => agent.like(post.uri, post.cid), onMutate(variables) { // optimistically update the post-shadow updatePostShadow(variables.uri, { @@ -59,27 +58,25 @@ export function usePostUnlikeMutation() { void, Error, {postUri: string; likeUri: string; likeCount: number} - >( - async ({likeUri}) => { + >({ + mutationFn: async ({likeUri}) => { await agent.deleteLike(likeUri) }, - { - onMutate(variables) { - // optimistically update the post-shadow - updatePostShadow(variables.postUri, { - likeCount: variables.likeCount - 1, - likeUri: undefined, - }) - }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.postUri, { - likeCount: variables.likeCount, - likeUri: variables.likeUri, - }) - }, + onMutate(variables) { + // optimistically update the post-shadow + updatePostShadow(variables.postUri, { + likeCount: variables.likeCount - 1, + likeUri: undefined, + }) + }, + onError(error, variables) { + // revert the optimistic update + updatePostShadow(variables.postUri, { + likeCount: variables.likeCount, + likeUri: variables.likeUri, + }) }, - ) + }) } export function usePostRepostMutation() { @@ -88,7 +85,8 @@ export function usePostRepostMutation() { {uri: string}, // responds with the uri of the repost Error, {uri: string; cid: string; repostCount: number} // the post's uri, cid, and reposts - >(post => agent.repost(post.uri, post.cid), { + >({ + mutationFn: post => agent.repost(post.uri, post.cid), onMutate(variables) { // optimistically update the post-shadow updatePostShadow(variables.uri, { @@ -118,39 +116,35 @@ export function usePostUnrepostMutation() { void, Error, {postUri: string; repostUri: string; repostCount: number} - >( - async ({repostUri}) => { + >({ + mutationFn: async ({repostUri}) => { await agent.deleteRepost(repostUri) }, - { - onMutate(variables) { - // optimistically update the post-shadow - updatePostShadow(variables.postUri, { - repostCount: variables.repostCount - 1, - repostUri: undefined, - }) - }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.postUri, { - repostCount: variables.repostCount, - repostUri: variables.repostUri, - }) - }, + onMutate(variables) { + // optimistically update the post-shadow + updatePostShadow(variables.postUri, { + repostCount: variables.repostCount - 1, + repostUri: undefined, + }) + }, + onError(error, variables) { + // revert the optimistic update + updatePostShadow(variables.postUri, { + repostCount: variables.repostCount, + repostUri: variables.repostUri, + }) }, - ) + }) } export function usePostDeleteMutation() { const {agent} = useSession() - return useMutation<void, Error, {uri: string}>( - async ({uri}) => { + return useMutation<void, Error, {uri: string}>({ + mutationFn: async ({uri}) => { await agent.deletePost(uri) }, - { - onSuccess(data, variables) { - updatePostShadow(variables.uri, {isDeleted: true}) - }, + onSuccess(data, variables) { + updatePostShadow(variables.uri, {isDeleted: true}) }, - ) + }) } diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts index 770be5cf8..26e0a475b 100644 --- a/src/state/queries/resolve-uri.ts +++ b/src/state/queries/resolve-uri.ts @@ -6,12 +6,15 @@ export const RQKEY = (uri: string) => ['resolved-uri', uri] export function useResolveUriQuery(uri: string) { const {agent} = useSession() - return useQuery<string | undefined, Error>(RQKEY(uri), async () => { - const urip = new AtUri(uri) - if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({handle: urip.host}) - urip.host = res.data.did - } - return urip.toString() + return useQuery<string | undefined, Error>({ + queryKey: RQKEY(uri), + async queryFn() { + const urip = new AtUri(uri) + if (!urip.host.startsWith('did:')) { + const res = await agent.resolveHandle({handle: urip.host}) + urip.host = res.data.did + } + return urip.toString() + }, }) } |