Add header to PostThread view; update navigation to include stacking so that each tab maintains its own browsing history

zio/stable
Paul Frazee 2022-07-21 16:43:47 -05:00
parent 28dbc5f5e6
commit 29ed3d2ecf
17 changed files with 128 additions and 81 deletions

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,25 +1,13 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
Image,
ImageSourcePropType,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import {bsky, AdxUri} from '@adxp/mock-api' import {bsky, AdxUri} from '@adxp/mock-api'
import moment from 'moment' import moment from 'moment'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {OnNavigateContent} from '../../routes/types' import {OnNavigateContent} from '../../routes/types'
import {FeedViewItemModel} from '../../../state/models/feed-view' import {FeedViewItemModel} from '../../../state/models/feed-view'
import {s} from '../../lib/styles' import {s} from '../../lib/styles'
import {AVIS} from '../../lib/assets'
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({ export const FeedItem = observer(function FeedItem({
item, item,
@ -40,10 +28,7 @@ export const FeedItem = observer(function FeedItem({
<TouchableOpacity style={styles.outer} onPress={onPressOuter}> <TouchableOpacity style={styles.outer} onPress={onPressOuter}>
{item.repostedBy && ( {item.repostedBy && (
<View style={styles.repostedBy}> <View style={styles.repostedBy}>
<FontAwesomeIcon <FontAwesomeIcon icon="retweet" style={styles.repostedByIcon} />
icon="retweet"
style={[styles.repostedByIcon, s.gray]}
/>
<Text style={[s.gray, s.bold, s.f13]}> <Text style={[s.gray, s.bold, s.f13]}>
Reposted by {item.repostedBy.displayName} Reposted by {item.repostedBy.displayName}
</Text> </Text>
@ -53,7 +38,7 @@ export const FeedItem = observer(function FeedItem({
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Image <Image
style={styles.avi} style={styles.avi}
source={IMAGES[item.author.name] || IMAGES['alice.com']} source={AVIS[item.author.name] || AVIS['alice.com']}
/> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
@ -74,14 +59,14 @@ export const FeedItem = observer(function FeedItem({
<View style={styles.ctrls}> <View style={styles.ctrls}>
<View style={styles.ctrl}> <View style={styles.ctrl}>
<FontAwesomeIcon <FontAwesomeIcon
style={[styles.ctrlIcon, s.gray]} style={styles.ctrlIcon}
icon={['far', 'comment']} icon={['far', 'comment']}
/> />
<Text>{item.replyCount}</Text> <Text>{item.replyCount}</Text>
</View> </View>
<View style={styles.ctrl}> <View style={styles.ctrl}>
<FontAwesomeIcon <FontAwesomeIcon
style={[styles.ctrlIcon, s.gray]} style={styles.ctrlIcon}
icon="retweet" icon="retweet"
size={22} size={22}
/> />
@ -89,14 +74,14 @@ export const FeedItem = observer(function FeedItem({
</View> </View>
<View style={styles.ctrl}> <View style={styles.ctrl}>
<FontAwesomeIcon <FontAwesomeIcon
style={[styles.ctrlIcon, s.gray]} style={styles.ctrlIcon}
icon={['far', 'heart']} icon={['far', 'heart']}
/> />
<Text>{item.likeCount}</Text> <Text>{item.likeCount}</Text>
</View> </View>
<View style={styles.ctrl}> <View style={styles.ctrl}>
<FontAwesomeIcon <FontAwesomeIcon
style={[styles.ctrlIcon, s.gray]} style={styles.ctrlIcon}
icon="share-from-square" icon="share-from-square"
/> />
</View> </View>
@ -120,6 +105,7 @@ const styles = StyleSheet.create({
}, },
repostedByIcon: { repostedByIcon: {
marginRight: 2, marginRight: 2,
color: 'gray',
}, },
layout: { layout: {
flexDirection: 'row', flexDirection: 'row',
@ -159,5 +145,6 @@ const styles = StyleSheet.create({
}, },
ctrlIcon: { ctrlIcon: {
marginRight: 5, marginRight: 5,
color: 'gray',
}, },
}) })

View File

@ -1,13 +1,6 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
Image,
ImageSourcePropType,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import {bsky, AdxUri} from '@adxp/mock-api' import {bsky, AdxUri} from '@adxp/mock-api'
import moment from 'moment' import moment from 'moment'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@ -15,14 +8,9 @@ import {OnNavigateContent} from '../../routes/types'
import {PostThreadViewPostModel} from '../../../state/models/post-thread-view' import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
import {s} from '../../lib/styles' import {s} from '../../lib/styles'
import {pluralize} from '../../lib/strings' import {pluralize} from '../../lib/strings'
import {AVIS} from '../../lib/assets'
const IMAGES: Record<string, ImageSourcePropType> = { function iter<T>(n: number, fn: (_i: number) => T): Array<T> {
'alice.com': require('../../assets/alice.jpg'),
'bob.com': require('../../assets/bob.jpg'),
'carla.com': require('../../assets/carla.jpg'),
}
function iter<T>(n: number, fn: (i: number) => T): Array<T> {
const arr: T[] = [] const arr: T[] = []
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
arr.push(fn(i)) arr.push(fn(i))
@ -49,13 +37,13 @@ export const PostThreadItem = observer(function PostThreadItem({
return ( return (
<TouchableOpacity style={styles.outer} onPress={onPressOuter}> <TouchableOpacity style={styles.outer} onPress={onPressOuter}>
<View style={styles.layout}> <View style={styles.layout}>
{iter(Math.abs(item._depth), () => ( {iter(Math.abs(item._depth), (i: number) => (
<View style={styles.replyBar} /> <View key={i} style={styles.replyBar} />
))} ))}
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Image <Image
style={styles.avi} style={styles.avi}
source={IMAGES[item.author.name] || IMAGES['alice.com']} source={AVIS[item.author.name] || AVIS['alice.com']}
/> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
@ -104,14 +92,14 @@ export const PostThreadItem = observer(function PostThreadItem({
<View style={styles.ctrls}> <View style={styles.ctrls}>
<View style={styles.ctrl}> <View style={styles.ctrl}>
<FontAwesomeIcon <FontAwesomeIcon
style={[styles.ctrlIcon, s.gray]} style={styles.ctrlIcon}
icon={['far', 'comment']} icon={['far', 'comment']}
/> />
<Text>{item.replyCount}</Text> <Text>{item.replyCount}</Text>
</View> </View>
<View style={styles.ctrl}> <View style={styles.ctrl}>
<FontAwesomeIcon <FontAwesomeIcon
style={[styles.ctrlIcon, s.gray]} style={styles.ctrlIcon}
icon="retweet" icon="retweet"
size={22} size={22}
/> />
@ -119,14 +107,14 @@ export const PostThreadItem = observer(function PostThreadItem({
</View> </View>
<View style={styles.ctrl}> <View style={styles.ctrl}>
<FontAwesomeIcon <FontAwesomeIcon
style={[styles.ctrlIcon, s.gray]} style={styles.ctrlIcon}
icon={['far', 'heart']} icon={['far', 'heart']}
/> />
<Text>{item.likeCount}</Text> <Text>{item.likeCount}</Text>
</View> </View>
<View style={styles.ctrl}> <View style={styles.ctrl}>
<FontAwesomeIcon <FontAwesomeIcon
style={[styles.ctrlIcon, s.gray]} style={styles.ctrlIcon}
icon="share-from-square" icon="share-from-square"
/> />
</View> </View>
@ -204,5 +192,6 @@ const styles = StyleSheet.create({
}, },
ctrlIcon: { ctrlIcon: {
marginRight: 5, marginRight: 5,
color: 'gray',
}, },
}) })

View File

@ -1,6 +1,7 @@
import moment from 'moment' import moment from 'moment'
import {library} from '@fortawesome/fontawesome-svg-core' import {library} from '@fortawesome/fontawesome-svg-core'
import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' import {faComment} from '@fortawesome/free-regular-svg-icons/faComment'
@ -32,6 +33,7 @@ export function setup() {
}, },
}) })
library.add( library.add(
faArrowLeft,
faBars, faBars,
faBell, faBell,
faComment, faComment,

View File

@ -0,0 +1,7 @@
import {ImageSourcePropType} from 'react-native'
export const AVIS: Record<string, ImageSourcePropType> = {
'alice.com': require('../../../public/img/alice.jpg'),
'bob.com': require('../../../public/img/bob.jpg'),
'carla.com': require('../../../public/img/carla.jpg'),
}

View File

@ -0,0 +1,7 @@
import {ImageSourcePropType} from 'react-native'
export const AVIS: Record<string, ImageSourcePropType> = {
'alice.com': {uri: '/img/alice.jpg'},
'bob.com': {uri: '/img/bob.jpg'},
'carla.com': {uri: '/img/carla.jpg'},
}

View File

@ -1,5 +1,5 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {Text, Linking} from 'react-native' import {Linking, Text} from 'react-native'
import { import {
NavigationContainer, NavigationContainer,
LinkingOptions, LinkingOptions,
@ -32,12 +32,12 @@ const linking: LinkingOptions<RootTabsParamList> = {
], ],
config: { config: {
screens: { screens: {
Home: '', HomeTab: '',
SearchTab: 'search',
NotificationsTab: 'notifications',
MenuTab: 'menu',
Profile: 'profile/:name', Profile: 'profile/:name',
PostThread: 'profile/:name/post/:recordKey', PostThread: 'profile/:name/post/:recordKey',
Search: 'search',
Notifications: 'notifications',
Menu: 'menu',
Login: 'login', Login: 'login',
Signup: 'signup', Signup: 'signup',
NotFound: '*', NotFound: '*',
@ -46,7 +46,9 @@ const linking: LinkingOptions<RootTabsParamList> = {
} }
export const RootTabs = createBottomTabNavigator<RootTabsParamList>() export const RootTabs = createBottomTabNavigator<RootTabsParamList>()
export const PrimaryStack = createNativeStackNavigator() export const HomeTabStack = createNativeStackNavigator()
export const SearchTabStack = createNativeStackNavigator()
export const NotificationsTabStack = createNativeStackNavigator()
const tabBarScreenOptions = ({ const tabBarScreenOptions = ({
route, route,
@ -56,18 +58,18 @@ const tabBarScreenOptions = ({
headerShown: false, headerShown: false,
tabBarIcon: (state: {focused: boolean; color: string; size: number}) => { tabBarIcon: (state: {focused: boolean; color: string; size: number}) => {
switch (route.name) { switch (route.name) {
case 'Home': case 'HomeTab':
return <FontAwesomeIcon icon="house" style={{color: state.color}} /> return <FontAwesomeIcon icon="house" style={{color: state.color}} />
case 'Search': case 'SearchTab':
return ( return (
<FontAwesomeIcon <FontAwesomeIcon
icon="magnifying-glass" icon="magnifying-glass"
style={{color: state.color}} style={{color: state.color}}
/> />
) )
case 'Notifications': case 'NotificationsTab':
return <FontAwesomeIcon icon="bell" style={{color: state.color}} /> return <FontAwesomeIcon icon="bell" style={{color: state.color}} />
case 'Menu': case 'MenuTab':
return <FontAwesomeIcon icon="bars" style={{color: state.color}} /> return <FontAwesomeIcon icon="bars" style={{color: state.color}} />
default: default:
return <FontAwesomeIcon icon="bars" style={{color: state.color}} /> return <FontAwesomeIcon icon="bars" style={{color: state.color}} />
@ -75,8 +77,46 @@ const tabBarScreenOptions = ({
}, },
}) })
const HIDE_HEADER = {headerShown: false}
const HIDE_TAB = {tabBarButton: () => null} const HIDE_TAB = {tabBarButton: () => null}
function HomeStackCom() {
return (
<HomeTabStack.Navigator>
<HomeTabStack.Screen name="Home" component={Home} options={HIDE_HEADER} />
<HomeTabStack.Screen name="Profile" component={Profile} />
<HomeTabStack.Screen name="PostThread" component={PostThread} />
</HomeTabStack.Navigator>
)
}
function SearchStackCom() {
return (
<SearchTabStack.Navigator>
<SearchTabStack.Screen
name="Search"
component={Search}
options={HIDE_HEADER}
/>
<SearchTabStack.Screen name="Profile" component={Profile} />
<SearchTabStack.Screen name="PostThread" component={PostThread} />
</SearchTabStack.Navigator>
)
}
function NotificationsStackCom() {
return (
<NotificationsTabStack.Navigator>
<NotificationsTabStack.Screen
name="Notifications"
component={Notifications}
/>
<NotificationsTabStack.Screen name="Profile" component={Profile} />
<NotificationsTabStack.Screen name="PostThread" component={PostThread} />
</NotificationsTabStack.Navigator>
)
}
export const Root = observer(() => { export const Root = observer(() => {
const store = useStores() const store = useStores()
@ -96,25 +136,18 @@ export const Root = observer(() => {
return ( return (
<NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}> <NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
<RootTabs.Navigator <RootTabs.Navigator
initialRouteName={store.session.isAuthed ? 'Home' : 'Login'} initialRouteName={store.session.isAuthed ? 'HomeTab' : 'Login'}
screenOptions={tabBarScreenOptions} screenOptions={tabBarScreenOptions}
tabBar={tabBar}> tabBar={tabBar}>
{store.session.isAuthed ? ( {store.session.isAuthed ? (
<> <>
<RootTabs.Screen name="Home" component={Home} /> <RootTabs.Screen name="HomeTab" component={HomeStackCom} />
<RootTabs.Screen name="Search" component={Search} /> <RootTabs.Screen name="SearchTab" component={SearchStackCom} />
<RootTabs.Screen name="Notifications" component={Notifications} />
<RootTabs.Screen name="Menu" component={Menu} />
<RootTabs.Screen <RootTabs.Screen
name="Profile" name="NotificationsTab"
component={Profile} component={NotificationsStackCom}
options={HIDE_TAB}
/>
<RootTabs.Screen
name="PostThread"
component={PostThread}
options={HIDE_TAB}
/> />
<RootTabs.Screen name="MenuTab" component={Menu} />
</> </>
) : ( ) : (
<> <>

View File

@ -1,10 +1,10 @@
import type {StackScreenProps} from '@react-navigation/stack' import type {StackScreenProps} from '@react-navigation/stack'
export type RootTabsParamList = { export type RootTabsParamList = {
Home: undefined HomeTab: undefined
Search: undefined SearchTab: undefined
Notifications: undefined NotificationsTab: undefined
Menu: undefined MenuTab: undefined
Profile: {name: string} Profile: {name: string}
PostThread: {name: string; recordKey: string} PostThread: {name: string; recordKey: string}
Login: undefined Login: undefined
@ -14,7 +14,10 @@ export type RootTabsParamList = {
export type RootTabsScreenProps<T extends keyof RootTabsParamList> = export type RootTabsScreenProps<T extends keyof RootTabsParamList> =
StackScreenProps<RootTabsParamList, T> StackScreenProps<RootTabsParamList, T>
export type OnNavigateContent = (screen: string, params: Record<string, string>): void export type OnNavigateContent = (
screen: string,
params: Record<string, string>,
) => void
/* /*
NOTE NOTE

View File

@ -5,15 +5,18 @@ import {Feed} from '../com/feed/Feed'
import type {RootTabsScreenProps} from '../routes/types' 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<'HomeTab'>) {
const store = useStores() const store = useStores()
useEffect(() => { useEffect(() => {
console.log('Fetching home feed') console.log('Fetching home feed')
store.homeFeed.setup() store.homeFeed.setup()
}, [store.homeFeed]) }, [store.homeFeed])
const onNavigateContent = (screen: string, props: Record<string, string>) => { const onNavigateContent = (screen: string, props: Record<string, string>) => {
// @ts-ignore it's up to the callers to supply correct params -prf
navigation.navigate(screen, props) navigation.navigate(screen, props)
} }
return ( return (
<Shell> <Shell>
<View> <View>

View File

@ -3,7 +3,7 @@ import {Shell} from '../shell'
import {ScrollView, Text, View} from 'react-native' import {ScrollView, Text, View} from 'react-native'
import type {RootTabsScreenProps} from '../routes/types' import type {RootTabsScreenProps} from '../routes/types'
export const Menu = (_props: RootTabsScreenProps<'Menu'>) => { export const Menu = (_props: RootTabsScreenProps<'MenuTab'>) => {
return ( return (
<Shell> <Shell>
<ScrollView contentInsetAdjustmentBehavior="automatic"> <ScrollView contentInsetAdjustmentBehavior="automatic">

View File

@ -8,7 +8,7 @@ export const NotFound = ({navigation}: RootTabsScreenProps<'NotFound'>) => {
<Shell> <Shell>
<View style={{justifyContent: 'center', alignItems: 'center'}}> <View style={{justifyContent: 'center', alignItems: 'center'}}>
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text> <Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text>
<Button title="Home" onPress={() => navigation.navigate('Home')} /> <Button title="Home" onPress={() => navigation.navigate('HomeTab')} />
</View> </View>
</Shell> </Shell>
) )

View File

@ -3,7 +3,9 @@ import {Shell} from '../shell'
import {Text, View} from 'react-native' import {Text, View} from 'react-native'
import type {RootTabsScreenProps} from '../routes/types' import type {RootTabsScreenProps} from '../routes/types'
export const Notifications = (_props: RootTabsScreenProps<'Notifications'>) => { export const Notifications = (
_props: RootTabsScreenProps<'NotificationsTab'>,
) => {
return ( return (
<Shell> <Shell>
<View style={{justifyContent: 'center', alignItems: 'center'}}> <View style={{justifyContent: 'center', alignItems: 'center'}}>

View File

@ -3,7 +3,7 @@ import {Shell} from '../shell'
import {Text, View} from 'react-native' import {Text, View} from 'react-native'
import type {RootTabsScreenProps} from '../routes/types' import type {RootTabsScreenProps} from '../routes/types'
export const Search = (_props: RootTabsScreenProps<'Search'>) => { export const Search = (_props: RootTabsScreenProps<'SearchTab'>) => {
return ( return (
<Shell> <Shell>
<View style={{justifyContent: 'center', alignItems: 'center'}}> <View style={{justifyContent: 'center', alignItems: 'center'}}>

View File

@ -1,4 +1,6 @@
import React from 'react' import React, {useLayoutEffect} from 'react'
import {TouchableOpacity} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AdxUri} from '@adxp/mock-api' import {AdxUri} from '@adxp/mock-api'
import {Shell} from '../../shell' import {Shell} from '../../shell'
import type {RootTabsScreenProps} from '../../routes/types' import type {RootTabsScreenProps} from '../../routes/types'
@ -16,8 +18,21 @@ export const PostThread = ({
urip.recordKey = recordKey urip.recordKey = recordKey
const uri = urip.toString() const uri = urip.toString()
useLayoutEffect(() => {
navigation.setOptions({
headerShown: true,
headerTitle: 'Thread',
headerLeft: () => (
<TouchableOpacity onPress={() => navigation.goBack()}>
<FontAwesomeIcon icon="arrow-left" />
</TouchableOpacity>
),
})
}, [navigation])
const onNavigateContent = (screen: string, props: Record<string, string>) => { const onNavigateContent = (screen: string, props: Record<string, string>) => {
navigation.navigate(screen, props) // @ts-ignore it's up to the callers to supply correct params -prf
navigation.push(screen, props)
} }
return ( return (
<Shell> <Shell>

View File

@ -7,7 +7,6 @@ Paul's todo list
- Navigate to profile on avi or username click - Navigate to profile on avi or username click
- Thread view - Thread view
- Mock API support fetch on thread that's not root - Mock API support fetch on thread that's not root
- Header (back btn, highlight)
- View likes list - View likes list
- View reposts list - View reposts list
- Reply control - Reply control