From 955ec252a421a5060345815bac0c63e8718bc3d8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 16 Apr 2023 02:10:48 +0200 Subject: [PATCH] Add client-side validation for taken username in sign-up form (#24546) --- .../mastodon/locales/defaultMessages.json | 17 +++++++ app/javascript/mastodon/locales/en.json | 3 ++ app/javascript/packs/public.jsx | 50 ++++++++++++------- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 4f06d585ab..6d6683808b 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -4358,5 +4358,22 @@ } ], "path": "app/javascript/mastodon/features/video/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "That username is taken. Try another", + "id": "username.taken" + }, + { + "defaultMessage": "Password confirmation exceeds the maximum password length", + "id": "password_confirmation.exceeds_maxlength" + }, + { + "defaultMessage": "Password confirmation does not match", + "id": "password_confirmation.mismatching" + } + ], + "path": "app/javascript/packs/public.json" } ] \ No newline at end of file diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index e29ec24408..31fa3ca3ac 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -443,6 +443,8 @@ "notifications_permission_banner.enable": "Enable desktop notifications", "notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.", "notifications_permission_banner.title": "Never miss a thing", + "password_confirmation.exceeds_maxlength": "Password confirmation exceeds the maximum password length", + "password_confirmation.mismatching": "Password confirmation does not match", "picture_in_picture.restore": "Put it back", "poll.closed": "Closed", "poll.refresh": "Refresh", @@ -651,6 +653,7 @@ "upload_modal.preview_label": "Preview ({ratio})", "upload_progress.label": "Uploading...", "upload_progress.processing": "Processing…", + "username.taken": "That username is taken. Try another", "video.close": "Close video", "video.download": "Download file", "video.exit_fullscreen": "Exit full screen", diff --git a/app/javascript/packs/public.jsx b/app/javascript/packs/public.jsx index 2642aae130..3e832c509e 100644 --- a/app/javascript/packs/public.jsx +++ b/app/javascript/packs/public.jsx @@ -5,6 +5,15 @@ import ready from '../mastodon/ready'; import { start } from '../mastodon/common'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; import 'cocoon-js-vanilla'; +import axios from 'axios'; +import { throttle } from 'lodash'; +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' }, + passwordExceedsLength: { id: 'password_confirmation.exceeds_maxlength', defaultMessage: 'Password confirmation exceeds the maximum password length' }, + passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' }, +}); start(); @@ -30,7 +39,7 @@ function main() { const { delegate } = require('@rails/ujs'); const emojify = require('../mastodon/features/emoji/emoji').default; const { getLocale } = require('../mastodon/locales'); - const { messages } = getLocale(); + const { localeData } = getLocale(); const React = require('react'); const ReactDOM = require('react-dom'); const { createBrowserHistory } = require('history'); @@ -75,6 +84,11 @@ function main() { hour12: false, }); + const formatMessage = ({ id, defaultMessage }, values) => { + const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale); + return messageFormat.format(values); + }; + [].forEach.call(document.querySelectorAll('.emojify'), (content) => { content.innerHTML = emojify(content.innerHTML); }); @@ -94,7 +108,7 @@ function main() { date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear(); }; - const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale); + const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale); [].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => { const datetime = new Date(content.getAttribute('datetime')); @@ -120,7 +134,7 @@ function main() { const timeGiven = content.getAttribute('datetime').includes('T'); content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime); content.textContent = timeAgoString({ - formatMessage: ({ id, defaultMessage }, values) => (new IntlMessageFormat(messages[id] || defaultMessage, locale)).format(values), + formatMessage, formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date), }, datetime, now, now.getFullYear(), timeGiven); }); @@ -150,17 +164,19 @@ function main() { scrollToDetailedStatus(); } - delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => { - const password = document.getElementById('registration_user_password'); - const confirmation = document.getElementById('registration_user_password_confirmation'); - if (confirmation.value && confirmation.value.length > password.maxLength) { - confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format()); - } else if (password.value && password.value !== confirmation.value) { - confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format()); + delegate(document, '#user_account_attributes_username', 'input', throttle(() => { + const username = document.getElementById('user_account_attributes_username'); + + if (username.value && username.value.length > 0) { + axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => { + username.setCustomValidity(formatMessage(messages.usernameTaken)); + }).catch(() => { + username.setCustomValidity(''); + }); } else { - confirmation.setCustomValidity(''); + username.setCustomValidity(''); } - }); + }, 500, { leading: false, trailing: true })); delegate(document, '#user_password,#user_password_confirmation', 'input', () => { const password = document.getElementById('user_password'); @@ -168,9 +184,9 @@ function main() { if (!confirmation) return; if (confirmation.value && confirmation.value.length > password.maxLength) { - confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format()); + confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength)); } else if (password.value && password.value !== confirmation.value) { - confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format()); + confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch)); } else { confirmation.setCustomValidity(''); } @@ -184,10 +200,10 @@ function main() { if (statusEl.dataset.spoiler === 'expanded') { statusEl.dataset.spoiler = 'folded'; - this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format(); + this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format(); } else { statusEl.dataset.spoiler = 'expanded'; - this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format(); + this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format(); } return false; @@ -195,7 +211,7 @@ function main() { [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => { const statusEl = spoilerLink.parentNode.parentNode; - const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more'); + const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more'); spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format(); }); });