import {useCallback, useEffect, useRef, useState} from 'react' import {Pressable, View} from 'react-native' import {type AppBskyActorDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' import { HITSLOP_10, MAX_DESCRIPTION, MAX_DISPLAY_NAME, urls, } from '#/lib/constants' import {cleanError} from '#/lib/strings/errors' import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' import {isValidWebsiteFormat} from '#/lib/strings/website' import {logger} from '#/logger' import {type ImageMeta} from '#/state/gallery' import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' import {useProfileUpdateMutation} from '#/state/queries/profile' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {EditableUserAvatar} from '#/view/com/util/UserAvatar' import {UserBanner} from '#/view/com/util/UserBanner' import {atoms as a, useTheme} from '#/alf' import * as tokens from '#/alf/tokens' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {CircleX_Stroke2_Corner0_Rounded as CircleX} from '#/components/icons/CircleX' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' const PRONOUNS_MAX_GRAPHEMES = 20 const WEBSITE_MAX_GRAPHEMES = 2048 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) const onPressCancel = useCallback(() => { if (dirty) { cancelControl.open() } else { control.close() } }, [dirty, control, cancelControl]) return ( { if (dirty) { cancelControl.open() } else { control.close() } }, }} testID="editProfileModal"> 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 enableSquareButtons = useEnableSquareButtons() const verification = useSimpleVerificationState({ profile, }) 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 initialPronouns = profile.pronouns || '' const [pronouns, setPronouns] = useState(initialPronouns) const initialWebsite = profile.website || '' const [website, setWebsite] = useState(initialWebsite) const websiteInputRef = useRef(null) const [userBanner, setUserBanner] = useState( profile.banner, ) const [userAvatar, setUserAvatar] = useState( profile.avatar, ) const [newUserBanner, setNewUserBanner] = useState< ImageMeta | undefined | null >() const [newUserAvatar, setNewUserAvatar] = useState< ImageMeta | undefined | null >() const dirty = displayName !== initialDisplayName || description !== initialDescription || userAvatar !== profile.avatar || userBanner !== profile.banner || pronouns !== initialPronouns || website !== initialWebsite const enableSquareAvatars = useEnableSquareAvatars() useEffect(() => { setDirty(dirty) }, [dirty, setDirty]) const onSelectNewAvatar = useCallback( (img: ImageMeta | null) => { setImageError('') if (img === null) { setNewUserAvatar(null) setUserAvatar(null) return } try { setNewUserAvatar(img) setUserAvatar(img.path) } catch (e: any) { setImageError(cleanError(e)) } }, [setNewUserAvatar, setUserAvatar, setImageError], ) const onSelectNewBanner = useCallback( (img: ImageMeta | null) => { setImageError('') if (!img) { setNewUserBanner(null) setUserBanner(null) return } try { setNewUserBanner(img) setUserBanner(img.path) } catch (e: any) { setImageError(cleanError(e)) } }, [setNewUserBanner, setUserBanner, setImageError], ) const onClearWebsite = useCallback(() => { setWebsite('') if (websiteInputRef.current) { websiteInputRef.current.clear() } }, [setWebsite]) const onPressSave = useCallback(async () => { setImageError('') try { await updateProfileMutation({ profile, updates: { displayName: displayName.trimEnd(), description: description.trimEnd(), pronouns: pronouns.trimEnd().toLowerCase(), website: website.trimEnd(), }, newUserAvatar, newUserBanner, }) control.close(() => onUpdate?.()) Toast.show(_(msg({message: 'Profile updated', context: 'toast'}))) } catch (e: any) { logger.error('Failed to update user profile', {message: String(e)}) } }, [ updateProfileMutation, profile, onUpdate, control, displayName, description, pronouns, website, newUserAvatar, newUserBanner, setImageError, _, ]) const displayNameTooLong = isOverMaxGraphemeCount({ text: displayName, maxCount: MAX_DISPLAY_NAME, }) const pronounsTooLong = isOverMaxGraphemeCount({ text: pronouns, maxCount: PRONOUNS_MAX_GRAPHEMES, }) const websiteTooLong = isOverMaxGraphemeCount({ text: website, maxCount: WEBSITE_MAX_GRAPHEMES, }) const websiteInvalidFormat = !isValidWebsiteFormat(website) const descriptionTooLong = isOverMaxGraphemeCount({ text: description, maxCount: MAX_DESCRIPTION, }) const cancelButton = useCallback( () => ( ), [onPressCancel, _, enableSquareButtons], ) const saveButton = useCallback( () => ( ), [ _, t, dirty, onPressSave, isUpdatingProfile, displayNameTooLong, descriptionTooLong, enableSquareButtons, ], ) return ( Edit profile }> {isUpdateProfileError && ( )} {imageError !== '' && ( )} Display name {displayNameTooLong && ( )} {verification.isVerified && verification.role === 'default' && displayName !== initialDisplayName && ( You are verified. You will lose your verification status if you change your display name.{' '} Learn more. )} Description {descriptionTooLong && ( )} Pronouns {pronounsTooLong && ( )} Website {website && } {website && ( )} {websiteTooLong && ( )} {websiteInvalidFormat && ( Website must be a valid URI (e.g., https://bsky.app) )} ) }