855 lines
25 KiB
TypeScript
855 lines
25 KiB
TypeScript
// Copyright 2021-2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
import classNames from 'classnames';
|
|
|
|
import * as log from '../logging/log';
|
|
import type { AvatarColorType } from '../types/Colors';
|
|
import { AvatarColors } from '../types/Colors';
|
|
import type {
|
|
AvatarDataType,
|
|
AvatarUpdateType,
|
|
DeleteAvatarFromDiskActionType,
|
|
ReplaceAvatarActionType,
|
|
SaveAvatarToDiskActionType,
|
|
} from '../types/Avatar';
|
|
import { AvatarEditor } from './AvatarEditor';
|
|
import { AvatarPreview } from './AvatarPreview';
|
|
import { Button, ButtonVariant } from './Button';
|
|
import { ConfirmDiscardDialog } from './ConfirmDiscardDialog';
|
|
import { Emoji } from './emoji/Emoji';
|
|
import type { Props as EmojiButtonProps } from './emoji/EmojiButton';
|
|
import { EmojiButton } from './emoji/EmojiButton';
|
|
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
|
import { Input } from './Input';
|
|
import { Intl } from './Intl';
|
|
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
|
|
import { Modal } from './Modal';
|
|
import { PanelRow } from './conversation/conversation-details/PanelRow';
|
|
import type { ProfileDataType } from '../state/ducks/conversations';
|
|
import { getEmojiData, unifiedToEmoji } from './emoji/lib';
|
|
import { missingCaseError } from '../util/missingCaseError';
|
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
|
import {
|
|
ConversationDetailsIcon,
|
|
IconType,
|
|
} from './conversation/conversation-details/ConversationDetailsIcon';
|
|
import { Spinner } from './Spinner';
|
|
import { UsernameSaveState } from '../state/ducks/conversationsEnums';
|
|
import { MAX_USERNAME, MIN_USERNAME } from '../types/Username';
|
|
import { isWhitespace, trim } from '../util/whitespaceStringUtil';
|
|
|
|
export enum EditState {
|
|
None = 'None',
|
|
BetterAvatar = 'BetterAvatar',
|
|
ProfileName = 'ProfileName',
|
|
Bio = 'Bio',
|
|
Username = 'Username',
|
|
}
|
|
|
|
enum UsernameEditState {
|
|
Editing = 'Editing',
|
|
ConfirmingDelete = 'ConfirmingDelete',
|
|
ShowingErrorPopup = 'ShowingErrorPopup',
|
|
Saving = 'Saving',
|
|
}
|
|
|
|
type PropsExternalType = {
|
|
onEditStateChanged: (editState: EditState) => unknown;
|
|
onProfileChanged: (
|
|
profileData: ProfileDataType,
|
|
avatar: AvatarUpdateType
|
|
) => unknown;
|
|
};
|
|
|
|
export type PropsDataType = {
|
|
aboutEmoji?: string;
|
|
aboutText?: string;
|
|
profileAvatarPath?: string;
|
|
color?: AvatarColorType;
|
|
conversationId: string;
|
|
familyName?: string;
|
|
firstName: string;
|
|
i18n: LocalizerType;
|
|
isUsernameFlagEnabled: boolean;
|
|
usernameSaveState: UsernameSaveState;
|
|
userAvatarData: Array<AvatarDataType>;
|
|
username?: string;
|
|
} & Pick<EmojiButtonProps, 'recentEmojis' | 'skinTone'>;
|
|
|
|
type PropsActionType = {
|
|
clearUsernameSave: () => unknown;
|
|
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
|
onSetSkinTone: (tone: number) => unknown;
|
|
replaceAvatar: ReplaceAvatarActionType;
|
|
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
|
saveUsername: (options: {
|
|
username: string | undefined;
|
|
previousUsername: string | undefined;
|
|
}) => unknown;
|
|
};
|
|
|
|
export type PropsType = PropsDataType & PropsActionType & PropsExternalType;
|
|
|
|
type DefaultBio = {
|
|
i18nLabel: string;
|
|
shortName: string;
|
|
};
|
|
|
|
const DEFAULT_BIOS: Array<DefaultBio> = [
|
|
{
|
|
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',
|
|
},
|
|
];
|
|
|
|
function getUsernameInvalidKey(
|
|
username: string | undefined
|
|
): { key: string; replacements?: ReplacementValuesType } | undefined {
|
|
if (!username) {
|
|
return undefined;
|
|
}
|
|
|
|
if (username.length < MIN_USERNAME) {
|
|
return {
|
|
key: 'ProfileEditor--username--check-character-min',
|
|
replacements: { min: MIN_USERNAME },
|
|
};
|
|
}
|
|
|
|
if (!/^[0-9a-z_]+$/.test(username)) {
|
|
return { key: 'ProfileEditor--username--check-characters' };
|
|
}
|
|
if (!/^[a-z_]/.test(username)) {
|
|
return { key: 'ProfileEditor--username--check-starting-character' };
|
|
}
|
|
|
|
if (username.length > MAX_USERNAME) {
|
|
return {
|
|
key: 'ProfileEditor--username--check-character-max',
|
|
replacements: { max: MAX_USERNAME },
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function mapSaveStateToEditState({
|
|
clearUsernameSave,
|
|
i18n,
|
|
setEditState,
|
|
setUsernameEditState,
|
|
setUsernameError,
|
|
usernameSaveState,
|
|
}: {
|
|
clearUsernameSave: () => unknown;
|
|
i18n: LocalizerType;
|
|
setEditState: (state: EditState) => unknown;
|
|
setUsernameEditState: (state: UsernameEditState) => unknown;
|
|
setUsernameError: (errorText: string) => unknown;
|
|
usernameSaveState: UsernameSaveState;
|
|
}): void {
|
|
if (usernameSaveState === UsernameSaveState.None) {
|
|
return;
|
|
}
|
|
if (usernameSaveState === UsernameSaveState.Saving) {
|
|
setUsernameEditState(UsernameEditState.Saving);
|
|
return;
|
|
}
|
|
|
|
clearUsernameSave();
|
|
|
|
if (usernameSaveState === UsernameSaveState.Success) {
|
|
setEditState(EditState.None);
|
|
setUsernameEditState(UsernameEditState.Editing);
|
|
|
|
return;
|
|
}
|
|
|
|
if (usernameSaveState === UsernameSaveState.UsernameMalformedError) {
|
|
setUsernameEditState(UsernameEditState.Editing);
|
|
setUsernameError(i18n('ProfileEditor--username--check-characters'));
|
|
return;
|
|
}
|
|
if (usernameSaveState === UsernameSaveState.UsernameTakenError) {
|
|
setUsernameEditState(UsernameEditState.Editing);
|
|
setUsernameError(i18n('ProfileEditor--username--check-username-taken'));
|
|
return;
|
|
}
|
|
if (usernameSaveState === UsernameSaveState.GeneralError) {
|
|
setUsernameEditState(UsernameEditState.ShowingErrorPopup);
|
|
return;
|
|
}
|
|
if (usernameSaveState === UsernameSaveState.DeleteFailed) {
|
|
setUsernameEditState(UsernameEditState.Editing);
|
|
return;
|
|
}
|
|
|
|
const state: never = usernameSaveState;
|
|
log.error(
|
|
`ProfileEditor: useEffect username didn't handle usernameSaveState '${state})'`
|
|
);
|
|
setEditState(EditState.None);
|
|
}
|
|
|
|
export const ProfileEditor = ({
|
|
aboutEmoji,
|
|
aboutText,
|
|
profileAvatarPath,
|
|
clearUsernameSave,
|
|
color,
|
|
conversationId,
|
|
deleteAvatarFromDisk,
|
|
familyName,
|
|
firstName,
|
|
i18n,
|
|
isUsernameFlagEnabled,
|
|
onEditStateChanged,
|
|
onProfileChanged,
|
|
onSetSkinTone,
|
|
recentEmojis,
|
|
replaceAvatar,
|
|
saveAvatarToDisk,
|
|
saveUsername,
|
|
skinTone,
|
|
userAvatarData,
|
|
username,
|
|
usernameSaveState,
|
|
}: PropsType): JSX.Element => {
|
|
const focusInputRef = useRef<HTMLInputElement | null>(null);
|
|
const [editState, setEditState] = useState<EditState>(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 [newUsername, setNewUsername] = useState<string | undefined>(username);
|
|
const [usernameError, setUsernameError] = useState<string | undefined>();
|
|
const [usernameEditState, setUsernameEditState] = useState<UsernameEditState>(
|
|
UsernameEditState.Editing
|
|
);
|
|
|
|
const [startingAvatarPath, setStartingAvatarPath] =
|
|
useState(profileAvatarPath);
|
|
|
|
const [oldAvatarBuffer, setOldAvatarBuffer] = useState<
|
|
Uint8Array | undefined
|
|
>(undefined);
|
|
const [avatarBuffer, setAvatarBuffer] = useState<Uint8Array | undefined>(
|
|
undefined
|
|
);
|
|
const [isLoadingAvatar, setIsLoadingAvatar] = useState(
|
|
Boolean(profileAvatarPath)
|
|
);
|
|
const [stagedProfile, setStagedProfile] = useState<ProfileDataType>({
|
|
aboutEmoji,
|
|
aboutText,
|
|
familyName,
|
|
firstName,
|
|
});
|
|
|
|
// To make AvatarEditor re-render less often
|
|
const handleBack = useCallback(() => {
|
|
setEditState(EditState.None);
|
|
onEditStateChanged(EditState.None);
|
|
}, [setEditState, onEditStateChanged]);
|
|
|
|
// To make EmojiButton re-render less often
|
|
const setAboutEmoji = useCallback(
|
|
(ev: EmojiPickDataType) => {
|
|
const emojiData = getEmojiData(ev.shortName, skinTone);
|
|
setStagedProfile(profileData => ({
|
|
...profileData,
|
|
aboutEmoji: unifiedToEmoji(emojiData.unified),
|
|
}));
|
|
},
|
|
[setStagedProfile, skinTone]
|
|
);
|
|
|
|
// To make AvatarEditor re-render less often
|
|
const handleAvatarChanged = useCallback(
|
|
(avatar: Uint8Array | undefined) => {
|
|
// Do not display stale avatar from disk anymore.
|
|
setStartingAvatarPath(undefined);
|
|
|
|
setAvatarBuffer(avatar);
|
|
setEditState(EditState.None);
|
|
onProfileChanged(
|
|
{
|
|
...stagedProfile,
|
|
firstName: trim(stagedProfile.firstName),
|
|
familyName: stagedProfile.familyName
|
|
? trim(stagedProfile.familyName)
|
|
: undefined,
|
|
},
|
|
{ oldAvatar: oldAvatarBuffer, newAvatar: avatar }
|
|
);
|
|
setOldAvatarBuffer(avatar);
|
|
},
|
|
[onProfileChanged, stagedProfile, oldAvatarBuffer]
|
|
);
|
|
|
|
const getFullNameText = () => {
|
|
return [fullName.firstName, fullName.familyName].filter(Boolean).join(' ');
|
|
};
|
|
|
|
useEffect(() => {
|
|
const focusNode = focusInputRef.current;
|
|
if (!focusNode) {
|
|
return;
|
|
}
|
|
|
|
focusNode.focus();
|
|
focusNode.setSelectionRange(focusNode.value.length, focusNode.value.length);
|
|
}, [editState]);
|
|
|
|
useEffect(() => {
|
|
onEditStateChanged(editState);
|
|
}, [editState, onEditStateChanged]);
|
|
|
|
// If there's some in-process username save, or just an unacknowledged save
|
|
// completion/error, we clear it out on mount, and then again on unmount.
|
|
useEffect(() => {
|
|
clearUsernameSave();
|
|
|
|
return () => {
|
|
clearUsernameSave();
|
|
};
|
|
});
|
|
|
|
useEffect(() => {
|
|
mapSaveStateToEditState({
|
|
clearUsernameSave,
|
|
i18n,
|
|
setEditState,
|
|
setUsernameEditState,
|
|
setUsernameError,
|
|
usernameSaveState,
|
|
});
|
|
}, [
|
|
clearUsernameSave,
|
|
i18n,
|
|
setEditState,
|
|
setUsernameEditState,
|
|
setUsernameError,
|
|
usernameSaveState,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
// Whenever the user makes a change, we'll get rid of the red error text
|
|
setUsernameError(undefined);
|
|
|
|
// And then we'll check the validity of that new username
|
|
const timeout = setTimeout(() => {
|
|
const key = getUsernameInvalidKey(newUsername);
|
|
if (key) {
|
|
setUsernameError(i18n(key.key, key.replacements));
|
|
}
|
|
}, 1000);
|
|
return () => {
|
|
clearTimeout(timeout);
|
|
};
|
|
}, [newUsername, i18n, setUsernameError]);
|
|
|
|
const isCurrentlySaving = usernameEditState === UsernameEditState.Saving;
|
|
const shouldDisableUsernameSave = Boolean(
|
|
newUsername === username ||
|
|
!newUsername ||
|
|
usernameError ||
|
|
isCurrentlySaving
|
|
);
|
|
|
|
const checkThenSaveUsername = () => {
|
|
if (isCurrentlySaving) {
|
|
log.error('checkThenSaveUsername: Already saving! Returning early');
|
|
return;
|
|
}
|
|
|
|
if (shouldDisableUsernameSave) {
|
|
return;
|
|
}
|
|
|
|
const invalidKey = getUsernameInvalidKey(newUsername);
|
|
if (invalidKey) {
|
|
setUsernameError(i18n(invalidKey.key, invalidKey.replacements));
|
|
return;
|
|
}
|
|
|
|
setUsernameError(undefined);
|
|
setUsernameEditState(UsernameEditState.Saving);
|
|
saveUsername({ username: newUsername, previousUsername: username });
|
|
};
|
|
|
|
const deleteUsername = () => {
|
|
if (isCurrentlySaving) {
|
|
log.error('deleteUsername: Already saving! Returning early');
|
|
return;
|
|
}
|
|
|
|
setNewUsername(undefined);
|
|
setUsernameError(undefined);
|
|
setUsernameEditState(UsernameEditState.Saving);
|
|
saveUsername({ username: undefined, previousUsername: username });
|
|
};
|
|
|
|
// To make AvatarEditor re-render less often
|
|
const handleAvatarLoaded = useCallback(
|
|
avatar => {
|
|
setAvatarBuffer(avatar);
|
|
setOldAvatarBuffer(avatar);
|
|
setIsLoadingAvatar(false);
|
|
},
|
|
[setAvatarBuffer, setOldAvatarBuffer, setIsLoadingAvatar]
|
|
);
|
|
|
|
let content: JSX.Element;
|
|
|
|
if (editState === EditState.BetterAvatar) {
|
|
content = (
|
|
<AvatarEditor
|
|
avatarColor={color || AvatarColors[0]}
|
|
avatarPath={startingAvatarPath}
|
|
avatarValue={avatarBuffer}
|
|
conversationId={conversationId}
|
|
conversationTitle={getFullNameText()}
|
|
deleteAvatarFromDisk={deleteAvatarFromDisk}
|
|
i18n={i18n}
|
|
onCancel={handleBack}
|
|
onSave={handleAvatarChanged}
|
|
userAvatarData={userAvatarData}
|
|
replaceAvatar={replaceAvatar}
|
|
saveAvatarToDisk={saveAvatarToDisk}
|
|
/>
|
|
);
|
|
} else if (editState === EditState.ProfileName) {
|
|
const shouldDisableSave =
|
|
isLoadingAvatar ||
|
|
!stagedProfile.firstName ||
|
|
(stagedProfile.firstName === fullName.firstName &&
|
|
stagedProfile.familyName === fullName.familyName) ||
|
|
isWhitespace(stagedProfile.firstName);
|
|
|
|
content = (
|
|
<>
|
|
<Input
|
|
i18n={i18n}
|
|
maxLengthCount={26}
|
|
maxByteCount={128}
|
|
whenToShowRemainingCount={0}
|
|
onChange={newFirstName => {
|
|
setStagedProfile(profileData => ({
|
|
...profileData,
|
|
firstName: String(newFirstName),
|
|
}));
|
|
}}
|
|
placeholder={i18n('ProfileEditor--first-name')}
|
|
ref={focusInputRef}
|
|
value={stagedProfile.firstName}
|
|
/>
|
|
<Input
|
|
i18n={i18n}
|
|
maxLengthCount={26}
|
|
maxByteCount={128}
|
|
whenToShowRemainingCount={0}
|
|
onChange={newFamilyName => {
|
|
setStagedProfile(profileData => ({
|
|
...profileData,
|
|
familyName: newFamilyName,
|
|
}));
|
|
}}
|
|
placeholder={i18n('ProfileEditor--last-name')}
|
|
value={stagedProfile.familyName}
|
|
/>
|
|
<Modal.ButtonFooter>
|
|
<Button
|
|
onClick={() => {
|
|
const handleCancel = () => {
|
|
handleBack();
|
|
setStagedProfile(profileData => ({
|
|
...profileData,
|
|
familyName,
|
|
firstName,
|
|
}));
|
|
};
|
|
|
|
const hasChanges =
|
|
stagedProfile.familyName !== fullName.familyName ||
|
|
stagedProfile.firstName !== fullName.firstName;
|
|
if (hasChanges) {
|
|
setConfirmDiscardAction(() => handleCancel);
|
|
} else {
|
|
handleCancel();
|
|
}
|
|
}}
|
|
variant={ButtonVariant.Secondary}
|
|
>
|
|
{i18n('cancel')}
|
|
</Button>
|
|
<Button
|
|
disabled={shouldDisableSave}
|
|
onClick={() => {
|
|
if (!stagedProfile.firstName) {
|
|
return;
|
|
}
|
|
setFullName({
|
|
firstName: stagedProfile.firstName,
|
|
familyName: stagedProfile.familyName,
|
|
});
|
|
|
|
onProfileChanged(stagedProfile, {
|
|
oldAvatar: oldAvatarBuffer,
|
|
newAvatar: avatarBuffer,
|
|
});
|
|
handleBack();
|
|
}}
|
|
>
|
|
{i18n('save')}
|
|
</Button>
|
|
</Modal.ButtonFooter>
|
|
</>
|
|
);
|
|
} else if (editState === EditState.Bio) {
|
|
const shouldDisableSave =
|
|
isLoadingAvatar ||
|
|
(stagedProfile.aboutText === fullBio.aboutText &&
|
|
stagedProfile.aboutEmoji === fullBio.aboutEmoji);
|
|
|
|
content = (
|
|
<>
|
|
<Input
|
|
expandable
|
|
hasClearButton
|
|
i18n={i18n}
|
|
icon={
|
|
<div className="module-composition-area__button-cell">
|
|
<EmojiButton
|
|
closeOnPick
|
|
emoji={stagedProfile.aboutEmoji}
|
|
i18n={i18n}
|
|
onPickEmoji={setAboutEmoji}
|
|
onSetSkinTone={onSetSkinTone}
|
|
recentEmojis={recentEmojis}
|
|
skinTone={skinTone}
|
|
/>
|
|
</div>
|
|
}
|
|
maxLengthCount={140}
|
|
maxByteCount={512}
|
|
moduleClassName="ProfileEditor__about-input"
|
|
onChange={value => {
|
|
if (value) {
|
|
setStagedProfile(profileData => ({
|
|
...profileData,
|
|
aboutEmoji: stagedProfile.aboutEmoji,
|
|
aboutText: value.replace(/(\r\n|\n|\r)/gm, ''),
|
|
}));
|
|
} else {
|
|
setStagedProfile(profileData => ({
|
|
...profileData,
|
|
aboutEmoji: undefined,
|
|
aboutText: '',
|
|
}));
|
|
}
|
|
}}
|
|
ref={focusInputRef}
|
|
placeholder={i18n('ProfileEditor--about-placeholder')}
|
|
value={stagedProfile.aboutText}
|
|
whenToShowRemainingCount={40}
|
|
/>
|
|
|
|
{DEFAULT_BIOS.map(defaultBio => (
|
|
<PanelRow
|
|
className="ProfileEditor__row"
|
|
key={defaultBio.shortName}
|
|
icon={
|
|
<div className="ProfileEditor__icon--container">
|
|
<Emoji shortName={defaultBio.shortName} size={24} />
|
|
</div>
|
|
}
|
|
label={i18n(defaultBio.i18nLabel)}
|
|
onClick={() => {
|
|
const emojiData = getEmojiData(defaultBio.shortName, skinTone);
|
|
|
|
setStagedProfile(profileData => ({
|
|
...profileData,
|
|
aboutEmoji: unifiedToEmoji(emojiData.unified),
|
|
aboutText: i18n(defaultBio.i18nLabel),
|
|
}));
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
<Modal.ButtonFooter>
|
|
<Button
|
|
onClick={() => {
|
|
const handleCancel = () => {
|
|
handleBack();
|
|
setStagedProfile(profileData => ({
|
|
...profileData,
|
|
...fullBio,
|
|
}));
|
|
};
|
|
|
|
const hasChanges =
|
|
stagedProfile.aboutText !== fullBio.aboutText ||
|
|
stagedProfile.aboutEmoji !== fullBio.aboutEmoji;
|
|
if (hasChanges) {
|
|
setConfirmDiscardAction(() => handleCancel);
|
|
} else {
|
|
handleCancel();
|
|
}
|
|
}}
|
|
variant={ButtonVariant.Secondary}
|
|
>
|
|
{i18n('cancel')}
|
|
</Button>
|
|
<Button
|
|
disabled={shouldDisableSave}
|
|
onClick={() => {
|
|
setFullBio({
|
|
aboutEmoji: stagedProfile.aboutEmoji,
|
|
aboutText: stagedProfile.aboutText,
|
|
});
|
|
|
|
onProfileChanged(stagedProfile, {
|
|
oldAvatar: oldAvatarBuffer,
|
|
newAvatar: avatarBuffer,
|
|
});
|
|
handleBack();
|
|
}}
|
|
>
|
|
{i18n('save')}
|
|
</Button>
|
|
</Modal.ButtonFooter>
|
|
</>
|
|
);
|
|
} else if (editState === EditState.Username) {
|
|
content = (
|
|
<>
|
|
<Input
|
|
i18n={i18n}
|
|
disabled={isCurrentlySaving}
|
|
disableSpellcheck
|
|
onChange={changedUsername => {
|
|
setUsernameError(undefined);
|
|
setNewUsername(changedUsername);
|
|
}}
|
|
onEnter={checkThenSaveUsername}
|
|
placeholder={i18n('ProfileEditor--username--placeholder')}
|
|
ref={focusInputRef}
|
|
value={newUsername}
|
|
/>
|
|
|
|
{usernameError && (
|
|
<div className="ProfileEditor__error">{usernameError}</div>
|
|
)}
|
|
<div
|
|
className={classNames(
|
|
'ProfileEditor__info',
|
|
!usernameError ? 'ProfileEditor__info--no-error' : undefined
|
|
)}
|
|
>
|
|
<Intl i18n={i18n} id="ProfileEditor--username--helper" />
|
|
</div>
|
|
|
|
<Modal.ButtonFooter>
|
|
<Button
|
|
disabled={isCurrentlySaving}
|
|
onClick={() => {
|
|
const handleCancel = () => {
|
|
handleBack();
|
|
setNewUsername(username);
|
|
};
|
|
|
|
const hasChanges = newUsername !== username;
|
|
if (hasChanges) {
|
|
setConfirmDiscardAction(() => handleCancel);
|
|
} else {
|
|
handleCancel();
|
|
}
|
|
}}
|
|
variant={ButtonVariant.Secondary}
|
|
>
|
|
{i18n('cancel')}
|
|
</Button>
|
|
<Button
|
|
disabled={shouldDisableUsernameSave}
|
|
onClick={checkThenSaveUsername}
|
|
>
|
|
{isCurrentlySaving ? (
|
|
<Spinner size="20px" svgSize="small" direction="on-avatar" />
|
|
) : (
|
|
i18n('save')
|
|
)}
|
|
</Button>
|
|
</Modal.ButtonFooter>
|
|
</>
|
|
);
|
|
} else if (editState === EditState.None) {
|
|
content = (
|
|
<>
|
|
<AvatarPreview
|
|
avatarColor={color}
|
|
avatarPath={startingAvatarPath}
|
|
avatarValue={avatarBuffer}
|
|
conversationTitle={getFullNameText()}
|
|
i18n={i18n}
|
|
isEditable
|
|
onAvatarLoaded={handleAvatarLoaded}
|
|
onClick={() => {
|
|
setEditState(EditState.BetterAvatar);
|
|
}}
|
|
style={{
|
|
height: 80,
|
|
width: 80,
|
|
}}
|
|
/>
|
|
<hr className="ProfileEditor__divider" />
|
|
<PanelRow
|
|
className="ProfileEditor__row"
|
|
icon={
|
|
<i className="ProfileEditor__icon--container ProfileEditor__icon ProfileEditor__icon--name" />
|
|
}
|
|
label={getFullNameText()}
|
|
onClick={() => {
|
|
setEditState(EditState.ProfileName);
|
|
}}
|
|
/>
|
|
{isUsernameFlagEnabled ? (
|
|
<PanelRow
|
|
className="ProfileEditor__row"
|
|
icon={
|
|
<i className="ProfileEditor__icon--container ProfileEditor__icon ProfileEditor__icon--username" />
|
|
}
|
|
label={username || i18n('ProfileEditor--username')}
|
|
onClick={
|
|
usernameEditState !== UsernameEditState.Saving
|
|
? () => {
|
|
setNewUsername(username);
|
|
setEditState(EditState.Username);
|
|
}
|
|
: undefined
|
|
}
|
|
actions={
|
|
username ? (
|
|
<ConversationDetailsIcon
|
|
ariaLabel={i18n('ProfileEditor--username--delete-username')}
|
|
icon={
|
|
usernameEditState === UsernameEditState.Saving
|
|
? IconType.spinner
|
|
: IconType.trash
|
|
}
|
|
disabled={usernameEditState === UsernameEditState.Saving}
|
|
fakeButton
|
|
onClick={() => {
|
|
setUsernameEditState(UsernameEditState.ConfirmingDelete);
|
|
}}
|
|
/>
|
|
) : null
|
|
}
|
|
/>
|
|
) : null}
|
|
<PanelRow
|
|
className="ProfileEditor__row"
|
|
icon={
|
|
fullBio.aboutEmoji ? (
|
|
<div className="ProfileEditor__icon--container">
|
|
<Emoji emoji={fullBio.aboutEmoji} size={24} />
|
|
</div>
|
|
) : (
|
|
<i className="ProfileEditor__icon--container ProfileEditor__icon ProfileEditor__icon--bio" />
|
|
)
|
|
}
|
|
label={fullBio.aboutText || i18n('ProfileEditor--about')}
|
|
onClick={() => {
|
|
setEditState(EditState.Bio);
|
|
}}
|
|
/>
|
|
<hr className="ProfileEditor__divider" />
|
|
<div className="ProfileEditor__info">
|
|
<Intl
|
|
i18n={i18n}
|
|
id="ProfileEditor--info"
|
|
components={{
|
|
learnMore: (
|
|
<a
|
|
href="https://support.signal.org/hc/en-us/articles/360007459591"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
{i18n('ProfileEditor--learnMore')}
|
|
</a>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
} else {
|
|
throw missingCaseError(editState);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{usernameEditState === UsernameEditState.ConfirmingDelete && (
|
|
<ConfirmationDialog
|
|
i18n={i18n}
|
|
onClose={() => setUsernameEditState(UsernameEditState.Editing)}
|
|
actions={[
|
|
{
|
|
text: i18n('ProfileEditor--username--confirm-delete-button'),
|
|
style: 'negative',
|
|
action: () => deleteUsername(),
|
|
},
|
|
]}
|
|
>
|
|
{i18n('ProfileEditor--username--confirm-delete-body')}
|
|
</ConfirmationDialog>
|
|
)}
|
|
{usernameEditState === UsernameEditState.ShowingErrorPopup && (
|
|
<ConfirmationDialog
|
|
cancelText={i18n('ok')}
|
|
cancelButtonVariant={ButtonVariant.Secondary}
|
|
i18n={i18n}
|
|
onClose={() => setUsernameEditState(UsernameEditState.Editing)}
|
|
>
|
|
{i18n('ProfileEditor--username--general-error')}
|
|
</ConfirmationDialog>
|
|
)}
|
|
{confirmDiscardAction && (
|
|
<ConfirmDiscardDialog
|
|
i18n={i18n}
|
|
onDiscard={confirmDiscardAction}
|
|
onClose={() => setConfirmDiscardAction(undefined)}
|
|
/>
|
|
)}
|
|
<div className="ProfileEditor">{content}</div>
|
|
</>
|
|
);
|
|
};
|