refactor profile pictures's state

This commit is contained in:
João Ferreiro
2022-12-06 14:50:20 +00:00
parent 90d851c0c5
commit ed6bcfdb75
5 changed files with 62 additions and 63 deletions
+12 -14
View File
@@ -6,7 +6,6 @@ import {extractEntities} from '../../lib/strings'
import {Declaration} from './_common' import {Declaration} from './_common'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api' import * as apilib from '../lib/api'
import {Image} from 'react-native-image-crop-picker'
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser' export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
export const ACTOR_TYPE_SCENE = 'app.bsky.system.actorScene' export const ACTOR_TYPE_SCENE = 'app.bsky.system.actorScene'
@@ -44,13 +43,13 @@ export class ProfileViewModel {
postsCount: number = 0 postsCount: number = 0
myState = new ProfileViewMyStateModel() myState = new ProfileViewMyStateModel()
// data to be implemented in the protocol
userAvatar: string | null = null
userBanner: string | null = null
// added data // added data
descriptionEntities?: Entity[] descriptionEntities?: Entity[]
// temp state while imagery is not supported by the protocol
userAvatar: Image | null = null
userBanner: Image | null = null
constructor( constructor(
public rootStore: RootStoreModel, public rootStore: RootStoreModel,
params: GetProfile.QueryParams, params: GetProfile.QueryParams,
@@ -120,22 +119,21 @@ export class ProfileViewModel {
} }
} }
async updateProfile(fn: (existing?: Profile.Record) => Profile.Record) { async updateProfile(
fn: (existing?: Profile.Record) => Profile.Record,
userAvatar: string | null,
userBanner: string | null,
) {
await apilib.updateProfile(this.rootStore, this.did, fn) await apilib.updateProfile(this.rootStore, this.did, fn)
// add userBanner & userAvatar in the protocol when suported
this.userAvatar = userAvatar
this.userBanner = userBanner
await this.refresh() await this.refresh()
} }
// state transitions // state transitions
// = // =
updateUserAvatar(img: Image) {
this.userAvatar = img
}
updateUserBanner(img: Image) {
this.userBanner = img
}
private _xLoading(isRefreshing = false) { private _xLoading(isRefreshing = false) {
this.isLoading = true this.isLoading = true
this.isRefreshing = isRefreshing this.isRefreshing = isRefreshing
+12 -4
View File
@@ -33,6 +33,12 @@ export function Component({
const [description, setDescription] = useState<string>( const [description, setDescription] = useState<string>(
profileView.description || '', profileView.description || '',
) )
const [userBanner, setUserBanner] = useState<string | null>(
profileView.userBanner,
)
const [userAvatar, setUserAvatar] = useState<string | null>(
profileView.userAvatar,
)
const onPressCancel = () => { const onPressCancel = () => {
store.shell.closeModal() store.shell.closeModal()
} }
@@ -53,6 +59,8 @@ export function Component({
description, description,
} }
}, },
userAvatar,
userBanner,
) )
Toast.show('Profile updated') Toast.show('Profile updated')
onUpdate?.() onUpdate?.()
@@ -72,17 +80,17 @@ export function Component({
<View style={styles.photos}> <View style={styles.photos}>
<UserBanner <UserBanner
isMe isMe
isEditable userBanner={userBanner}
setUserBanner={setUserBanner}
handle={profileView.handle} handle={profileView.handle}
profileView={profileView}
/> />
<View style={styles.avi}> <View style={styles.avi}>
<UserAvatar <UserAvatar
isMe isMe
isEditable
size={80} size={80}
userAvatar={userAvatar}
handle={profileView.handle} handle={profileView.handle}
profileView={profileView} setUserAvatar={setUserAvatar}
displayName={profileView.displayName} displayName={profileView.displayName}
/> />
</View> </View>
+6 -2
View File
@@ -152,14 +152,18 @@ export const ProfileHeader = observer(function ProfileHeader({
} }
return ( return (
<View style={styles.outer}> <View style={styles.outer}>
<UserBanner isMe={isMe} handle={view.handle} profileView={view} /> <UserBanner
isMe={isMe}
handle={view.handle}
userBanner={view.userBanner}
/>
<View style={styles.avi}> <View style={styles.avi}>
<UserAvatar <UserAvatar
size={80} size={80}
isMe={isMe} isMe={isMe}
handle={view.handle} handle={view.handle}
displayName={view.displayName} displayName={view.displayName}
profileView={view} userAvatar={view.userAvatar}
/> />
</View> </View>
<View style={styles.content}> <View style={styles.content}>
+18 -21
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useState} from 'react' import React, {useCallback} from 'react'
import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native' import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg' import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -10,28 +10,25 @@ import {
import {getGradient} from '../../lib/asset-gen' import {getGradient} from '../../lib/asset-gen'
import {colors} from '../../lib/styles' import {colors} from '../../lib/styles'
import {IMAGES_ENABLED} from '../../../build-flags' import {IMAGES_ENABLED} from '../../../build-flags'
import {ProfileViewModel} from '../../../state/models/profile-view'
export function UserAvatar({ export function UserAvatar({
size, size,
handle, handle,
profileView, userAvatar,
displayName, displayName,
isMe = false, isMe = false,
isEditable = false, setUserAvatar,
}: { }: {
size: number size: number
handle: string handle: string
isMe?: boolean isMe?: boolean
isEditable?: boolean
displayName: string | undefined displayName: string | undefined
profileView: ProfileViewModel userAvatar: string | null
setUserAvatar?: React.Dispatch<React.SetStateAction<string | null>>
}) { }) {
const initials = getInitials(displayName || handle) const initials = getInitials(displayName || handle)
const gradient = getGradient(handle) const gradient = getGradient(handle)
const [tempUserAvatar, setTempUserAvatar] = useState<string | null>(null)
const handleEditAvatar = useCallback(() => { const handleEditAvatar = useCallback(() => {
Alert.alert('Select upload method', '', [ Alert.alert('Select upload method', '', [
{ {
@@ -44,8 +41,9 @@ export function UserAvatar({
height: 80, height: 80,
cropperCircleOverlay: true, cropperCircleOverlay: true,
}).then(item => { }).then(item => {
profileView.updateUserAvatar(item) if (setUserAvatar != null) {
setTempUserAvatar(item.path) setUserAvatar(item.path)
}
}) })
}, },
}, },
@@ -62,14 +60,15 @@ export function UserAvatar({
height: 80, height: 80,
cropperCircleOverlay: true, cropperCircleOverlay: true,
}).then(croppedItem => { }).then(croppedItem => {
profileView.updateUserAvatar(croppedItem) if (setUserAvatar != null) {
setTempUserAvatar(croppedItem.path) setUserAvatar(croppedItem.path)
}
}) })
}) })
}, },
}, },
]) ])
}, [profileView]) }, [setUserAvatar])
const renderSvg = (size: number, initials: string) => ( const renderSvg = (size: number, initials: string) => (
<Svg width={size} height={size} viewBox="0 0 100 100"> <Svg width={size} height={size} viewBox="0 0 100 100">
@@ -92,14 +91,12 @@ export function UserAvatar({
</Svg> </Svg>
) )
return isEditable && IMAGES_ENABLED ? ( // setUserAvatar is only passed as prop on the EditProfile component
return setUserAvatar != null && IMAGES_ENABLED ? (
<TouchableOpacity onPress={handleEditAvatar}> <TouchableOpacity onPress={handleEditAvatar}>
{/* Added a react state temporary photo while the protocol does not support imagery */} {/* Added a react state temporary photo while the protocol does not support imagery */}
{profileView.userAvatar != null || tempUserAvatar != null ? ( {userAvatar != null ? (
<Image <Image style={styles.avatarImage} source={{uri: userAvatar}} />
style={styles.avatarImage}
source={{uri: tempUserAvatar ?? profileView.userAvatar?.path}}
/>
) : ( ) : (
renderSvg(size, initials) renderSvg(size, initials)
)} )}
@@ -111,11 +108,11 @@ export function UserAvatar({
/> />
</View> </View>
</TouchableOpacity> </TouchableOpacity>
) : isMe && profileView.userAvatar != null ? ( ) : isMe && userAvatar != null ? (
<Image <Image
style={styles.avatarImage} style={styles.avatarImage}
resizeMode="stretch" resizeMode="stretch"
source={{uri: profileView.userAvatar.path}} source={{uri: userAvatar}}
/> />
) : ( ) : (
renderSvg(size, initials) renderSvg(size, initials)
+14 -22
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useState} from 'react' import React, {useCallback} from 'react'
import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native' import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg' import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -10,23 +10,20 @@ import {
openPicker, openPicker,
} from 'react-native-image-crop-picker' } from 'react-native-image-crop-picker'
import {IMAGES_ENABLED} from '../../../build-flags' import {IMAGES_ENABLED} from '../../../build-flags'
import {ProfileViewModel} from '../../../state/models/profile-view'
export function UserBanner({ export function UserBanner({
handle, handle,
profileView, userBanner,
isMe = false, isMe = false,
isEditable = false, setUserBanner,
}: { }: {
handle: string handle: string
isMe?: boolean isMe?: boolean
profileView: ProfileViewModel userBanner: string | null
isEditable?: boolean setUserBanner?: React.Dispatch<React.SetStateAction<string | null>>
}) { }) {
const gradient = getGradient(handle) const gradient = getGradient(handle)
const [tempUserBanner, setTempUserBanner] = useState<string | null>(null)
const handleEditBanner = useCallback(() => { const handleEditBanner = useCallback(() => {
Alert.alert('Select upload method', '', [ Alert.alert('Select upload method', '', [
{ {
@@ -38,8 +35,7 @@ export function UserBanner({
width: 1500, width: 1500,
height: 500, height: 500,
}).then(item => { }).then(item => {
profileView.updateUserBanner(item) setUserBanner(item.path)
setTempUserBanner(item.path)
}) })
}, },
}, },
@@ -55,14 +51,13 @@ export function UserBanner({
width: 1500, width: 1500,
height: 500, height: 500,
}).then(croppedItem => { }).then(croppedItem => {
profileView.updateUserBanner(croppedItem) setUserBanner(croppedItem.path)
setTempUserBanner(croppedItem.path)
}) })
}) })
}, },
}, },
]) ])
}, [profileView]) }, [setUserBanner])
const renderSvg = () => ( const renderSvg = () => (
<Svg width="100%" height="120" viewBox="50 0 200 100"> <Svg width="100%" height="120" viewBox="50 0 200 100">
@@ -81,15 +76,12 @@ export function UserBanner({
</Svg> </Svg>
) )
return isEditable && IMAGES_ENABLED ? ( // setUserBanner is only passed as prop on the EditProfile component
return setUserBanner != null && IMAGES_ENABLED ? (
<TouchableOpacity onPress={handleEditBanner}> <TouchableOpacity onPress={handleEditBanner}>
{/* Added a react state temporary photo while the protocol does not support imagery */} {/* Added a react state temporary photo while the protocol does not support imagery */}
{profileView.userBanner != null || tempUserBanner != null ? ( {userBanner != null ? (
<Image <Image style={styles.bannerImage} source={{uri: userBanner}} />
style={styles.bannerImage}
resizeMode="stretch"
source={{uri: tempUserBanner ?? profileView.userBanner?.path}}
/>
) : ( ) : (
renderSvg() renderSvg()
)} )}
@@ -101,11 +93,11 @@ export function UserBanner({
/> />
</View> </View>
</TouchableOpacity> </TouchableOpacity>
) : isMe && profileView.userBanner != null ? ( ) : isMe && userBanner != null ? (
<Image <Image
style={styles.bannerImage} style={styles.bannerImage}
resizeMode="stretch" resizeMode="stretch"
source={{uri: profileView.userBanner.path}} source={{uri: userBanner}}
/> />
) : ( ) : (
renderSvg() renderSvg()