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/onboarding.ts94
-rw-r--r--src/state/models/feeds/custom-feed.ts13
-rw-r--r--src/state/models/root-store.ts6
-rw-r--r--src/state/models/ui/create-account.ts4
-rw-r--r--src/state/models/ui/shell.ts7
5 files changed, 115 insertions, 9 deletions
diff --git a/src/state/models/discovery/onboarding.ts b/src/state/models/discovery/onboarding.ts
new file mode 100644
index 000000000..09c9eac04
--- /dev/null
+++ b/src/state/models/discovery/onboarding.ts
@@ -0,0 +1,94 @@
+import {makeAutoObservable} from 'mobx'
+import {RootStoreModel} from '../root-store'
+import {hasProp} from 'lib/type-guards'
+import {track} from 'lib/analytics/analytics'
+
+export const OnboardingScreenSteps = {
+  Welcome: 'Welcome',
+  RecommendedFeeds: 'RecommendedFeeds',
+  Home: 'Home',
+} as const
+
+type OnboardingStep =
+  (typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
+const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
+export class OnboardingModel {
+  // state
+  step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start()
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(this, {
+      rootStore: false,
+      hydrate: false,
+      serialize: false,
+    })
+  }
+
+  serialize(): unknown {
+    return {
+      step: this.step,
+    }
+  }
+
+  hydrate(v: unknown) {
+    if (typeof v === 'object' && v !== null) {
+      if (
+        hasProp(v, 'step') &&
+        typeof v.step === 'string' &&
+        OnboardingStepsArray.includes(v.step as OnboardingStep)
+      ) {
+        this.step = v.step as OnboardingStep
+      }
+    } else {
+      // if there is no valid state, we'll just reset
+      this.reset()
+    }
+  }
+
+  /**
+   * Returns the name of the next screen in the onboarding process based on the current step or screen name provided.
+   * @param {OnboardingStep} [currentScreenName]
+   * @returns name of next screen in the onboarding process
+   */
+  next(currentScreenName?: OnboardingStep) {
+    currentScreenName = currentScreenName || this.step
+    if (currentScreenName === 'Welcome') {
+      this.step = 'RecommendedFeeds'
+      return this.step
+    } else if (this.step === 'RecommendedFeeds') {
+      this.finish()
+      return this.step
+    } else {
+      // if we get here, we're in an invalid state, let's just go Home
+      return 'Home'
+    }
+  }
+
+  start() {
+    this.step = 'Welcome'
+    track('Onboarding:Begin')
+  }
+
+  finish() {
+    this.step = 'Home'
+    track('Onboarding:Complete')
+  }
+
+  reset() {
+    this.step = 'Welcome'
+    track('Onboarding:Reset')
+  }
+
+  skip() {
+    this.step = 'Home'
+    track('Onboarding:Skipped')
+  }
+
+  get isComplete() {
+    return this.step === 'Home'
+  }
+
+  get isActive() {
+    return !this.isComplete
+  }
+}
diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts
index 3c6d52755..2de4534e7 100644
--- a/src/state/models/feeds/custom-feed.ts
+++ b/src/state/models/feeds/custom-feed.ts
@@ -67,6 +67,19 @@ export class CustomFeedModel {
     }
   }
 
+  async pin() {
+    try {
+      await this.rootStore.preferences.addPinnedFeed(this.uri)
+    } catch (error) {
+      this.rootStore.log.error('Failed to pin feed', error)
+    } finally {
+      track('CustomFeed:Pin', {
+        name: this.data.displayName,
+        uri: this.uri,
+      })
+    }
+  }
+
   async unsave() {
     try {
       await this.rootStore.preferences.removeSavedFeed(this.uri)
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 1d6d3a0d0..6204e0d10 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -27,6 +27,7 @@ import {reset as resetNavigation} from '../../Navigation'
 // remove after backend testing finishes
 // -prf
 import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
+import {OnboardingModel} from './discovery/onboarding'
 
 export const appInfo = z.object({
   build: z.string(),
@@ -44,6 +45,7 @@ export class RootStoreModel {
   shell = new ShellUiModel(this)
   preferences = new PreferencesModel(this)
   me = new MeModel(this)
+  onboarding = new OnboardingModel(this)
   invitedUsers = new InvitedUsers(this)
   handleResolutions = new HandleResolutionsCache()
   profiles = new ProfilesCache(this)
@@ -70,6 +72,7 @@ export class RootStoreModel {
       appInfo: this.appInfo,
       session: this.session.serialize(),
       me: this.me.serialize(),
+      onboarding: this.onboarding.serialize(),
       shell: this.shell.serialize(),
       preferences: this.preferences.serialize(),
       invitedUsers: this.invitedUsers.serialize(),
@@ -88,6 +91,9 @@ export class RootStoreModel {
       if (hasProp(v, 'me')) {
         this.me.hydrate(v.me)
       }
+      if (hasProp(v, 'onboarding')) {
+        this.onboarding.hydrate(v.onboarding)
+      }
       if (hasProp(v, 'session')) {
         this.session.hydrate(v.session)
       }
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
index 04e1554c6..d9d4f51b9 100644
--- a/src/state/models/ui/create-account.ts
+++ b/src/state/models/ui/create-account.ts
@@ -109,10 +109,10 @@ export class CreateAccountModel {
     this.setError('')
     this.setIsProcessing(true)
 
-    // open the onboarding modal after the session is created
+    // open the onboarding screens after the session is created
     const sessionReadySub = this.rootStore.onSessionReady(() => {
       sessionReadySub.remove()
-      this.rootStore.shell.openModal({name: 'onboarding'})
+      this.rootStore.onboarding.start()
     })
 
     try {
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index d19de4b96..33fdd5710 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -136,10 +136,6 @@ export interface PostLanguagesSettingsModal {
   name: 'post-languages-settings'
 }
 
-export interface OnboardingModal {
-  name: 'onboarding'
-}
-
 export type Modal =
   // Account
   | AddAppPasswordModal
@@ -171,9 +167,6 @@ export type Modal =
   | WaitlistModal
   | InviteCodesModal
 
-  // Onboarding
-  | OnboardingModal
-
   // Generic
   | ConfirmModal