You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
| import { ChangeEvent, useState } from 'react';
 | |
| import { useDispatch, useSelector } from 'react-redux';
 | |
| import styled from 'styled-components';
 | |
| import { Avatar, AvatarSize } from '../avatar/Avatar';
 | |
| 
 | |
| import { SyncUtils, ToastUtils, UserUtils } from '../../session/utils';
 | |
| import { YourSessionIDPill, YourSessionIDSelectable } from '../basic/YourSessionIDPill';
 | |
| 
 | |
| import { useOurAvatarPath, useOurConversationUsername } from '../../hooks/useParamSelector';
 | |
| import { ConversationTypeEnum } from '../../models/conversationAttributes';
 | |
| import { MAX_NAME_LENGTH_BYTES } from '../../session/constants';
 | |
| import { getConversationController } from '../../session/conversations';
 | |
| import { sanitizeSessionUsername } from '../../session/utils/String';
 | |
| import { editProfileModal, updateEditProfilePictureModel } from '../../state/ducks/modalDialog';
 | |
| import { getTheme } from '../../state/selectors/theme';
 | |
| import { getThemeValue } from '../../themes/globals';
 | |
| import { setLastProfileUpdateTimestamp } from '../../util/storage';
 | |
| import { SessionQRCode } from '../SessionQRCode';
 | |
| import { SessionWrapperModal } from '../SessionWrapperModal';
 | |
| import { Flex } from '../basic/Flex';
 | |
| import { SessionButton } from '../basic/SessionButton';
 | |
| import { Spacer2XL, Spacer3XL, SpacerLG, SpacerSM, SpacerXL, SpacerXS } from '../basic/Text';
 | |
| import { SessionIconButton } from '../icon';
 | |
| import { SessionSpinner } from '../loading';
 | |
| 
 | |
| const StyledEditProfileDialog = styled.div`
 | |
|   .session-modal {
 | |
|     width: 468px;
 | |
|     .session-modal__body {
 | |
|       overflow: initial;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   .avatar-center-inner {
 | |
|     position: relative;
 | |
| 
 | |
|     .qr-view-button {
 | |
|       cursor: pointer;
 | |
|       display: flex;
 | |
|       align-items: center;
 | |
|       justify-content: center;
 | |
|       position: absolute;
 | |
|       top: -8px;
 | |
|       right: -8px;
 | |
|       height: 34px;
 | |
|       width: 34px;
 | |
|       border-radius: 50%;
 | |
|       background-color: var(--white-color);
 | |
|       transition: var(--default-duration);
 | |
| 
 | |
|       &:hover {
 | |
|         filter: brightness(90%);
 | |
|       }
 | |
| 
 | |
|       .session-icon-button {
 | |
|         opacity: 1;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| `;
 | |
| 
 | |
| const StyledProfileName = styled(Flex)`
 | |
|   input {
 | |
|     height: 38px;
 | |
|     border-radius: 5px;
 | |
|     text-align: center;
 | |
|     font-size: var(--font-size-md);
 | |
|   }
 | |
| 
 | |
|   &.uneditable {
 | |
|     p {
 | |
|       margin: 0;
 | |
|       padding: 0px var(--margins-lg) 0 var(--margins-sm);
 | |
|     }
 | |
| 
 | |
|     .session-icon-button {
 | |
|       padding: 0px;
 | |
|     }
 | |
|   }
 | |
| `;
 | |
| 
 | |
| const StyledSessionIdSection = styled(Flex)`
 | |
|   .session-button {
 | |
|     width: 160px;
 | |
|   }
 | |
| `;
 | |
| 
 | |
| const QRView = ({ sessionID }: { sessionID: string }) => {
 | |
|   const theme = useSelector(getTheme);
 | |
| 
 | |
|   return (
 | |
|     <SessionQRCode
 | |
|       id={'session-account-id'}
 | |
|       value={sessionID}
 | |
|       size={170}
 | |
|       backgroundColor={getThemeValue(
 | |
|         theme.includes('dark') ? '--text-primary-color' : '--background-primary-color'
 | |
|       )}
 | |
|       foregroundColor={getThemeValue(
 | |
|         theme.includes('dark') ? '--background-primary-color' : '--text-primary-color'
 | |
|       )}
 | |
|       logoImage={'./images/session/qr/brand.svg'}
 | |
|       logoWidth={40}
 | |
|       logoHeight={40}
 | |
|       logoIsSVG={true}
 | |
|       theme={theme}
 | |
|     />
 | |
|   );
 | |
| };
 | |
| 
 | |
| const updateDisplayName = async (newName: string) => {
 | |
|   const ourNumber = UserUtils.getOurPubKeyStrFromCache();
 | |
|   const conversation = await getConversationController().getOrCreateAndWait(
 | |
|     ourNumber,
 | |
|     ConversationTypeEnum.PRIVATE
 | |
|   );
 | |
|   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);
 | |
