Replace mobx-state-tree with mobx and get a basic home feed rendering

zio/stable
Paul Frazee 2022-07-19 15:37:24 -05:00
parent 6b32698b3e
commit dc55f58004
20 changed files with 534 additions and 273 deletions

View File

@ -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.

View File

@ -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",

View File

@ -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(() => {

View File

@ -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

BIN
src/assets/bob.jpg 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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'

View File

@ -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',

View File

@ -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
}

View 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))
}
}
}

View File

@ -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 {}
} }

View File

@ -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)

View File

@ -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',
}
}

View File

@ -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>
)
})

View File

@ -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]}>
&middot; {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,
},
})

View File

@ -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>
) )

View File

@ -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>
) )
}) },
)

View File

@ -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>
) )

View File

@ -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"