Add header to PostThread view; update navigation to include stacking so that each tab maintains its own browsing history
parent
28dbc5f5e6
commit
29ed3d2ecf
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
@ -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',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
}
|
|
@ -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'},
|
||||
}
|
|
@ -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} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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'}}>
|
||||
|
|
|
@ -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'}}>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue