diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/profile-view.ts | 14 | ||||
-rw-r--r-- | src/view/com/modals/EditProfile.tsx | 51 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeader.tsx | 5 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 108 | ||||
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 107 |
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, + }, +}) |