From 4e913f14391e553119324c811d632eb5c64450ca Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 4 Oct 2022 15:29:37 +1100 Subject: [PATCH 1/2] fix: displayName allowed length based on bytes rather than char --- _locales/en/messages.json | 1 + ts/components/dialog/EditProfileDialog.tsx | 65 ++++++++++++------- .../registration/RegistrationStages.tsx | 1 - .../registration/RegistrationUserDetails.tsx | 4 +- ts/components/registration/SignInTab.tsx | 15 +++-- ts/components/registration/SignUpTab.tsx | 15 +++-- ts/session/constants.ts | 2 + ts/session/utils/String.ts | 14 +++- ts/types/LocalizerKeys.ts | 1 + 9 files changed, 82 insertions(+), 36 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 922e556c6..bfb81776a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -360,6 +360,7 @@ "notificationPreview": "Preview", "recoveryPhraseEmpty": "Enter your recovery phrase", "displayNameEmpty": "Please enter a display name", + "displayNameTooLong": "Display name is too long", "members": "$count$ members", "join": "Join", "joinOpenGroup": "Join Community", 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) { - 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/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..ecb40feaf 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'; @@ -148,10 +149,16 @@ 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); + try { + const sanitizedName = sanitizeSessionUsername(name); + const trimName = sanitizedName.trim(); + setDisplayName(sanitizedName); + setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined); + } catch (e) { + setDisplayName(name); + setDisplayNameError(window.i18n('displayNameTooLong')); + ToastUtils.pushToastError('toolong', window.i18n('displayNameTooLong')); + } }} onSeedChanged={(seed: string) => { setRecoveryPhrase(seed); diff --git a/ts/components/registration/SignUpTab.tsx b/ts/components/registration/SignUpTab.tsx index 66dcefa78..958f36927 100644 --- a/ts/components/registration/SignUpTab.tsx +++ b/ts/components/registration/SignUpTab.tsx @@ -1,4 +1,5 @@ import React, { useContext, useEffect, 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'; @@ -144,10 +145,16 @@ 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); + try { + const sanitizedName = sanitizeSessionUsername(name); + const trimName = sanitizedName.trim(); + setDisplayName(sanitizedName); + setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined); + } catch (e) { + setDisplayName(name); + setDisplayNameError(window.i18n('displayNameTooLong')); + ToastUtils.pushToastError('toolong', window.i18n('displayNameTooLong')); + } }} 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 f23f5fb23..7d0d4f85e 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -491,6 +491,7 @@ export type LocalizerKeys = | 'trustThisContactDialogDescription' | 'unknownCountry' | 'searchFor...' + | 'displayNameTooLong' | 'joinedTheGroup' | 'editGroupName' | 'reportIssue'; From 8d946da4907908024562af7a788500e002225b72 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 4 Oct 2022 16:27:15 +1100 Subject: [PATCH 2/2] fix: click on toast does not close dialogs --- ts/components/SessionWrapperModal.tsx | 12 +++------- ts/components/basic/SessionToast.tsx | 14 +++++------- ts/components/leftpane/ActionsPanel.tsx | 3 --- ts/components/leftpane/LeftPane.tsx | 2 ++ ts/components/registration/SignInTab.tsx | 28 +++++++++++++++--------- ts/components/registration/SignUpTab.tsx | 15 ++----------- 6 files changed, 31 insertions(+), 43 deletions(-) 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 (
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 - + 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 = () => {
- -
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 = () => {
+ diff --git a/ts/components/registration/SignInTab.tsx b/ts/components/registration/SignInTab.tsx index ecb40feaf..eec08d732 100644 --- a/ts/components/registration/SignInTab.tsx +++ b/ts/components/registration/SignInTab.tsx @@ -96,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); @@ -149,16 +166,7 @@ export const SignInTab = () => { displayName={displayName} handlePressEnter={continueYourSession} onDisplayNameChanged={(name: string) => { - try { - const sanitizedName = sanitizeSessionUsername(name); - const trimName = sanitizedName.trim(); - setDisplayName(sanitizedName); - setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined); - } catch (e) { - setDisplayName(name); - setDisplayNameError(window.i18n('displayNameTooLong')); - ToastUtils.pushToastError('toolong', window.i18n('displayNameTooLong')); - } + sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError); }} onSeedChanged={(seed: string) => { setRecoveryPhrase(seed); diff --git a/ts/components/registration/SignUpTab.tsx b/ts/components/registration/SignUpTab.tsx index 958f36927..586eb26a7 100644 --- a/ts/components/registration/SignUpTab.tsx +++ b/ts/components/registration/SignUpTab.tsx @@ -1,13 +1,11 @@ import React, { useContext, useEffect, 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'; 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 { @@ -145,16 +143,7 @@ export const SignUpTab = () => { displayName={displayName} handlePressEnter={signUpWithDetails} onDisplayNameChanged={(name: string) => { - try { - const sanitizedName = sanitizeSessionUsername(name); - const trimName = sanitizedName.trim(); - setDisplayName(sanitizedName); - setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined); - } catch (e) { - setDisplayName(name); - setDisplayNameError(window.i18n('displayNameTooLong')); - ToastUtils.pushToastError('toolong', window.i18n('displayNameTooLong')); - } + sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError); }} stealAutoFocus={true} />