diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d00963349..32d44f8e9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -363,6 +363,7 @@ "notificationPreview": "Preview", "recoveryPhraseEmpty": "Enter your recovery phrase", "displayNameEmpty": "Please enter a display name", + "displayNameTooLong": "Display name is too long", "members": "$count$ members", "activeMembers": "$count$ active members", "join": "Join", diff --git a/ts/components/SessionWrapperModal.tsx b/ts/components/SessionWrapperModal.tsx index 6b650900a..d808ad266 100644 --- a/ts/components/SessionWrapperModal.tsx +++ b/ts/components/SessionWrapperModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useRef } from 'react'; import classNames from 'classnames'; import { SessionIconButton } from './icon/'; @@ -63,17 +63,11 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => { } }; - useEffect(() => { - document.addEventListener('mousedown', handleClick); - - return () => { - document.removeEventListener('mousedown', handleClick); - }; - }, []); - return ( <div className={classNames('loki-dialog modal', additionalClassName ? additionalClassName : null)} + onClick={handleClick} + role="dialog" > <div className="session-confirm-wrapper"> <div ref={modalRef} className="session-modal"> diff --git a/ts/components/basic/SessionToast.tsx b/ts/components/basic/SessionToast.tsx index 3231e3825..61db4b941 100644 --- a/ts/components/basic/SessionToast.tsx +++ b/ts/components/basic/SessionToast.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { Flex } from '../basic/Flex'; import styled from 'styled-components'; -import { noop } from 'lodash'; import { SessionIcon, SessionIconType } from '../icon'; +import { noop } from 'lodash'; export enum SessionToastType { Info = 'info', @@ -44,6 +44,8 @@ const IconDiv = styled.div` padding-inline-end: var(--margins-xs); `; +// tslint:disable: use-simple-attributes + export const SessionToast = (props: Props) => { const { title, description, type, icon } = props; @@ -71,14 +73,10 @@ export const SessionToast = (props: Props) => { } } + const onToastClick = props?.onToastClick || noop; + return ( - // tslint:disable-next-line: use-simple-attributes - <Flex - container={true} - alignItems="center" - onClick={props?.onToastClick || noop} - data-testid="session-toast" - > + <Flex container={true} alignItems="center" onClick={onToastClick} data-testid="session-toast"> <IconDiv> <SessionIcon iconType={toastIcon} iconSize={toastIconSize} /> </IconDiv> diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx index 69270fdd5..20af35eb7 100644 --- a/ts/components/dialog/EditProfileDialog.tsx +++ b/ts/components/dialog/EditProfileDialog.tsx @@ -16,12 +16,12 @@ import { uploadOurAvatar } from '../../interactions/conversationInteractions'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionSpinner } from '../basic/SessionSpinner'; import { SessionIconButton } from '../icon'; -import { MAX_USERNAME_LENGTH } from '../registration/RegistrationStages'; import { SessionWrapperModal } from '../SessionWrapperModal'; import { pickFileForAvatar } from '../../types/attachments/VisualAttachment'; import { sanitizeSessionUsername } from '../../session/utils/String'; import { setLastProfileUpdateTimestamp } from '../../util/storage'; import { ConversationTypeEnum } from '../../models/conversationAttributes'; +import { MAX_USERNAME_BYTES } from '../../session/constants'; interface State { profileName: string; @@ -214,7 +214,7 @@ export class EditProfileDialog extends React.Component<{}, State> { value={this.state.profileName} placeholder={placeholderText} onChange={this.onNameEdited} - maxLength={MAX_USERNAME_LENGTH} + maxLength={MAX_USERNAME_BYTES} tabIndex={0} required={true} aria-required={true} @@ -240,10 +240,18 @@ export class EditProfileDialog extends React.Component<{}, State> { } private onNameEdited(event: ChangeEvent<HTMLInputElement>) { - const newName = sanitizeSessionUsername(event.target.value); - this.setState({ - profileName: newName, - }); + const displayName = event.target.value; + try { + const newName = sanitizeSessionUsername(displayName); + this.setState({ + profileName: newName, + }); + } catch (e) { + this.setState({ + profileName: displayName, + }); + ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong')); + } } private onKeyUp(event: any) { @@ -266,26 +274,37 @@ export class EditProfileDialog extends React.Component<{}, State> { */ private onClickOK() { const { newAvatarObjectUrl, profileName } = this.state; - const newName = profileName ? profileName.trim() : ''; + 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, + + mode: 'default', + updatedProfileName: this.state.profileName, + }); + } + ); + } catch (e) { + ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong')); - if (newName.length === 0 || newName.length > MAX_USERNAME_LENGTH) { return; } - - this.setState( - { - loading: true, - }, - async () => { - await commitProfileEdits(newName, newAvatarObjectUrl); - this.setState({ - loading: false, - - mode: 'default', - updatedProfileName: this.state.profileName, - }); - } - ); } private closeDialog() { diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index a0cf34f39..ad1f782d2 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -39,7 +39,6 @@ import { getSwarmPollingInstance } from '../../session/apis/snode_api'; import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool'; import { Avatar, AvatarSize } from '../avatar/Avatar'; import { SessionIconButton } from '../icon'; -import { SessionToastContainer } from '../SessionToastContainer'; import { LeftPaneSectionContainer } from './LeftPaneSectionContainer'; import { ipcRenderer } from 'electron'; import { UserUtils } from '../../session/utils'; @@ -277,8 +276,6 @@ export const ActionsPanel = () => { <Section type={SectionType.Message} /> <Section type={SectionType.Settings} /> - <SessionToastContainer /> - <Section type={SectionType.PathIndicator} /> <Section type={SectionType.Moon} /> </LeftPaneSectionContainer> diff --git a/ts/components/leftpane/LeftPane.tsx b/ts/components/leftpane/LeftPane.tsx index fdeae0b7c..376719647 100644 --- a/ts/components/leftpane/LeftPane.tsx +++ b/ts/components/leftpane/LeftPane.tsx @@ -12,6 +12,7 @@ import { CallInFullScreenContainer } from '../calling/CallInFullScreenContainer' import { DraggableCallContainer } from '../calling/DraggableCallContainer'; import { IncomingCallDialog } from '../calling/IncomingCallDialog'; import { ModalContainer } from '../dialog/ModalContainer'; +import { SessionToastContainer } from '../SessionToastContainer'; import { ActionsPanel } from './ActionsPanel'; import { LeftPaneMessageSection } from './LeftPaneMessageSection'; import { LeftPaneSettingSection } from './LeftPaneSettingSection'; @@ -71,6 +72,7 @@ export const LeftPane = () => { <div className="module-left-pane-session"> <ModalContainer /> <CallContainer /> + <SessionToastContainer /> <ActionsPanel /> <StyledLeftPane className="module-left-pane"> diff --git a/ts/components/registration/RegistrationStages.tsx b/ts/components/registration/RegistrationStages.tsx index 04e0ef7c6..9985552a2 100644 --- a/ts/components/registration/RegistrationStages.tsx +++ b/ts/components/registration/RegistrationStages.tsx @@ -17,7 +17,6 @@ import { import { fromHex } from '../../session/utils/String'; import { setSignInByLinking, setSignWithRecoveryPhrase, Storage } from '../../util/storage'; -export const MAX_USERNAME_LENGTH = 26; // tslint:disable: use-simple-attributes export async function resetRegistration() { diff --git a/ts/components/registration/RegistrationUserDetails.tsx b/ts/components/registration/RegistrationUserDetails.tsx index 9f99f8026..dd56fe3c3 100644 --- a/ts/components/registration/RegistrationUserDetails.tsx +++ b/ts/components/registration/RegistrationUserDetails.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import React from 'react'; +import { MAX_USERNAME_BYTES } from '../../session/constants'; import { SessionInput } from '../basic/SessionInput'; -import { MAX_USERNAME_LENGTH } from './RegistrationStages'; const DisplayNameInput = (props: { stealAutoFocus?: boolean; @@ -17,7 +17,7 @@ const DisplayNameInput = (props: { type="text" placeholder={window.i18n('enterDisplayName')} value={props.displayName} - maxLength={MAX_USERNAME_LENGTH} + maxLength={MAX_USERNAME_BYTES} onValueChanged={props.onDisplayNameChanged} onEnterPressed={props.handlePressEnter} inputDataTestId="display-name-input" diff --git a/ts/components/registration/SignInTab.tsx b/ts/components/registration/SignInTab.tsx index f0505c862..eec08d732 100644 --- a/ts/components/registration/SignInTab.tsx +++ b/ts/components/registration/SignInTab.tsx @@ -1,4 +1,5 @@ import React, { useContext, useState } from 'react'; +import { ToastUtils } from '../../session/utils'; import { sanitizeSessionUsername } from '../../session/utils/String'; import { Flex } from '../basic/Flex'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; @@ -95,6 +96,23 @@ const SignInButtons = (props: { ); }; +export function sanitizeDisplayNameOrToast( + displayName: string, + setDisplayName: (sanitized: string) => void, + setDisplayNameError: (error: string | undefined) => void +) { + try { + const sanitizedName = sanitizeSessionUsername(displayName); + const trimName = sanitizedName.trim(); + setDisplayName(sanitizedName); + setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined); + } catch (e) { + setDisplayName(displayName); + setDisplayNameError(window.i18n('displayNameTooLong')); + ToastUtils.pushToastError('toolong', window.i18n('displayNameTooLong')); + } +} + export const SignInTab = () => { const { setRegistrationPhase, signInMode, setSignInMode } = useContext(RegistrationContext); @@ -148,10 +166,7 @@ export const SignInTab = () => { displayName={displayName} handlePressEnter={continueYourSession} onDisplayNameChanged={(name: string) => { - const sanitizedName = sanitizeSessionUsername(name); - const trimName = sanitizedName.trim(); - setDisplayName(sanitizedName); - setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined); + sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError); }} onSeedChanged={(seed: string) => { setRecoveryPhrase(seed); diff --git a/ts/components/registration/SignUpTab.tsx b/ts/components/registration/SignUpTab.tsx index 66dcefa78..586eb26a7 100644 --- a/ts/components/registration/SignUpTab.tsx +++ b/ts/components/registration/SignUpTab.tsx @@ -1,12 +1,11 @@ import React, { useContext, useEffect, useState } from 'react'; -import { sanitizeSessionUsername } from '../../session/utils/String'; import { Flex } from '../basic/Flex'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionIdEditable } from '../basic/SessionIdEditable'; import { SessionIconButton } from '../icon'; import { RegistrationContext, RegistrationPhase, signUp } from './RegistrationStages'; import { RegistrationUserDetails } from './RegistrationUserDetails'; -import { SignInMode } from './SignInTab'; +import { sanitizeDisplayNameOrToast, SignInMode } from './SignInTab'; import { TermsAndConditions } from './TermsAndConditions'; export enum SignUpMode { @@ -144,10 +143,7 @@ export const SignUpTab = () => { displayName={displayName} handlePressEnter={signUpWithDetails} onDisplayNameChanged={(name: string) => { - const sanitizedName = sanitizeSessionUsername(name); - const trimName = sanitizedName.trim(); - setDisplayName(sanitizedName); - setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined); + sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError); }} stealAutoFocus={true} /> diff --git a/ts/session/constants.ts b/ts/session/constants.ts index 9517c6a29..35bd981fb 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -60,3 +60,5 @@ export const UI = { export const QUOTED_TEXT_MAX_LENGTH = 150; export const DEFAULT_RECENT_REACTS = ['😂', '🥰', '😢', '😡', '😮', '😈']; + +export const MAX_USERNAME_BYTES = 64; diff --git a/ts/session/utils/String.ts b/ts/session/utils/String.ts index ba056fa81..b12f09971 100644 --- a/ts/session/utils/String.ts +++ b/ts/session/utils/String.ts @@ -1,4 +1,5 @@ import ByteBuffer from 'bytebuffer'; +import { MAX_USERNAME_BYTES } from '../constants'; export type Encoding = 'base64' | 'hex' | 'binary' | 'utf8'; export type BufferType = ByteBuffer | Buffer | ArrayBuffer | Uint8Array; @@ -54,10 +55,19 @@ const forbiddenDisplayCharRegex = /\uFFD2*/g; * * This function removes any forbidden char from a given display name. * This does not trim it as otherwise, a user cannot type User A as when he hits the space, it gets trimmed right away. - * The trimming should hence happen after calling this and on saving the display name + * The trimming should hence happen after calling this and on saving the display name. + * + * This functions makes sure that the MAX_USERNAME_BYTES is verified for utf8 byte length * @param inputName the input to sanitize * @returns a sanitized string, untrimmed */ export const sanitizeSessionUsername = (inputName: string) => { - return inputName.replace(forbiddenDisplayCharRegex, ''); + const validChars = inputName.replace(forbiddenDisplayCharRegex, ''); + + const lengthBytes = encode(validChars, 'utf8').byteLength; + if (lengthBytes > MAX_USERNAME_BYTES) { + throw new Error('Display name is too long'); + } + + return validChars; }; diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 95e8907dd..2aa7aff76 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -495,6 +495,7 @@ export type LocalizerKeys = | 'trustThisContactDialogDescription' | 'unknownCountry' | 'searchFor...' + | 'displayNameTooLong' | 'joinedTheGroup' | 'editGroupName' | 'reportIssue';