Implement working screen-state management and remove extraneous loads

zio/stable
Paul Frazee 2022-09-01 12:00:08 -05:00
parent 346385ce43
commit bb51af5ae9
14 changed files with 118 additions and 245 deletions

View File

@ -27,7 +27,6 @@ export async function setupState() {
// track changes & save to storage
autorun(() => {
const snapshot = rootStore.serialize()
console.log('saving', snapshot)
storage.save(ROOT_STATE_STORAGE_KEY, snapshot)
})

View File

@ -9,15 +9,11 @@ import {isObj, hasProp} from '../lib/type-guards'
import {SessionModel} from './session'
import {NavigationModel} from './navigation'
import {MeModel} from './me'
import {FeedViewModel} from './feed-view'
import {NotificationsViewModel} from './notifications-view'
export class RootStoreModel {
session = new SessionModel()
nav = new NavigationModel()
me = new MeModel(this)
homeFeed = new FeedViewModel(this, {})
notesFeed = new NotificationsViewModel(this, {})
constructor(public api: AdxClient) {
makeAutoObservable(this, {

View File

@ -1,12 +0,0 @@
import {useEffect} from 'react'
import {useStores} from '../../state'
type CB = () => void
/**
* This custom effect hook will trigger on every "navigation"
* Use this in screens to handle any loading behaviors needed
*/
export function useLoadEffect(cb: CB, deps: any[] = []) {
const store = useStores()
useEffect(cb, [store.nav.tab, ...deps])
}

View File

@ -16,6 +16,7 @@ import {ProfileFollows} from './screens/ProfileFollows'
export type ScreenParams = {
params: Record<string, any>
visible: boolean
}
export type Route = [React.FC<ScreenParams>, IconProp, RegExp]
export type MatchResult = {

View File

@ -1,53 +1,31 @@
import React, {useState, useEffect, useLayoutEffect} from 'react'
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import React, {useState, useEffect} from 'react'
import {View} from 'react-native'
import {Feed} from '../com/feed/Feed'
import {FAB} from '../com/util/FloatingActionButton'
import {useStores} from '../../state'
import {useLoadEffect} from '../lib/navigation'
import {AVIS} from '../lib/assets'
import {FeedViewModel} from '../../state/models/feed-view'
import {ScreenParams} from '../routes'
export function Home({params}: ScreenParams) {
export function Home({visible}: ScreenParams) {
const [hasSetup, setHasSetup] = useState<boolean>(false)
const [feedView, setFeedView] = useState<FeedViewModel | undefined>()
const store = useStores()
useLoadEffect(() => {
store.nav.setTitle('Home')
console.log('Fetching home feed')
store.homeFeed.setup().then(() => setHasSetup(true))
}, [store.nav, store.homeFeed])
// TODO
// useEffect(() => {
// return navigation.addListener('focus', () => {
// if (hasSetup) {
// console.log('Updating home feed')
// store.homeFeed.update()
// }
// })
// }, [navigation, store.homeFeed, hasSetup])
// TODO
// useLayoutEffect(() => {
// navigation.setOptions({
// headerShown: true,
// headerTitle: 'V I B E',
// headerLeft: () => (
// <TouchableOpacity
// onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
// <Image source={AVIS['alice.com']} style={styles.avi} />
// </TouchableOpacity>
// ),
// headerRight: () => (
// <TouchableOpacity
// onPress={() => {
// navigation.push('Composer', {})
// }}>
// <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
// </TouchableOpacity>
// ),
// })
// }, [navigation])
useEffect(() => {
if (!visible) {
return
}
if (hasSetup) {
console.log('Updating home feed')
feedView?.update()
} else {
store.nav.setTitle('Home')
console.log('Fetching home feed')
const newFeedView = new FeedViewModel(store, {})
setFeedView(newFeedView)
newFeedView.setup().then(() => setHasSetup(true))
}
}, [visible, store])
const onComposePress = () => {
store.nav.navigate('/compose')
@ -55,17 +33,8 @@ export function Home({params}: ScreenParams) {
return (
<View>
<Feed feed={store.homeFeed} />
{feedView && <Feed feed={feedView} />}
<FAB icon="pen-nib" onPress={onComposePress} />
</View>
)
}
const styles = StyleSheet.create({
avi: {
width: 20,
height: 20,
borderRadius: 10,
resizeMode: 'cover',
},
})

View File

@ -1,65 +1,32 @@
import React, {useState, useEffect, useLayoutEffect} from 'react'
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import React, {useState, useEffect} from 'react'
import {View} from 'react-native'
import {Feed} from '../com/notifications/Feed'
import {useStores} from '../../state'
import {AVIS} from '../lib/assets'
import {NotificationsViewModel} from '../../state/models/notifications-view'
import {ScreenParams} from '../routes'
import {useLoadEffect} from '../lib/navigation'
export const Notifications = ({params}: ScreenParams) => {
export const Notifications = ({visible}: ScreenParams) => {
const [hasSetup, setHasSetup] = useState<boolean>(false)
const [notesView, setNotesView] = useState<
NotificationsViewModel | undefined
>()
const store = useStores()
useLoadEffect(() => {
store.nav.setTitle('Notifications')
console.log('Fetching notifications feed')
store.notesFeed.setup().then(() => setHasSetup(true))
}, [store.notesFeed])
// TODO
// useEffect(() => {
// return navigation.addListener('focus', () => {
// if (hasSetup) {
// console.log('Updating notifications feed')
// store.notesFeed.update()
// }
// })
// }, [navigation, store.notesFeed, hasSetup])
useEffect(() => {
if (!visible) {
return
}
if (hasSetup) {
console.log('Updating notifications feed')
notesView?.update()
} else {
store.nav.setTitle('Notifications')
console.log('Fetching notifications feed')
const newNotesView = new NotificationsViewModel(store, {})
setNotesView(newNotesView)
newNotesView.setup().then(() => setHasSetup(true))
}
}, [visible, store])
// TODO
// useLayoutEffect(() => {
// navigation.setOptions({
// headerShown: true,
// headerTitle: 'Notifications',
// headerLeft: () => (
// <TouchableOpacity
// onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
// <Image source={AVIS['alice.com']} style={styles.avi} />
// </TouchableOpacity>
// ),
// headerRight: () => (
// <TouchableOpacity
// onPress={() => {
// navigation.push('Composer', {})
// }}>
// <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
// </TouchableOpacity>
// ),
// })
// }, [navigation])
return (
<View>
<Feed view={store.notesFeed} />
</View>
)
return <View>{notesView && <Feed view={notesView} />}</View>
}
const styles = StyleSheet.create({
avi: {
width: 20,
height: 20,
borderRadius: 10,
resizeMode: 'cover',
},
})

View File

@ -1,26 +1,19 @@
import React, {useLayoutEffect} from 'react'
import {TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import React, {useEffect} from 'react'
import {makeRecordUri} from '../lib/strings'
import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
import {ScreenParams} from '../routes'
import {useStores} from '../../state'
export const PostLikedBy = ({params}: ScreenParams) => {
export const PostLikedBy = ({visible, params}: ScreenParams) => {
const store = useStores()
const {name, recordKey} = params
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
// TODO
// useLayoutEffect(() => {
// navigation.setOptions({
// headerShown: true,
// headerTitle: 'Liked By',
// headerLeft: () => (
// <TouchableOpacity onPress={() => navigation.goBack()}>
// <FontAwesomeIcon icon="arrow-left" />
// </TouchableOpacity>
// ),
// })
// }, [navigation])
useEffect(() => {
if (visible) {
store.nav.setTitle('Liked by')
}
}, [store, visible])
return <PostLikedByComponent uri={uri} />
}

View File

@ -1,26 +1,19 @@
import React, {useLayoutEffect} from 'react'
import {TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import React, {useEffect} from 'react'
import {makeRecordUri} from '../lib/strings'
import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
import {ScreenParams} from '../routes'
import {useStores} from '../../state'
export const PostRepostedBy = ({params}: ScreenParams) => {
export const PostRepostedBy = ({visible, params}: ScreenParams) => {
const store = useStores()
const {name, recordKey} = params
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
// TODO
// useLayoutEffect(() => {
// navigation.setOptions({
// headerShown: true,
// headerTitle: 'Reposted By',
// headerLeft: () => (
// <TouchableOpacity onPress={() => navigation.goBack()}>
// <FontAwesomeIcon icon="arrow-left" />
// </TouchableOpacity>
// ),
// })
// }, [navigation])
useEffect(() => {
if (visible) {
store.nav.setTitle('Reposted by')
}
}, [store, visible])
return <PostRepostedByComponent uri={uri} />
}

View File

@ -1,32 +1,19 @@
import React, {useEffect, useLayoutEffect} from 'react'
import {TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import React, {useEffect} from 'react'
import {makeRecordUri} from '../lib/strings'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {ScreenParams} from '../routes'
import {useStores} from '../../state'
import {useLoadEffect} from '../lib/navigation'
export const PostThread = ({params}: ScreenParams) => {
export const PostThread = ({visible, params}: ScreenParams) => {
const store = useStores()
const {name, recordKey} = params
const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
useLoadEffect(() => {
store.nav.setTitle(`Post by ${name}`)
}, [store.nav, name])
// TODO
// useLayoutEffect(() => {
// navigation.setOptions({
// headerShown: true,
// headerTitle: 'Thread',
// headerLeft: () => (
// <TouchableOpacity onPress={() => navigation.goBack()}>
// <FontAwesomeIcon icon="arrow-left" />
// </TouchableOpacity>
// ),
// })
// }, [navigation])
useEffect(() => {
if (visible) {
store.nav.setTitle(`Post by ${name}`)
}
}, [visible, store.nav, name])
return <PostThreadComponent uri={uri} />
}

View File

@ -5,39 +5,33 @@ import {useStores} from '../../state'
import {ProfileHeader} from '../com/profile/ProfileHeader'
import {Feed} from '../com/feed/Feed'
import {ScreenParams} from '../routes'
import {useLoadEffect} from '../lib/navigation'
export const Profile = ({params}: ScreenParams) => {
export const Profile = ({visible, params}: ScreenParams) => {
const store = useStores()
const [hasSetup, setHasSetup] = useState<string>('')
const [hasSetup, setHasSetup] = useState<boolean>(false)
const [feedView, setFeedView] = useState<FeedViewModel | undefined>()
useLoadEffect(() => {
const author = params.name
if (feedView?.params.author === author) {
return // no change needed? or trigger refresh?
useEffect(() => {
if (!visible) {
return
}
console.log('Fetching profile feed', author)
const newFeedView = new FeedViewModel(store, {author})
setFeedView(newFeedView)
newFeedView
.setup()
.catch(err => console.error('Failed to fetch feed', err))
.then(() => {
setHasSetup(author)
store.nav.setTitle(author)
})
}, [params.name, feedView?.params.author, store])
// TODO
// useEffect(() => {
// return navigation.addListener('focus', () => {
// if (hasSetup === feedView?.params.author) {
// console.log('Updating profile feed', hasSetup)
// feedView?.update()
// }
// })
// }, [navigation, feedView, hasSetup])
const author = params.name
if (hasSetup) {
console.log('Updating profile feed for', author)
feedView?.update()
} else {
console.log('Fetching profile feed for', author)
const newFeedView = new FeedViewModel(store, {author})
setFeedView(newFeedView)
newFeedView
.setup()
.catch(err => console.error('Failed to fetch feed', err))
.then(() => {
setHasSetup(true)
store.nav.setTitle(author)
})
}
}, [visible, params.name, store])
return (
<View style={styles.container}>

View File

@ -1,24 +1,17 @@
import React, {useLayoutEffect} from 'react'
import {TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import React, {useEffect} from 'react'
import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
import {ScreenParams} from '../routes'
import {useStores} from '../../state'
export const ProfileFollowers = ({params}: ScreenParams) => {
export const ProfileFollowers = ({visible, params}: ScreenParams) => {
const store = useStores()
const {name} = params
// TODO
// useLayoutEffect(() => {
// navigation.setOptions({
// headerShown: true,
// headerTitle: 'Followers',
// headerLeft: () => (
// <TouchableOpacity onPress={() => navigation.goBack()}>
// <FontAwesomeIcon icon="arrow-left" />
// </TouchableOpacity>
// ),
// })
// }, [navigation])
useEffect(() => {
if (visible) {
store.nav.setTitle('Followers of')
}
}, [store, visible])
return <ProfileFollowersComponent name={name} />
}

View File

@ -1,24 +1,17 @@
import React, {useLayoutEffect} from 'react'
import {TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import React, {useEffect} from 'react'
import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
import {ScreenParams} from '../routes'
import {useStores} from '../../state'
export const ProfileFollows = ({params}: ScreenParams) => {
export const ProfileFollows = ({visible, params}: ScreenParams) => {
const store = useStores()
const {name} = params
// TODO
// useLayoutEffect(() => {
// navigation.setOptions({
// headerShown: true,
// headerTitle: 'Following',
// headerLeft: () => (
// <TouchableOpacity onPress={() => navigation.goBack()}>
// <FontAwesomeIcon icon="arrow-left" />
// </TouchableOpacity>
// ),
// })
// }, [navigation])
useEffect(() => {
if (visible) {
store.nav.setTitle('Followers of')
}
}, [store, visible])
return <ProfileFollowsComponent name={name} />
}

View File

@ -113,12 +113,12 @@ export const MobileShell: React.FC = observer(() => {
</View>
<SafeAreaView style={styles.innerContainer}>
<ScreenContainer style={styles.screenContainer}>
{screenRenderDesc.screens.map(({Com, params, key, activityState}) => (
{screenRenderDesc.screens.map(({Com, params, key, visible}) => (
<Screen
key={key}
style={[StyleSheet.absoluteFill, styles.screen]}
activityState={activityState}>
<Com params={params} />
activityState={visible ? 2 : 0}>
<Com params={params} visible={visible} />
</Screen>
))}
</ScreenContainer>
@ -156,7 +156,7 @@ export const MobileShell: React.FC = observer(() => {
* This method produces the information needed by the shell to
* render the current screens with screen-caching behaviors.
*/
type ScreenRenderDesc = MatchResult & {key: string; activityState: 0 | 1 | 2}
type ScreenRenderDesc = MatchResult & {key: string; visible: boolean}
function constructScreenRenderDesc(nav: NavigationModel): {
icon: IconProp
screens: ScreenRenderDesc[]
@ -176,7 +176,7 @@ function constructScreenRenderDesc(nav: NavigationModel): {
}
return Object.assign(matchRes, {
key: `t${tab.id}-s${screen.index}`,
activityState: isCurrent ? 2 : 0,
visible: isCurrent,
}) as ScreenRenderDesc
})
screens = screens.concat(parsedTabScreens)

View File

@ -12,6 +12,6 @@ Paul's todo list
- Reposted by
- Followers list
- Follows list
- Navigation
- Restore all functionality that was disabled during the refactor
- Reduce extraneous triggers of useLoadEffect
- Bugs
- Check that sub components arent reloading too much
- Check that caching is choosing the right views