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/content/post-thread.ts37
-rw-r--r--src/state/models/discovery/onboarding.ts11
-rw-r--r--src/state/models/discovery/suggested-actors.ts19
-rw-r--r--src/state/models/ui/my-feeds.ts8
-rw-r--r--src/state/models/ui/preferences.ts43
5 files changed, 110 insertions, 8 deletions
diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts
index 7e3650948..2d3a3d64a 100644
--- a/src/state/models/content/post-thread.ts
+++ b/src/state/models/content/post-thread.ts
@@ -241,7 +241,7 @@ export class PostThreadModel {
       res.data.thread as AppBskyFeedDefs.ThreadViewPost,
       thread.uri,
     )
-    sortThread(thread)
+    sortThread(thread, this.rootStore.preferences)
     this.thread = thread
   }
 }
@@ -263,11 +263,16 @@ function pruneReplies(post: MaybePost) {
   }
 }
 
+interface SortSettings {
+  threadDefaultSort: string
+  threadFollowedUsersFirst: boolean
+}
+
 type MaybeThreadItem =
   | PostThreadItemModel
   | AppBskyFeedDefs.NotFoundPost
   | AppBskyFeedDefs.BlockedPost
-function sortThread(item: MaybeThreadItem) {
+function sortThread(item: MaybeThreadItem, opts: SortSettings) {
   if ('notFound' in item) {
     return
   }
@@ -296,13 +301,31 @@ function sortThread(item: MaybeThreadItem) {
       if (modScore(a.moderation) !== modScore(b.moderation)) {
         return modScore(a.moderation) - modScore(b.moderation)
       }
-      if (a.post.likeCount === b.post.likeCount) {
-        return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest
-      } else {
-        return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes
+      if (opts.threadFollowedUsersFirst) {
+        const af = a.post.author.viewer?.following
+        const bf = b.post.author.viewer?.following
+        if (af && !bf) {
+          return -1
+        } else if (!af && bf) {
+          return 1
+        }
+      }
+      if (opts.threadDefaultSort === 'oldest') {
+        return a.post.indexedAt.localeCompare(b.post.indexedAt)
+      } else if (opts.threadDefaultSort === 'newest') {
+        return b.post.indexedAt.localeCompare(a.post.indexedAt)
+      } else if (opts.threadDefaultSort === 'most-likes') {
+        if (a.post.likeCount === b.post.likeCount) {
+          return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest
+        } else {
+          return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes
+        }
+      } else if (opts.threadDefaultSort === 'random') {
+        return 0.5 - Math.random() // this is vaguely criminal but we can get away with it
       }
+      return b.post.indexedAt.localeCompare(a.post.indexedAt)
     })
-    item.replies.forEach(reply => sortThread(reply))
+    item.replies.forEach(reply => sortThread(reply, opts))
   }
 }
 
diff --git a/src/state/models/discovery/onboarding.ts b/src/state/models/discovery/onboarding.ts
index 09c9eac04..8ad321ed9 100644
--- a/src/state/models/discovery/onboarding.ts
+++ b/src/state/models/discovery/onboarding.ts
@@ -2,10 +2,12 @@ import {makeAutoObservable} from 'mobx'
 import {RootStoreModel} from '../root-store'
 import {hasProp} from 'lib/type-guards'
 import {track} from 'lib/analytics/analytics'
+import {SuggestedActorsModel} from './suggested-actors'
 
 export const OnboardingScreenSteps = {
   Welcome: 'Welcome',
   RecommendedFeeds: 'RecommendedFeeds',
+  RecommendedFollows: 'RecommendedFollows',
   Home: 'Home',
 } as const
 
