diff options
Diffstat (limited to 'src/state/models')
-rw-r--r-- | src/state/models/discovery/foafs.ts | 110 | ||||
-rw-r--r-- | src/state/models/feed-view.ts | 85 | ||||
-rw-r--r-- | src/state/models/me.ts | 1 | ||||
-rw-r--r-- | src/state/models/my-follows.ts | 6 | ||||
-rw-r--r-- | src/state/models/session.ts | 11 | ||||
-rw-r--r-- | src/state/models/ui/shell.ts | 15 |
6 files changed, 198 insertions, 30 deletions
diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts new file mode 100644 index 000000000..241338a16 --- /dev/null +++ b/src/state/models/discovery/foafs.ts @@ -0,0 +1,110 @@ +import {AppBskyActorProfile, AppBskyActorRef} from '@atproto/api' +import {makeAutoObservable, runInAction} from 'mobx' +import sampleSize from 'lodash.samplesize' +import {bundleAsync} from 'lib/async/bundle' +import {RootStoreModel} from '../root-store' + +export type RefWithInfoAndFollowers = AppBskyActorRef.WithInfo & { + followers: AppBskyActorProfile.View[] +} + +export type ProfileViewFollows = AppBskyActorProfile.View & { + follows: AppBskyActorRef.WithInfo[] +} + +export class FoafsModel { + isLoading = false + hasData = false + sources: string[] = [] + foafs: Map<string, ProfileViewFollows> = new Map() + popular: RefWithInfoAndFollowers[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable(this) + } + + get hasContent() { + if (this.popular.length > 0) { + return true + } + for (const foaf of this.foafs.values()) { + if (foaf.follows.length) { + return true + } + } + return false + } + + fetch = bundleAsync(async () => { + try { + this.isLoading = true + await this.rootStore.me.follows.fetchIfNeeded() + // grab 10 of the users followed by the user + this.sources = sampleSize( + Object.keys(this.rootStore.me.follows.followDidToRecordMap), + 10, + ) + if (this.sources.length === 0) { + return + } + this.foafs.clear() + this.popular.length = 0 + + // fetch their profiles + const profiles = await this.rootStore.api.app.bsky.actor.getProfiles({ + actors: this.sources, + }) + + // fetch their follows + const results = await Promise.allSettled( + this.sources.map(source => + this.rootStore.api.app.bsky.graph.getFollows({user: source}), + ), + ) + + // store the follows and construct a "most followed" set + const popular: RefWithInfoAndFollowers[] = [] + for (let i = 0; i < results.length; i++) { + const res = results[i] + const profile = profiles.data.profiles[i] + const source = this.sources[i] + if (res.status === 'fulfilled' && profile) { + // filter out users already followed by the user or that *is* the user + res.value.data.follows = res.value.data.follows.filter(follow => { + return ( + follow.did !== this.rootStore.me.did && + !this.rootStore.me.follows.isFollowing(follow.did) + ) + }) + + runInAction(() => { + this.foafs.set(source, { + ...profile, + follows: res.value.data.follows, + }) + }) + for (const follow of res.value.data.follows) { + let item = popular.find(p => p.did === follow.did) + if (!item) { + item = {...follow, followers: []} + popular.push(item) + } + item.followers.push(profile) + } + } + } + + popular.sort((a, b) => b.followers.length - a.followers.length) + runInAction(() => { + this.popular = popular.filter(p => p.followers.length > 1).slice(0, 20) + }) + this.hasData = true + } catch (e) { + console.error('Failed to fetch FOAFs', e) + } finally { + runInAction(() => { + this.isLoading = false + }) + } + }) +} diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 42b753b24..c412065dd 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -257,7 +257,7 @@ export class FeedModel { constructor( public rootStore: RootStoreModel, - public feedType: 'home' | 'author' | 'suggested', + public feedType: 'home' | 'author' | 'suggested' | 'goodstuff', params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams, ) { makeAutoObservable( @@ -336,6 +336,20 @@ export class FeedModel { return this.setup() } + private get feedTuners() { + if (this.feedType === 'goodstuff') { + return [ + FeedTuner.dedupReposts, + FeedTuner.likedRepliesOnly, + FeedTuner.englishOnly, + ] + } + if (this.feedType === 'home') { + return [FeedTuner.dedupReposts, FeedTuner.likedRepliesOnly] + } + return [] + } + /** * Load for first render */ @@ -399,6 +413,7 @@ export class FeedModel { params: this.params, e, }) + this.hasMore = false } } finally { this.lock.release() @@ -476,7 +491,8 @@ export class FeedModel { } const res = await this._getFeed({limit: 1}) const currentLatestUri = this.pollCursor - const item = res.data.feed[0] + const slices = this.tuner.tune(res.data.feed, this.feedTuners) + const item = slices[0]?.rootItem if (!item) { return } @@ -541,12 +557,7 @@ export class FeedModel { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor - const slices = this.tuner.tune( - res.data.feed, - this.feedType === 'home' - ? [FeedTuner.dedupReposts, FeedTuner.likedRepliesOnly] - : [], - ) + const slices = this.tuner.tune(res.data.feed, this.feedTuners) const toAppend: FeedSliceModel[] = [] for (const slice of slices) { @@ -571,12 +582,7 @@ export class FeedModel { ) { this.pollCursor = res.data.feed[0]?.post.uri - const slices = this.tuner.tune( - res.data.feed, - this.feedType === 'home' - ? [FeedTuner.dedupReposts, FeedTuner.likedRepliesOnly] - : [], - ) + const slices = this.tuner.tune(res.data.feed, this.feedTuners) const toPrepend: FeedSliceModel[] = [] for (const slice of slices) { @@ -634,6 +640,15 @@ export class FeedModel { return this.rootStore.api.app.bsky.feed.getTimeline( params as GetTimeline.QueryParams, ) + } else if (this.feedType === 'goodstuff') { + const res = await getGoodStuff( + this.rootStore.session.currentSession?.accessJwt || '', + params as GetTimeline.QueryParams, + ) + res.data.feed = (res.data.feed || []).filter( + item => !item.post.author.viewer?.muted, + ) + return res } else { return this.rootStore.api.app.bsky.feed.getAuthorFeed( params as GetAuthorFeed.QueryParams, @@ -641,3 +656,45 @@ export class FeedModel { } } } + +// HACK +// temporary off-spec route to get the good stuff +// -prf +async function getGoodStuff( + accessJwt: string, + params: GetTimeline.QueryParams, +): Promise<GetTimeline.Response> { + const controller = new AbortController() + const to = setTimeout(() => controller.abort(), 15e3) + + const uri = new URL('https://bsky.social/xrpc/app.bsky.unspecced.getPopular') + let k: keyof GetTimeline.QueryParams + for (k in params) { + if (typeof params[k] !== 'undefined') { + uri.searchParams.set(k, String(params[k])) + } + } + + const res = await fetch(String(uri), { + method: 'get', + headers: { + accept: 'application/json', + authorization: `Bearer ${accessJwt}`, + }, + signal: controller.signal, + }) + + const resHeaders: Record<string, string> = {} + res.headers.forEach((value: string, key: string) => { + resHeaders[key] = value + }) + let resBody = await res.json() + + clearTimeout(to) + + return { + success: res.status === 200, + headers: resHeaders, + data: resBody, + } +} diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 077c65595..192e8f19f 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -33,6 +33,7 @@ export class MeModel { clear() { this.mainFeed.clear() this.notifications.clear() + this.follows.clear() this.did = '' this.handle = '' this.displayName = '' diff --git a/src/state/models/my-follows.ts b/src/state/models/my-follows.ts index 732c2fe73..bf1bf9600 100644 --- a/src/state/models/my-follows.ts +++ b/src/state/models/my-follows.ts @@ -35,6 +35,12 @@ export class MyFollowsModel { // public api // = + clear() { + this.followDidToRecordMap = {} + this.lastSync = 0 + this.myDid = undefined + } + fetchIfNeeded = bundleAsync(async () => { if ( this.myDid !== this.rootStore.me.did || diff --git a/src/state/models/session.ts b/src/state/models/session.ts index 306c265d8..e131b2b2c 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -154,13 +154,13 @@ export class SessionModel { /** * Sets the active session */ - setActiveSession(agent: AtpAgent, did: string) { + async setActiveSession(agent: AtpAgent, did: string) { this._log('SessionModel:setActiveSession') this.data = { service: agent.service.toString(), did, } - this.rootStore.handleSessionChange(agent) + await this.rootStore.handleSessionChange(agent) } /** @@ -304,7 +304,7 @@ export class SessionModel { return false } - this.setActiveSession(agent, account.did) + await this.setActiveSession(agent, account.did) return true } @@ -337,7 +337,7 @@ export class SessionModel { }, ) - this.setActiveSession(agent, did) + await this.setActiveSession(agent, did) this._log('SessionModel:login succeeded') } @@ -376,8 +376,7 @@ export class SessionModel { }, ) - this.setActiveSession(agent, did) - this.rootStore.shell.setOnboarding(true) + await this.setActiveSession(agent, did) this._log('SessionModel:createAccount succeeded') } diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index d6fefb850..fec1e2899 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -122,13 +122,13 @@ export class ShellUiModel { darkMode = false minimalShellMode = false isDrawerOpen = false + isDrawerSwipeDisabled = false isModalActive = false activeModals: Modal[] = [] isLightboxActive = false activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined isComposerActive = false composerOpts: ComposerOpts | undefined - isOnboarding = false constructor(public rootStore: RootStoreModel) { makeAutoObservable(this, { @@ -168,6 +168,10 @@ export class ShellUiModel { this.isDrawerOpen = false } + setIsDrawerSwipeDisabled(v: boolean) { + this.isDrawerSwipeDisabled = v + } + openModal(modal: Modal) { this.rootStore.emitNavigation() this.isModalActive = true @@ -200,13 +204,4 @@ export class ShellUiModel { this.isComposerActive = false this.composerOpts = undefined } - - setOnboarding(v: boolean) { - this.isOnboarding = v - if (this.isOnboarding) { - this.rootStore.me.mainFeed.switchFeedType('suggested') - } else { - this.rootStore.me.mainFeed.switchFeedType('home') - } - } } |