Add onboarding (WIP)

This commit is contained in:
Paul Frazee 2022-11-07 15:35:51 -06:00
parent b4097e25d6
commit d228a5f4f5
15 changed files with 613 additions and 34 deletions

View file

@ -23,13 +23,14 @@ export async function setupState() {
console.error('Failed to load state from storage', e)
}
await rootStore.session.setup()
// track changes & save to storage
autorun(() => {
const snapshot = rootStore.serialize()
storage.save(ROOT_STATE_STORAGE_KEY, snapshot)
})
await rootStore.session.setup()
await rootStore.fetchStateUpdate()
console.log(rootStore.me)

View file

@ -8,3 +8,7 @@ export function hasProp<K extends PropertyKey>(
): data is Record<K, unknown> {
return prop in data
}
export function isStrArray(v: unknown): v is string[] {
return Array.isArray(v) && v.every(item => typeof item === 'string')
}

View file

@ -0,0 +1,62 @@
import {makeAutoObservable} from 'mobx'
import {isObj, hasProp} from '../lib/type-guards'
export const OnboardStage = {
Explainers: 'explainers',
Follows: 'follows',
}
export const OnboardStageOrder = [OnboardStage.Explainers, OnboardStage.Follows]
export class OnboardModel {
isOnboarding: boolean = true
stage: string = OnboardStageOrder[0]
constructor() {
makeAutoObservable(this, {
serialize: false,
hydrate: false,
})
}
serialize(): unknown {
return {
isOnboarding: this.isOnboarding,
stage: this.stage,
}
}
hydrate(v: unknown) {
if (isObj(v)) {
if (hasProp(v, 'isOnboarding') && typeof v.isOnboarding === 'boolean') {
this.isOnboarding = v.isOnboarding
}
if (
hasProp(v, 'stage') &&
typeof v.stage === 'string' &&
OnboardStageOrder.includes(v.stage)
) {
this.stage = v.stage
}
}
}
start() {
this.isOnboarding = true
}
stop() {
this.isOnboarding = false
}
next() {
if (!this.isOnboarding) return
let i = OnboardStageOrder.indexOf(this.stage)
i++
if (i >= OnboardStageOrder.length) {
this.isOnboarding = false
} else {
this.stage = OnboardStageOrder[i]
}
}
}

View file

@ -11,12 +11,14 @@ import {SessionModel} from './session'
import {NavigationModel} from './navigation'
import {ShellModel} from './shell'
import {MeModel} from './me'
import {OnboardModel} from './onboard'
export class RootStoreModel {
session = new SessionModel(this)
nav = new NavigationModel()
shell = new ShellModel()
me = new MeModel(this)
onboard = new OnboardModel()
constructor(public api: SessionServiceClient) {
makeAutoObservable(this, {
@ -53,6 +55,7 @@ export class RootStoreModel {
return {
session: this.session.serialize(),
nav: this.nav.serialize(),
onboard: this.onboard.serialize(),
}
}
@ -64,6 +67,9 @@ export class RootStoreModel {
if (hasProp(v, 'nav')) {
this.nav.hydrate(v.nav)
}
if (hasProp(v, 'onboard')) {
this.onboard.hydrate(v.onboard)
}
}
}

View file

@ -15,17 +15,8 @@ interface SessionData {
did: string
}
export enum OnboardingStage {
Init = 'init',
}
interface OnboardingState {
stage: OnboardingStage
}
export class SessionModel {
data: SessionData | null = null
onboardingState: OnboardingState | null = null
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {
@ -42,7 +33,6 @@ export class SessionModel {
serialize(): unknown {
return {
data: this.data,
onboardingState: this.onboardingState,
}
}
@ -87,18 +77,6 @@ export class SessionModel {
this.data = data
}
}
if (
this.data &&
hasProp(v, 'onboardingState') &&
isObj(v.onboardingState)
) {
if (
hasProp(v.onboardingState, 'stage') &&
typeof v.onboardingState === 'string'
) {
this.onboardingState = v.onboardingState
}
}
}
}
@ -212,7 +190,7 @@ export class SessionModel {
handle: res.data.handle,
did: res.data.did,
})
this.setOnboardingStage(OnboardingStage.Init)
this.rootStore.onboard.start()
this.configureApi()
this.rootStore.me.load().catch(e => {
console.error('Failed to fetch local user information', e)
@ -228,12 +206,4 @@ export class SessionModel {
}
this.rootStore.clearAll()
}
setOnboardingStage(stage: OnboardingStage | null) {
if (stage === null) {
this.onboardingState = null
} else {
this.onboardingState = {stage}
}
}
}

View file

@ -0,0 +1,121 @@
import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from './root-store'
interface Response {
data: {
suggestions: ResponseSuggestedActor[]
}
}
export type ResponseSuggestedActor = {
did: string
handle: string
displayName?: string
description?: string
createdAt?: string
indexedAt: string
}
export type SuggestedActor = ResponseSuggestedActor & {
_reactKey: string
}
export class SuggestedActorsViewModel {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
// data
suggestions: SuggestedActor[] = []
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(
this,
{
rootStore: false,
},
{autoBind: true},
)
}
get hasContent() {
return this.suggestions.length > 0
}
get hasError() {
return this.error !== ''
}
get isEmpty() {
return this.hasLoaded && !this.hasContent
}
// public api
// =
async setup() {
await this._fetch()
}
async refresh() {
await this._fetch(true)
}
async loadMore() {
// TODO
}
// state transitions
// =
private _xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err: string = '') {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = err
}
// loader functions
// =
private async _fetch(isRefreshing = false) {
this._xLoading(isRefreshing)
try {
const debugRes = await this.rootStore.api.app.bsky.graph.getFollowers({
user: 'alice.test',
})
const res = {
data: {
suggestions: debugRes.data.followers,
},
}
this._replaceAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle(e.toString())
}
}
private _replaceAll(res: Response) {
this.suggestions.length = 0
let counter = 0
for (const item of res.data.suggestions) {
this._append({
_reactKey: `item-${counter++}`,
description: 'Just another cool person using Bluesky',
...item,
})
}
}
private _append(item: SuggestedActor) {
this.suggestions.push(item)
}
}