about summary refs log tree commit diff
path: root/src/state/models
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models')
-rw-r--r--src/state/models/discovery/foafs.ts110
-rw-r--r--src/state/models/feed-view.ts85
-rw-r--r--src/state/models/me.ts1
-rw-r--r--src/state/models/my-follows.ts6
-rw-r--r--src/state/models/session.ts11
-rw-r--r--src/state/models/ui/shell.ts15
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')
-    }
-  }
 }