about summary refs log tree commit diff
path: root/src/state/models/discovery/foafs.ts
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-03-19 18:53:57 -0500
committerGitHub <noreply@github.com>2023-03-19 18:53:57 -0500
commit1de724b24b9607d4ee83dc0dbb92c13b2b77dcaf (patch)
treede1b244a976e55818f1181e6bf2b727237aff7c2 /src/state/models/discovery/foafs.ts
parentc31ffdac1b970d8d51c538f931cc64a942670740 (diff)
downloadvoidsky-1de724b24b9607d4ee83dc0dbb92c13b2b77dcaf.tar.zst
Add custom feeds selector, rework search, simplify onboarding (#325)
* Get home screen's swipable pager working with the drawer

* Add tab bar to pager

* Implement popular & following views on home screen

* Visual tune-up

* Move the feed selector to the footer

* Fix to 'new posts' poll

* Add the view header as a feed item

* Use the native driver on the tabbar indicator to improve perf

* Reduce home polling to the currently active page; also reuse some code

* Add soft reset on tap selected in tab bar

* Remove explicit 'onboarding' flow

* Choose good stuff based on service

* Add foaf-based follow discovery

* Fall back to who to follow

* Fix backgrounds

* Switch to the off-spec goodstuff route

* 1.8

* Fix for dev & staging

* Swap the tab bar items and rename suggested to what's hot

* Go to whats-hot by default if you have no follows

* Implement pager and tabbar for desktop web

* Pin deps to make expo happy

* Add language filtering to goodstuff
Diffstat (limited to 'src/state/models/discovery/foafs.ts')
-rw-r--r--src/state/models/discovery/foafs.ts110
1 files changed, 110 insertions, 0 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
+      })
+    }
+  })
+}