Replace mobx-state-tree with mobx and get a basic home feed rendering
This commit is contained in:
parent
6b32698b3e
commit
dc55f58004
20 changed files with 534 additions and 273 deletions
98
src/state/models/feed-view.ts
Normal file
98
src/state/models/feed-view.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {bsky} from '@adxp/mock-api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export class FeedViewItemModel implements bsky.FeedView.FeedItem {
|
||||
key: string = ''
|
||||
uri: string = ''
|
||||
author: bsky.FeedView.User = {did: '', name: '', displayName: ''}
|
||||
repostedBy?: bsky.FeedView.User
|
||||
record: Record<string, unknown> = {}
|
||||
embed?:
|
||||
| bsky.FeedView.RecordEmbed
|
||||
| bsky.FeedView.ExternalEmbed
|
||||
| bsky.FeedView.UnknownEmbed
|
||||
replyCount: number = 0
|
||||
repostCount: number = 0
|
||||
likeCount: number = 0
|
||||
indexedAt: string = ''
|
||||
|
||||
constructor(key: string, v: bsky.FeedView.FeedItem) {
|
||||
makeAutoObservable(this)
|
||||
this.key = key
|
||||
Object.assign(this, v)
|
||||
}
|
||||
}
|
||||
|
||||
export class FeedViewModel implements bsky.FeedView.Response {
|
||||
state = 'idle'
|
||||
error = ''
|
||||
params: bsky.FeedView.Params
|
||||
feed: FeedViewItemModel[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel, params: bsky.FeedView.Params) {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{rootStore: false, params: false},
|
||||
{autoBind: true},
|
||||
)
|
||||
this.params = params
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
return this.feed.length !== 0
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
return this.error !== ''
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return this.state === 'loading'
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return !this.hasContent && !this.hasError && !this.isLoading
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
if (this.hasContent) {
|
||||
await this.updateContent()
|
||||
} else {
|
||||
await this.initialLoad()
|
||||
}
|
||||
}
|
||||
|
||||
async initialLoad() {
|
||||
this.state = 'loading'
|
||||
this.error = ''
|
||||
try {
|
||||
const res = (await this.rootStore.api.mainPds.view(
|
||||
'blueskyweb.xyz:FeedView',
|
||||
this.params,
|
||||
)) as bsky.FeedView.Response
|
||||
this._replaceAll(res)
|
||||
runInAction(() => {
|
||||
this.state = 'idle'
|
||||
})
|
||||
} catch (e: any) {
|
||||
runInAction(() => {
|
||||
this.state = 'error'
|
||||
this.error = `Failed to load feed: ${e.toString()}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async updateContent() {
|
||||
// TODO: refetch and update items
|
||||
}
|
||||
|
||||
private _replaceAll(res: bsky.FeedView.Response) {
|
||||
this.feed.length = 0
|
||||
let counter = 0
|
||||
for (const item of res.feed) {
|
||||
// TODO: validate .record
|
||||
this.feed.push(new FeedViewItemModel(`item-${counter++}`, item))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +1,41 @@
|
|||
import {Instance, SnapshotOut, types, flow, getRoot} from 'mobx-state-tree'
|
||||
import {RootStore} from './root-store'
|
||||
import {withEnvironment} from '../env'
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export const MeModel = types
|
||||
.model('Me')
|
||||
.props({
|
||||
did: types.maybe(types.string),
|
||||
name: types.maybe(types.string),
|
||||
displayName: types.maybe(types.string),
|
||||
description: types.maybe(types.string),
|
||||
})
|
||||
.extend(withEnvironment)
|
||||
.actions(self => ({
|
||||
load: flow(function* () {
|
||||
const sess = (getRoot(self) as RootStore).session
|
||||
if (sess.isAuthed) {
|
||||
// TODO temporary
|
||||
const userDb = self.env.adx.mockDb.mainUser
|
||||
self.did = userDb.did
|
||||
self.name = userDb.name
|
||||
const profile = yield self.env.adx
|
||||
.repo(self.did, true)
|
||||
.collection('blueskyweb.xyz:Profiles')
|
||||
.get('Profile', 'profile')
|
||||
.catch(_ => undefined)
|
||||
export class MeModel {
|
||||
did?: string
|
||||
name?: string
|
||||
displayName?: string
|
||||
description?: string
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this, {rootStore: false}, {autoBind: true})
|
||||
}
|
||||
|
||||
async load() {
|
||||
const sess = this.rootStore.session
|
||||
if (sess.isAuthed) {
|
||||
const userDb = this.rootStore.api.mockDb.mainUser
|
||||
this.did = userDb.did
|
||||
this.name = userDb.name
|
||||
const profile = await this.rootStore.api
|
||||
.repo(this.did, true)
|
||||
.collection('blueskyweb.xyz:Profiles')
|
||||
.get('Profile', 'profile')
|
||||
.catch(_ => undefined)
|
||||
runInAction(() => {
|
||||
if (profile?.valid) {
|
||||
self.displayName = profile.value.displayName
|
||||
self.description = profile.value.description
|
||||
this.displayName = profile.value.displayName
|
||||
this.description = profile.value.description
|
||||
} else {
|
||||
self.displayName = ''
|
||||
self.description = ''
|
||||
this.displayName = ''
|
||||
this.description = ''
|
||||
}
|
||||
} else {
|
||||
self.did = undefined
|
||||
self.name = undefined
|
||||
self.displayName = undefined
|
||||
self.description = undefined
|
||||
}
|
||||
}),
|
||||
}))
|
||||
|
||||
export interface Me extends Instance<typeof MeModel> {}
|
||||
export interface MeSnapshot extends SnapshotOut<typeof MeModel> {}
|
||||
|
||||
export function createDefaultMe() {
|
||||
return {}
|
||||
})
|
||||
} else {
|
||||
this.did = undefined
|
||||
this.name = undefined
|
||||
this.displayName = undefined
|
||||
this.description = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,27 +2,43 @@
|
|||
* The root store is the base of all modeled state.
|
||||
*/
|
||||
|
||||
import {Instance, SnapshotOut, types} from 'mobx-state-tree'
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {adx, AdxClient} from '@adxp/mock-api'
|
||||
import {createContext, useContext} from 'react'
|
||||
import {SessionModel, createDefaultSession} from './session'
|
||||
import {MeModel, createDefaultMe} from './me'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {SessionModel} from './session'
|
||||
import {MeModel} from './me'
|
||||
import {FeedViewModel} from './feed-view'
|
||||
|
||||
export const RootStoreModel = types.model('RootStore').props({
|
||||
session: SessionModel,
|
||||
me: MeModel,
|
||||
})
|
||||
export class RootStoreModel {
|
||||
session = new SessionModel()
|
||||
me = new MeModel(this)
|
||||
homeFeed = new FeedViewModel(this, {})
|
||||
|
||||
export interface RootStore extends Instance<typeof RootStoreModel> {}
|
||||
export interface RootStoreSnapshot extends SnapshotOut<typeof RootStoreModel> {}
|
||||
constructor(public api: AdxClient) {
|
||||
makeAutoObservable(this, {
|
||||
api: false,
|
||||
serialize: false,
|
||||
hydrate: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function createDefaultRootStore() {
|
||||
return {
|
||||
session: createDefaultSession(),
|
||||
me: createDefaultMe(),
|
||||
serialize(): unknown {
|
||||
return {
|
||||
session: this.session.serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
if (isObj(v)) {
|
||||
if (hasProp(v, 'session')) {
|
||||
this.session.hydrate(v.session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// react context & hook utilities
|
||||
const RootStoreContext = createContext<RootStore>({} as RootStore)
|
||||
const throwawayInst = new RootStoreModel(adx) // this will be replaced by the loader
|
||||
const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
|
||||
export const RootStoreProvider = RootStoreContext.Provider
|
||||
export const useStores = () => useContext(RootStoreContext)
|
||||
|
|
|
@ -1,106 +1,109 @@
|
|||
import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree'
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
// import {UserConfig} from '../../api'
|
||||
// import * as auth from '../lib/auth'
|
||||
import {withEnvironment} from '../env'
|
||||
|
||||
export const SessionModel = types
|
||||
.model('Session')
|
||||
.props({
|
||||
isAuthed: types.boolean,
|
||||
uiIsProcessing: types.maybe(types.boolean),
|
||||
uiError: types.maybe(types.string),
|
||||
export class SessionModel {
|
||||
isAuthed = false
|
||||
|
||||
// TODO: these should be stored somewhere secret
|
||||
serverUrl: types.maybe(types.string),
|
||||
secretKeyStr: types.maybe(types.string),
|
||||
rootAuthToken: types.maybe(types.string),
|
||||
})
|
||||
.extend(withEnvironment)
|
||||
.actions(self => ({
|
||||
setAuthed: (v: boolean) => {
|
||||
self.isAuthed = v
|
||||
},
|
||||
login: flow(function* () {
|
||||
/*self.uiIsProcessing = true
|
||||
self.uiError = undefined
|
||||
try {
|
||||
if (!self.env.authStore) {
|
||||
throw new Error('Auth store not initialized')
|
||||
}
|
||||
const res = yield auth.requestAppUcan(self.env.authStore)
|
||||
self.isAuthed = res
|
||||
self.uiIsProcessing = false
|
||||
return res
|
||||
} catch (e: any) {
|
||||
console.error('Failed to request app ucan', e)
|
||||
self.uiError = e.toString()
|
||||
self.uiIsProcessing = false
|
||||
return false
|
||||
}*/
|
||||
}),
|
||||
logout: flow(function* () {
|
||||
/*self.uiIsProcessing = true
|
||||
self.uiError = undefined
|
||||
try {
|
||||
if (!self.env.authStore) {
|
||||
throw new Error('Auth store not initialized')
|
||||
}
|
||||
const res = yield auth.logout(self.env.authStore)
|
||||
self.isAuthed = false
|
||||
self.uiIsProcessing = false
|
||||
return res
|
||||
} catch (e: any) {
|
||||
console.error('Failed to log out', e)
|
||||
self.uiError = e.toString()
|
||||
self.uiIsProcessing = false
|
||||
return false
|
||||
}*/
|
||||
}),
|
||||
/*loadAccount: flow(function* () {
|
||||
self.uiIsProcessing = true
|
||||
self.uiError = undefined
|
||||
try {
|
||||
// const cfg = yield UserConfig.hydrate({
|
||||
// serverUrl: self.serverUrl,
|
||||
// secretKeyStr: self.secretKeyStr,
|
||||
// rootAuthToken: self.rootAuthToken,
|
||||
// })
|
||||
// self.env.api.setUserCfg(cfg)
|
||||
self.isAuthed = true
|
||||
self.uiIsProcessing = false
|
||||
return true
|
||||
} catch (e: any) {
|
||||
console.error('Failed to create test account', e)
|
||||
self.uiError = e.toString()
|
||||
self.uiIsProcessing = false
|
||||
return false
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
serialize: false,
|
||||
hydrate: false,
|
||||
})
|
||||
}
|
||||
|
||||
serialize(): unknown {
|
||||
return {
|
||||
isAuthed: this.isAuthed,
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
if (isObj(v)) {
|
||||
if (hasProp(v, 'isAuthed') && typeof v.isAuthed === 'boolean') {
|
||||
this.isAuthed = v.isAuthed
|
||||
}
|
||||
}),
|
||||
createTestAccount: flow(function* (_serverUrl: string) {
|
||||
self.uiIsProcessing = true
|
||||
self.uiError = undefined
|
||||
try {
|
||||
// const cfg = yield UserConfig.createTest(serverUrl)
|
||||
// const state = yield cfg.serialize()
|
||||
// self.serverUrl = state.serverUrl
|
||||
// self.secretKeyStr = state.secretKeyStr
|
||||
// self.rootAuthToken = state.rootAuthToken
|
||||
self.isAuthed = true
|
||||
// self.env.api.setUserCfg(cfg)
|
||||
} catch (e: any) {
|
||||
console.error('Failed to create test account', e)
|
||||
self.uiError = e.toString()
|
||||
}
|
||||
self.uiIsProcessing = false
|
||||
}),*/
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export interface Session extends Instance<typeof SessionModel> {}
|
||||
export interface SessionSnapshot extends SnapshotOut<typeof SessionModel> {}
|
||||
|
||||
export function createDefaultSession() {
|
||||
return {
|
||||
isAuthed: false,
|
||||
uiState: 'idle',
|
||||
setAuthed(v: boolean) {
|
||||
this.isAuthed = v
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
/*login: flow(function* () {
|
||||
/*self.uiIsProcessing = true
|
||||
self.uiError = undefined
|
||||
try {
|
||||
if (!self.env.authStore) {
|
||||
throw new Error('Auth store not initialized')
|
||||
}
|
||||
const res = yield auth.requestAppUcan(self.env.authStore)
|
||||
self.isAuthed = res
|
||||
self.uiIsProcessing = false
|
||||
return res
|
||||
} catch (e: any) {
|
||||
console.error('Failed to request app ucan', e)
|
||||
self.uiError = e.toString()
|
||||
self.uiIsProcessing = false
|
||||
return false
|
||||
}
|
||||
}),
|
||||
logout: flow(function* () {
|
||||
self.uiIsProcessing = true
|
||||
self.uiError = undefined
|
||||
try {
|
||||
if (!self.env.authStore) {
|
||||
throw new Error('Auth store not initialized')
|
||||
}
|
||||
const res = yield auth.logout(self.env.authStore)
|
||||
self.isAuthed = false
|
||||
self.uiIsProcessing = false
|
||||
return res
|
||||
} catch (e: any) {
|
||||
console.error('Failed to log out', e)
|
||||
self.uiError = e.toString()
|
||||
self.uiIsProcessing = false
|
||||
return false
|
||||
}
|
||||
}),
|
||||
loadAccount: flow(function* () {
|
||||
self.uiIsProcessing = true
|
||||
self.uiError = undefined
|
||||
try {
|
||||
// const cfg = yield UserConfig.hydrate({
|
||||
// serverUrl: self.serverUrl,
|
||||
// secretKeyStr: self.secretKeyStr,
|
||||
// rootAuthToken: self.rootAuthToken,
|
||||
// })
|
||||
// self.env.api.setUserCfg(cfg)
|
||||
self.isAuthed = true
|
||||
self.uiIsProcessing = false
|
||||
return true
|
||||
} catch (e: any) {
|
||||
console.error('Failed to create test account', e)
|
||||
self.uiError = e.toString()
|
||||
self.uiIsProcessing = false
|
||||
return false
|
||||
}
|
||||
}),
|
||||
createTestAccount: flow(function* (_serverUrl: string) {
|
||||
self.uiIsProcessing = true
|
||||
self.uiError = undefined
|
||||
try {
|
||||
// const cfg = yield UserConfig.createTest(serverUrl)
|
||||
// const state = yield cfg.serialize()
|
||||
// self.serverUrl = state.serverUrl
|
||||
// self.secretKeyStr = state.secretKeyStr
|
||||
// self.rootAuthToken = state.rootAuthToken
|
||||
self.isAuthed = true
|
||||
// self.env.api.setUserCfg(cfg)
|
||||
} catch (e: any) {
|
||||
console.error('Failed to create test account', e)
|
||||
self.uiError = e.toString()
|
||||
}
|
||||
self.uiIsProcessing = false
|
||||
}),
|
||||
}))*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue