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

View File

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

View File

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

View File

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

View File

@ -5,15 +5,18 @@ import {Feed} from '../com/feed/Feed'
import type {RootTabsScreenProps} from '../routes/types'
import {useStores} from '../../state'
export function Home({navigation}: RootTabsScreenProps<'Home'>) {
export function Home({navigation}: RootTabsScreenProps<'HomeTab'>) {
const store = useStores()
useEffect(() => {
console.log('Fetching home feed')
store.homeFeed.setup()
}, [store.homeFeed])
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)
}
return (
<Shell>
<View>

View File

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

View File

@ -8,7 +8,7 @@ export const NotFound = ({navigation}: RootTabsScreenProps<'NotFound'>) => {
<Shell>
<View style={{justifyContent: 'center', alignItems: 'center'}}>
<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>
</Shell>
)

View File

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

View File

@ -3,7 +3,7 @@ import {Shell} from '../shell'
import {Text, View} from 'react-native'
import type {RootTabsScreenProps} from '../routes/types'
export const Search = (_props: RootTabsScreenProps<'Search'>) => {
export const Search = (_props: RootTabsScreenProps<'SearchTab'>) => {
return (
<Shell>
<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 {Shell} from '../../shell'
import type {RootTabsScreenProps} from '../../routes/types'
@ -16,8 +18,21 @@ export const PostThread = ({
urip.recordKey = recordKey
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>) => {
navigation.navigate(screen, props)
// @ts-ignore it's up to the callers to supply correct params -prf
navigation.push(screen, props)
}
return (
<Shell>

View File

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