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 // track changes & save to storage
autorun(() => { autorun(() => {
const snapshot = rootStore.serialize() const snapshot = rootStore.serialize()
console.log('saving', snapshot)
storage.save(ROOT_STATE_STORAGE_KEY, 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 {SessionModel} from './session'
import {NavigationModel} from './navigation' import {NavigationModel} from './navigation'
import {MeModel} from './me' import {MeModel} from './me'
import {FeedViewModel} from './feed-view'
import {NotificationsViewModel} from './notifications-view'
export class RootStoreModel { export class RootStoreModel {
session = new SessionModel() session = new SessionModel()
nav = new NavigationModel() nav = new NavigationModel()
me = new MeModel(this) me = new MeModel(this)
homeFeed = new FeedViewModel(this, {})
notesFeed = new NotificationsViewModel(this, {})
constructor(public api: AdxClient) { constructor(public api: AdxClient) {
makeAutoObservable(this, { 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 = { export type ScreenParams = {
params: Record<string, any> params: Record<string, any>
visible: boolean
} }
export type Route = [React.FC<ScreenParams>, IconProp, RegExp] export type Route = [React.FC<ScreenParams>, IconProp, RegExp]
export type MatchResult = { export type MatchResult = {

View File

@ -1,53 +1,31 @@
import React, {useState, useEffect, useLayoutEffect} from 'react' import React, {useState, useEffect} from 'react'
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' import {View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Feed} from '../com/feed/Feed' import {Feed} from '../com/feed/Feed'
import {FAB} from '../com/util/FloatingActionButton' import {FAB} from '../com/util/FloatingActionButton'
import {useStores} from '../../state' import {useStores} from '../../state'
import {useLoadEffect} from '../lib/navigation' import {FeedViewModel} from '../../state/models/feed-view'
import {AVIS} from '../lib/assets'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
export function Home({params}: ScreenParams) { export function Home({visible}: ScreenParams) {
const [hasSetup, setHasSetup] = useState<boolean>(false) const [hasSetup, setHasSetup] = useState<boolean>(false)
const [feedView, setFeedView] = useState<FeedViewModel | undefined>()
const store = useStores() const store = useStores()
useLoadEffect(() => {
useEffect(() => {
if (!visible) {
return
}
if (hasSetup) {
console.log('Updating home feed')
feedView?.update()
} else {
store.nav.setTitle('Home') store.nav.setTitle('Home')
console.log('Fetching home feed') console.log('Fetching home feed')
store.homeFeed.setup().then(() => setHasSetup(true)) const newFeedView = new FeedViewModel(store, {})
}, [store.nav, store.homeFeed]) setFeedView(newFeedView)
newFeedView.setup().then(() => setHasSetup(true))
// TODO }
// useEffect(() => { }, [visible, store])
// 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])
const onComposePress = () => { const onComposePress = () => {
store.nav.navigate('/compose') store.nav.navigate('/compose')
@ -55,17 +33,8 @@ export function Home({params}: ScreenParams) {
return ( return (
<View> <View>
<Feed feed={store.homeFeed} /> {feedView && <Feed feed={feedView} />}
<FAB icon="pen-nib" onPress={onComposePress} /> <FAB icon="pen-nib" onPress={onComposePress} />
</View> </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 React, {useState, useEffect} from 'react'
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' import {View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
import {useStores} from '../../state' import {useStores} from '../../state'
import {AVIS} from '../lib/assets' import {NotificationsViewModel} from '../../state/models/notifications-view'
import {ScreenParams} from '../routes' 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 [hasSetup, setHasSetup] = useState<boolean>(false)
const [notesView, setNotesView] = useState<
NotificationsViewModel | undefined
>()
const store = useStores() const store = useStores()
useLoadEffect(() => {
useEffect(() => {
if (!visible) {
return
}
if (hasSetup) {
console.log('Updating notifications feed')
notesView?.update()
} else {
store.nav.setTitle('Notifications') store.nav.setTitle('Notifications')
console.log('Fetching notifications feed') console.log('Fetching notifications feed')
store.notesFeed.setup().then(() => setHasSetup(true)) const newNotesView = new NotificationsViewModel(store, {})
}, [store.notesFeed]) setNotesView(newNotesView)
newNotesView.setup().then(() => setHasSetup(true))
// TODO
// useEffect(() => {
// return navigation.addListener('focus', () => {
// if (hasSetup) {
// console.log('Updating notifications feed')
// store.notesFeed.update()
// }
// })
// }, [navigation, store.notesFeed, hasSetup])
// 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>
)
} }
}, [visible, store])
const styles = StyleSheet.create({ return <View>{notesView && <Feed view={notesView} />}</View>
avi: { }
width: 20,
height: 20,
borderRadius: 10,
resizeMode: 'cover',
},
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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