From c3d0cc55d98fb32b25cd2164cfa1c399985e7c84 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 15 Oct 2024 21:57:28 +0300 Subject: Edit profile dialog ALF refresh (#5633) --- src/screens/Profile/Header/EditProfileDialog.tsx | 370 +++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 src/screens/Profile/Header/EditProfileDialog.tsx (limited to 'src/screens/Profile/Header/EditProfileDialog.tsx') diff --git a/src/screens/Profile/Header/EditProfileDialog.tsx b/src/screens/Profile/Header/EditProfileDialog.tsx new file mode 100644 index 000000000..3cbae2a60 --- /dev/null +++ b/src/screens/Profile/Header/EditProfileDialog.tsx @@ -0,0 +1,370 @@ +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}. + + + )} + + + + ) +} -- cgit 1.4.1