Skip to content

Commit 141b067

Browse files
authored
Mobile profile header redesign (#14100)
1 parent 56ef027 commit 141b067

4 files changed

Lines changed: 109 additions & 60 deletions

File tree

packages/mobile/src/screens/app-screen/AppTabScreen.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,11 @@ export const AppTabScreen = ({ baseScreen, Stack }: AppTabScreenProps) => {
219219
<Stack.Screen name='Track' component={TrackScreen} />
220220
<Stack.Screen name='TrackRemixes' component={TrackRemixesScreen} />
221221
<Stack.Screen name='Collection' component={CollectionScreen} />
222-
<Stack.Screen name='Profile' component={ProfileScreen} />
222+
<Stack.Screen
223+
name='Profile'
224+
component={ProfileScreen}
225+
options={{ headerShown: false }}
226+
/>
223227
<Stack.Group>
224228
<Stack.Screen name='Followers' component={FollowersScreen} />
225229
<Stack.Screen name='Following' component={FollowingScreen} />

packages/mobile/src/screens/profile-screen/ProfileCoverPhoto.tsx

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,65 @@
1-
import { useProfileUser } from '@audius/common/api'
1+
import { useCallback } from 'react'
2+
3+
import { useCurrentUserId, useProfileUser } from '@audius/common/api'
4+
import { ShareSource } from '@audius/common/models'
5+
import { modalsActions, shareModalUIActions } from '@audius/common/store'
26
import { BlurView } from '@react-native-community/blur'
37
import { StyleSheet } from 'react-native'
48
import { useCurrentTabScrollY } from 'react-native-collapsible-tab-view'
59
import Animated, {
610
interpolate,
711
useAnimatedStyle
812
} from 'react-native-reanimated'
13+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
14+
import { useDispatch } from 'react-redux'
915

10-
import { Flex } from '@audius/harmony-native'
16+
import {
17+
Flex,
18+
IconButton,
19+
IconCaretLeft,
20+
IconKebabHorizontal,
21+
IconShare
22+
} from '@audius/harmony-native'
1123
import BadgeArtist from 'app/assets/images/badgeArtist.svg'
1224
import { CoverPhoto } from 'app/components/image/CoverPhoto'
25+
import { useNavigation } from 'app/hooks/useNavigation'
1326
import { makeStyles } from 'app/styles'
1427

28+
const { requestOpen: requestOpenShareModal } = shareModalUIActions
29+
const { setVisibility } = modalsActions
30+
1531
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
1632

33+
// Height below the status bar. The avatar is 80px tall and is positioned so
34+
// it extends ~32px below the cover photo, matching the Figma spec.
35+
const COVER_PHOTO_CONTENT_HEIGHT = 96
36+
1737
const useStyles = makeStyles(({ spacing }) => ({
18-
coverPhoto: {
19-
height: 96
38+
darkOverlay: {
39+
...StyleSheet.absoluteFillObject,
40+
backgroundColor: 'rgba(0, 0, 0, 0.2)'
41+
},
42+
actionsRow: {
43+
position: 'absolute',
44+
left: 0,
45+
right: 0,
46+
paddingHorizontal: spacing(4),
47+
paddingVertical: spacing(2)
2048
},
2149
artistBadge: {
2250
position: 'absolute',
23-
top: spacing(3),
24-
right: spacing(3)
51+
right: spacing(4),
52+
bottom: spacing(2)
2553
}
2654
}))
2755

2856
export const ProfileCoverPhoto = () => {
2957
const styles = useStyles()
58+
const insets = useSafeAreaInsets()
59+
const dispatch = useDispatch()
60+
const navigation = useNavigation()
61+
const { data: accountId } = useCurrentUserId()
62+
3063
const { user_id, track_count } =
3164
useProfileUser({
3265
select: (user) => ({
@@ -37,7 +70,9 @@ export const ProfileCoverPhoto = () => {
3770

3871
const scrollY = useCurrentTabScrollY()
3972

40-
const isArtist = track_count && track_count > 0
73+
const isArtist = !!track_count && track_count > 0
74+
const isOwner = !!user_id && user_id === accountId
75+
const coverPhotoHeight = insets.top + COVER_PHOTO_CONTENT_HEIGHT
4176

4277
const blurViewStyle = useAnimatedStyle(() => ({
4378
...StyleSheet.absoluteFillObject,
@@ -49,7 +84,6 @@ export const ProfileCoverPhoto = () => {
4984
}))
5085

5186
const badgeStyle = useAnimatedStyle(() => ({
52-
...styles.artistBadge,
5387
transform: [
5488
{
5589
translateY: interpolate(scrollY.value, [-200, 0], [-200, 0], {
@@ -60,19 +94,70 @@ export const ProfileCoverPhoto = () => {
6094
]
6195
}))
6296

97+
const handleBack = useCallback(() => {
98+
navigation.goBack()
99+
}, [navigation])
100+
101+
const handlePressActions = useCallback(() => {
102+
if (!user_id) return
103+
if (!isOwner) {
104+
dispatch(
105+
setVisibility({
106+
modal: 'ProfileActions',
107+
visible: true
108+
})
109+
)
110+
} else {
111+
dispatch(
112+
requestOpenShareModal({
113+
type: 'profile',
114+
profileId: user_id,
115+
source: ShareSource.PAGE
116+
})
117+
)
118+
}
119+
}, [dispatch, isOwner, user_id])
120+
63121
if (!user_id) return null
64122

65123
return (
66-
<Flex pointerEvents='none' h={96} backgroundColor='surface2'>
67-
<CoverPhoto style={styles.coverPhoto} userId={user_id}>
124+
<Flex
125+
pointerEvents='box-none'
126+
h={coverPhotoHeight}
127+
backgroundColor='surface2'
128+
>
129+
<CoverPhoto style={{ height: coverPhotoHeight }} userId={user_id}>
68130
<AnimatedBlurView
69131
blurType='dark'
70132
blurAmount={100}
71133
style={blurViewStyle}
72134
/>
135+
{isArtist ? <Animated.View style={styles.darkOverlay} /> : null}
73136
</CoverPhoto>
137+
<Flex
138+
direction='row'
139+
justifyContent='space-between'
140+
alignItems='center'
141+
pointerEvents='box-none'
142+
style={[styles.actionsRow, { top: insets.top }]}
143+
>
144+
<IconButton
145+
icon={IconCaretLeft}
146+
color='staticWhite'
147+
shadow='emphasis'
148+
onPress={handleBack}
149+
aria-label='Back'
150+
/>
151+
<IconButton
152+
icon={isOwner ? IconShare : IconKebabHorizontal}
153+
color='staticWhite'
154+
shadow='emphasis'
155+
onPress={handlePressActions}
156+
aria-label={isOwner ? 'Share profile' : 'Profile actions'}
157+
/>
158+
</Flex>
74159
{isArtist ? (
75-
<Animated.View style={badgeStyle}>
160+
<Animated.View style={[styles.artistBadge, badgeStyle]}>
76161
<BadgeArtist />
77162
</Animated.View>
78163
) : null}

packages/mobile/src/screens/profile-screen/ProfileHeader/ProfileHeader.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { useTierAndVerifiedForUser } from '@audius/common/store'
1010
import { css } from '@emotion/native'
1111
import { LayoutAnimation } from 'react-native'
12+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
1213
import { useToggle } from 'react-use'
1314

1415
import { Box, Divider, Flex, useTheme } from '@audius/harmony-native'
@@ -101,15 +102,16 @@ export const ProfileHeader = memo(() => {
101102
}, [isExpanded, setIsExpanded])
102103

103104
const { spacing } = useTheme()
105+
const insets = useSafeAreaInsets()
104106

105107
return (
106108
<>
107109
<ProfileCoverPhoto />
108110
<Box
109111
style={css({
110112
position: 'absolute',
111-
top: spacing.unit13,
112-
left: spacing.unit3,
113+
top: insets.top + spacing.unit12,
114+
left: spacing.unit4,
113115
zIndex: zIndex.PROFILE_PAGE_PROFILE_PICTURE
114116
})}
115117
>

packages/mobile/src/screens/profile-screen/ProfileScreen.tsx

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { useCallback } from 'react'
22

3-
import { useCurrentUserId, useUserByParams } from '@audius/common/api'
4-
import { ShareSource } from '@audius/common/models'
3+
import { useUserByParams } from '@audius/common/api'
54
import {
65
profilePageActions,
76
reachabilitySelectors,
8-
shareModalUIActions,
9-
modalsActions,
107
ProfilePageTabs
118
} from '@audius/common/store'
129
import { encodeUrlName } from '@audius/common/utils'
@@ -15,27 +12,21 @@ import { useFocusEffect, useNavigationState } from '@react-navigation/native'
1512
import { View } from 'react-native'
1613
import { useDispatch, useSelector } from 'react-redux'
1714

18-
import {
19-
IconButton,
20-
IconKebabHorizontal,
21-
IconShare
22-
} from '@audius/harmony-native'
2315
import { Screen, ScreenContent } from 'app/components/core'
2416
import { ScreenPrimaryContent } from 'app/components/core/Screen/ScreenPrimaryContent'
2517
import { ScreenSecondaryContent } from 'app/components/core/Screen/ScreenSecondaryContent'
2618
import { OfflinePlaceholder } from 'app/components/offline-placeholder'
2719
import { useRoute } from 'app/hooks/useRoute'
20+
import { useStatusBarStyle } from 'app/hooks/useStatusBarStyle'
2821
import { makeStyles } from 'app/styles'
2922

3023
import { DeactivatedProfileTombstone } from './DeactivatedProfileTombstone'
3124
import { ProfileHeader } from './ProfileHeader'
3225
import { ProfileScreenSkeleton } from './ProfileScreenSkeleton'
3326
import { ProfileTabNavigator } from './ProfileTabs/ProfileTabNavigator'
3427
import { useRefreshProfile } from './useRefreshProfile'
35-
const { requestOpen: requestOpenShareModal } = shareModalUIActions
3628
const { setCurrentUser: setCurrentUserAction } = profilePageActions
3729
const { getIsReachable } = reachabilitySelectors
38-
const { setVisibility } = modalsActions
3930

4031
const useStyles = makeStyles(() => ({
4132
navigator: {
@@ -59,8 +50,6 @@ export const ProfileScreen = () => {
5950
const handle =
6051
userHandle && userHandle !== 'accountUser' ? userHandle : profile?.handle
6152
const handleLower = handle?.toLowerCase() ?? ''
62-
const { data: accountUserId } = useCurrentUserId()
63-
const isOwner = accountUserId === profile?.user_id
6453
const dispatch = useDispatch()
6554
const isNotReachable = useSelector(getIsReachable) === false
6655

@@ -104,43 +93,12 @@ export const ProfileScreen = () => {
10493
)
10594

10695
useFocusEffect(setCurrentUser)
107-
108-
const handlePressTopRight = useCallback(() => {
109-
if (profile) {
110-
if (!isOwner) {
111-
dispatch(
112-
setVisibility({
113-
modal: 'ProfileActions',
114-
visible: true
115-
})
116-
)
117-
} else {
118-
dispatch(
119-
requestOpenShareModal({
120-
type: 'profile',
121-
profileId: profile.user_id,
122-
source: ShareSource.PAGE
123-
})
124-
)
125-
}
126-
}
127-
}, [profile, dispatch, isOwner])
128-
129-
const topbarRight = (
130-
<IconButton
131-
color='subdued'
132-
icon={!isOwner ? IconKebabHorizontal : IconShare}
133-
onPress={handlePressTopRight}
134-
/>
135-
)
96+
useStatusBarStyle('light-content')
13697

13798
const renderHeader = useCallback(() => <ProfileHeader />, [])
13899

139100
return (
140-
<Screen
141-
topbarRight={topbarRight}
142-
url={handle && `/${encodeUrlName(handle)}`}
143-
>
101+
<Screen url={handle && `/${encodeUrlName(handle)}`}>
144102
<ScreenContent isOfflineCapable>
145103
{!profile ? (
146104
<ProfileScreenSkeleton />

0 commit comments

Comments
 (0)