Add banner image upload to profiles

zio/stable
Paul Frazee 2022-12-12 13:47:18 -06:00
parent 22ddfaa2b5
commit 693d6bfd0a
4 changed files with 50 additions and 35 deletions

View File

@ -39,15 +39,13 @@ export class ProfileViewModel {
displayName?: string displayName?: string
description?: string description?: string
avatar?: string avatar?: string
banner?: string
followersCount: number = 0 followersCount: number = 0
followsCount: number = 0 followsCount: number = 0
membersCount: number = 0 membersCount: number = 0
postsCount: number = 0 postsCount: number = 0
myState = new ProfileViewMyStateModel() myState = new ProfileViewMyStateModel()
// TODO TEMP data to be implemented in the protocol
userBanner: string | null = null
// added data // added data
descriptionEntities?: Entity[] descriptionEntities?: Entity[]
@ -123,11 +121,8 @@ export class ProfileViewModel {
async updateProfile( async updateProfile(
updates: Profile.Record, updates: Profile.Record,
newUserAvatar: PickedImage | undefined, newUserAvatar: PickedImage | undefined,
userBanner: string | null, // TODO TEMP newUserBanner: PickedImage | undefined,
) { ) {
// TODO TEMP add userBanner to the protocol when suported
this.userBanner = userBanner
if (newUserAvatar) { if (newUserAvatar) {
const res = await this.rootStore.api.com.atproto.blob.upload( const res = await this.rootStore.api.com.atproto.blob.upload(
newUserAvatar.path, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts newUserAvatar.path, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
@ -140,6 +135,18 @@ export class ProfileViewModel {
mimeType: newUserAvatar.mime, mimeType: newUserAvatar.mime,
} }
} }
if (newUserBanner) {
const res = await this.rootStore.api.com.atproto.blob.upload(
newUserBanner.path, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
{
encoding: newUserBanner.mime,
},
)
updates.banner = {
cid: res.data.cid,
mimeType: newUserBanner.mime,
}
}
await this.rootStore.api.app.bsky.actor.updateProfile(updates) await this.rootStore.api.app.bsky.actor.updateProfile(updates)
await this.rootStore.me.load() await this.rootStore.me.load()
await this.refresh() await this.refresh()
@ -187,6 +194,7 @@ export class ProfileViewModel {
this.displayName = res.data.displayName this.displayName = res.data.displayName
this.description = res.data.description this.description = res.data.description
this.avatar = res.data.avatar this.avatar = res.data.avatar
this.banner = res.data.banner
this.followersCount = res.data.followersCount this.followersCount = res.data.followersCount
this.followsCount = res.data.followsCount this.followsCount = res.data.followsCount
this.membersCount = res.data.membersCount this.membersCount = res.data.membersCount

View File

@ -41,12 +41,13 @@ export function Component({
const [description, setDescription] = useState<string>( const [description, setDescription] = useState<string>(
profileView.description || '', profileView.description || '',
) )
const [userBanner, setUserBanner] = useState<string | null>( const [userBanner, setUserBanner] = useState<string | undefined>(
profileView.userBanner, profileView.banner,
) )
const [userAvatar, setUserAvatar] = useState<string | undefined>( const [userAvatar, setUserAvatar] = useState<string | undefined>(
profileView.avatar, profileView.avatar,
) )
const [newUserBanner, setNewUserBanner] = useState<PickedImage | undefined>()
const [newUserAvatar, setNewUserAvatar] = useState<PickedImage | undefined>() const [newUserAvatar, setNewUserAvatar] = useState<PickedImage | undefined>()
const onPressCancel = () => { const onPressCancel = () => {
store.shell.closeModal() store.shell.closeModal()
@ -55,6 +56,10 @@ export function Component({
setNewUserAvatar(img) setNewUserAvatar(img)
setUserAvatar(img.path) setUserAvatar(img.path)
} }
const onSelectNewBanner = (img: PickedImage) => {
setNewUserBanner(img)
setUserBanner(img.path)
}
const onPressSave = async () => { const onPressSave = async () => {
setProcessing(true) setProcessing(true)
if (error) { if (error) {
@ -67,7 +72,7 @@ export function Component({
description, description,
}, },
newUserAvatar, newUserAvatar,
userBanner, // TEMP newUserBanner,
) )
Toast.show('Profile updated') Toast.show('Profile updated')
onUpdate?.() onUpdate?.()
@ -90,8 +95,8 @@ export function Component({
<Text style={styles.title}>Edit my profile</Text> <Text style={styles.title}>Edit my profile</Text>
<View style={styles.photos}> <View style={styles.photos}>
<UserBanner <UserBanner
userBanner={userBanner} banner={userBanner}
setUserBanner={setUserBanner} onSelectNewBanner={onSelectNewBanner}
handle={profileView.handle} handle={profileView.handle}
/> />
<View style={styles.avi}> <View style={styles.avi}>

View File

@ -152,7 +152,7 @@ export const ProfileHeader = observer(function ProfileHeader({
} }
return ( return (
<View style={styles.outer}> <View style={styles.outer}>
<UserBanner handle={view.handle} userBanner={view.userBanner} /> <UserBanner handle={view.handle} banner={view.banner} />
<View style={styles.avi}> <View style={styles.avi}>
<UserAvatar <UserAvatar
size={80} size={80}

View File

@ -2,6 +2,7 @@ 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'
import {Image as PickedImage} from 'react-native-image-crop-picker'
import {getGradient} from '../../lib/asset-gen' import {getGradient} from '../../lib/asset-gen'
import {colors} from '../../lib/styles' import {colors} from '../../lib/styles'
import { import {
@ -9,16 +10,15 @@ import {
openCropper, openCropper,
openPicker, openPicker,
} from 'react-native-image-crop-picker' } from 'react-native-image-crop-picker'
import {IMAGES_ENABLED} from '../../../build-flags'
export function UserBanner({ export function UserBanner({
handle, handle,
userBanner, banner,
setUserBanner, onSelectNewBanner,
}: { }: {
handle: string handle: string
userBanner?: string | null banner?: string | null
setUserBanner?: React.Dispatch<React.SetStateAction<string | null>> onSelectNewBanner?: (img: PickedImage) => void
}) { }) {
const gradient = getGradient(handle) const gradient = getGradient(handle)
@ -30,13 +30,14 @@ export function UserBanner({
openCamera({ openCamera({
mediaType: 'photo', mediaType: 'photo',
cropping: true, cropping: true,
compressImageMaxWidth: 1500,
width: 1500, width: 1500,
compressImageMaxHeight: 500,
height: 500, height: 500,
}).then(item => { forceJpg: true, // ios only
if (setUserBanner != null) { compressImageQuality: 0.4,
setUserBanner(item.path) includeExif: true,
} }).then(onSelectNewBanner)
})
}, },
}, },
{ {
@ -48,18 +49,19 @@ export function UserBanner({
await openCropper({ await openCropper({
mediaType: 'photo', mediaType: 'photo',
path: item.path, path: item.path,
compressImageMaxWidth: 1500,
width: 1500, width: 1500,
compressImageMaxHeight: 500,
height: 500, height: 500,
}).then(croppedItem => { forceJpg: true, // ios only
if (setUserBanner != null) { compressImageQuality: 0.4,
setUserBanner(croppedItem.path) includeExif: true,
} }).then(onSelectNewBanner)
})
}) })
}, },
}, },
]) ])
}, [setUserBanner]) }, [onSelectNewBanner])
const renderSvg = () => ( const renderSvg = () => (
<Svg width="100%" height="120" viewBox="50 0 200 100"> <Svg width="100%" height="120" viewBox="50 0 200 100">
@ -79,10 +81,10 @@ export function UserBanner({
) )
// setUserBanner is only passed as prop on the EditProfile component // setUserBanner is only passed as prop on the EditProfile component
return setUserBanner != null && IMAGES_ENABLED ? ( return onSelectNewBanner ? (
<TouchableOpacity onPress={handleEditBanner}> <TouchableOpacity onPress={handleEditBanner}>
{userBanner ? ( {banner ? (
<Image style={styles.bannerImage} source={{uri: userBanner}} /> <Image style={styles.bannerImage} source={{uri: banner}} />
) : ( ) : (
renderSvg() renderSvg()
)} )}
@ -94,11 +96,11 @@ export function UserBanner({
/> />
</View> </View>
</TouchableOpacity> </TouchableOpacity>
) : userBanner ? ( ) : banner ? (
<Image <Image
style={styles.bannerImage} style={styles.bannerImage}
resizeMode="stretch" resizeMode="cover"
source={{uri: userBanner}} source={{uri: banner}}
/> />
) : ( ) : (
renderSvg() renderSvg()