feat: convered EditProfileDialog to a functional component

pull/2765/head
William Grant 2 years ago
parent 448c9dc14b
commit a9e8862c0a

@ -1,20 +1,15 @@
import React, { ChangeEvent, MouseEvent } from 'react';
import React, { ChangeEvent, MouseEvent, ReactElement, useState } from 'react';
import { QRCode } from 'react-qr-svg';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { SyncUtils, ToastUtils, UserUtils } from '../../session/utils';
import { YourSessionIDPill, YourSessionIDSelectable } from '../basic/YourSessionIDPill';
import { ConversationModel } from '../../models/conversation';
import autoBind from 'auto-bind';
import styled from 'styled-components';
import { uploadOurAvatar } from '../../interactions/conversationInteractions';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { MAX_USERNAME_BYTES } from '../../session/constants';
import { getConversationController } from '../../session/conversations';
import { sanitizeSessionUsername } from '../../session/utils/String';
import { editProfileModal } from '../../state/ducks/modalDialog';
import { pickFileForAvatar } from '../../types/attachments/VisualAttachment';
import { saveQRCode } from '../../util/saveQRCode';
@ -23,6 +18,9 @@ import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
import { SessionSpinner } from '../basic/SessionSpinner';
import { SessionIconButton } from '../icon';
import { sanitizeSessionUsername } from '../../session/utils/String';
import { useOurConversationUsername } from '../../hooks/useParamSelector';
import { useOurAvatarPath } from '../../hooks/useParamSelector';
const handleSaveQRCode = (event: MouseEvent) => {
event.preventDefault();
@ -51,311 +49,279 @@ const QRView = ({ sessionID }: { sessionID: string }) => {
);
};
interface State {
profileName: string;
updatedProfileName: string;
oldAvatarPath: string;
newAvatarObjectUrl: string | null;
mode: 'default' | 'edit' | 'qr';
loading: boolean;
}
export class EditProfileDialog extends React.Component<{}, State> {
private readonly convo: ConversationModel;
constructor(props: any) {
super(props);
autoBind(this);
this.convo = getConversationController().get(UserUtils.getOurPubKeyStrFromCache());
this.state = {
profileName: this.convo.getRealSessionUsername() || '',
updatedProfileName: this.convo.getRealSessionUsername() || '',
oldAvatarPath: this.convo.getAvatarPath() || '',
newAvatarObjectUrl: null,
mode: 'default',
loading: false,
};
}
const commitProfileEdits = async (newName: string, scaledAvatarUrl: string | null) => {
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
const conversation = await getConversationController().getOrCreateAndWait(
ourNumber,
ConversationTypeEnum.PRIVATE
);
public componentDidMount() {
window.addEventListener('keyup', this.onKeyUp);
if (scaledAvatarUrl?.length) {
try {
const blobContent = await (await fetch(scaledAvatarUrl)).blob();
if (!blobContent || !blobContent.size) {
throw new Error('Failed to fetch blob content from scaled avatar');
}
await uploadOurAvatar(await blobContent.arrayBuffer());
} catch (error) {
if (error.message && error.message.length) {
ToastUtils.pushToastError('edit-profile', error.message);
}
window.log.error(
'showEditProfileDialog Error ensuring that image is properly sized:',
error && error.stack ? error.stack : error
);
}
return;
}
// do not update the avatar if it did not change
conversation.setSessionDisplayNameNoCommit(newName);
public componentWillUnmount() {
window.removeEventListener('keyup', this.onKeyUp);
}
// might be good to not trigger a sync if the name did not change
await conversation.commit();
await setLastProfileUpdateTimestamp(Date.now());
await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
};
public render() {
const i18n = window.i18n;
type ProfileAvatarProps = {
newAvatarObjectUrl: string | null;
oldAvatarPath: string | null;
profileName: string | undefined;
ourId: string;
};
const viewDefault = this.state.mode === 'default';
const viewEdit = this.state.mode === 'edit';
const viewQR = this.state.mode === 'qr';
const ProfileAvatar = (props: ProfileAvatarProps): ReactElement => {
const { newAvatarObjectUrl, oldAvatarPath, profileName, ourId } = props;
return (
<Avatar
forcedAvatarPath={newAvatarObjectUrl || oldAvatarPath}
forcedName={profileName || ourId}
size={AvatarSize.XL}
pubkey={ourId}
/>
);
};
const sessionID = UserUtils.getOurPubKeyStrFromCache();
type ProfileHeaderProps = ProfileAvatarProps & {
fireInputEvent: () => void;
setMode: (mode: ProfileDialogModes) => void;
};
const backButton =
viewEdit || viewQR
? [
{
iconType: 'chevron',
iconRotation: 90,
onClick: () => {
this.setState({ mode: 'default' });
},
},
]
: undefined;
const ProfileHeader = (props: ProfileHeaderProps): ReactElement => {
const { newAvatarObjectUrl, oldAvatarPath, profileName, ourId, fireInputEvent, setMode } = props;
return (
<div className="edit-profile-dialog" data-testid="edit-profile-dialog">
<SessionWrapperModal
title={i18n('editProfileModalTitle')}
onClose={this.closeDialog}
headerIconButtons={backButton}
showExitIcon={true}
return (
<div className="avatar-center">
<div className="avatar-center-inner">
<ProfileAvatar
newAvatarObjectUrl={newAvatarObjectUrl}
oldAvatarPath={oldAvatarPath}
profileName={profileName}
ourId={ourId}
/>
<div
className="image-upload-section"
role="button"
onClick={async () => {
void fireInputEvent();
}}
data-testid="image-upload-section"
/>
<div
className="qr-view-button"
onClick={() => {
setMode('qr');
}}
role="button"
>
{viewQR && <QRView sessionID={sessionID} />}
{viewDefault && this.renderDefaultView()}
{viewEdit && this.renderEditView()}
<div className="session-id-section">
<YourSessionIDPill />
<YourSessionIDSelectable />
<SessionSpinner loading={this.state.loading} />
{viewDefault || viewQR ? (
<SessionButton
text={window.i18n('editMenuCopy')}
buttonType={SessionButtonType.Simple}
onClick={() => {
window.clipboard.writeText(sessionID);
ToastUtils.pushCopiedToClipBoard();
}}
dataTestId="copy-button-profile-update"
/>
) : (
!this.state.loading && (
<SessionButton
text={window.i18n('save')}
buttonType={SessionButtonType.Simple}
onClick={this.onClickOK}
disabled={this.state.loading}
dataTestId="save-button-profile-update"
/>
)
)}
</div>
</SessionWrapperModal>
</div>
);
}
private renderProfileHeader() {
return (
<>
<div className="avatar-center">
<div className="avatar-center-inner">
{this.renderAvatar()}
<div
className="image-upload-section"
role="button"
onClick={this.fireInputEvent}
data-testid="image-upload-section"
/>
<div
className="qr-view-button"
onClick={() => {
this.setState(state => ({ ...state, mode: 'qr' }));
}}
role="button"
>
<SessionIconButton iconType="qr" iconSize="small" iconColor="var(--black-color)" />
</div>
</div>
<SessionIconButton iconType="qr" iconSize="small" iconColor="var(--black-color)" />
</div>
</>
);
}
</div>
</div>
);
};
private async fireInputEvent() {
const scaledAvatarUrl = await pickFileForAvatar();
type ProfileDialogModes = 'default' | 'edit' | 'qr';
if (scaledAvatarUrl) {
this.setState({
newAvatarObjectUrl: scaledAvatarUrl,
mode: 'edit',
});
}
}
export const EditProfileDialog = (): ReactElement => {
const _profileName = useOurConversationUsername() || '';
const [profileName, setProfileName] = useState(_profileName);
const [updatedProfileName, setUpdateProfileName] = useState(profileName);
const oldAvatarPath = useOurAvatarPath() || '';
const [newAvatarObjectUrl, setNewAvatarObjectUrl] = useState<string | null>(null);
private renderDefaultView() {
const name = this.state.updatedProfileName || this.state.profileName;
return (
<>
{this.renderProfileHeader()}
const [mode, setMode] = useState<ProfileDialogModes>('default');
const [loading, setLoading] = useState(false);
<div className="profile-name-uneditable">
<p data-testid="your-profile-name">{name}</p>
<SessionIconButton
iconType="pencil"
iconSize="medium"
onClick={() => {
this.setState({ mode: 'edit' });
}}
dataTestId="edit-profile-icon"
/>
</div>
</>
);
}
const ourId = UserUtils.getOurPubKeyStrFromCache();
private renderEditView() {
const placeholderText = window.i18n('displayName');
const closeDialog = () => {
window.removeEventListener('keyup', handleOnKeyUp);
window.inboxStore?.dispatch(editProfileModal(null));
};
const backButton =
mode === 'edit' || mode === 'qr'
? [
{
iconType: 'chevron',
iconRotation: 90,
onClick: () => {
setMode('default');
},
},
]
: undefined;
const onClickOK = async () => {
/**
* Tidy the profile name input text and save the new profile name and avatar
*/
try {
const newName = profileName ? profileName.trim() : '';
return (
<>
{this.renderProfileHeader()}
<div className="profile-name">
<input
type="text"
className="profile-name-input"
value={this.state.profileName}
placeholder={placeholderText}
onChange={this.onNameEdited}
maxLength={MAX_USERNAME_BYTES}
tabIndex={0}
required={true}
aria-required={true}
data-testid="profile-name-input"
/>
</div>
</>
);
}
if (newName.length === 0 || newName.length > MAX_USERNAME_BYTES) {
return;
}
private renderAvatar() {
const { oldAvatarPath, newAvatarObjectUrl, profileName } = this.state;
const userName = profileName || this.convo.id;
// this throw if the length in bytes is too long
const sanitizedName = sanitizeSessionUsername(newName);
const trimName = sanitizedName.trim();
return (
<Avatar
forcedAvatarPath={newAvatarObjectUrl || oldAvatarPath}
forcedName={userName}
size={AvatarSize.XL}
pubkey={this.convo.id}
/>
);
}
setUpdateProfileName(trimName);
setLoading(true);
private onNameEdited(event: ChangeEvent<HTMLInputElement>) {
const displayName = event.target.value;
try {
const newName = sanitizeSessionUsername(displayName);
this.setState({
profileName: newName,
});
await commitProfileEdits(newName, newAvatarObjectUrl);
setMode('default');
setUpdateProfileName(profileName);
setLoading(false);
} catch (e) {
this.setState({
profileName: displayName,
});
ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong'));
}
}
};
private onKeyUp(event: any) {
const handleOnKeyUp = (event: any) => {
switch (event.key) {
case 'Enter':
if (this.state.mode === 'edit') {
this.onClickOK();
if (mode === 'edit') {
onClickOK();
}
break;
case 'Esc':
case 'Escape':
this.closeDialog();
closeDialog();
break;
default:
}
}
};
/**
* Tidy the profile name input text and save the new profile name and avatar
*/
private onClickOK() {
const { newAvatarObjectUrl, profileName } = this.state;
try {
const newName = profileName ? profileName.trim() : '';
if (newName.length === 0 || newName.length > MAX_USERNAME_BYTES) {
return;
}
// this throw if the length in bytes is too long
const sanitizedName = sanitizeSessionUsername(newName);
const trimName = sanitizedName.trim();
this.setState(
{
profileName: trimName,
loading: true,
},
async () => {
await commitProfileEdits(newName, newAvatarObjectUrl);
this.setState({
loading: false,
const fireInputEvent = async () => {
const scaledAvatarUrl = await pickFileForAvatar();
if (scaledAvatarUrl) {
setNewAvatarObjectUrl(scaledAvatarUrl);
setMode('edit');
}
};
mode: 'default',
updatedProfileName: this.state.profileName,
});
}
);
const onNameEdited = (event: ChangeEvent<HTMLInputElement>) => {
const displayName = event.target.value;
try {
const newName = sanitizeSessionUsername(displayName);
setProfileName(newName);
} catch (e) {
setProfileName(displayName);
ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong'));
return;
}
}
};
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
window.inboxStore?.dispatch(editProfileModal(null));
}
}
return (
<div className="edit-profile-dialog" data-testid="edit-profile-dialog" onKeyUp={handleOnKeyUp}>
<SessionWrapperModal
title={window.i18n('editProfileModalTitle')}
onClose={closeDialog}
headerIconButtons={backButton}
showExitIcon={true}
>
{mode === 'qr' && <QRView sessionID={ourId} />}
{mode === 'default' && (
<>
<ProfileHeader
newAvatarObjectUrl={newAvatarObjectUrl}
oldAvatarPath={oldAvatarPath}
profileName={profileName}
ourId={ourId}
fireInputEvent={fireInputEvent}
setMode={setMode}
/>
<div className="profile-name-uneditable">
<p data-testid="your-profile-name">{updatedProfileName || profileName}</p>
<SessionIconButton
iconType="pencil"
iconSize="medium"
onClick={() => {
setMode('edit');
}}
dataTestId="edit-profile-icon"
/>
</div>
</>
)}
{mode === 'edit' && (
<>
<ProfileHeader
newAvatarObjectUrl={newAvatarObjectUrl}
oldAvatarPath={oldAvatarPath}
profileName={profileName}
ourId={ourId}
fireInputEvent={fireInputEvent}
setMode={setMode}
/>
<div className="profile-name">
<input
type="text"
className="profile-name-input"
value={profileName}
placeholder={window.i18n('displayName')}
onChange={onNameEdited}
maxLength={MAX_USERNAME_BYTES}
tabIndex={0}
required={true}
aria-required={true}
data-testid="profile-name-input"
/>
</div>
</>
)}
async function commitProfileEdits(newName: string, scaledAvatarUrl: string | null) {
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
const conversation = await getConversationController().getOrCreateAndWait(
ourNumber,
ConversationTypeEnum.PRIVATE
);
<div className="session-id-section">
<YourSessionIDPill />
<YourSessionIDSelectable />
if (scaledAvatarUrl?.length) {
try {
const blobContent = await (await fetch(scaledAvatarUrl)).blob();
if (!blobContent || !blobContent.size) {
throw new Error('Failed to fetch blob content from scaled avatar');
}
await uploadOurAvatar(await blobContent.arrayBuffer());
} catch (error) {
if (error.message && error.message.length) {
ToastUtils.pushToastError('edit-profile', error.message);
}
window.log.error(
'showEditProfileDialog Error ensuring that image is properly sized:',
error && error.stack ? error.stack : error
);
}
return;
}
// do not update the avatar if it did not change
conversation.setSessionDisplayNameNoCommit(newName);
<SessionSpinner loading={loading} />
// might be good to not trigger a sync if the name did not change
await conversation.commit();
await setLastProfileUpdateTimestamp(Date.now());
await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
}
{mode === 'default' || mode === 'qr' ? (
<SessionButton
text={window.i18n('editMenuCopy')}
buttonType={SessionButtonType.Simple}
onClick={() => {
window.clipboard.writeText(ourId);
ToastUtils.pushCopiedToClipBoard();
}}
dataTestId="copy-button-profile-update"
/>
) : (
!loading && (
<SessionButton
text={window.i18n('save')}
buttonType={SessionButtonType.Simple}
onClick={onClickOK}
disabled={loading}
dataTestId="save-button-profile-update"
/>
)
)}
</div>
</SessionWrapperModal>
</div>
);
};

Loading…
Cancel
Save