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,
"scripts": {
"postinstall": "patch-package",
"prebuild": "expo prebuild",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",

View File

@ -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.

View File

@ -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
})
}

View File

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

View File

@ -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')
)
}

View File

@ -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

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}
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">

View File

@ -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])

View File

@ -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}

View File

@ -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}