about summary refs log tree commit diff
path: root/src/state/models/discovery/foafs.ts
blob: 241338a16cfbc23a8a7225d58a3aa85e74d75a68 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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
      })
    }
  })
}