diff options
Diffstat (limited to 'src/state/models/suggested-posts-view.ts')
-rw-r--r-- | src/state/models/suggested-posts-view.ts | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/src/state/models/suggested-posts-view.ts b/src/state/models/suggested-posts-view.ts new file mode 100644 index 000000000..7b44370de --- /dev/null +++ b/src/state/models/suggested-posts-view.ts @@ -0,0 +1,148 @@ +import {makeAutoObservable, runInAction} from 'mobx' +import { + AppBskyFeedFeedViewPost, + AppBskyFeedGetAuthorFeed as GetAuthorFeed, +} from '@atproto/api' +type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost +import {RootStoreModel} from './root-store' +import {FeedItemModel} from './feed-view' +import {cleanError} from 'lib/strings/errors' + +const TEAM_HANDLES = [ + 'jay.bsky.social', + 'paul.bsky.social', + 'dan.bsky.social', + 'divy.bsky.social', + 'why.bsky.social', + 'iamrosewang.bsky.social', +] + +export class SuggestedPostsView { + // state + isLoading = false + hasLoaded = false + error = '' + + // data + posts: FeedItemModel[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasContent() { + return this.posts.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + async setup() { + this._xLoading() + try { + const responses = await Promise.all( + TEAM_HANDLES.map(handle => + this.rootStore.api.app.bsky.feed + .getAuthorFeed({author: handle, limit: 10}) + .catch(_err => ({success: false, headers: {}, data: {feed: []}})), + ), + ) + runInAction(() => { + this.posts = mergeAndFilterResponses(this.rootStore, responses) + }) + this._xIdle() + } catch (e: any) { + this.rootStore.log.error('SuggestedPostsView: Failed to load posts', { + e, + }) + this._xIdle() // dont bubble to the user + } + } + + // state transitions + // = + + private _xLoading() { + this.isLoading = true + this.error = '' + } + + private _xIdle(err?: any) { + this.isLoading = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch suggested posts', err) + } + } +} + +function mergeAndFilterResponses( + store: RootStoreModel, + responses: GetAuthorFeed.Response[], +): FeedItemModel[] { + let posts: AppBskyFeedFeedViewPost.Main[] = [] + + // merge into one array + for (const res of responses) { + if (res.success) { + posts = posts.concat(res.data.feed) + } + } + + // filter down to reposts of other users + const now = Date.now() + const uris = new Set() + posts = posts.filter(p => { + if (isARepostOfSomeoneElse(p) && isRecentEnough(now, p)) { + if (uris.has(p.post.uri)) { + return false + } + uris.add(p.post.uri) + return true + } + return false + }) + + // sort by index time + posts.sort((a, b) => { + return ( + Number(new Date(b.post.indexedAt)) - Number(new Date(a.post.indexedAt)) + ) + }) + + // hydrate into models and strip the reasons to hide that these are reposts + return posts.map((post, i) => { + delete post.reason + return new FeedItemModel(store, `post-${i}`, post) + }) +} + +function isARepostOfSomeoneElse(post: AppBskyFeedFeedViewPost.Main): boolean { + return ( + post.reason?.$type === 'app.bsky.feed.feedViewPost#reasonRepost' && + post.post.author.did !== (post.reason as ReasonRepost).by.did + ) +} + +const THREE_DAYS = 3 * 24 * 60 * 60 * 1000 +function isRecentEnough( + now: number, + post: AppBskyFeedFeedViewPost.Main, +): boolean { + return now - Number(new Date(post.post.indexedAt)) < THREE_DAYS +} |