diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/index.ts | 3 | ||||
-rw-r--r-- | src/state/lib/type-guards.ts | 4 | ||||
-rw-r--r-- | src/state/models/onboard.ts | 62 | ||||
-rw-r--r-- | src/state/models/root-store.ts | 6 | ||||
-rw-r--r-- | src/state/models/session.ts | 32 | ||||
-rw-r--r-- | src/state/models/suggested-actors-view.ts | 121 |
6 files changed, 196 insertions, 32 deletions
diff --git a/src/state/index.ts b/src/state/index.ts index 0716ab592..2c3df7d07 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -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) diff --git a/src/state/lib/type-guards.ts b/src/state/lib/type-guards.ts index 4ae31f3ac..8fe651ffb 100644 --- a/src/state/lib/type-guards.ts +++ b/src/state/lib/type-guards.ts @@ -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') +} diff --git a/src/state/models/onboard.ts b/src/state/models/onboard.ts new file mode 100644 index 000000000..77a066332 --- /dev/null +++ b/src/state/models/onboard.ts @@ -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] + } + } +} diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 717caa4a9..da846a3b0 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -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) + } } } diff --git a/src/state/models/session.ts b/src/state/models/session.ts index e29960954..e10a08e86 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -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} - } - } } diff --git a/src/state/models/suggested-actors-view.ts b/src/state/models/suggested-actors-view.ts new file mode 100644 index 000000000..245dbf020 --- /dev/null +++ b/src/state/models/suggested-actors-view.ts @@ -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) + } +} |