From 29563b0e5100ac41a121a643e5e63fca26bae51c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 19 Oct 2021 09:23:06 +0100 Subject: [PATCH 1/3] Do pre-submit availability check on username during registration --- .../views/auth/RegistrationForm.tsx | 25 ++++++++++++++++++- src/components/views/elements/Validation.tsx | 14 ++++++++--- src/i18n/strings/en_EN.json | 1 + 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index c66d6b80fd6..ac8804c2cc1 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -31,6 +31,7 @@ import Field from '../elements/Field'; import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import CountryDropdown from "./CountryDropdown"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { logger } from "matrix-js-sdk/src/logger"; @@ -367,7 +368,11 @@ export default class RegistrationForm extends React.PureComponent _t("Use lowercase letters, numbers, dashes and underscores only"), + description: (_, results) => { + // omit the description if the only failing result is the `available` one as it makes no sense for it. + if (results.every(({ key, valid }) => key === "available" || valid)) return; + return _t("Use lowercase letters, numbers, dashes and underscores only"); + }, hideDescriptionIfValid: true, rules: [ { @@ -380,6 +385,24 @@ export default class RegistrationForm extends React.PureComponent !value || SAFE_LOCALPART_REGEX.test(value), invalid: () => _t("Some characters not allowed"), }, + { + key: "available", + final: true, + test: async ({ value }) => { + if (!value) { + return true; + } + + try { + await MatrixClientPeg.get().isUsernameAvailable(value); + return true; + } catch (err) { + return false; + } + }, + invalid: () => _t("That username already exists, please try another. " + + "If it belongs to you then click 'Sign in here' below"), + }, ], }); diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index f887741ff7a..a7dbf66ac1e 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -21,6 +21,12 @@ import classNames from "classnames"; type Data = Pick; +interface IResult { + key: string; + valid: boolean; + text: string; +} + interface IRule { key: string; final?: boolean; @@ -32,7 +38,7 @@ interface IRule { interface IArgs { rules: IRule[]; - description?(this: T, derivedData: D): React.ReactChild; + description?(this: T, derivedData: D, results: IResult[]): React.ReactChild; hideDescriptionIfValid?: boolean; deriveData?(data: Data): Promise; } @@ -88,7 +94,7 @@ export default function withValidation({ const data = { value, allowEmpty }; const derivedData = deriveData ? await deriveData(data) : undefined; - const results = []; + const results: IResult[] = []; let valid = true; if (rules && rules.length) { for (const rule of rules) { @@ -164,8 +170,8 @@ export default function withValidation({ if (description && (details || !hideDescriptionIfValid)) { // We're setting `this` to whichever component holds the validation // function. That allows rules to access the state of the component. - const content = description.call(this, derivedData); - summary =
{ content }
; + const content = description.call(this, derivedData, results); + summary = content ?
{ content }
: undefined; } let feedback; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 24ac3d162d8..9a923e66d0f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2768,6 +2768,7 @@ "Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details", "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)", "Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only", + "That username already exists, please try another. If it belongs to you then click 'Sign in here' below": "That username already exists, please try another. If it belongs to you then click 'Sign in here' below", "Phone (optional)": "Phone (optional)", "Register": "Register", "Add an email to be able to reset your password.": "Add an email to be able to reset your password.", From ccecdd80a7d6e11ecb23f3bd904c895d9f783277 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Oct 2021 15:11:21 +0100 Subject: [PATCH 2/3] Fix matrixClient usage during registration flow --- src/components/structures/auth/Registration.tsx | 1 + src/components/views/auth/RegistrationForm.tsx | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 3c66a1ab86d..594959a05fd 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -507,6 +507,7 @@ export default class Registration extends React.Component { flows={this.state.flows} serverConfig={this.props.serverConfig} canSubmit={!this.state.serverErrorIsFatal} + matrixClient={this.state.matrixClient} /> ; } diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index ac8804c2cc1..434ada893ed 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; import * as Email from '../../../email'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; @@ -31,7 +32,6 @@ import Field from '../elements/Field'; import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import CountryDropdown from "./CountryDropdown"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { logger } from "matrix-js-sdk/src/logger"; @@ -57,6 +57,7 @@ interface IProps { }[]; serverConfig: ValidatedServerConfig; canSubmit?: boolean; + matrixClient: MatrixClient; onRegisterClick(params: { username: string; @@ -394,7 +395,7 @@ export default class RegistrationForm extends React.PureComponent Date: Wed, 3 Nov 2021 17:42:31 +0000 Subject: [PATCH 3/3] update copy based on review --- src/components/structures/auth/Registration.tsx | 2 +- src/components/views/auth/RegistrationForm.tsx | 3 +-- src/i18n/strings/en_EN.json | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 594959a05fd..a019e0d933f 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -304,7 +304,7 @@ export default class Registration extends React.Component { msg = _t('This server does not support authentication with a phone number.'); } } else if (response.errcode === "M_USER_IN_USE") { - msg = _t("That username already exists, please try another."); + msg = _t("Someone already has that username, please try another."); } this.setState({ busy: false, diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index 434ada893ed..aa79789510b 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -401,8 +401,7 @@ export default class RegistrationForm extends React.PureComponent _t("That username already exists, please try another. " + - "If it belongs to you then click 'Sign in here' below"), + invalid: () => _t("Someone already has that username. Try another or if it is you, sign in below."), }, ], }); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9a923e66d0f..5d7cc1bdf55 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2768,7 +2768,7 @@ "Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details", "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)", "Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only", - "That username already exists, please try another. If it belongs to you then click 'Sign in here' below": "That username already exists, please try another. If it belongs to you then click 'Sign in here' below", + "Someone already has that username. Try another or if it is you, sign in below.": "Someone already has that username. Try another or if it is you, sign in below.", "Phone (optional)": "Phone (optional)", "Register": "Register", "Add an email to be able to reset your password.": "Add an email to be able to reset your password.", @@ -3055,7 +3055,7 @@ "Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", - "That username already exists, please try another.": "That username already exists, please try another.", + "Someone already has that username, please try another.": "Someone already has that username, please try another.", "Continue with %(ssoButtons)s": "Continue with %(ssoButtons)s", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Or %(usernamePassword)s", "Already have an account? Sign in here": "Already have an account? Sign in here",