diff options
Diffstat (limited to 'src/lib/api')
-rw-r--r-- | src/lib/api/feed-manip.ts | 45 | ||||
-rw-r--r-- | src/lib/api/feed/author.ts | 28 | ||||
-rw-r--r-- | src/lib/api/feed/custom.ts | 28 | ||||
-rw-r--r-- | src/lib/api/feed/following.ts | 25 | ||||
-rw-r--r-- | src/lib/api/feed/likes.ts | 28 | ||||
-rw-r--r-- | src/lib/api/feed/list.ts | 28 | ||||
-rw-r--r-- | src/lib/api/feed/merge.ts | 82 | ||||
-rw-r--r-- | src/lib/api/feed/types.ts | 21 | ||||
-rw-r--r-- | src/lib/api/index.ts | 48 |
9 files changed, 160 insertions, 173 deletions
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 8f259a910..1123c4e23 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -4,7 +4,7 @@ import { AppBskyEmbedRecordWithMedia, AppBskyEmbedRecord, } from '@atproto/api' -import {FeedSourceInfo} from './feed/types' +import {ReasonFeedSource} from './feed/types' import {isPostInLanguage} from '../../locale/helpers' type FeedViewPost = AppBskyFeedDefs.FeedViewPost @@ -16,13 +16,7 @@ export type FeedTunerFn = ( export class FeedViewPostsSlice { isFlattenedReply = false - constructor(public items: FeedViewPost[] = []) {} - - get _reactKey() { - return `slice-${this.items[0].post.uri}-${ - this.items[0].reason?.indexedAt || this.items[0].post.indexedAt - }` - } + constructor(public items: FeedViewPost[], public _reactKey: string) {} get uri() { if (this.isFlattenedReply) { @@ -65,9 +59,9 @@ export class FeedViewPostsSlice { ) } - get source(): FeedSourceInfo | undefined { + get source(): ReasonFeedSource | undefined { return this.items.find(item => '__source' in item && !!item.__source) - ?.__source as FeedSourceInfo + ?.__source as ReasonFeedSource } containsUri(uri: string) { @@ -116,18 +110,35 @@ export class FeedViewPostsSlice { } } +export class NoopFeedTuner { + private keyCounter = 0 + + reset() { + this.keyCounter = 0 + } + tune( + feed: FeedViewPost[], + _opts?: {dryRun: boolean; maintainOrder: boolean}, + ): FeedViewPostsSlice[] { + return feed.map( + item => new FeedViewPostsSlice([item], `slice-${this.keyCounter++}`), + ) + } +} + export class FeedTuner { + private keyCounter = 0 seenUris: Set<string> = new Set() - constructor() {} + constructor(public tunerFns: FeedTunerFn[]) {} reset() { + this.keyCounter = 0 this.seenUris.clear() } tune( feed: FeedViewPost[], - tunerFns: FeedTunerFn[] = [], {dryRun, maintainOrder}: {dryRun: boolean; maintainOrder: boolean} = { dryRun: false, maintainOrder: false, @@ -136,7 +147,9 @@ export class FeedTuner { let slices: FeedViewPostsSlice[] = [] if (maintainOrder) { - slices = feed.map(item => new FeedViewPostsSlice([item])) + slices = feed.map( + item => new FeedViewPostsSlice([item], `slice-${this.keyCounter++}`), + ) } else { // arrange the posts into thread slices for (let i = feed.length - 1; i >= 0; i--) { @@ -152,12 +165,14 @@ export class FeedTuner { continue } } - slices.unshift(new FeedViewPostsSlice([item])) + slices.unshift( + new FeedViewPostsSlice([item], `slice-${this.keyCounter++}`), + ) } } // run the custom tuners - for (const tunerFn of tunerFns) { + for (const tunerFn of this.tunerFns) { slices = tunerFn(this, slices.slice()) } diff --git a/src/lib/api/feed/author.ts b/src/lib/api/feed/author.ts index ec8795e1a..92df84f8b 100644 --- a/src/lib/api/feed/author.ts +++ b/src/lib/api/feed/author.ts @@ -2,37 +2,33 @@ import { AppBskyFeedDefs, AppBskyFeedGetAuthorFeed as GetAuthorFeed, } from '@atproto/api' -import {RootStoreModel} from 'state/index' import {FeedAPI, FeedAPIResponse} from './types' +import {getAgent} from '#/state/session' export class AuthorFeedAPI implements FeedAPI { - cursor: string | undefined - - constructor( - public rootStore: RootStoreModel, - public params: GetAuthorFeed.QueryParams, - ) {} - - reset() { - this.cursor = undefined - } + constructor(public params: GetAuthorFeed.QueryParams) {} async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { - const res = await this.rootStore.agent.getAuthorFeed({ + const res = await getAgent().getAuthorFeed({ ...this.params, limit: 1, }) return res.data.feed[0] } - async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> { - const res = await this.rootStore.agent.getAuthorFeed({ + async fetch({ + cursor, + limit, + }: { + cursor: string | undefined + limit: number + }): Promise<FeedAPIResponse> { + const res = await getAgent().getAuthorFeed({ ...this.params, - cursor: this.cursor, + cursor, limit, }) if (res.success) { - this.cursor = res.data.cursor return { cursor: res.data.cursor, feed: this._filter(res.data.feed), diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index d05d5acd6..47ffc65ed 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -2,37 +2,33 @@ import { AppBskyFeedDefs, AppBskyFeedGetFeed as GetCustomFeed, } from '@atproto/api' -import {RootStoreModel} from 'state/index' import {FeedAPI, FeedAPIResponse} from './types' +import {getAgent} from '#/state/session' export class CustomFeedAPI implements FeedAPI { - cursor: string | undefined - - constructor( - public rootStore: RootStoreModel, - public params: GetCustomFeed.QueryParams, - ) {} - - reset() { - this.cursor = undefined - } + constructor(public params: GetCustomFeed.QueryParams) {} async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { - const res = await this.rootStore.agent.app.bsky.feed.getFeed({ + const res = await getAgent().app.bsky.feed.getFeed({ ...this.params, limit: 1, }) return res.data.feed[0] } - async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> { - const res = await this.rootStore.agent.app.bsky.feed.getFeed({ + async fetch({ + cursor, + limit, + }: { + cursor: string | undefined + limit: number + }): Promise<FeedAPIResponse> { + const res = await getAgent().app.bsky.feed.getFeed({ ...this.params, - cursor: this.cursor, + cursor, limit, }) if (res.success) { - this.cursor = res.data.cursor // NOTE // some custom feeds fail to enforce the pagination limit // so we manually truncate here diff --git a/src/lib/api/feed/following.ts b/src/lib/api/feed/following.ts index f14807a57..24389b5ed 100644 --- a/src/lib/api/feed/following.ts +++ b/src/lib/api/feed/following.ts @@ -1,30 +1,29 @@ import {AppBskyFeedDefs} from '@atproto/api' -import {RootStoreModel} from 'state/index' import {FeedAPI, FeedAPIResponse} from './types' +import {getAgent} from '#/state/session' export class FollowingFeedAPI implements FeedAPI { - cursor: string | undefined - - constructor(public rootStore: RootStoreModel) {} - - reset() { - this.cursor = undefined - } + constructor() {} async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { - const res = await this.rootStore.agent.getTimeline({ + const res = await getAgent().getTimeline({ limit: 1, }) return res.data.feed[0] } - async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> { - const res = await this.rootStore.agent.getTimeline({ - cursor: this.cursor, + async fetch({ + cursor, + limit, + }: { + cursor: string | undefined + limit: number + }): Promise<FeedAPIResponse> { + const res = await getAgent().getTimeline({ + cursor, limit, }) if (res.success) { - this.cursor = res.data.cursor return { cursor: res.data.cursor, feed: res.data.feed, diff --git a/src/lib/api/feed/likes.ts b/src/lib/api/feed/likes.ts index e9bb14b0b..2b0afdf11 100644 --- a/src/lib/api/feed/likes.ts +++ b/src/lib/api/feed/likes.ts @@ -2,37 +2,33 @@ import { AppBskyFeedDefs, AppBskyFeedGetActorLikes as GetActorLikes, } from '@atproto/api' -import {RootStoreModel} from 'state/index' import {FeedAPI, FeedAPIResponse} from './types' +import {getAgent} from '#/state/session' export class LikesFeedAPI implements FeedAPI { - cursor: string | undefined - - constructor( - public rootStore: RootStoreModel, - public params: GetActorLikes.QueryParams, - ) {} - - reset() { - this.cursor = undefined - } + constructor(public params: GetActorLikes.QueryParams) {} async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { - const res = await this.rootStore.agent.getActorLikes({ + const res = await getAgent().getActorLikes({ ...this.params, limit: 1, }) return res.data.feed[0] } - async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> { - const res = await this.rootStore.agent.getActorLikes({ + async fetch({ + cursor, + limit, + }: { + cursor: string | undefined + limit: number + }): Promise<FeedAPIResponse> { + const res = await getAgent().getActorLikes({ ...this.params, - cursor: this.cursor, + cursor, limit, }) if (res.success) { - this.cursor = res.data.cursor return { cursor: res.data.cursor, feed: res.data.feed, diff --git a/src/lib/api/feed/list.ts b/src/lib/api/feed/list.ts index e58494675..19f2ff177 100644 --- a/src/lib/api/feed/list.ts +++ b/src/lib/api/feed/list.ts @@ -2,37 +2,33 @@ import { AppBskyFeedDefs, AppBskyFeedGetListFeed as GetListFeed, } from '@atproto/api' -import {RootStoreModel} from 'state/index' import {FeedAPI, FeedAPIResponse} from './types' +import {getAgent} from '#/state/session' export class ListFeedAPI implements FeedAPI { - cursor: string | undefined - - constructor( - public rootStore: RootStoreModel, - public params: GetListFeed.QueryParams, - ) {} - - reset() { - this.cursor = undefined - } + constructor(public params: GetListFeed.QueryParams) {} async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { - const res = await this.rootStore.agent.app.bsky.feed.getListFeed({ + const res = await getAgent().app.bsky.feed.getListFeed({ ...this.params, limit: 1, }) return res.data.feed[0] } - async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> { - const res = await this.rootStore.agent.app.bsky.feed.getListFeed({ + async fetch({ + cursor, + limit, + }: { + cursor: string | undefined + limit: number + }): Promise<FeedAPIResponse> { + const res = await getAgent().app.bsky.feed.getListFeed({ ...this.params, - cursor: this.cursor, + cursor, limit, }) if (res.success) { - this.cursor = res.data.cursor return { cursor: res.data.cursor, feed: res.data.feed, diff --git a/src/lib/api/feed/merge.ts b/src/lib/api/feed/merge.ts index e0fbcecd8..11e963f0a 100644 --- a/src/lib/api/feed/merge.ts +++ b/src/lib/api/feed/merge.ts @@ -1,11 +1,13 @@ import {AppBskyFeedDefs, AppBskyFeedGetTimeline} from '@atproto/api' import shuffle from 'lodash.shuffle' -import {RootStoreModel} from 'state/index' import {timeout} from 'lib/async/timeout' import {bundleAsync} from 'lib/async/bundle' import {feedUriToHref} from 'lib/strings/url-helpers' import {FeedTuner} from '../feed-manip' -import {FeedAPI, FeedAPIResponse, FeedSourceInfo} from './types' +import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types' +import {FeedParams} from '#/state/queries/post-feed' +import {FeedTunerFn} from '../feed-manip' +import {getAgent} from '#/state/session' const REQUEST_WAIT_MS = 500 // 500ms const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours @@ -17,28 +19,44 @@ export class MergeFeedAPI implements FeedAPI { itemCursor = 0 sampleCursor = 0 - constructor(public rootStore: RootStoreModel) { - this.following = new MergeFeedSource_Following(this.rootStore) + constructor(public params: FeedParams, public feedTuners: FeedTunerFn[]) { + this.following = new MergeFeedSource_Following(this.feedTuners) } reset() { - this.following = new MergeFeedSource_Following(this.rootStore) + this.following = new MergeFeedSource_Following(this.feedTuners) this.customFeeds = [] // just empty the array, they will be captured in _fetchNext() this.feedCursor = 0 this.itemCursor = 0 this.sampleCursor = 0 + if (this.params.mergeFeedEnabled && this.params.mergeFeedSources) { + this.customFeeds = shuffle( + this.params.mergeFeedSources.map( + feedUri => new MergeFeedSource_Custom(feedUri, this.feedTuners), + ), + ) + } else { + this.customFeeds = [] + } } async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> { - const res = await this.rootStore.agent.getTimeline({ + const res = await getAgent().getTimeline({ limit: 1, }) return res.data.feed[0] } - async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> { - // we capture here to ensure the data has loaded - this._captureFeedsIfNeeded() + async fetch({ + cursor, + limit, + }: { + cursor: string | undefined + limit: number + }): Promise<FeedAPIResponse> { + if (!cursor) { + this.reset() + } const promises = [] @@ -76,7 +94,7 @@ export class MergeFeedAPI implements FeedAPI { } return { - cursor: posts.length ? 'fake' : undefined, + cursor: posts.length ? String(this.itemCursor) : undefined, feed: posts, } } @@ -107,28 +125,15 @@ export class MergeFeedAPI implements FeedAPI { // provide follow return this.following.take(1) } - - _captureFeedsIfNeeded() { - if (!this.rootStore.preferences.homeFeed.lab_mergeFeedEnabled) { - return - } - if (this.customFeeds.length === 0) { - this.customFeeds = shuffle( - this.rootStore.preferences.savedFeeds.map( - feedUri => new MergeFeedSource_Custom(this.rootStore, feedUri), - ), - ) - } - } } class MergeFeedSource { - sourceInfo: FeedSourceInfo | undefined + sourceInfo: ReasonFeedSource | undefined cursor: string | undefined = undefined queue: AppBskyFeedDefs.FeedViewPost[] = [] hasMore = true - constructor(public rootStore: RootStoreModel) {} + constructor(public feedTuners: FeedTunerFn[]) {} get numReady() { return this.queue.length @@ -175,7 +180,7 @@ class MergeFeedSource { } class MergeFeedSource_Following extends MergeFeedSource { - tuner = new FeedTuner() + tuner = new FeedTuner(this.feedTuners) reset() { super.reset() @@ -190,16 +195,12 @@ class MergeFeedSource_Following extends MergeFeedSource { cursor: string | undefined, limit: number, ): Promise<AppBskyFeedGetTimeline.Response> { - const res = await this.rootStore.agent.getTimeline({cursor, limit}) + const res = await getAgent().getTimeline({cursor, limit}) // run the tuner pre-emptively to ensure better mixing - const slices = this.tuner.tune( - res.data.feed, - this.rootStore.preferences.getFeedTuners('home'), - { - dryRun: false, - maintainOrder: true, - }, - ) + const slices = this.tuner.tune(res.data.feed, { + dryRun: false, + maintainOrder: true, + }) res.data.feed = slices.map(slice => slice.rootItem) return res } @@ -208,15 +209,16 @@ class MergeFeedSource_Following extends MergeFeedSource { class MergeFeedSource_Custom extends MergeFeedSource { minDate: Date - constructor(public rootStore: RootStoreModel, public feedUri: string) { - super(rootStore) + constructor(public feedUri: string, public feedTuners: FeedTunerFn[]) { + super(feedTuners) this.sourceInfo = { + $type: 'reasonFeedSource', displayName: feedUri.split('/').pop() || '', uri: feedUriToHref(feedUri), } this.minDate = new Date(Date.now() - POST_AGE_CUTOFF) - this.rootStore.agent.app.bsky.feed - .getFeedGenerator({ + getAgent() + .app.bsky.feed.getFeedGenerator({ feed: feedUri, }) .then( @@ -234,7 +236,7 @@ class MergeFeedSource_Custom extends MergeFeedSource { limit: number, ): Promise<AppBskyFeedGetTimeline.Response> { try { - const res = await this.rootStore.agent.app.bsky.feed.getFeed({ + const res = await getAgent().app.bsky.feed.getFeed({ cursor, limit, feed: this.feedUri, diff --git a/src/lib/api/feed/types.ts b/src/lib/api/feed/types.ts index 006344334..5d2a90c1d 100644 --- a/src/lib/api/feed/types.ts +++ b/src/lib/api/feed/types.ts @@ -6,12 +6,27 @@ export interface FeedAPIResponse { } export interface FeedAPI { - reset(): void peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> - fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> + fetch({ + cursor, + limit, + }: { + cursor: string | undefined + limit: number + }): Promise<FeedAPIResponse> } -export interface FeedSourceInfo { +export interface ReasonFeedSource { + $type: 'reasonFeedSource' uri: string displayName: string } + +export function isReasonFeedSource(v: unknown): v is ReasonFeedSource { + return ( + !!v && + typeof v === 'object' && + '$type' in v && + v.$type === 'reasonFeedSource' + ) +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 9d48a78c0..a78abcacd 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -4,12 +4,12 @@ import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyRichtextFacet, + BskyAgent, ComAtprotoLabelDefs, ComAtprotoRepoUploadBlob, RichText, } from '@atproto/api' import {AtUri} from '@atproto/api' -import {RootStoreModel} from 'state/models/root-store' import {isNetworkError} from 'lib/strings/errors' import {LinkMeta} from '../link-meta/link-meta' import {isWeb} from 'platform/detection' @@ -25,46 +25,19 @@ export interface ExternalEmbedDraft { localThumb?: ImageModel } -export async function resolveName(store: RootStoreModel, didOrHandle: string) { - if (!didOrHandle) { - throw new Error('Invalid handle: ""') - } - if (didOrHandle.startsWith('did:')) { - return didOrHandle - } - - // we run the resolution always to ensure freshness - const promise = store.agent - .resolveHandle({ - handle: didOrHandle, - }) - .then(res => { - store.handleResolutions.cache.set(didOrHandle, res.data.did) - return res.data.did - }) - - // but we can return immediately if it's cached - const cached = store.handleResolutions.cache.get(didOrHandle) - if (cached) { - return cached - } - - return promise -} - export async function uploadBlob( - store: RootStoreModel, + agent: BskyAgent, blob: string, encoding: string, ): Promise<ComAtprotoRepoUploadBlob.Response> { if (isWeb) { // `blob` should be a data uri - return store.agent.uploadBlob(convertDataURIToUint8Array(blob), { + return agent.uploadBlob(convertDataURIToUint8Array(blob), { encoding, }) } else { // `blob` should be a path to a file in the local FS - return store.agent.uploadBlob( + return agent.uploadBlob( blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts {encoding}, ) @@ -81,12 +54,11 @@ interface PostOpts { extLink?: ExternalEmbedDraft images?: ImageModel[] labels?: string[] - knownHandles?: Set<string> onStateChange?: (state: string) => void langs?: string[] } -export async function post(store: RootStoreModel, opts: PostOpts) { +export async function post(agent: BskyAgent, opts: PostOpts) { let embed: | AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main @@ -102,7 +74,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { ) opts.onStateChange?.('Processing...') - await rt.detectFacets(store.agent) + await rt.detectFacets(agent) rt = shortenLinks(rt) // filter out any mention facets that didn't map to a user @@ -135,7 +107,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { await image.compress() const path = image.compressed?.path ?? image.path const {width, height} = image.compressed || image - const res = await uploadBlob(store, path, 'image/jpeg') + const res = await uploadBlob(agent, path, 'image/jpeg') images.push({ image: res.data.blob, alt: image.altText ?? '', @@ -185,7 +157,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { } if (encoding) { const thumbUploadRes = await uploadBlob( - store, + agent, opts.extLink.localThumb.path, encoding, ) @@ -224,7 +196,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { // add replyTo if post is a reply to another post if (opts.replyTo) { const replyToUrip = new AtUri(opts.replyTo) - const parentPost = await store.agent.getPost({ + const parentPost = await agent.getPost({ repo: replyToUrip.host, rkey: replyToUrip.rkey, }) @@ -257,7 +229,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { try { opts.onStateChange?.('Posting...') - return await store.agent.post({ + return await agent.post({ text: rt.text, facets: rt.facets, reply, |