11import type { AlertRef } from '@packrat/ui/nativewindui' ;
22import {
33 ActivityIndicator ,
4- Alert ,
4+ Alert as AlertComponent ,
55 Avatar ,
66 AvatarFallback ,
7+ AvatarImage ,
78 Button ,
89 ESTIMATED_ITEM_HEIGHT ,
910 List ,
@@ -18,13 +19,17 @@ import { withAuthWall } from 'expo-app/features/auth/hocs';
1819import { useAuth } from 'expo-app/features/auth/hooks/useAuth' ;
1920import { useUser } from 'expo-app/features/auth/hooks/useUser' ;
2021import { ProfileAuthWall } from 'expo-app/features/profile/components' ;
22+ import { useUpdateProfile } from 'expo-app/features/profile/hooks/useUpdateProfile' ;
23+ import { useImagePicker } from 'expo-app/features/packs/hooks/useImagePicker' ;
24+ import { uploadImage } from 'expo-app/features/packs/utils/uploadImage' ;
2125import { cn } from 'expo-app/lib/cn' ;
2226import { hasUnsyncedChanges } from 'expo-app/lib/hasUnsyncedChanges' ;
2327import { useTranslation } from 'expo-app/lib/hooks/useTranslation' ;
24- import { Stack } from 'expo-router' ;
28+ import { buildPackTemplateItemImageUrl } from 'expo-app/lib/utils/buildPackTemplateItemImageUrl' ;
29+ import { router , Stack } from 'expo-router' ;
2530import * as Updates from 'expo-updates' ;
2631import { useRef , useState } from 'react' ;
27- import { Platform , SafeAreaView , View } from 'react-native' ;
32+ import { Alert , Platform , SafeAreaView , TouchableOpacity , View } from 'react-native' ;
2833
2934const ESTIMATED_ITEM_SIZE =
3035 ESTIMATED_ITEM_HEIGHT [ Platform . OS === 'ios' ? 'titleOnly' : 'withSubTitle' ] ;
@@ -52,6 +57,7 @@ function Profile() {
5257 {
5358 id : 'name' ,
5459 title : t ( 'common.name' ) ,
60+ onPress : ( ) => router . push ( '/(app)/(tabs)/profile/name' ) ,
5561 ...( Platform . OS === 'ios' ? { value : displayName } : { subTitle : displayName } ) ,
5662 } ,
5763 {
@@ -92,6 +98,7 @@ function Item({ info }: { info: ListRenderItemInfo<DataItem> }) {
9298 return (
9399 < ListItem
94100 titleClassName = "text-lg"
101+ onPress = { info . item . onPress }
95102 rightView = {
96103 < View className = "flex-1 flex-row items-center gap-0.5 px-2" >
97104 { ! ! info . item . value && < Text className = "text-muted-foreground" > { info . item . value } </ Text > }
@@ -104,6 +111,11 @@ function Item({ info }: { info: ListRenderItemInfo<DataItem> }) {
104111
105112function ListHeaderComponent ( ) {
106113 const user = useUser ( ) ;
114+ const { updateProfile } = useUpdateProfile ( ) ;
115+ const { pickImage } = useImagePicker ( ) ;
116+ const [ isUploading , setIsUploading ] = useState ( false ) ;
117+ const { t } = useTranslation ( ) ;
118+
107119 const initials =
108120 user ?. firstName && user ?. lastName
109121 ? `${ user . firstName [ 0 ] } ${ user . lastName [ 0 ] } `
@@ -116,21 +128,51 @@ function ListHeaderComponent() {
116128
117129 const username = user ?. email || '' ;
118130
131+ // Build the full avatar URL from the stored R2 key or an absolute URL
132+ const avatarUri = user ?. avatarUrl ? buildPackTemplateItemImageUrl ( user . avatarUrl ) : null ;
133+
134+ async function handleAvatarPress ( ) {
135+ try {
136+ const image = await pickImage ( ) ;
137+ if ( ! image ) return ;
138+ setIsUploading ( true ) ;
139+ const remoteFileName = await uploadImage ( image . fileName , image . uri ) ;
140+ if ( remoteFileName ) {
141+ await updateProfile ( { avatarUrl : remoteFileName } ) ;
142+ }
143+ } catch ( err ) {
144+ if ( err instanceof Error && err . message !== 'Permission to access media library was denied' ) {
145+ Alert . alert ( t ( 'errors.somethingWentWrong' ) , t ( 'errors.tryAgain' ) ) ;
146+ }
147+ } finally {
148+ setIsUploading ( false ) ;
149+ }
150+ }
151+
119152 return (
120153 < SafeAreaView className = "ios:pb-8 items-center pb-4 pt-8" >
121- < Avatar alt = { `${ displayName } 's Profile` } className = "h-24 w-24" >
122- < AvatarFallback >
123- < Text
124- variant = "largeTitle"
125- className = { cn (
126- 'font-medium text-white dark:text-background' ,
127- Platform . OS === 'ios' && 'dark:text-foreground' ,
154+ < TouchableOpacity onPress = { handleAvatarPress } disabled = { isUploading } >
155+ < Avatar alt = { `${ displayName } 's Profile` } className = "h-24 w-24" >
156+ { avatarUri ? (
157+ < AvatarImage source = { { uri : avatarUri } } />
158+ ) : null }
159+ < AvatarFallback >
160+ { isUploading ? (
161+ < ActivityIndicator />
162+ ) : (
163+ < Text
164+ variant = "largeTitle"
165+ className = { cn (
166+ 'font-medium text-white dark:text-background' ,
167+ Platform . OS === 'ios' && 'dark:text-foreground' ,
168+ ) }
169+ >
170+ { initials }
171+ </ Text >
128172 ) }
129- >
130- { initials }
131- </ Text >
132- </ AvatarFallback >
133- </ Avatar >
173+ </ AvatarFallback >
174+ </ Avatar >
175+ </ TouchableOpacity >
134176 < View className = "p-1" />
135177 < Text variant = "title1" > { displayName } </ Text >
136178 < Text className = "text-muted-foreground" > { username } </ Text >
@@ -216,7 +258,7 @@ function ListFooterComponent() {
216258 < Text className = "text-destructive" > { t ( 'auth.logOut' ) } </ Text >
217259 ) }
218260 </ Button >
219- < Alert title = "" buttons = { [ ] } ref = { alertRef } />
261+ < AlertComponent title = "" buttons = { [ ] } ref = { alertRef } />
220262 </ View >
221263 ) ;
222264}
0 commit comments