From a9e8862c0a79dd2df12085539001def4248cb165 Mon Sep 17 00:00:00 2001 From: William Grant Date: Mon, 22 May 2023 16:11:49 +1000 Subject: [PATCH] feat: convered EditProfileDialog to a functional component --- ts/components/dialog/EditProfileDialog.tsx | 512 ++++++++++----------- 1 file changed, 239 insertions(+), 273 deletions(-) diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx index 34d4947a1..0f664633f 100644 --- a/ts/components/dialog/EditProfileDialog.tsx +++ b/ts/components/dialog/EditProfileDialog.tsx @@ -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 ( + + ); +}; - 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 ( -
- +
+ +
{ + void fireInputEvent(); + }} + data-testid="image-upload-section" + /> +
{ + setMode('qr'); + }} + role="button" > - {viewQR && } - {viewDefault && this.renderDefaultView()} - {viewEdit && this.renderEditView()} - -
- - - - - - {viewDefault || viewQR ? ( - { - window.clipboard.writeText(sessionID); - ToastUtils.pushCopiedToClipBoard(); - }} - dataTestId="copy-button-profile-update" - /> - ) : ( - !this.state.loading && ( - - ) - )} -
- -
- ); - } - - private renderProfileHeader() { - return ( - <> -
-
- {this.renderAvatar()} -
-
{ - this.setState(state => ({ ...state, mode: 'qr' })); - }} - role="button" - > - -
-
+
- - ); - } +
+
+ ); +}; - 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(null); - private renderDefaultView() { - const name = this.state.updatedProfileName || this.state.profileName; - return ( - <> - {this.renderProfileHeader()} + const [mode, setMode] = useState('default'); + const [loading, setLoading] = useState(false); -
-

{name}

- { - this.setState({ mode: 'edit' }); - }} - dataTestId="edit-profile-icon" - /> -
- - ); - } + 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()} -
- -
- - ); - } + 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 ( - - ); - } + setUpdateProfileName(trimName); + setLoading(true); - private onNameEdited(event: ChangeEvent) { - 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) => { + 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 ( +
+ + {mode === 'qr' && } + {mode === 'default' && ( + <> + +
+

{updatedProfileName || profileName}

+ { + setMode('edit'); + }} + dataTestId="edit-profile-icon" + /> +
+ + )} + {mode === 'edit' && ( + <> + +
+ +
+ + )} -async function commitProfileEdits(newName: string, scaledAvatarUrl: string | null) { - const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - const conversation = await getConversationController().getOrCreateAndWait( - ourNumber, - ConversationTypeEnum.PRIVATE - ); +
+ + - 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); + - // 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' ? ( + { + window.clipboard.writeText(ourId); + ToastUtils.pushCopiedToClipBoard(); + }} + dataTestId="copy-button-profile-update" + /> + ) : ( + !loading && ( + + ) + )} +
+
+
+ ); +};