Replace mobx-state-tree with mobx and get a basic home feed rendering
parent
6b32698b3e
commit
dc55f58004
|
@ -7,7 +7,7 @@ Uses:
|
||||||
- [React Native](https://reactnative.dev)
|
- [React Native](https://reactnative.dev)
|
||||||
- [React Native for Web](https://necolas.github.io/react-native-web/)
|
- [React Native for Web](https://necolas.github.io/react-native-web/)
|
||||||
- [React Navigation](https://reactnative.dev/docs/navigation#react-navigation)
|
- [React Navigation](https://reactnative.dev/docs/navigation#react-navigation)
|
||||||
- [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/)
|
- [MobX](https://mobx.js.org/README.html)
|
||||||
- [Async Storage](https://github.com/react-native-async-storage/async-storage)
|
- [Async Storage](https://github.com/react-native-async-storage/async-storage)
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
@ -54,7 +54,7 @@ The `metro.config.js` file rewrites a couple of imports. This is partly to work
|
||||||
|
|
||||||
### Cryptography
|
### Cryptography
|
||||||
|
|
||||||
For native builds, we must provide a polyfill of `webcrypto`. We use a custom native module AppSecureRandom (based on [react-native-securerandom](https://github.com/robhogan/react-native-securerandom)) for the CRNG and [msrcrypto](https://github.com/kevlened/msrCrypto) for the cryptography.
|
For native builds, we must provide a polyfill of `webcrypto`. We use a custom native module AppSecureRandom (based on [react-native-securerandom](https://github.com/robhogan/react-native-securerandom)) for the CRNG and [msrcrypto](https://github.com/microsoft/MSR-JavaScript-Crypto) for the cryptography.
|
||||||
|
|
||||||
**NOTE** Keys are not currently stored securely.
|
**NOTE** Keys are not currently stored securely.
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adxp/auth": "*",
|
"@adxp/auth": "*",
|
||||||
"@adxp/common": "*",
|
"@adxp/common": "*",
|
||||||
"@adxp/mock-api": "git+ssh://git@github.com:bluesky-social/adx-mock-api.git#0bccd04217c78a7c9786a45684ac2ffb9767429b",
|
"@adxp/mock-api": "git+ssh://git@github.com:bluesky-social/adx-mock-api.git#74a1f810a342aa4b58a54724e21c57d2faa5e72e",
|
||||||
"@react-native-async-storage/async-storage": "^1.17.6",
|
"@react-native-async-storage/async-storage": "^1.17.6",
|
||||||
"@react-navigation/bottom-tabs": "^6.3.1",
|
"@react-navigation/bottom-tabs": "^6.3.1",
|
||||||
"@react-navigation/native": "^6.0.10",
|
"@react-navigation/native": "^6.0.10",
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
"@react-navigation/stack": "^6.2.1",
|
"@react-navigation/stack": "^6.2.1",
|
||||||
"@zxing/text-encoding": "^0.9.0",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"mobx": "^6.6.0",
|
"mobx": "^6.6.1",
|
||||||
"mobx-react-lite": "^3.4.0",
|
"mobx-react-lite": "^3.4.0",
|
||||||
"mobx-state-tree": "^5.1.5",
|
"moment": "^2.29.4",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-native": "0.68.2",
|
"react-native": "0.68.2",
|
||||||
|
|
|
@ -1,11 +1,35 @@
|
||||||
import 'react-native-url-polyfill/auto'
|
import 'react-native-url-polyfill/auto'
|
||||||
import React, {useState, useEffect} from 'react'
|
import React, {useState, useEffect} from 'react'
|
||||||
|
import moment from 'moment'
|
||||||
import {whenWebCrypto} from './platform/polyfills.native'
|
import {whenWebCrypto} from './platform/polyfills.native'
|
||||||
import {RootStore, setupState, RootStoreProvider} from './state'
|
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||||
import * as Routes from './view/routes'
|
import * as Routes from './view/routes'
|
||||||
|
|
||||||
|
moment.updateLocale('en', {
|
||||||
|
relativeTime: {
|
||||||
|
future: 'in %s',
|
||||||
|
past: '%s ago',
|
||||||
|
s: 'a few seconds',
|
||||||
|
ss: '%ds',
|
||||||
|
m: 'a minute',
|
||||||
|
mm: '%dm',
|
||||||
|
h: 'an hour',
|
||||||
|
hh: '%dh',
|
||||||
|
d: 'a day',
|
||||||
|
dd: '%dd',
|
||||||
|
w: 'a week',
|
||||||
|
ww: '%dw',
|
||||||
|
M: 'a month',
|
||||||
|
MM: '%dmo',
|
||||||
|
y: 'a year',
|
||||||
|
yy: '%dy',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
|
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,9 +1,33 @@
|
||||||
import React, {useState, useEffect} from 'react'
|
import React, {useState, useEffect} from 'react'
|
||||||
import {RootStore, setupState, RootStoreProvider} from './state'
|
import moment from 'moment'
|
||||||
|
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||||
import * as Routes from './view/routes'
|
import * as Routes from './view/routes'
|
||||||
|
|
||||||
|
moment.updateLocale('en', {
|
||||||
|
relativeTime: {
|
||||||
|
future: 'in %s',
|
||||||
|
past: '%s ago',
|
||||||
|
s: 'a few seconds',
|
||||||
|
ss: '%ds',
|
||||||
|
m: 'a minute',
|
||||||
|
mm: '%dm',
|
||||||
|
h: 'an hour',
|
||||||
|
hh: '%dh',
|
||||||
|
d: 'a day',
|
||||||
|
dd: '%dd',
|
||||||
|
w: 'a week',
|
||||||
|
ww: '%dw',
|
||||||
|
M: 'a month',
|
||||||
|
MM: '%dmo',
|
||||||
|
y: 'a year',
|
||||||
|
yy: '%dy',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
|
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -1,33 +1,35 @@
|
||||||
import {onSnapshot} from 'mobx-state-tree'
|
import {autorun} from 'mobx'
|
||||||
import {
|
import {AdxClient, blueskywebSchemas} from '@adxp/mock-api'
|
||||||
RootStoreModel,
|
import {RootStoreModel} from './models/root-store'
|
||||||
RootStore,
|
import * as libapi from './lib/api'
|
||||||
createDefaultRootStore,
|
|
||||||
} from './models/root-store'
|
|
||||||
import {Environment} from './env'
|
|
||||||
import * as storage from './lib/storage'
|
import * as storage from './lib/storage'
|
||||||
// import * as auth from './auth' TODO
|
// import * as auth from './auth' TODO
|
||||||
|
|
||||||
const ROOT_STATE_STORAGE_KEY = 'root'
|
const ROOT_STATE_STORAGE_KEY = 'root'
|
||||||
|
|
||||||
export async function setupState() {
|
export async function setupState() {
|
||||||
let rootStore: RootStore
|
let rootStore: RootStoreModel
|
||||||
let data: any
|
let data: any
|
||||||
|
|
||||||
const env = new Environment()
|
const api = new AdxClient({
|
||||||
await env.setup()
|
pds: 'http://localhost',
|
||||||
|
schemas: blueskywebSchemas,
|
||||||
|
})
|
||||||
|
await libapi.setup(api)
|
||||||
|
rootStore = new RootStoreModel(api)
|
||||||
try {
|
try {
|
||||||
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
||||||
rootStore = RootStoreModel.create(data, env)
|
rootStore.hydrate(data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load state from storage', e)
|
console.error('Failed to load state from storage', e)
|
||||||
rootStore = RootStoreModel.create(createDefaultRootStore(), env)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// track changes & save to storage
|
// track changes & save to storage
|
||||||
onSnapshot(rootStore, snapshot =>
|
autorun(() => {
|
||||||
storage.save(ROOT_STATE_STORAGE_KEY, snapshot),
|
const snapshot = rootStore.serialize()
|
||||||
)
|
console.log('saving', snapshot)
|
||||||
|
storage.save(ROOT_STATE_STORAGE_KEY, snapshot)
|
||||||
|
})
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
rootStore.session.setAuthed(true)
|
rootStore.session.setAuthed(true)
|
||||||
|
@ -47,4 +49,3 @@ export async function setupState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export {useStores, RootStoreModel, RootStoreProvider} from './models/root-store'
|
export {useStores, RootStoreModel, RootStoreProvider} from './models/root-store'
|
||||||
export type {RootStore} from './models/root-store'
|
|
||||||
|
|
|
@ -3,49 +3,20 @@
|
||||||
* models live. They are made available to every model via dependency injection.
|
* models live. They are made available to every model via dependency injection.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {getEnv, IStateTreeNode} from 'mobx-state-tree'
|
|
||||||
// import {ReactNativeStore} from './auth'
|
// import {ReactNativeStore} from './auth'
|
||||||
import {AdxClient, blueskywebSchemas, AdxRepoClient} from '@adxp/mock-api'
|
import {AdxClient, AdxRepoClient} from '@adxp/mock-api'
|
||||||
import * as storage from './lib/storage'
|
import * as storage from './storage'
|
||||||
|
|
||||||
export const adx = new AdxClient({
|
export async function setup(adx: AdxClient) {
|
||||||
pds: 'http://localhost',
|
|
||||||
schemas: blueskywebSchemas,
|
|
||||||
})
|
|
||||||
|
|
||||||
export class Environment {
|
|
||||||
adx = adx
|
|
||||||
// authStore?: ReactNativeStore
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
async setup() {
|
|
||||||
await adx.setupMock(
|
await adx.setupMock(
|
||||||
() => storage.load('mock-root'),
|
() => storage.load('mock-root'),
|
||||||
async root => {
|
async root => {
|
||||||
await storage.save('mock-root', root)
|
await storage.save('mock-root', root)
|
||||||
},
|
},
|
||||||
generateMockData,
|
() => generateMockData(adx),
|
||||||
)
|
)
|
||||||
// this.authStore = await ReactNativeStore.load()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension to the MST models that adds the env property.
|
|
||||||
* Usage:
|
|
||||||
*
|
|
||||||
* .extend(withEnvironment)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const withEnvironment = (self: IStateTreeNode) => ({
|
|
||||||
views: {
|
|
||||||
get env() {
|
|
||||||
return getEnv<Environment>(self)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// TEMPORARY
|
// TEMPORARY
|
||||||
// mock api config
|
// mock api config
|
||||||
// =======
|
// =======
|
||||||
|
@ -59,20 +30,20 @@ function* dateGen() {
|
||||||
}
|
}
|
||||||
const date = dateGen()
|
const date = dateGen()
|
||||||
|
|
||||||
function repo(didOrName: string) {
|
function repo(adx: AdxClient, didOrName: string) {
|
||||||
const userDb = adx.mockDb.getUser(didOrName)
|
const userDb = adx.mockDb.getUser(didOrName)
|
||||||
if (!userDb) throw new Error(`User not found: ${didOrName}`)
|
if (!userDb) throw new Error(`User not found: ${didOrName}`)
|
||||||
return adx.mainPds.repo(userDb.did, userDb.writable)
|
return adx.mainPds.repo(userDb.did, userDb.writable)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMockData() {
|
export async function generateMockData(adx: AdxClient) {
|
||||||
await adx.mockDb.addUser({name: 'alice.com', writable: true})
|
await adx.mockDb.addUser({name: 'alice.com', writable: true})
|
||||||
await adx.mockDb.addUser({name: 'bob.com', writable: true})
|
await adx.mockDb.addUser({name: 'bob.com', writable: true})
|
||||||
await adx.mockDb.addUser({name: 'carla.com', writable: true})
|
await adx.mockDb.addUser({name: 'carla.com', writable: true})
|
||||||
|
|
||||||
const alice = repo('alice.com')
|
const alice = repo(adx, 'alice.com')
|
||||||
const bob = repo('bob.com')
|
const bob = repo(adx, 'bob.com')
|
||||||
const carla = repo('carla.com')
|
const carla = repo(adx, 'carla.com')
|
||||||
|
|
||||||
await alice.collection('blueskyweb.xyz:Profiles').put('Profile', 'profile', {
|
await alice.collection('blueskyweb.xyz:Profiles').put('Profile', 'profile', {
|
||||||
$type: 'blueskyweb.xyz:Profile',
|
$type: 'blueskyweb.xyz:Profile',
|
|
@ -0,0 +1,10 @@
|
||||||
|
export function isObj(v: unknown): v is Record<string, unknown> {
|
||||||
|
return !!v && typeof v === 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasProp<K extends PropertyKey>(
|
||||||
|
data: object,
|
||||||
|
prop: K,
|
||||||
|
): data is Record<K, unknown> {
|
||||||
|
return prop in data
|
||||||
|
}
|
|
@ -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 {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {RootStore} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {withEnvironment} from '../env'
|
|
||||||
|
|
||||||
export const MeModel = types
|
export class MeModel {
|
||||||
.model('Me')
|
did?: string
|
||||||
.props({
|
name?: string
|
||||||
did: types.maybe(types.string),
|
displayName?: string
|
||||||
name: types.maybe(types.string),
|
description?: string
|
||||||
displayName: types.maybe(types.string),
|
|
||||||
description: types.maybe(types.string),
|
constructor(public rootStore: RootStoreModel) {
|
||||||
})
|
makeAutoObservable(this, {rootStore: false}, {autoBind: true})
|
||||||
.extend(withEnvironment)
|
}
|
||||||
.actions(self => ({
|
|
||||||
load: flow(function* () {
|
async load() {
|
||||||
const sess = (getRoot(self) as RootStore).session
|
const sess = this.rootStore.session
|
||||||
if (sess.isAuthed) {
|
if (sess.isAuthed) {
|
||||||
// TODO temporary
|
const userDb = this.rootStore.api.mockDb.mainUser
|
||||||
const userDb = self.env.adx.mockDb.mainUser
|
this.did = userDb.did
|
||||||
self.did = userDb.did
|
this.name = userDb.name
|
||||||
self.name = userDb.name
|
const profile = await this.rootStore.api
|
||||||
const profile = yield self.env.adx
|
.repo(this.did, true)
|
||||||
.repo(self.did, true)
|
|
||||||
.collection('blueskyweb.xyz:Profiles')
|
.collection('blueskyweb.xyz:Profiles')
|
||||||
.get('Profile', 'profile')
|
.get('Profile', 'profile')
|
||||||
.catch(_ => undefined)
|
.catch(_ => undefined)
|
||||||
|
runInAction(() => {
|
||||||
if (profile?.valid) {
|
if (profile?.valid) {
|
||||||
self.displayName = profile.value.displayName
|
this.displayName = profile.value.displayName
|
||||||
self.description = profile.value.description
|
this.description = profile.value.description
|
||||||
} else {
|
} else {
|
||||||
self.displayName = ''
|
this.displayName = ''
|
||||||
self.description = ''
|
this.description = ''
|
||||||
}
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
self.did = undefined
|
this.did = undefined
|
||||||
self.name = undefined
|
this.name = undefined
|
||||||
self.displayName = undefined
|
this.displayName = undefined
|
||||||
self.description = undefined
|
this.description = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
export interface Me extends Instance<typeof MeModel> {}
|
|
||||||
export interface MeSnapshot extends SnapshotOut<typeof MeModel> {}
|
|
||||||
|
|
||||||
export function createDefaultMe() {
|
|
||||||
return {}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,27 +2,43 @@
|
||||||
* The root store is the base of all modeled state.
|
* 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 {createContext, useContext} from 'react'
|
||||||
import {SessionModel, createDefaultSession} from './session'
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
import {MeModel, createDefaultMe} from './me'
|
import {SessionModel} from './session'
|
||||||
|
import {MeModel} from './me'
|
||||||
|
import {FeedViewModel} from './feed-view'
|
||||||
|
|
||||||
export const RootStoreModel = types.model('RootStore').props({
|
export class RootStoreModel {
|
||||||
session: SessionModel,
|
session = new SessionModel()
|
||||||
me: MeModel,
|
me = new MeModel(this)
|
||||||
})
|
homeFeed = new FeedViewModel(this, {})
|
||||||
|
|
||||||
export interface RootStore extends Instance<typeof RootStoreModel> {}
|
constructor(public api: AdxClient) {
|
||||||
export interface RootStoreSnapshot extends SnapshotOut<typeof RootStoreModel> {}
|
makeAutoObservable(this, {
|
||||||
|
api: false,
|
||||||
|
serialize: false,
|
||||||
|
hydrate: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function createDefaultRootStore() {
|
serialize(): unknown {
|
||||||
return {
|
return {
|
||||||
session: createDefaultSession(),
|
session: this.session.serialize(),
|
||||||
me: createDefaultMe(),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrate(v: unknown) {
|
||||||
|
if (isObj(v)) {
|
||||||
|
if (hasProp(v, 'session')) {
|
||||||
|
this.session.hydrate(v.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// react context & hook utilities
|
const throwawayInst = new RootStoreModel(adx) // this will be replaced by the loader
|
||||||
const RootStoreContext = createContext<RootStore>({} as RootStore)
|
const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
|
||||||
export const RootStoreProvider = RootStoreContext.Provider
|
export const RootStoreProvider = RootStoreContext.Provider
|
||||||
export const useStores = () => useContext(RootStoreContext)
|
export const useStores = () => useContext(RootStoreContext)
|
||||||
|
|
|
@ -1,26 +1,39 @@
|
||||||
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 {UserConfig} from '../../api'
|
||||||
// import * as auth from '../lib/auth'
|
// import * as auth from '../lib/auth'
|
||||||
import {withEnvironment} from '../env'
|
|
||||||
|
|
||||||
export const SessionModel = types
|
export class SessionModel {
|
||||||
.model('Session')
|
isAuthed = false
|
||||||
.props({
|
|
||||||
isAuthed: types.boolean,
|
|
||||||
uiIsProcessing: types.maybe(types.boolean),
|
|
||||||
uiError: types.maybe(types.string),
|
|
||||||
|
|
||||||
// TODO: these should be stored somewhere secret
|
constructor() {
|
||||||
serverUrl: types.maybe(types.string),
|
makeAutoObservable(this, {
|
||||||
secretKeyStr: types.maybe(types.string),
|
serialize: false,
|
||||||
rootAuthToken: types.maybe(types.string),
|
hydrate: false,
|
||||||
})
|
})
|
||||||
.extend(withEnvironment)
|
}
|
||||||
.actions(self => ({
|
|
||||||
setAuthed: (v: boolean) => {
|
serialize(): unknown {
|
||||||
self.isAuthed = v
|
return {
|
||||||
},
|
isAuthed: this.isAuthed,
|
||||||
login: flow(function* () {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrate(v: unknown) {
|
||||||
|
if (isObj(v)) {
|
||||||
|
if (hasProp(v, 'isAuthed') && typeof v.isAuthed === 'boolean') {
|
||||||
|
this.isAuthed = v.isAuthed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthed(v: boolean) {
|
||||||
|
this.isAuthed = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
/*login: flow(function* () {
|
||||||
/*self.uiIsProcessing = true
|
/*self.uiIsProcessing = true
|
||||||
self.uiError = undefined
|
self.uiError = undefined
|
||||||
try {
|
try {
|
||||||
|
@ -36,10 +49,10 @@ export const SessionModel = types
|
||||||
self.uiError = e.toString()
|
self.uiError = e.toString()
|
||||||
self.uiIsProcessing = false
|
self.uiIsProcessing = false
|
||||||
return false
|
return false
|
||||||
}*/
|
}
|
||||||
}),
|
}),
|
||||||
logout: flow(function* () {
|
logout: flow(function* () {
|
||||||
/*self.uiIsProcessing = true
|
self.uiIsProcessing = true
|
||||||
self.uiError = undefined
|
self.uiError = undefined
|
||||||
try {
|
try {
|
||||||
if (!self.env.authStore) {
|
if (!self.env.authStore) {
|
||||||
|
@ -54,9 +67,9 @@ export const SessionModel = types
|
||||||
self.uiError = e.toString()
|
self.uiError = e.toString()
|
||||||
self.uiIsProcessing = false
|
self.uiIsProcessing = false
|
||||||
return false
|
return false
|
||||||
}*/
|
}
|
||||||
}),
|
}),
|
||||||
/*loadAccount: flow(function* () {
|
loadAccount: flow(function* () {
|
||||||
self.uiIsProcessing = true
|
self.uiIsProcessing = true
|
||||||
self.uiError = undefined
|
self.uiError = undefined
|
||||||
try {
|
try {
|
||||||
|
@ -75,8 +88,8 @@ export const SessionModel = types
|
||||||
self.uiIsProcessing = false
|
self.uiIsProcessing = false
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createTestAccount: flow(function* (_serverUrl: string) {
|
createTestAccount: flow(function* (_serverUrl: string) {
|
||||||
self.uiIsProcessing = true
|
self.uiIsProcessing = true
|
||||||
self.uiError = undefined
|
self.uiError = undefined
|
||||||
try {
|
try {
|
||||||
|
@ -92,15 +105,5 @@ export const SessionModel = types
|
||||||
self.uiError = e.toString()
|
self.uiError = e.toString()
|
||||||
}
|
}
|
||||||
self.uiIsProcessing = false
|
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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {Text, View} from 'react-native'
|
||||||
|
import {FeedViewModel} from '../../state/models/feed-view'
|
||||||
|
import {FeedItem} from './FeedItem'
|
||||||
|
|
||||||
|
export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{feed.isLoading && <Text>Loading...</Text>}
|
||||||
|
{feed.hasError && <Text>{feed.error}</Text>}
|
||||||
|
{feed.hasContent &&
|
||||||
|
feed.feed.map(item => <FeedItem key={item.key} item={item} />)}
|
||||||
|
{feed.isEmpty && <Text>This feed is empty!</Text>}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
|
@ -0,0 +1,104 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {Text, Image, ImageSourcePropType, StyleSheet, View} from 'react-native'
|
||||||
|
import {bsky} from '@adxp/mock-api'
|
||||||
|
import moment from 'moment'
|
||||||
|
import {FeedViewItemModel} from '../../state/models/feed-view'
|
||||||
|
|
||||||
|
const IMAGES: Record<string, ImageSourcePropType> = {
|
||||||
|
'alice.com': require('../../assets/alice.jpg'),
|
||||||
|
'bob.com': require('../../assets/bob.jpg'),
|
||||||
|
'carla.com': require('../../assets/carla.jpg'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeedItem = observer(function FeedItem({
|
||||||
|
item,
|
||||||
|
}: {
|
||||||
|
item: FeedViewItemModel
|
||||||
|
}) {
|
||||||
|
const record = item.record as unknown as bsky.Post.Record
|
||||||
|
return (
|
||||||
|
<View style={styles.outer}>
|
||||||
|
{item.repostedBy && (
|
||||||
|
<Text style={styles.repostedBy}>
|
||||||
|
Reposted by {item.repostedBy.displayName}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<View style={styles.layout}>
|
||||||
|
<View style={styles.layoutAvi}>
|
||||||
|
<Image
|
||||||
|
style={styles.avi}
|
||||||
|
source={IMAGES[item.author.name] || IMAGES['alice.com']}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.layoutContent}>
|
||||||
|
<View style={styles.meta}>
|
||||||
|
<Text style={[styles.metaItem, styles.metaDisplayName]}>
|
||||||
|
{item.author.displayName}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.metaItem, styles.metaName]}>
|
||||||
|
@{item.author.name}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.metaItem, styles.metaDate]}>
|
||||||
|
· {moment(item.indexedAt).fromNow(true)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.postText}>{record.text}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
outer: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#e8e8e8',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
repostedBy: {
|
||||||
|
paddingLeft: 70,
|
||||||
|
color: 'gray',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
layoutAvi: {
|
||||||
|
width: 70,
|
||||||
|
},
|
||||||
|
avi: {
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
borderRadius: 30,
|
||||||
|
resizeMode: 'cover',
|
||||||
|
},
|
||||||
|
layoutContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingTop: 2,
|
||||||
|
paddingBottom: 4,
|
||||||
|
},
|
||||||
|
metaItem: {
|
||||||
|
paddingRight: 5,
|
||||||
|
},
|
||||||
|
metaDisplayName: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
metaName: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: 'gray',
|
||||||
|
},
|
||||||
|
metaDate: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: 'gray',
|
||||||
|
},
|
||||||
|
postText: {
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,20 +1,23 @@
|
||||||
import React from 'react'
|
import React, {useEffect} from 'react'
|
||||||
import {Text, Button, View} from 'react-native'
|
import {Text, View} from 'react-native'
|
||||||
import {Shell} from '../shell'
|
import {Shell} from '../shell'
|
||||||
import type {RootTabsScreenProps} from '../routes/types'
|
import {Feed} from '../com/Feed'
|
||||||
|
// import type {RootTabsScreenProps} from '../routes/types'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
|
|
||||||
export function Home({navigation}: RootTabsScreenProps<'Home'>) {
|
export function Home(/*{navigation}: RootTabsScreenProps<'Home'>*/) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Fetching home feed')
|
||||||
|
store.homeFeed.fetch()
|
||||||
|
}, [store.homeFeed])
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
<View style={{alignItems: 'center'}}>
|
<View>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Home</Text>
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>
|
||||||
<Button
|
Hello, {store.me.displayName} ({store.me.name})
|
||||||
title="Go to Jane's profile"
|
</Text>
|
||||||
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
|
<Feed feed={store.homeFeed} />
|
||||||
/>
|
|
||||||
<Button title="Logout" onPress={() => store.session.logout()} />
|
|
||||||
</View>
|
</View>
|
||||||
</Shell>
|
</Shell>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Text, Button, View, ActivityIndicator} from 'react-native'
|
import {Text, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Shell} from '../shell'
|
import {Shell} from '../shell'
|
||||||
import type {RootTabsScreenProps} from '../routes/types'
|
// import type {RootTabsScreenProps} from '../routes/types'
|
||||||
import {useStores} from '../../state'
|
// import {useStores} from '../../state'
|
||||||
|
|
||||||
export const Login = observer(({navigation}: RootTabsScreenProps<'Login'>) => {
|
export const Login = observer(
|
||||||
const store = useStores()
|
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
|
||||||
|
// const store = useStores()
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
||||||
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
{/*store.session.uiError && <Text>{store.session.uiError}</Text>}
|
||||||
{!store.session.uiIsProcessing ? (
|
{!store.session.uiIsProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Button title="Login" onPress={() => store.session.login()} />
|
<Button title="Login" onPress={() => store.session.login()} />
|
||||||
|
@ -22,8 +23,9 @@ export const Login = observer(({navigation}: RootTabsScreenProps<'Login'>) => {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
)}
|
)*/}
|
||||||
</View>
|
</View>
|
||||||
</Shell>
|
</Shell>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Text, Button, View, ActivityIndicator} from 'react-native'
|
import {Text, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Shell} from '../shell'
|
import {Shell} from '../shell'
|
||||||
import type {RootTabsScreenProps} from '../routes/types'
|
// import type {RootTabsScreenProps} from '../routes/types'
|
||||||
import {useStores} from '../../state'
|
// import {useStores} from '../../state'
|
||||||
|
|
||||||
export const Signup = observer(
|
export const Signup = observer(
|
||||||
({navigation}: RootTabsScreenProps<'Signup'>) => {
|
(/*{navigation}: RootTabsScreenProps<'Signup'>*/) => {
|
||||||
const store = useStores()
|
// const store = useStores()
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
||||||
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
{/*store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||||
{!store.session.uiIsProcessing ? (
|
{!store.session.uiIsProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
@ -26,7 +26,7 @@ export const Signup = observer(
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
)}
|
)*/}
|
||||||
</View>
|
</View>
|
||||||
</Shell>
|
</Shell>
|
||||||
)
|
)
|
||||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -55,9 +55,9 @@
|
||||||
ucans "0.9.0-alpha3"
|
ucans "0.9.0-alpha3"
|
||||||
uint8arrays "^3.0.0"
|
uint8arrays "^3.0.0"
|
||||||
|
|
||||||
"@adxp/mock-api@git+ssh://git@github.com:bluesky-social/adx-mock-api.git#0bccd04217c78a7c9786a45684ac2ffb9767429b":
|
"@adxp/mock-api@git+ssh://git@github.com:bluesky-social/adx-mock-api.git#74a1f810a342aa4b58a54724e21c57d2faa5e72e":
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "git+ssh://git@github.com:bluesky-social/adx-mock-api.git#0bccd04217c78a7c9786a45684ac2ffb9767429b"
|
resolved "git+ssh://git@github.com:bluesky-social/adx-mock-api.git#74a1f810a342aa4b58a54724e21c57d2faa5e72e"
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv "^8.11.0"
|
ajv "^8.11.0"
|
||||||
ajv-formats "^2.1.1"
|
ajv-formats "^2.1.1"
|
||||||
|
@ -9399,15 +9399,15 @@ mobx-react-lite@^3.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz#d59156a96889cdadad751e5e4dab95f28926dfff"
|
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz#d59156a96889cdadad751e5e4dab95f28926dfff"
|
||||||
integrity sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==
|
integrity sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==
|
||||||
|
|
||||||
mobx-state-tree@^5.1.5:
|
mobx@^6.6.1:
|
||||||
version "5.1.5"
|
version "6.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-5.1.5.tgz#7344d61072705747abb98d23ad21302e38200105"
|
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.6.1.tgz#70ee6aa82f25aeb7e7d522bd621207434e509318"
|
||||||
integrity sha512-jugIic0PYWW+nzzYfp4RUy9dec002Z778OC6KzoOyBHnqxupK9iPCsUJYkHjmNRHjZ8E4Z7qQpsKV3At/ntGVw==
|
integrity sha512-7su3UZv5JF+ohLr2opabjbUAERfXstMY+wiBtey8yNAPoB8H187RaQXuhFjNkH8aE4iHbDWnhDFZw0+5ic4nGQ==
|
||||||
|
|
||||||
mobx@^6.6.0:
|
moment@^2.29.4:
|
||||||
version "6.6.0"
|
version "2.29.4"
|
||||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.6.0.tgz#617ca1f3b745a781fa89c5eb94a773e3cbeff8ae"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||||
integrity sha512-MNTKevLH/6DShLZcmSL351+JgiJPO56A4GUpoiDQ3/yZ0mAtclNLdHK9q4BcQhibx8/JSDupfTpbX2NZPemlRg==
|
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
@ -9424,11 +9424,6 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
msrcrypto@^1.5.8:
|
|
||||||
version "1.5.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c"
|
|
||||||
integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q==
|
|
||||||
|
|
||||||
multicast-dns@^7.2.5:
|
multicast-dns@^7.2.5:
|
||||||
version "7.2.5"
|
version "7.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced"
|
resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced"
|
||||||
|
|
Loading…
Reference in New Issue