APP-70 give profile its own tab mobile (#469)
* add prebuild command to package.json * add ProfileTab navigator and screen * add prop to remove back button from profile * fix MyProfileTabNavigatorParams type * fix dep array for rendering ProfileHeader * just added ts-ignore * enable opening drawer in profile tab * clean up useNavigationTabState * clean up code * fix hideBackButton code flowzio/stable
parent
2509290fdd
commit
10621e86e4
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"prebuild": "expo prebuild",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
NotificationsTabNavigatorParams,
|
||||
FlatNavigatorParams,
|
||||
AllNavigatorParams,
|
||||
MyProfileTabNavigatorParams,
|
||||
BottomTabNavigatorParams,
|
||||
} from 'lib/routes/types'
|
||||
import {BottomBar} from './view/shell/bottom-bar/BottomBar'
|
||||
import {buildStateObject} from 'lib/routes/helpers'
|
||||
|
@ -41,6 +43,7 @@ import {TermsOfServiceScreen} from './view/screens/TermsOfService'
|
|||
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
|
||||
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useStores} from './state'
|
||||
|
||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||
|
||||
|
@ -48,8 +51,9 @@ const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>()
|
|||
const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>()
|
||||
const NotificationsTab =
|
||||
createNativeStackNavigator<NotificationsTabNavigatorParams>()
|
||||
const MyProfileTab = createNativeStackNavigator<MyProfileTabNavigatorParams>()
|
||||
const Flat = createNativeStackNavigator<FlatNavigatorParams>()
|
||||
const Tab = createBottomTabNavigator()
|
||||
const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
|
||||
|
||||
/**
|
||||
* These "common screens" are reused across stacks.
|
||||
|
@ -100,6 +104,7 @@ function TabsNavigator() {
|
|||
component={NotificationsTabNavigator}
|
||||
/>
|
||||
<Tab.Screen name="SearchTab" component={SearchTabNavigator} />
|
||||
<Tab.Screen name="MyProfileTab" component={MyProfileTabNavigator} />
|
||||
</Tab.Navigator>
|
||||
)
|
||||
}
|
||||
|
@ -158,6 +163,32 @@ function NotificationsTabNavigator() {
|
|||
)
|
||||
}
|
||||
|
||||
function MyProfileTabNavigator() {
|
||||
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
|
||||
const store = useStores()
|
||||
return (
|
||||
<MyProfileTab.Navigator
|
||||
screenOptions={{
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
animationDuration: 250,
|
||||
contentStyle,
|
||||
}}>
|
||||
<MyProfileTab.Screen
|
||||
name="MyProfile"
|
||||
// @ts-ignore // TODO: fix this broken type in ProfileScreen
|
||||
component={ProfileScreen}
|
||||
initialParams={{
|
||||
name: store.me.handle,
|
||||
hideBackButton: true,
|
||||
}}
|
||||
/>
|
||||
{commonScreens(MyProfileTab as typeof HomeTab)}
|
||||
</MyProfileTab.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The FlatNavigator is used by Web to represent the routes
|
||||
* in a single ("flat") stack.
|
||||
|
|
|
@ -3,11 +3,24 @@ import {getTabState, TabState} from 'lib/routes/helpers'
|
|||
|
||||
export function useNavigationTabState() {
|
||||
return useNavigationState(state => {
|
||||
return {
|
||||
const res = {
|
||||
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
|
||||
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
|
||||
isAtNotifications:
|
||||
getTabState(state, 'Notifications') !== TabState.Outside,
|
||||
isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside,
|
||||
}
|
||||
if (
|
||||
!res.isAtHome &&
|
||||
!res.isAtNotifications &&
|
||||
!res.isAtSearch &&
|
||||
!res.isAtMyProfile
|
||||
) {
|
||||
// HACK for some reason useNavigationState will give us pre-hydration results
|
||||
// and not update after, so we force isAtHome if all came back false
|
||||
// -prf
|
||||
res.isAtHome = true
|
||||
}
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export function useNavigationTabState() {
|
|||
isAtHome: currentRoute === 'Home',
|
||||
isAtSearch: currentRoute === 'Search',
|
||||
isAtNotifications: currentRoute === 'Notifications',
|
||||
isAtMyProfile: currentRoute === 'MyProfile',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -20,7 +20,8 @@ export function isStateAtTabRoot(state: State | undefined) {
|
|||
return (
|
||||
isTab(currentRoute.name, 'Home') ||
|
||||
isTab(currentRoute.name, 'Search') ||
|
||||
isTab(currentRoute.name, 'Notifications')
|
||||
isTab(currentRoute.name, 'Notifications') ||
|
||||
isTab(currentRoute.name, 'MyProfile')
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
|||
export type CommonNavigatorParams = {
|
||||
NotFound: undefined
|
||||
Settings: undefined
|
||||
Profile: {name: string}
|
||||
Profile: {name: string; hideBackButton?: boolean}
|
||||
ProfileFollowers: {name: string}
|
||||
ProfileFollows: {name: string}
|
||||
PostThread: {name: string; rkey: string}
|
||||
|
@ -21,6 +21,13 @@ export type CommonNavigatorParams = {
|
|||
CopyrightPolicy: undefined
|
||||
}
|
||||
|
||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||
HomeTab: undefined
|
||||
SearchTab: undefined
|
||||
NotificationsTab: undefined
|
||||
MyProfileTab: undefined
|
||||
}
|
||||
|
||||
export type HomeTabNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
}
|
||||
|
@ -33,6 +40,10 @@ export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
|||
Notifications: undefined
|
||||
}
|
||||
|
||||
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
|
||||
MyProfile: undefined
|
||||
}
|
||||
|
||||
export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
Search: {q?: string}
|
||||
|
@ -46,6 +57,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||
Search: {q?: string}
|
||||
NotificationsTab: undefined
|
||||
Notifications: undefined
|
||||
MyProfileTab: undefined
|
||||
}
|
||||
|
||||
// NOTE
|
||||
|
|
|
@ -36,8 +36,14 @@ import {FollowState} from 'state/models/cache/my-follows'
|
|||
|
||||
const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
|
||||
|
||||
interface Props {
|
||||
view: ProfileModel
|
||||
onRefreshAll: () => void
|
||||
hideBackButton?: boolean
|
||||
}
|
||||
|
||||
export const ProfileHeader = observer(
|
||||
({view, onRefreshAll}: {view: ProfileModel; onRefreshAll: () => void}) => {
|
||||
({view, onRefreshAll, hideBackButton = false}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
|
||||
// loading
|
||||
|
@ -80,17 +86,21 @@ export const ProfileHeader = observer(
|
|||
|
||||
// loaded
|
||||
// =
|
||||
return <ProfileHeaderLoaded view={view} onRefreshAll={onRefreshAll} />
|
||||
return (
|
||||
<ProfileHeaderLoaded
|
||||
view={view}
|
||||
onRefreshAll={onRefreshAll}
|
||||
hideBackButton={hideBackButton}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
|
||||
view,
|
||||
onRefreshAll,
|
||||
}: {
|
||||
view: ProfileModel
|
||||
onRefreshAll: () => void
|
||||
}) {
|
||||
hideBackButton = false,
|
||||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
@ -336,7 +346,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
|
|||
</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
{!isDesktopWeb && (
|
||||
{!isDesktopWeb && !hideBackButton && (
|
||||
<TouchableWithoutFeedback onPress={onPressBack} hitSlop={BACK_HITSLOP}>
|
||||
<View style={styles.backBtnWrapper}>
|
||||
<BlurView style={styles.backBtn} blurType="dark">
|
||||
|
|
|
@ -96,8 +96,14 @@ export const ProfileScreen = withAuthRequired(
|
|||
if (!uiState) {
|
||||
return <View />
|
||||
}
|
||||
return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} />
|
||||
}, [uiState, onRefresh])
|
||||
return (
|
||||
<ProfileHeader
|
||||
view={uiState.profile}
|
||||
onRefreshAll={onRefresh}
|
||||
hideBackButton={route.params.hideBackButton}
|
||||
/>
|
||||
)
|
||||
}, [uiState, onRefresh, route.params.hideBackButton])
|
||||
const Footer = React.useMemo(() => {
|
||||
return uiState.showLoadingMoreFooter ? LoadingMoreFooter : undefined
|
||||
}, [uiState.showLoadingMoreFooter])
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
MagnifyingGlassIcon2,
|
||||
MagnifyingGlassIcon2Solid,
|
||||
MoonIcon,
|
||||
UserIconSolid,
|
||||
} from 'lib/icons'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
|
@ -45,7 +46,8 @@ export const DrawerContent = observer(() => {
|
|||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationTabState()
|
||||
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
|
||||
useNavigationTabState()
|
||||
|
||||
// events
|
||||
// =
|
||||
|
@ -56,7 +58,7 @@ export const DrawerContent = observer(() => {
|
|||
const state = navigation.getState()
|
||||
store.shell.closeDrawer()
|
||||
if (isWeb) {
|
||||
// @ts-ignore must be Home, Search, or Notifications
|
||||
// @ts-ignore must be Home, Search, Notifications, or MyProfile
|
||||
navigation.navigate(tab)
|
||||
} else {
|
||||
const tabState = getTabState(state, tab)
|
||||
|
@ -65,7 +67,7 @@ export const DrawerContent = observer(() => {
|
|||
} else if (tabState === TabState.Inside) {
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
} else {
|
||||
// @ts-ignore must be Home, Search, or Notifications
|
||||
// @ts-ignore must be Home, Search, Notifications, or MyProfile
|
||||
navigation.navigate(`${tab}Tab`)
|
||||
}
|
||||
}
|
||||
|
@ -86,10 +88,8 @@ export const DrawerContent = observer(() => {
|
|||
)
|
||||
|
||||
const onPressProfile = React.useCallback(() => {
|
||||
track('Menu:ItemClicked', {url: 'Profile'})
|
||||
navigation.navigate('Profile', {name: store.me.handle})
|
||||
store.shell.closeDrawer()
|
||||
}, [navigation, track, store.me.handle, store.shell])
|
||||
onPressTab('MyProfile')
|
||||
}, [onPressTab])
|
||||
|
||||
const onPressSettings = React.useCallback(() => {
|
||||
track('Menu:ItemClicked', {url: 'Settings'})
|
||||
|
@ -211,11 +211,19 @@ export const DrawerContent = observer(() => {
|
|||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
isAtMyProfile ? (
|
||||
<UserIconSolid
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="26"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
) : (
|
||||
<UserIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="26"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label="Profile"
|
||||
onPress={onPressProfile}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {StackActions, useNavigationState} from '@react-navigation/native'
|
||||
import {StackActions} from '@react-navigation/native'
|
||||
import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
|
@ -21,34 +21,21 @@ import {
|
|||
BellIcon,
|
||||
BellIconSolid,
|
||||
UserIcon,
|
||||
UserIconSolid,
|
||||
} from 'lib/icons'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {getTabState, TabState} from 'lib/routes/helpers'
|
||||
import {styles} from './BottomBarStyles'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
|
||||
|
||||
export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const safeAreaInsets = useSafeAreaInsets()
|
||||
const {track} = useAnalytics()
|
||||
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState(
|
||||
state => {
|
||||
const res = {
|
||||
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
|
||||
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
|
||||
isAtNotifications:
|
||||
getTabState(state, 'Notifications') !== TabState.Outside,
|
||||
}
|
||||
if (!res.isAtHome && !res.isAtNotifications && !res.isAtSearch) {
|
||||
// HACK for some reason useNavigationState will give us pre-hydration results
|
||||
// and not update after, so we force isAtHome if all came back false
|
||||
// -prf
|
||||
res.isAtHome = true
|
||||
}
|
||||
return res
|
||||
},
|
||||
)
|
||||
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
|
||||
useNavigationTabState()
|
||||
|
||||
const {footerMinimalShellTransform} = useMinimalShellMode()
|
||||
|
||||
|
@ -77,9 +64,8 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
|||
[onPressTab],
|
||||
)
|
||||
const onPressProfile = React.useCallback(() => {
|
||||
track('MobileShell:ProfileButtonPressed')
|
||||
navigation.navigate('Profile', {name: store.me.handle})
|
||||
}, [navigation, track, store.me.handle])
|
||||
onPressTab('MyProfile')
|
||||
}, [onPressTab])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
|
@ -154,11 +140,19 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
|||
testID="bottomBarProfileBtn"
|
||||
icon={
|
||||
<View style={styles.ctrlIconSizingWrapper}>
|
||||
{isAtMyProfile ? (
|
||||
<UserIconSolid
|
||||
size={28}
|
||||
strokeWidth={1.5}
|
||||
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
|
||||
/>
|
||||
) : (
|
||||
<UserIcon
|
||||
size={28}
|
||||
strokeWidth={1.5}
|
||||
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
onPress={onPressProfile}
|
||||
|
|
Loading…
Reference in New Issue