about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/state/models/profile-view.ts14
-rw-r--r--src/view/com/modals/EditProfile.tsx51
-rw-r--r--src/view/com/profile/ProfileHeader.tsx5
-rw-r--r--src/view/com/util/UserAvatar.tsx108
-rw-r--r--src/view/com/util/UserBanner.tsx107
5 files changed, 269 insertions, 16 deletions
diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts
index 927374cc6..83389c82d 100644
--- a/src/state/models/profile-view.ts
+++ b/src/state/models/profile-view.ts
@@ -43,6 +43,10 @@ export class ProfileViewModel {
   postsCount: number = 0
   myState = new ProfileViewMyStateModel()
 
+  // TODO TEMP data to be implemented in the protocol
+  userAvatar: string | null = null
+  userBanner: string | null = null
+
   // added data
   descriptionEntities?: Entity[]
 
@@ -115,7 +119,15 @@ export class ProfileViewModel {
     }
   }
 
-  async updateProfile(fn: (existing?: Profile.Record) => Profile.Record) {
+  async updateProfile(
+    fn: (existing?: Profile.Record) => Profile.Record,
+    userAvatar: string | null, // TODO TEMP
+    userBanner: string | null, // TODO TEMP
+  ) {
+    // TODO TEMP add userBanner & userAvatar in the protocol when suported
+    this.userAvatar = userAvatar
+    this.userBanner = userBanner
+
     await apilib.updateProfile(this.rootStore, this.did, fn)
     await this.refresh()
   }
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index 1b5c99d97..895fb05a7 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -13,8 +13,10 @@ import {
   MAX_DESCRIPTION,
 } from '../../../lib/strings'
 import * as Profile from '../../../third-party/api/src/client/types/app/bsky/actor/profile'
+import {UserBanner} from '../util/UserBanner'
+import {UserAvatar} from '../util/UserAvatar'
 
-export const snapPoints = ['60%']
+export const snapPoints = ['80%']
 
 export function Component({
   profileView,
@@ -31,6 +33,12 @@ export function Component({
   const [description, setDescription] = useState<string>(
     profileView.description || '',
   )
+  const [userBanner, setUserBanner] = useState<string | null>(
+    profileView.userBanner,
+  )
+  const [userAvatar, setUserAvatar] = useState<string | null>(
+    profileView.userAvatar,
+  )
   const onPressCancel = () => {
     store.shell.closeModal()
   }
@@ -51,6 +59,8 @@ export function Component({
             description,
           }
         },
+        userAvatar, // TEMP
+        userBanner, // TEMP
       )
       Toast.show('Profile updated')
       onUpdate?.()
@@ -67,12 +77,28 @@ export function Component({
     <View style={s.flex1}>
       <BottomSheetScrollView style={styles.inner}>
         <Text style={styles.title}>Edit my profile</Text>
+        <View style={styles.photos}>
+          <UserBanner
+            userBanner={userBanner}
+            setUserBanner={setUserBanner}
+            handle={profileView.handle}
+          />
+          <View style={styles.avi}>
+            <UserAvatar
+              size={80}
+              userAvatar={userAvatar}
+              handle={profileView.handle}
+              setUserAvatar={setUserAvatar}
+              displayName={profileView.displayName}
+            />
+          </View>
+        </View>
         {error !== '' && (
           <View style={s.mb10}>
             <ErrorMessage message={error} />
           </View>
         )}
-        <View style={styles.group}>
+        <View>
           <Text style={styles.label}>Display Name</Text>
           <BottomSheetTextInput
             style={styles.textInput}
@@ -81,7 +107,7 @@ export function Component({
             onChangeText={v => setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))}
           />
         </View>
-        <View style={styles.group}>
+        <View style={s.pb10}>
           <Text style={styles.label}>Description</Text>
           <BottomSheetTextInput
             style={[styles.textArea]}
@@ -120,13 +146,11 @@ const styles = StyleSheet.create({
     fontSize: 24,
     marginBottom: 18,
   },
-  group: {
-    marginBottom: 10,
-  },
   label: {
     fontWeight: 'bold',
     paddingHorizontal: 4,
     paddingBottom: 4,
+    marginTop: 20,
   },
   textInput: {
     borderWidth: 1,
@@ -155,4 +179,19 @@ const styles = StyleSheet.create({
     padding: 10,
     marginBottom: 10,
   },
+  avi: {
+    position: 'absolute',
+    top: 80,
+    left: 10,
+    width: 84,
+    height: 84,
+    borderWidth: 2,
+    borderRadius: 42,
+    borderColor: colors.white,
+    backgroundColor: colors.white,
+  },
+  photos: {
+    marginBottom: 36,
+    marginHorizontal: -14,
+  },
 })
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 1b25c7c13..d23be65a3 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -152,12 +152,13 @@ export const ProfileHeader = observer(function ProfileHeader({
   }
   return (
     <View style={styles.outer}>
-      <UserBanner handle={view.handle} />
+      <UserBanner handle={view.handle} userBanner={view.userBanner} />
       <View style={styles.avi}>
         <UserAvatar
           size={80}
-          displayName={view.displayName}
           handle={view.handle}
+          displayName={view.displayName}
+          userAvatar={view.userAvatar}
         />
       </View>
       <View style={styles.content}>
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 9016cc4cc..2ed161253 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -1,19 +1,74 @@
-import React from 'react'
+import React, {useCallback} from 'react'
+import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
 import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {
+  openCamera,
+  openCropper,
+  openPicker,
+} from 'react-native-image-crop-picker'
 import {getGradient} from '../../lib/asset-gen'
+import {colors} from '../../lib/styles'
+import {IMAGES_ENABLED} from '../../../build-flags'
 
 export function UserAvatar({
   size,
-  displayName,
   handle,
+  userAvatar,
+  displayName,
+  setUserAvatar,
 }: {
   size: number
-  displayName: string | undefined
   handle: string
+  displayName: string | undefined
+  userAvatar: string | null
+  setUserAvatar?: React.Dispatch<React.SetStateAction<string | null>>
 }) {
   const initials = getInitials(displayName || handle)
   const gradient = getGradient(handle)
-  return (
+
+  const handleEditAvatar = useCallback(() => {
+    Alert.alert('Select upload method', '', [
+      {
+        text: 'Take a new photo',
+        onPress: () => {
+          openCamera({
+            mediaType: 'photo',
+            cropping: true,
+            width: 80,
+            height: 80,
+            cropperCircleOverlay: true,
+          }).then(item => {
+            if (setUserAvatar != null) {
+              setUserAvatar(item.path)
+            }
+          })
+        },
+      },
+      {
+        text: 'Select from gallery',
+        onPress: () => {
+          openPicker({
+            mediaType: 'photo',
+          }).then(async item => {
+            await openCropper({
+              mediaType: 'photo',
+              path: item.path,
+              width: 80,
+              height: 80,
+              cropperCircleOverlay: true,
+            }).then(croppedItem => {
+              if (setUserAvatar != null) {
+                setUserAvatar(croppedItem.path)
+              }
+            })
+          })
+        },
+      },
+    ])
+  }, [setUserAvatar])
+
+  const renderSvg = (size: number, initials: string) => (
     <Svg width={size} height={size} viewBox="0 0 100 100">
       <Defs>
         <LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
@@ -33,6 +88,32 @@ export function UserAvatar({
       </Text>
     </Svg>
   )
+
+  // setUserAvatar is only passed as prop on the EditProfile component
+  return setUserAvatar != null && IMAGES_ENABLED ? (
+    <TouchableOpacity onPress={handleEditAvatar}>
+      {userAvatar != null ? (
+        <Image style={styles.avatarImage} source={{uri: userAvatar}} />
+      ) : (
+        renderSvg(size, initials)
+      )}
+      <View style={styles.editButtonContainer}>
+        <FontAwesomeIcon
+          icon="camera"
+          size={12}
+          style={{color: colors.white}}
+        />
+      </View>
+    </TouchableOpacity>
+  ) : userAvatar != null ? (
+    <Image
+      style={styles.avatarImage}
+      resizeMode="stretch"
+      source={{uri: userAvatar}}
+    />
+  ) : (
+    renderSvg(size, initials)
+  )
 }
 
 function getInitials(str: string): string {
@@ -50,3 +131,22 @@ function getInitials(str: string): string {
   }
   return 'X'
 }
+
+const styles = StyleSheet.create({
+  editButtonContainer: {
+    position: 'absolute',
+    width: 24,
+    height: 24,
+    bottom: 0,
+    right: 0,
+    borderRadius: 12,
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: colors.gray5,
+  },
+  avatarImage: {
+    width: 80,
+    height: 80,
+    borderRadius: 40,
+  },
+})
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index 16e18c84d..c0421fe12 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -1,10 +1,67 @@
-import React from 'react'
+import React, {useCallback} from 'react'
+import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
 import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {getGradient} from '../../lib/asset-gen'
+import {colors} from '../../lib/styles'
+import {
+  openCamera,
+  openCropper,
+  openPicker,
+} from 'react-native-image-crop-picker'
+import {IMAGES_ENABLED} from '../../../build-flags'
 
-export function UserBanner({handle}: {handle: string}) {
+export function UserBanner({
+  handle,
+  userBanner,
+  setUserBanner,
+}: {
+  handle: string
+  userBanner: string | null
+  setUserBanner?: React.Dispatch<React.SetStateAction<string | null>>
+}) {
   const gradient = getGradient(handle)
-  return (
+
+  const handleEditBanner = useCallback(() => {
+    Alert.alert('Select upload method', '', [
+      {
+        text: 'Take a new photo',
+        onPress: () => {
+          openCamera({
+            mediaType: 'photo',
+            cropping: true,
+            width: 1500,
+            height: 500,
+          }).then(item => {
+            if (setUserBanner != null) {
+              setUserBanner(item.path)
+            }
+          })
+        },
+      },
+      {
+        text: 'Select from gallery',
+        onPress: () => {
+          openPicker({
+            mediaType: 'photo',
+          }).then(async item => {
+            await openCropper({
+              mediaType: 'photo',
+              path: item.path,
+              width: 1500,
+              height: 500,
+            }).then(croppedItem => {
+              if (setUserBanner != null) {
+                setUserBanner(croppedItem.path)
+              }
+            })
+          })
+        },
+      },
+    ])
+  }, [setUserBanner])
+
+  const renderSvg = () => (
     <Svg width="100%" height="120" viewBox="50 0 200 100">
       <Defs>
         <LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
@@ -20,4 +77,48 @@ export function UserBanner({handle}: {handle: string}) {
       <Rect x="0" y="0" width="400" height="100" fill="url(#grad2)" />
     </Svg>
   )
+
+  // setUserBanner is only passed as prop on the EditProfile component
+  return setUserBanner != null && IMAGES_ENABLED ? (
+    <TouchableOpacity onPress={handleEditBanner}>
+      {userBanner != null ? (
+        <Image style={styles.bannerImage} source={{uri: userBanner}} />
+      ) : (
+        renderSvg()
+      )}
+      <View style={styles.editButtonContainer}>
+        <FontAwesomeIcon
+          icon="camera"
+          size={12}
+          style={{color: colors.white}}
+        />
+      </View>
+    </TouchableOpacity>
+  ) : userBanner != null ? (
+    <Image
+      style={styles.bannerImage}
+      resizeMode="stretch"
+      source={{uri: userBanner}}
+    />
+  ) : (
+    renderSvg()
+  )
 }
+
+const styles = StyleSheet.create({
+  editButtonContainer: {
+    position: 'absolute',
+    width: 24,
+    height: 24,
+    bottom: 8,
+    right: 8,
+    borderRadius: 12,
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: colors.gray5,
+  },
+  bannerImage: {
+    width: '100%',
+    height: 120,
+  },
+})