Add onboarding (WIP)
This commit is contained in:
parent
b4097e25d6
commit
d228a5f4f5
15 changed files with 613 additions and 34 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
62
src/state/models/onboard.ts
Normal file
62
src/state/models/onboard.ts
Normal 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]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
121
src/state/models/suggested-actors-view.ts
Normal file
121
src/state/models/suggested-actors-view.ts
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue