import {useCallback, useState} from 'react' import { ActivityIndicator, KeyboardAvoidingView, ScrollView, StyleSheet, TextInput, TouchableOpacity, View, } from 'react-native' import {Image as RNImage} from 'react-native-image-crop-picker' import Animated, {FadeOut} from 'react-native-reanimated' import {LinearGradient} from 'expo-linear-gradient' import {AppBskyActorDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from '#/lib/constants' import {usePalette} from '#/lib/hooks/usePalette' import {compressIfNeeded} from '#/lib/media/manip' import {cleanError} from '#/lib/strings/errors' import {enforceLen} from '#/lib/strings/helpers' import {colors, gradients, s} from '#/lib/styles' import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {useModalControls} from '#/state/modals' import {useProfileUpdateMutation} from '#/state/queries/profile' import {Text} from '#/view/com/util/text/Text' import * as Toast from '#/view/com/util/Toast' import {EditableUserAvatar} from '#/view/com/util/UserAvatar' import {UserBanner} from '#/view/com/util/UserBanner' import {ErrorMessage} from '../util/error/ErrorMessage' const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) export const snapPoints = ['fullscreen'] export function Component({ profile, onUpdate, }: { profile: AppBskyActorDefs.ProfileViewDetailed onUpdate?: () => void }) { const pal = usePalette('default') const theme = useTheme() const {_} = useLingui() const {closeModal} = useModalControls() const updateMutation = useProfileUpdateMutation() const [imageError, setImageError] = useState('') const [displayName, setDisplayName] = useState( profile.displayName || '', ) const [description, setDescription] = useState( profile.description || '', ) const [userBanner, setUserBanner] = useState( profile.banner, ) const [userAvatar, setUserAvatar] = useState( profile.avatar, ) const [newUserBanner, setNewUserBanner] = useState< RNImage | undefined | null >() const [newUserAvatar, setNewUserAvatar] = useState< RNImage | undefined | null >() const onPressCancel = () => { closeModal() } const onSelectNewAvatar = useCallback( async (img: RNImage | null) => { setImageError('') if (img === null) { setNewUserAvatar(null) setUserAvatar(null) return } try { const finalImg = await compressIfNeeded(img, 1000000) setNewUserAvatar(finalImg) setUserAvatar(finalImg.path) } catch (e: any) { setImageError(cleanError(e)) } }, [setNewUserAvatar, setUserAvatar, setImageError], ) const onSelectNewBanner = useCallback( async (img: RNImage | null) => { setImageError('') if (!img) { setNewUserBanner(null) setUserBanner(null) return } try { const finalImg = await compressIfNeeded(img, 1000000) setNewUserBanner(finalImg) setUserBanner(finalImg.path) } catch (e: any) { setImageError(cleanError(e)) } }, [setNewUserBanner, setUserBanner, setImageError], ) const onPressSave = useCallback(async () => { setImageError('') try { await updateMutation.mutateAsync({ profile, updates: { displayName, description, }, newUserAvatar, newUserBanner, }) Toast.show(_(msg`Profile updated`)) onUpdate?.() closeModal() } catch (e: any) { logger.error('Failed to update user profile', {message: String(e)}) } }, [ updateMutation, profile, onUpdate, closeModal, displayName, description, newUserAvatar, newUserBanner, setImageError, _, ]) return ( Edit my profile {updateMutation.isError && ( )} {imageError !== '' && ( )} Display Name setDisplayName(enforceLen(v, MAX_DISPLAY_NAME)) } accessible={true} accessibilityLabel={_(msg`Display name`)} accessibilityHint={_(msg`Edit your display name`)} /> Description setDescription(enforceLen(v, MAX_DESCRIPTION))} accessible={true} accessibilityLabel={_(msg`Description`)} accessibilityHint={_(msg`Edit your profile description`)} /> {updateMutation.isPending ? ( ) : ( Save Changes )} {!updateMutation.isPending && ( Cancel )} ) } const styles = StyleSheet.create({ title: { textAlign: 'center', fontWeight: '600', fontSize: 24, marginBottom: 18, }, label: { fontWeight: '600', paddingHorizontal: 4, paddingBottom: 4, marginTop: 20, }, form: { paddingHorizontal: 14, }, textInput: { borderWidth: 1, borderRadius: 6, paddingHorizontal: 14, paddingVertical: 10, fontSize: 16, }, textArea: { borderWidth: 1, borderRadius: 6, paddingHorizontal: 12, paddingTop: 10, fontSize: 16, height: 120, textAlignVertical: 'top', }, btn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', width: '100%', borderRadius: 32, padding: 10, marginBottom: 10, }, avi: { position: 'absolute', top: 80, left: 24, width: 84, height: 84, borderWidth: 2, borderRadius: 42, }, photos: { marginBottom: 36, marginHorizontal: -14, }, errorContainer: {marginTop: 20}, })