283 lines
8.2 KiB
TypeScript
283 lines
8.2 KiB
TypeScript
import { ChangeEvent } from 'react';
|
|
import React, {
|
|
FC, useState, useCallback, memo, useEffect, useMemo,
|
|
} from '../../../lib/teact/teact';
|
|
import { withGlobal } from '../../../lib/teact/teactn';
|
|
|
|
import { ApiMediaFormat } from '../../../api/types';
|
|
import { GlobalActions } from '../../../global/types';
|
|
import { ProfileEditProgress } from '../../../types';
|
|
|
|
import { throttle } from '../../../util/schedulers';
|
|
import { pick } from '../../../util/iteratees';
|
|
import { selectUser } from '../../../modules/selectors';
|
|
import { getChatAvatarHash } from '../../../modules/helpers';
|
|
import useMedia from '../../../hooks/useMedia';
|
|
import useLang from '../../../hooks/useLang';
|
|
|
|
import AvatarEditable from '../../ui/AvatarEditable';
|
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
|
import Spinner from '../../ui/Spinner';
|
|
import InputText from '../../ui/InputText';
|
|
import renderText from '../../common/helpers/renderText';
|
|
import UsernameInput from '../../common/UsernameInput';
|
|
|
|
type StateProps = {
|
|
currentAvatarHash?: string;
|
|
currentFirstName?: string;
|
|
currentLastName?: string;
|
|
currentBio?: string;
|
|
currentUsername?: string;
|
|
progress?: ProfileEditProgress;
|
|
isUsernameAvailable?: boolean;
|
|
};
|
|
|
|
type DispatchProps = Pick<GlobalActions, (
|
|
'loadCurrentUser' | 'updateProfile' | 'checkUsername'
|
|
)>;
|
|
|
|
const runThrottled = throttle((cb) => cb(), 60000, true);
|
|
|
|
const MAX_BIO_LENGTH = 70;
|
|
|
|
const ERROR_FIRST_NAME_MISSING = 'Please provide your first name';
|
|
const ERROR_BIO_TOO_LONG = 'Bio can\' be longer than 70 characters';
|
|
|
|
const SettingsEditProfile: FC<StateProps & DispatchProps> = ({
|
|
currentAvatarHash,
|
|
currentFirstName,
|
|
currentLastName,
|
|
currentBio,
|
|
currentUsername,
|
|
progress,
|
|
isUsernameAvailable,
|
|
loadCurrentUser,
|
|
updateProfile,
|
|
checkUsername,
|
|
}) => {
|
|
const [isUsernameTouched, setIsUsernameTouched] = useState(false);
|
|
const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false);
|
|
const [error, setError] = useState<string | undefined>();
|
|
|
|
const [photo, setPhoto] = useState<File | undefined>();
|
|
const [firstName, setFirstName] = useState(currentFirstName || '');
|
|
const [lastName, setLastName] = useState(currentLastName || '');
|
|
const [bio, setBio] = useState(currentBio || '');
|
|
const [username, setUsername] = useState<string | false>(currentUsername || '');
|
|
|
|
const currentAvatarBlobUrl = useMedia(currentAvatarHash, false, ApiMediaFormat.BlobUrl);
|
|
|
|
const isLoading = progress === ProfileEditProgress.InProgress;
|
|
const isUsernameError = username === false;
|
|
|
|
const isSaveButtonShown = useMemo(() => {
|
|
if (isUsernameError) {
|
|
return false;
|
|
}
|
|
|
|
return Boolean(photo) || isProfileFieldsTouched || isUsernameAvailable === true;
|
|
}, [photo, isProfileFieldsTouched, isUsernameError, isUsernameAvailable]);
|
|
|
|
// Due to the parent Transition, this component never gets unmounted,
|
|
// that's why we use throttled API call on every update.
|
|
useEffect(() => {
|
|
runThrottled(() => {
|
|
loadCurrentUser();
|
|
});
|
|
}, [loadCurrentUser]);
|
|
|
|
useEffect(() => {
|
|
setPhoto(undefined);
|
|
}, [currentAvatarBlobUrl]);
|
|
|
|
useEffect(() => {
|
|
setFirstName(currentFirstName || '');
|
|
setLastName(currentLastName || '');
|
|
setBio(currentBio || '');
|
|
}, [currentFirstName, currentLastName, currentBio]);
|
|
|
|
useEffect(() => {
|
|
setUsername(currentUsername || '');
|
|
}, [currentUsername]);
|
|
|
|
useEffect(() => {
|
|
if (progress === ProfileEditProgress.Complete) {
|
|
setIsProfileFieldsTouched(false);
|
|
setIsUsernameTouched(false);
|
|
setError(undefined);
|
|
}
|
|
}, [progress]);
|
|
|
|
const handlePhotoChange = useCallback((newPhoto: File) => {
|
|
setPhoto(newPhoto);
|
|
}, []);
|
|
|
|
const handleFirstNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
setFirstName(e.target.value);
|
|
setIsProfileFieldsTouched(true);
|
|
}, []);
|
|
|
|
const handleLastNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
setLastName(e.target.value);
|
|
setIsProfileFieldsTouched(true);
|
|
}, []);
|
|
|
|
const handleBioChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
setBio(e.target.value);
|
|
setIsProfileFieldsTouched(true);
|
|
}, []);
|
|
|
|
const handleUsernameChange = useCallback((value: string | false) => {
|
|
setUsername(value);
|
|
setIsUsernameTouched(true);
|
|
}, []);
|
|
|
|
const handleProfileSave = useCallback(() => {
|
|
const trimmedFirstName = firstName.trim();
|
|
const trimmedLastName = lastName.trim();
|
|
const trimmedBio = bio.trim();
|
|
|
|
if (!trimmedFirstName.length) {
|
|
setError(ERROR_FIRST_NAME_MISSING);
|
|
return;
|
|
}
|
|
|
|
if (trimmedBio.length > MAX_BIO_LENGTH) {
|
|
setError(ERROR_BIO_TOO_LONG);
|
|
return;
|
|
}
|
|
|
|
updateProfile({
|
|
photo,
|
|
...(isProfileFieldsTouched && {
|
|
firstName: trimmedFirstName,
|
|
lastName: trimmedLastName,
|
|
bio: trimmedBio,
|
|
}),
|
|
...(isUsernameTouched && {
|
|
username,
|
|
}),
|
|
});
|
|
}, [
|
|
photo,
|
|
firstName, lastName, bio, isProfileFieldsTouched,
|
|
username, isUsernameTouched,
|
|
updateProfile,
|
|
]);
|
|
|
|
const lang = useLang();
|
|
|
|
return (
|
|
<div className="settings-fab-wrapper">
|
|
<div className="settings-content custom-scroll">
|
|
<div className="settings-edit-profile">
|
|
<AvatarEditable
|
|
currentAvatarBlobUrl={currentAvatarBlobUrl}
|
|
onChange={handlePhotoChange}
|
|
title="Edit your profile photo"
|
|
disabled={isLoading}
|
|
/>
|
|
<InputText
|
|
value={firstName}
|
|
onChange={handleFirstNameChange}
|
|
label={lang('FirstName')}
|
|
disabled={isLoading}
|
|
error={error === ERROR_FIRST_NAME_MISSING ? error : undefined}
|
|
/>
|
|
<InputText
|
|
value={lastName}
|
|
onChange={handleLastNameChange}
|
|
label={lang('LastName')}
|
|
disabled={isLoading}
|
|
/>
|
|
<InputText
|
|
value={bio}
|
|
onChange={handleBioChange}
|
|
label={lang('UserBio')}
|
|
disabled={isLoading}
|
|
error={error === ERROR_BIO_TOO_LONG ? error : undefined}
|
|
/>
|
|
|
|
<p className="settings-item-description">
|
|
{renderText(lang('lng_settings_about_bio'), ['br', 'simple_markdown'])}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="settings-item">
|
|
<h4 className="settings-item-header">{lang('Username')}</h4>
|
|
|
|
<UsernameInput
|
|
currentUsername={username || ''}
|
|
isLoading={isLoading}
|
|
isUsernameAvailable={isUsernameAvailable}
|
|
checkUsername={checkUsername}
|
|
onChange={handleUsernameChange}
|
|
/>
|
|
|
|
<p className="settings-item-description">
|
|
{renderText(lang('UsernameHelp'), ['br', 'simple_markdown'])}
|
|
</p>
|
|
{username && (
|
|
<p className="settings-item-description">
|
|
{lang('lng_username_link')}<br />
|
|
<span className="username-link">https://t.me/{username}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<FloatingActionButton
|
|
isShown={isSaveButtonShown}
|
|
onClick={handleProfileSave}
|
|
disabled={isLoading}
|
|
ariaLabel="Save changes"
|
|
>
|
|
{isLoading ? (
|
|
<Spinner color="white" />
|
|
) : (
|
|
<i className="icon-check" />
|
|
)}
|
|
</FloatingActionButton>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal(
|
|
(global): StateProps => {
|
|
const { currentUserId } = global;
|
|
const { progress, isUsernameAvailable } = global.profileEdit || {};
|
|
const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined;
|
|
|
|
if (!currentUser) {
|
|
return {
|
|
progress,
|
|
isUsernameAvailable,
|
|
};
|
|
}
|
|
|
|
const {
|
|
firstName: currentFirstName,
|
|
lastName: currentLastName,
|
|
username: currentUsername,
|
|
fullInfo,
|
|
} = currentUser;
|
|
const { bio: currentBio } = fullInfo || {};
|
|
const currentAvatarHash = getChatAvatarHash(currentUser);
|
|
|
|
return {
|
|
currentAvatarHash,
|
|
currentFirstName,
|
|
currentLastName,
|
|
currentBio,
|
|
currentUsername,
|
|
progress,
|
|
isUsernameAvailable,
|
|
};
|
|
},
|
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
|
'loadCurrentUser',
|
|
'updateProfile',
|
|
'checkUsername',
|
|
]),
|
|
)(SettingsEditProfile));
|