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 flow
zio/stable
Ansh 2023-04-18 09:19:37 -07:00 committed by GitHub
parent 2509290fdd
commit 10621e86e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 128 additions and 51 deletions

View File

@ -4,6 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"postinstall": "patch-package", "postinstall": "patch-package",
"prebuild": "expo prebuild",
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios", "ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",

View File

@ -13,6 +13,8 @@ import {
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
FlatNavigatorParams, FlatNavigatorParams,
AllNavigatorParams, AllNavigatorParams,
MyProfileTabNavigatorParams,
BottomTabNavigatorParams,
} from 'lib/routes/types' } from 'lib/routes/types'
import {BottomBar} from './view/shell/bottom-bar/BottomBar' import {BottomBar} from './view/shell/bottom-bar/BottomBar'
import {buildStateObject} from 'lib/routes/helpers' import {buildStateObject} from 'lib/routes/helpers'
@ -41,6 +43,7 @@ import {TermsOfServiceScreen} from './view/screens/TermsOfService'
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines' import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy' import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from './state'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>() const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@ -48,8 +51,9 @@ const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>()
const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>() const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>()
const NotificationsTab = const NotificationsTab =
createNativeStackNavigator<NotificationsTabNavigatorParams>() createNativeStackNavigator<NotificationsTabNavigatorParams>()
const MyProfileTab = createNativeStackNavigator<MyProfileTabNavigatorParams>()
const Flat = createNativeStackNavigator<FlatNavigatorParams>() const Flat = createNativeStackNavigator<FlatNavigatorParams>()
const Tab = createBottomTabNavigator() const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
/** /**
* These "common screens" are reused across stacks. * These "common screens" are reused across stacks.
@ -100,6 +104,7 @@ function TabsNavigator() {
component={NotificationsTabNavigator} component={NotificationsTabNavigator}
/> />
<Tab.Screen name="SearchTab" component={SearchTabNavigator} /> <Tab.Screen name="SearchTab" component={SearchTabNavigator} />
<Tab.Screen name="MyProfileTab" component={MyProfileTabNavigator} />
</Tab.Navigator> </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 * The FlatNavigator is used by Web to represent the routes
* in a single ("flat") stack. * in a single ("flat") stack.

View File

@ -3,11 +3,24 @@ import {getTabState, TabState} from 'lib/routes/helpers'
export function useNavigationTabState() { export function useNavigationTabState() {
return useNavigationState(state => { return useNavigationState(state => {
return { const res = {
isAtHome: getTabState(state, 'Home') !== TabState.Outside, isAtHome: getTabState(state, 'Home') !== TabState.Outside,
isAtSearch: getTabState(state, 'Search') !== TabState.Outside, isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
isAtNotifications: isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside, 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
}) })
} }

View File

@ -8,6 +8,7 @@ export function useNavigationTabState() {
isAtHome: currentRoute === 'Home', isAtHome: currentRoute === 'Home',
isAtSearch: currentRoute === 'Search', isAtSearch: currentRoute === 'Search',
isAtNotifications: currentRoute === 'Notifications', isAtNotifications: currentRoute === 'Notifications',
isAtMyProfile: currentRoute === 'MyProfile',
} }
}) })
} }

View File

@ -20,7 +20,8 @@ export function isStateAtTabRoot(state: State | undefined) {
return ( return (
isTab(currentRoute.name, 'Home') || isTab(currentRoute.name, 'Home') ||
isTab(currentRoute.name, 'Search') || isTab(currentRoute.name, 'Search') ||
isTab(currentRoute.name, 'Notifications') isTab(currentRoute.name, 'Notifications') ||
isTab(currentRoute.name, 'MyProfile')
) )
} }

View File

@ -6,7 +6,7 @@ export type {NativeStackScreenProps} from '@react-navigation/native-stack'
export type CommonNavigatorParams = { export type CommonNavigatorParams = {
NotFound: undefined NotFound: undefined
Settings: undefined Settings: undefined
Profile: {name: string} Profile: {name: string; hideBackButton?: boolean}
ProfileFollowers: {name: string} ProfileFollowers: {name: string}
ProfileFollows: {name: string} ProfileFollows: {name: string}
PostThread: {name: string; rkey: string} PostThread: {name: string; rkey: string}
@ -21,6 +21,13 @@ export type CommonNavigatorParams = {
CopyrightPolicy: undefined CopyrightPolicy: undefined
} }
export type BottomTabNavigatorParams = CommonNavigatorParams & {
HomeTab: undefined
SearchTab: undefined
NotificationsTab: undefined
MyProfileTab: undefined
}
export type HomeTabNavigatorParams = CommonNavigatorParams & { export type HomeTabNavigatorParams = CommonNavigatorParams & {
Home: undefined Home: undefined
} }
@ -33,6 +40,10 @@ export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined Notifications: undefined
} }
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
MyProfile: undefined
}
export type FlatNavigatorParams = CommonNavigatorParams & { export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined Home: undefined
Search: {q?: string} Search: {q?: string}
@ -46,6 +57,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Search: {q?: string} Search: {q?: string}
NotificationsTab: undefined NotificationsTab: undefined
Notifications: undefined Notifications: undefined
MyProfileTab: undefined
} }
// NOTE // NOTE

View File

@ -36,8 +36,14 @@ import {FollowState} from 'state/models/cache/my-follows'
const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30} const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
interface Props {
view: ProfileModel
onRefreshAll: () => void
hideBackButton?: boolean
}
export const ProfileHeader = observer( export const ProfileHeader = observer(
({view, onRefreshAll}: {view: ProfileModel; onRefreshAll: () => void}) => { ({view, onRefreshAll, hideBackButton = false}: Props) => {
const pal = usePalette('default') const pal = usePalette('default')
// loading // loading
@ -80,17 +86,21 @@ export const ProfileHeader = observer(
// loaded // loaded
// = // =
return <ProfileHeaderLoaded view={view} onRefreshAll={onRefreshAll} /> return (
<ProfileHeaderLoaded
view={view}
onRefreshAll={onRefreshAll}
hideBackButton={hideBackButton}
/>
)
}, },
) )
const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
view, view,
onRefreshAll, onRefreshAll,
}: { hideBackButton = false,
view: ProfileModel }: Props) {
onRefreshAll: () => void
}) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@ -336,7 +346,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
</View> </View>
) : undefined} ) : undefined}
</View> </View>
{!isDesktopWeb && ( {!isDesktopWeb && !hideBackButton && (
<TouchableWithoutFeedback onPress={onPressBack} hitSlop={BACK_HITSLOP}> <TouchableWithoutFeedback onPress={onPressBack} hitSlop={BACK_HITSLOP}>
<View style={styles.backBtnWrapper}> <View style={styles.backBtnWrapper}>
<BlurView style={styles.backBtn} blurType="dark"> <BlurView style={styles.backBtn} blurType="dark">

View File

@ -96,8 +96,14 @@ export const ProfileScreen = withAuthRequired(
if (!uiState) { if (!uiState) {
return <View /> return <View />
} }
return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} /> return (
}, [uiState, onRefresh]) <ProfileHeader
view={uiState.profile}
onRefreshAll={onRefresh}
hideBackButton={route.params.hideBackButton}
/>
)
}, [uiState, onRefresh, route.params.hideBackButton])
const Footer = React.useMemo(() => { const Footer = React.useMemo(() => {
return uiState.showLoadingMoreFooter ? LoadingMoreFooter : undefined return uiState.showLoadingMoreFooter ? LoadingMoreFooter : undefined
}, [uiState.showLoadingMoreFooter]) }, [uiState.showLoadingMoreFooter])

View File

@ -27,6 +27,7 @@ import {
MagnifyingGlassIcon2, MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid, MagnifyingGlassIcon2Solid,
MoonIcon, MoonIcon,
UserIconSolid,
} from 'lib/icons' } from 'lib/icons'
import {UserAvatar} from 'view/com/util/UserAvatar' import {UserAvatar} from 'view/com/util/UserAvatar'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
@ -45,7 +46,8 @@ export const DrawerContent = observer(() => {
const store = useStores() const store = useStores()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics() const {track} = useAnalytics()
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationTabState() const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
useNavigationTabState()
// events // events
// = // =
@ -56,7 +58,7 @@ export const DrawerContent = observer(() => {
const state = navigation.getState() const state = navigation.getState()
store.shell.closeDrawer() store.shell.closeDrawer()
if (isWeb) { if (isWeb) {
// @ts-ignore must be Home, Search, or Notifications // @ts-ignore must be Home, Search, Notifications, or MyProfile
navigation.navigate(tab) navigation.navigate(tab)
} else { } else {
const tabState = getTabState(state, tab) const tabState = getTabState(state, tab)
@ -65,7 +67,7 @@ export const DrawerContent = observer(() => {
} else if (tabState === TabState.Inside) { } else if (tabState === TabState.Inside) {
navigation.dispatch(StackActions.popToTop()) navigation.dispatch(StackActions.popToTop())
} else { } else {
// @ts-ignore must be Home, Search, or Notifications // @ts-ignore must be Home, Search, Notifications, or MyProfile
navigation.navigate(`${tab}Tab`) navigation.navigate(`${tab}Tab`)
} }
} }
@ -86,10 +88,8 @@ export const DrawerContent = observer(() => {
) )
const onPressProfile = React.useCallback(() => { const onPressProfile = React.useCallback(() => {
track('Menu:ItemClicked', {url: 'Profile'}) onPressTab('MyProfile')
navigation.navigate('Profile', {name: store.me.handle}) }, [onPressTab])
store.shell.closeDrawer()
}, [navigation, track, store.me.handle, store.shell])
const onPressSettings = React.useCallback(() => { const onPressSettings = React.useCallback(() => {
track('Menu:ItemClicked', {url: 'Settings'}) track('Menu:ItemClicked', {url: 'Settings'})
@ -211,11 +211,19 @@ export const DrawerContent = observer(() => {
/> />
<MenuItem <MenuItem
icon={ icon={
isAtMyProfile ? (
<UserIconSolid
style={pal.text as StyleProp<ViewStyle>}
size="26"
strokeWidth={1.5}
/>
) : (
<UserIcon <UserIcon
style={pal.text as StyleProp<ViewStyle>} style={pal.text as StyleProp<ViewStyle>}
size="26" size="26"
strokeWidth={1.5} strokeWidth={1.5}
/> />
)
} }
label="Profile" label="Profile"
onPress={onPressProfile} onPress={onPressProfile}

View File

@ -5,7 +5,7 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native' } from 'react-native'
import {StackActions, useNavigationState} from '@react-navigation/native' import {StackActions} from '@react-navigation/native'
import {BottomTabBarProps} from '@react-navigation/bottom-tabs' import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
@ -21,34 +21,21 @@ import {
BellIcon, BellIcon,
BellIconSolid, BellIconSolid,
UserIcon, UserIcon,
UserIconSolid,
} from 'lib/icons' } from 'lib/icons'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {getTabState, TabState} from 'lib/routes/helpers' import {getTabState, TabState} from 'lib/routes/helpers'
import {styles} from './BottomBarStyles' import {styles} from './BottomBarStyles'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
export const BottomBar = observer(({navigation}: BottomTabBarProps) => { export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
const store = useStores() const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const {track} = useAnalytics() const {track} = useAnalytics()
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState( const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
state => { useNavigationTabState()
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 {footerMinimalShellTransform} = useMinimalShellMode() const {footerMinimalShellTransform} = useMinimalShellMode()
@ -77,9 +64,8 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
[onPressTab], [onPressTab],
) )
const onPressProfile = React.useCallback(() => { const onPressProfile = React.useCallback(() => {
track('MobileShell:ProfileButtonPressed') onPressTab('MyProfile')
navigation.navigate('Profile', {name: store.me.handle}) }, [onPressTab])
}, [navigation, track, store.me.handle])
return ( return (
<Animated.View <Animated.View
@ -154,11 +140,19 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
testID="bottomBarProfileBtn" testID="bottomBarProfileBtn"
icon={ icon={
<View style={styles.ctrlIconSizingWrapper}> <View style={styles.ctrlIconSizingWrapper}>
{isAtMyProfile ? (
<UserIconSolid
size={28}
strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
/>
) : (
<UserIcon <UserIcon
size={28} size={28}
strokeWidth={1.5} strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]} style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
/> />
)}
</View> </View>
} }
onPress={onPressProfile} onPress={onPressProfile}