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.
		
		
		
		
		
			
		
			
				
	
	
		
			303 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			303 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			TypeScript
		
	
import { useDispatch } from 'react-redux';
 | 
						|
// eslint-disable-next-line import/no-named-default
 | 
						|
import { ChangeEvent, MouseEvent, default as React, ReactElement, useState } from 'react';
 | 
						|
import { QRCode } from 'react-qr-svg';
 | 
						|
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_USERNAME_BYTES } from '../../session/constants';
 | 
						|
import { getConversationController } from '../../session/conversations';
 | 
						|
import { sanitizeSessionUsername } from '../../session/utils/String';
 | 
						|
import { editProfileModal, updateEditProfilePictureModel } from '../../state/ducks/modalDialog';
 | 
						|
import { saveQRCode } from '../../util/saveQRCode';
 | 
						|
import { setLastProfileUpdateTimestamp } from '../../util/storage';
 | 
						|
import { SessionWrapperModal } from '../SessionWrapperModal';
 | 
						|
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
 | 
						|
import { SessionSpinner } from '../basic/SessionSpinner';
 | 
						|
import { SessionIconButton } from '../icon';
 | 
						|
 | 
						|
const handleSaveQRCode = (event: MouseEvent) => {
 | 
						|
  event.preventDefault();
 | 
						|
  saveQRCode('session-id', '220px', '220px', 'var(--white-color)', 'var(--black-color)');
 | 
						|
};
 | 
						|
 | 
						|
const StyledQRView = styled.div`
 | 
						|
  cursor: pointer;
 | 
						|
`;
 | 
						|
 | 
						|
const QRView = ({ sessionID }: { sessionID: string }) => {
 | 
						|
  return (
 | 
						|
    <StyledQRView
 | 
						|
      aria-label={window.i18n('clickToTrustContact')}
 | 
						|
      title={window.i18n('clickToTrustContact')}
 | 
						|
      className="qr-image"
 | 
						|
      onClick={handleSaveQRCode}
 | 
						|
    >
 | 
						|
      <QRCode
 | 
						|
        value={sessionID}
 | 
						|
        bgColor="var(--white-color)"
 | 
						|
        fgColor="var(--black-color)"
 | 
						|
        level="L"
 | 
						|
      />
 | 
						|
    </StyledQRView>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
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): ReactElement => {
 | 
						|
  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): ReactElement => {
 | 
						|
  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="small" iconColor="var(--black-color)" />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
type ProfileDialogModes = 'default' | 'edit' | 'qr';
 | 
						|
// tslint:disable-next-line: max-func-body-length
 | 
						|
export const EditProfileDialog = (): ReactElement => {
 | 
						|
  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_USERNAME_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 */
 | 
						|
    /* tslint:disable-next-line: react-a11y-event-has-role */
 | 
						|
    <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
 | 
						|
              avatarPath={avatarPath}
 | 
						|
              profileName={profileName}
 | 
						|
              ourId={ourId}
 | 
						|
              onClick={handleProfileHeaderClick}
 | 
						|
              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
 | 
						|
              avatarPath={avatarPath}
 | 
						|
              profileName={profileName}
 | 
						|
              ourId={ourId}
 | 
						|
              onClick={handleProfileHeaderClick}
 | 
						|
              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>
 | 
						|
          </>
 | 
						|
        )}
 | 
						|
 | 
						|
        <div className="session-id-section">
 | 
						|
          <YourSessionIDPill />
 | 
						|
          <YourSessionIDSelectable />
 | 
						|
 | 
						|
          <SessionSpinner loading={loading} />
 | 
						|
 | 
						|
          {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>
 | 
						|
  );
 | 
						|
};
 |