import React, {useCallback, useEffect, useState} from 'react' import {Dimensions, View} from 'react-native' import {Image as RNImage} from 'react-native-image-crop-picker' import {AppBskyActorDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {compressIfNeeded} from '#/lib/media/manip' import {cleanError} from '#/lib/strings/errors' import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {useProfileUpdateMutation} from '#/state/queries/profile' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import * as Toast from '#/view/com/util/Toast' import {EditableUserAvatar} from '#/view/com/util/UserAvatar' import {UserBanner} from '#/view/com/util/UserBanner' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import * as Prompt from '#/components/Prompt' const DISPLAY_NAME_MAX_GRAPHEMES = 64 const DESCRIPTION_MAX_GRAPHEMES = 256 const SCREEN_HEIGHT = Dimensions.get('window').height export function EditProfileDialog({ profile, control, onUpdate, }: { profile: AppBskyActorDefs.ProfileViewDetailed control: Dialog.DialogControlProps onUpdate?: () => void }) { const {_} = useLingui() const cancelControl = Dialog.useDialogControl() const [dirty, setDirty] = useState(false) // 'You might lose unsaved changes' warning useEffect(() => { if (isWeb && dirty) { const abortController = new AbortController() const {signal} = abortController window.addEventListener('beforeunload', evt => evt.preventDefault(), { signal, }) return () => { abortController.abort() } } }, [dirty]) const onPressCancel = useCallback(() => { if (dirty) { cancelControl.open() } else { control.close() } }, [dirty, control, cancelControl]) return ( control.close()} confirmButtonCta={_(msg`Discard`)} confirmButtonColor="negative" /> ) } function DialogInner({ profile, onUpdate, setDirty, onPressCancel, }: { profile: AppBskyActorDefs.ProfileViewDetailed onUpdate?: () => void setDirty: (dirty: boolean) => void onPressCancel: () => void }) { const {_} = useLingui() const t = useTheme() const control = Dialog.useDialogContext() const { mutateAsync: updateProfileMutation, error: updateProfileError, isError: isUpdateProfileError, isPending: isUpdatingProfile, } = useProfileUpdateMutation() const [imageError, setImageError] = useState('') const initialDisplayName = profile.displayName || '' const [displayName, setDisplayName] = useState(initialDisplayName) const initialDescription = profile.description || '' const [description, setDescription] = useState(initialDescription) 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 dirty = displayName !== initialDisplayName || description !== initialDescription || userAvatar !== profile.avatar || userBanner !== profile.banner useEffect(() => { setDirty(dirty) }, [dirty, setDirty]) 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 updateProfileMutation({ profile, updates: { displayName: displayName.trimEnd(), description: description.trimEnd(), }, newUserAvatar, newUserBanner, }) onUpdate?.() control.close() Toast.show(_(msg`Profile updated`)) } catch (e: any) { logger.error('Failed to update user profile', {message: String(e)}) } }, [ updateProfileMutation, profile, onUpdate, control, displayName, description, newUserAvatar, newUserBanner, setImageError, _, ]) const displayNameTooLong = useWarnMaxGraphemeCount({ text: displayName, maxCount: DISPLAY_NAME_MAX_GRAPHEMES, }) const descriptionTooLong = useWarnMaxGraphemeCount({ text: description, maxCount: DESCRIPTION_MAX_GRAPHEMES, }) const cancelButton = useCallback( () => ( ), [onPressCancel, _], ) const saveButton = useCallback( () => ( ), [ _, t, dirty, onPressSave, isUpdatingProfile, displayNameTooLong, descriptionTooLong, ], ) return ( Edit profile }> {isUpdateProfileError && ( )} {imageError !== '' && ( )} Display name {displayNameTooLong && ( Display name is too long. The maximum number of characters is{' '} {DISPLAY_NAME_MAX_GRAPHEMES}. )} Description {descriptionTooLong && ( Description is too long. The maximum number of characters is{' '} {DESCRIPTION_MAX_GRAPHEMES}. )} ) }