| };
 | |
| 
 | |
| type ProfileAvatarProps = {
 | |
|   avatarPath: string | null;
 | |
|   newAvatarObjectUrl?: string | null;
 | |
|   profileName: string | undefined;
 | |
|   ourId: string;
 | |
| };
 | |
| 
 | |
| export const ProfileAvatar = (props: ProfileAvatarProps) => {
 | |
|   const { newAvatarObjectUrl, avatarPath, profileName, ourId } = props;
 | |
|   return (
 | |
|     <Avatar
 | |
|       forcedAvatarPath={newAvatarObjectUrl || avatarPath}
 | |
|       forcedName={profileName || ourId}
 | |
|       size={AvatarSize.XL}
 | |
|       pubkey={ourId}
 | |
|     />
 | |
|   );
 | |
| };
 | |
| 
 | |
| type ProfileHeaderProps = ProfileAvatarProps & {
 | |
|   onClick: () => void;
 | |
|   setMode: (mode: ProfileDialogModes) => void;
 | |
| };
 | |
| 
 | |
| const ProfileHeader = (props: ProfileHeaderProps) => {
 | |
|   const { avatarPath, profileName, ourId, onClick, setMode } = props;
 | |
| 
 | |
|   return (
 | |
|     <div className="avatar-center">
 | |
|       <div className="avatar-center-inner">
 | |
|         <ProfileAvatar avatarPath={avatarPath} profileName={profileName} ourId={ourId} />
 | |
|         <div
 | |
|           className="image-upload-section"
 | |
|           role="button"
 | |
|           onClick={onClick}
 | |
|           data-testid="image-upload-section"
 | |
|         />
 | |
|         <div
 | |
|           className="qr-view-button"
 | |
|           onClick={() => {
 | |
|             setMode('qr');
 | |
|           }}
 | |
|           role="button"
 | |
|         >
 | |
|           <SessionIconButton iconType="qr" iconSize={26} iconColor="var(--black-color)" />
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| type ProfileDialogModes = 'default' | 'edit' | 'qr';
 | |
| 
 | |
| export const EditProfileDialog = () => {
 | |
|   const dispatch = useDispatch();
 | |
| 
 | |
|   const _profileName = useOurConversationUsername() || '';
 | |
|   const [profileName, setProfileName] = useState(_profileName);
 | |
|   const [updatedProfileName, setUpdateProfileName] = useState(profileName);
 | |
|   const avatarPath = useOurAvatarPath() || '';
 | |
|   const ourId = UserUtils.getOurPubKeyStrFromCache();
 | |
| 
 | |
|   const [mode, setMode] = useState<ProfileDialogModes>('default');
 | |
|   const [loading, setLoading] = useState(false);
 | |
| 
 | |
|   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() : '';
 | |
| 
 | |
|       if (newName.length === 0 || newName.length > MAX_NAME_LENGTH_BYTES) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // this throw if the length in bytes is too long
 | |
|       const sanitizedName = sanitizeSessionUsername(newName);
 | |
|       const trimName = sanitizedName.trim();
 | |
| 
 | |
|       setUpdateProfileName(trimName);
 | |
|       setLoading(true);
 | |
| 
 | |
|       await updateDisplayName(newName);
 | |
|       setMode('default');
 | |
|       setUpdateProfileName(profileName);
 | |
|       setLoading(false);
 | |
|     } catch (e) {
 | |
|       ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong'));
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const handleOnKeyUp = (event: any) => {
 | |
|     switch (event.key) {
 | |
|       case 'Enter':
 | |
|         if (mode === 'edit') {
 | |
|           void onClickOK();
 | |
|         }
 | |
|         break;
 | |
|       case 'Esc':
 | |
|       case 'Escape':
 | |
|         closeDialog();
 | |
|         break;
 | |
|       default:
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const handleProfileHeaderClick = () => {
 | |
|     closeDialog();
 | |
|     dispatch(
 | |
|       updateEditProfilePictureModel({
 | |
|         avatarPath,
 | |
|         profileName,
 | |
|         ourId,
 | |
|       })
 | |
|     );
 | |
|   };
 | |
| 
 | |
|   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 (
 | |
|     /* The <div> element has a child <input> element that allows keyboard interaction
 | |
|     We use edit-profile-default class to prevent the qr icon on the avatar from clipping
 | |
|     */
 | |
|     <StyledEditProfileDialog
 | |
|       className="edit-profile-dialog"
 | |
|       data-testid="edit-profile-dialog"
 | |
|       onKeyUp={handleOnKeyUp}
 | |
|     >
 | |
|       <SessionWrapperModal
 | |
|         title={window.i18n('editProfileModalTitle')}
 | |
|         onClose={closeDialog}
 | |
|         headerIconButtons={backButton}
 | |
|         headerReverse={true}
 | |
|         showExitIcon={true}
 | |
|         additionalClassName={mode === 'default' ? 'edit-profile-default' : undefined}
 | |
|       >
 | |
|         {mode === 'qr' && (
 | |
|           <>
 | |
|             <QRView sessionID={ourId} />
 | |
|             <SpacerXS />
 | |
|           </>
 | |
|         )}
 | |
|         {mode === 'default' && (
 | |
|           <>
 | |
|             <SpacerSM />
 | |
|             <ProfileHeader
 | |
|               avatarPath={avatarPath}
 | |
|               profileName={profileName}
 | |
|               ourId={ourId}
 | |
|               onClick={handleProfileHeaderClick}
 | |
|               setMode={setMode}
 | |
|             />
 | |
|             <SpacerXL />
 | |
|             <StyledProfileName
 | |
|               container={true}
 | |
|               justifyContent="center"
 | |
|               alignItems="center"
 | |
|               className="uneditable"
 | |
|             >
 | |
|               <SessionIconButton
 | |
|                 iconType="pencil"
 | |
|                 iconSize="large"
 | |
|                 onClick={() => {
 | |
|                   setMode('edit');
 | |
|                 }}
 | |
|                 dataTestId="edit-profile-icon"
 | |
|               />
 | |
|               <p data-testid="your-profile-name">{updatedProfileName || profileName}</p>
 | |
|             </StyledProfileName>
 | |
|             <Spacer3XL />
 | |
|           </>
 | |
|         )}
 | |
|         {mode === 'edit' && (
 | |
|           <>
 | |
|             <ProfileHeader
 | |
|               avatarPath={avatarPath}
 | |
|               profileName={profileName}
 | |
|               ourId={ourId}
 | |
|               onClick={handleProfileHeaderClick}
 | |
|               setMode={setMode}
 | |
|             />
 | |
|             <StyledProfileName container={true} justifyContent="center" alignItems="center">
 | |
|               {/* TODO swap with new session input */}
 | |
|               <input
 | |
|                 type="text"
 | |
|                 value={profileName}
 | |
|                 placeholder={window.i18n('displayName')}
 | |
|                 onChange={onNameEdited}
 | |
|                 maxLength={MAX_NAME_LENGTH_BYTES}
 | |
|                 tabIndex={0}
 | |
|                 required={true}
 | |
|                 aria-required={true}
 | |
|                 data-testid="profile-name-input"
 | |
|               />
 | |
|             </StyledProfileName>
 | |
|           </>
 | |
|         )}
 | |
| 
 | |
|         <StyledSessionIdSection
 | |
|           container={true}
 | |
|           flexDirection="column"
 | |
|           justifyContent="center"
 | |
|           alignItems="center"
 | |
|           width="calc(100% - 80px)"
 | |
|         >
 | |
|           <SpacerLG />
 | |
|           <YourSessionIDPill />
 | |
|           <SpacerLG />
 | |
|           <YourSessionIDSelectable />
 | |
| 
 | |
|           <SessionSpinner loading={loading} />
 | |
|           <Spacer2XL />
 | |
| 
 | |
|           {mode === 'default' || mode === 'qr' ? (
 | |
|             <Flex
 | |
|               container={true}
 | |
|               justifyContent={mode === 'default' ? 'space-between' : 'center'}
 | |
|               alignItems="center"
 | |
|               width={'100%'}
 | |
|             >
 | |
|               <SessionButton
 | |
|                 text={window.i18n('editMenuCopy')}
 | |
|                 onClick={() => {
 | |
|                   window.clipboard.writeText(ourId);
 | |
|                   ToastUtils.pushCopiedToClipBoard();
 | |
|                 }}
 | |
|                 dataTestId="copy-button-profile-update"
 | |
|               />
 | |
|               {mode === 'default' ? (
 | |
|                 <SessionButton
 | |
|                   text={window.i18n('qrView')}
 | |
|                   onClick={() => {
 | |
|                     setMode('qr');
 | |
|                   }}
 | |
|                   dataTestId="qr-view-profile-update"
 | |
|                 />
 | |
|               ) : null}
 | |
|             </Flex>
 | |
|           ) : (
 | |
|             !loading && (
 | |
|               <SessionButton
 | |
|                 text={window.i18n('save')}
 | |
|                 onClick={onClickOK}
 | |
|                 disabled={loading}
 | |
|                 dataTestId="save-button-profile-update"
 | |
|               />
 | |
|             )
 | |
|           )}
 | |
|           <SpacerSM />
 | |
|         </StyledSessionIdSection>
 | |
|       </SessionWrapperModal>
 | |
|     </StyledEditProfileDialog>
 | |
|   );
 | |
| };
 |