@@ -16,7 +18,11 @@ export class OnboardingModel {
   // state
   step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start()
 
+  // data
+  suggestedActors: SuggestedActorsModel
+
   constructor(public rootStore: RootStoreModel) {
+    this.suggestedActors = new SuggestedActorsModel(this.rootStore)
     makeAutoObservable(this, {
       rootStore: false,
       hydrate: false,
@@ -56,6 +62,11 @@ export class OnboardingModel {
       this.step = 'RecommendedFeeds'
       return this.step
     } else if (this.step === 'RecommendedFeeds') {
+      this.step = 'RecommendedFollows'
+      // prefetch recommended follows
+      this.suggestedActors.loadMore(true)
+      return this.step
+    } else if (this.step === 'RecommendedFollows') {
       this.finish()
       return this.step
     } else {
diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts
index 0b3d36952..afa5e74e3 100644
--- a/src/state/models/discovery/suggested-actors.ts
+++ b/src/state/models/discovery/suggested-actors.ts
@@ -19,6 +19,7 @@ export class SuggestedActorsModel {
   loadMoreCursor: string | undefined = undefined
   error = ''
   hasMore = false
+  lastInsertedAtIndex = -1
 
   // data
   suggestions: SuggestedActor[] = []
@@ -110,6 +111,24 @@ export class SuggestedActorsModel {
     }
   })
 
+  async insertSuggestionsByActor(actor: string, indexToInsertAt: number) {
+    // fetch suggestions
+    const res =
+      await this.rootStore.agent.app.bsky.graph.getSuggestedFollowsByActor({
+        actor: actor,
+      })
+    const {suggestions: moreSuggestions} = res.data
+    this.rootStore.me.follows.hydrateProfiles(moreSuggestions)
+    // dedupe
+    const toInsert = moreSuggestions.filter(
+      s => !this.suggestions.find(s2 => s2.did === s.did),
+    )
+    //  insert
+    this.suggestions.splice(indexToInsertAt + 1, 0, ...toInsert)
+    // update index
+    this.lastInsertedAtIndex = indexToInsertAt
+  }
+
   // state transitions
   // =
 
diff --git a/src/state/models/ui/my-feeds.ts b/src/state/models/ui/my-feeds.ts
index f9ad06f77..6b017709e 100644
--- a/src/state/models/ui/my-feeds.ts
+++ b/src/state/models/ui/my-feeds.ts
@@ -10,6 +10,11 @@ export type MyFeedsItem =
     }
   | {
       _reactKey: string
+      type: 'saved-feeds-loading'
+      numItems: number
+    }
+  | {
+      _reactKey: string
       type: 'discover-feeds-loading'
     }
   | {
@@ -91,7 +96,8 @@ export class MyFeedsUIModel {
     if (this.saved.isLoading) {
       items.push({
         _reactKey: '__saved_feeds_loading__',
-        type: 'spinner',
+        type: 'saved-feeds-loading',
+        numItems: this.rootStore.preferences.savedFeeds.length || 3,
       })
     } else if (this.saved.hasError) {
       items.push({
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index 3790b3a92..5c6ea230b 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -25,6 +25,7 @@ const VISIBILITY_VALUES = ['ignore', 'warn', 'hide']
 const DEFAULT_LANG_CODES = (deviceLocales || [])
   .concat(['en', 'ja', 'pt', 'de'])
   .slice(0, 6)
+const THREAD_SORT_VALUES = ['oldest', 'newest', 'most-likes', 'random']
 
 export class LabelPreferencesModel {
   nsfw: LabelPreference = 'hide'
@@ -55,6 +56,9 @@ export class PreferencesModel {
   homeFeedRepostsEnabled: boolean = true
   homeFeedQuotePostsEnabled: boolean = true
   homeFeedMergeFeedEnabled: boolean = false
+  threadDefaultSort: string = 'oldest'
+  threadFollowedUsersFirst: boolean = true
+  threadTreeViewEnabled: boolean = false
   requireAltTextEnabled: boolean = false
 
   // used to linearize async modifications to state
@@ -86,6 +90,9 @@ export class PreferencesModel {
       homeFeedRepostsEnabled: this.homeFeedRepostsEnabled,
       homeFeedQuotePostsEnabled: this.homeFeedQuotePostsEnabled,
       homeFeedMergeFeedEnabled: this.homeFeedMergeFeedEnabled,
+      threadDefaultSort: this.threadDefaultSort,
+      threadFollowedUsersFirst: this.threadFollowedUsersFirst,
+      threadTreeViewEnabled: this.threadTreeViewEnabled,
       requireAltTextEnabled: this.requireAltTextEnabled,
     }
   }
@@ -189,6 +196,28 @@ export class PreferencesModel {
       ) {
         this.homeFeedMergeFeedEnabled = v.homeFeedMergeFeedEnabled
       }
+      // check if thread sort order is set in preferences, then hydrate
+      if (
+        hasProp(v, 'threadDefaultSort') &&
+        typeof v.threadDefaultSort === 'string' &&
+        THREAD_SORT_VALUES.includes(v.threadDefaultSort)
+      ) {
+        this.threadDefaultSort = v.threadDefaultSort
+      }
+      // check if thread followed-users-first is enabled in preferences, then hydrate
+      if (
+        hasProp(v, 'threadFollowedUsersFirst') &&
+        typeof v.threadFollowedUsersFirst === 'boolean'
+      ) {
+        this.threadFollowedUsersFirst = v.threadFollowedUsersFirst
+      }
+      // check if thread treeview is enabled in preferences, then hydrate
+      if (
+        hasProp(v, 'threadTreeViewEnabled') &&
+        typeof v.threadTreeViewEnabled === 'boolean'
+      ) {
+        this.threadTreeViewEnabled = v.threadTreeViewEnabled
+      }
       // check if requiring alt text is enabled in preferences, then hydrate
       if (
         hasProp(v, 'requireAltTextEnabled') &&
@@ -494,6 +523,20 @@ export class PreferencesModel {
     this.homeFeedMergeFeedEnabled = !this.homeFeedMergeFeedEnabled
   }
 
+  setThreadDefaultSort(v: string) {
+    if (THREAD_SORT_VALUES.includes(v)) {
+      this.threadDefaultSort = v
+    }
+  }
+
+  toggleThreadFollowedUsersFirst() {
+    this.threadFollowedUsersFirst = !this.threadFollowedUsersFirst
+  }
+
+  toggleThreadTreeViewEnabled() {
+    this.threadTreeViewEnabled = !this.threadTreeViewEnabled
+  }
+
   toggleRequireAltTextEnabled() {
     this.requireAltTextEnabled = !this.requireAltTextEnabled
   }