// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useCallback, useEffect, useRef, useState } from 'react'; import * as grapheme from '../util/grapheme'; import { AvatarInputContainer } from './AvatarInputContainer'; import { AvatarInputType } from './AvatarInput'; import { Button, ButtonVariant } from './Button'; import { ConfirmationDialog } from './ConfirmationDialog'; import { Emoji } from './emoji/Emoji'; import { EmojiButton, Props as EmojiButtonProps } from './emoji/EmojiButton'; import { EmojiPickDataType } from './emoji/EmojiPicker'; import { Input } from './Input'; import { Intl } from './Intl'; import { LocalizerType } from '../types/Util'; import { PanelRow } from './conversation/conversation-details/PanelRow'; import { ProfileDataType } from '../state/ducks/conversations'; import { getEmojiData, unifiedToEmoji } from './emoji/lib'; import { missingCaseError } from '../util/missingCaseError'; export enum EditState { None = 'None', ProfileName = 'ProfileName', Bio = 'Bio', } type PropsExternalType = { onEditStateChanged: (editState: EditState) => unknown; onProfileChanged: ( profileData: ProfileDataType, avatarData?: ArrayBuffer ) => unknown; }; export type PropsDataType = { aboutEmoji?: string; aboutText?: string; avatarPath?: string; familyName?: string; firstName: string; i18n: LocalizerType; } & Pick; type PropsActionType = { onSetSkinTone: (tone: number) => unknown; }; export type PropsType = PropsDataType & PropsActionType & PropsExternalType; type DefaultBio = { i18nLabel: string; shortName: string; }; const DEFAULT_BIOS: Array = [ { i18nLabel: 'Bio--speak-freely', shortName: 'wave', }, { i18nLabel: 'Bio--encrypted', shortName: 'zipper_mouth_face', }, { i18nLabel: 'Bio--free-to-chat', shortName: '+1', }, { i18nLabel: 'Bio--coffee-lover', shortName: 'coffee', }, { i18nLabel: 'Bio--taking-break', shortName: 'mobile_phone_off', }, ]; export const ProfileEditor = ({ aboutEmoji, aboutText, avatarPath, familyName, firstName, i18n, onEditStateChanged, onProfileChanged, onSetSkinTone, recentEmojis, skinTone, }: PropsType): JSX.Element => { const focusInputRef = useRef(null); const [editState, setEditState] = useState(EditState.None); const [confirmDiscardAction, setConfirmDiscardAction] = useState< (() => unknown) | undefined >(undefined); // This is here to avoid component re-render jitters in the time it takes // redux to come back with the correct state const [fullName, setFullName] = useState({ familyName, firstName, }); const [fullBio, setFullBio] = useState({ aboutEmoji, aboutText, }); const [avatarData, setAvatarData] = useState( undefined ); const [stagedProfile, setStagedProfile] = useState({ aboutEmoji, aboutText, familyName, firstName, }); let content: JSX.Element; const handleBack = useCallback(() => { setEditState(EditState.None); onEditStateChanged(EditState.None); }, [setEditState, onEditStateChanged]); const setAboutEmoji = useCallback( (ev: EmojiPickDataType) => { const emojiData = getEmojiData(ev.shortName, skinTone); setStagedProfile(profileData => ({ ...profileData, aboutEmoji: unifiedToEmoji(emojiData.unified), })); }, [setStagedProfile, skinTone] ); const handleAvatarChanged = useCallback( (avatar: ArrayBuffer | undefined) => { setAvatarData(avatar); }, [setAvatarData] ); const calculateGraphemeCount = useCallback((name = '') => { return 256 - grapheme.count(name); }, []); useEffect(() => { const focusNode = focusInputRef.current; if (!focusNode) { return; } focusNode.focus(); }, [editState]); if (editState === EditState.ProfileName) { content = ( <> { setStagedProfile(profileData => ({ ...profileData, firstName: String(newFirstName), })); }} placeholder={i18n('ProfileEditor--first-name')} ref={focusInputRef} value={stagedProfile.firstName} /> { setStagedProfile(profileData => ({ ...profileData, familyName: newFamilyName, })); }} placeholder={i18n('ProfileEditor--last-name')} value={stagedProfile.familyName} />
); } else if (editState === EditState.Bio) { content = ( <> } maxGraphemeCount={140} moduleClassName="ProfileEditor__about-input" onChange={value => { if (value) { setStagedProfile(profileData => ({ ...profileData, aboutEmoji: stagedProfile.aboutEmoji, aboutText: value, })); } else { setStagedProfile(profileData => ({ ...profileData, aboutEmoji: undefined, aboutText: '', })); } }} ref={focusInputRef} placeholder={i18n('ProfileEditor--about-placeholder')} value={stagedProfile.aboutText} whenToShowRemainingCount={40} /> {DEFAULT_BIOS.map(defaultBio => ( } label={i18n(defaultBio.i18nLabel)} onClick={() => { const emojiData = getEmojiData(defaultBio.shortName, skinTone); setStagedProfile(profileData => ({ ...profileData, aboutEmoji: unifiedToEmoji(emojiData.unified), aboutText: i18n(defaultBio.i18nLabel), })); }} /> ))}
); } else if (editState === EditState.None) { const fullNameText = [fullName.firstName, fullName.familyName] .filter(Boolean) .join(' '); content = ( <> { handleAvatarChanged(avatar); onProfileChanged(stagedProfile, avatar); }} onAvatarLoaded={handleAvatarChanged} type={AvatarInputType.Profile} />
} label={fullNameText} onClick={() => { setEditState(EditState.ProfileName); onEditStateChanged(EditState.ProfileName); }} /> ) : ( ) } label={fullBio.aboutText || i18n('ProfileEditor--about')} onClick={() => { setEditState(EditState.Bio); onEditStateChanged(EditState.Bio); }} />
{i18n('ProfileEditor--learnMore')} ), }} />
); } else { throw missingCaseError(editState); } return ( <> {confirmDiscardAction && ( setConfirmDiscardAction(undefined)} > {i18n('ProfileEditor--discard')} )}
{content}
); };