diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx
deleted file mode 100644
index 446cc2586a..0000000000
--- a/app/javascript/mastodon/features/interaction_modal/index.jsx
+++ /dev/null
@@ -1,427 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
-
-import classNames from 'classnames';
-
-import { connect } from 'react-redux';
-
-import { throttle, escapeRegExp } from 'lodash';
-
-import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
-import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
-import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
-import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
-import StarIcon from '@/material-icons/400-24px/star.svg?react';
-import { openModal, closeModal } from 'mastodon/actions/modal';
-import api from 'mastodon/api';
-import { Button } from 'mastodon/components/button';
-import { Icon } from 'mastodon/components/icon';
-import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
-
-const messages = defineMessages({
- loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' },
-});
-
-const mapStateToProps = (state, { accountId }) => ({
- displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
- signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
-});
-
-const mapDispatchToProps = (dispatch) => ({
- onSignupClick() {
- dispatch(closeModal({
- modalType: undefined,
- ignoreFocus: false,
- }));
- dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
- },
-});
-
-const PERSISTENCE_KEY = 'mastodon_home';
-
-const isValidDomain = value => {
- const url = new URL('https:///path');
- url.hostname = value;
- return url.hostname === value;
-};
-
-const valueToDomain = value => {
- // If the user starts typing an URL
- if (/^https?:\/\//.test(value)) {
- try {
- const url = new URL(value);
-
- // Consider that if there is a path, the URL is more meaningful than a bare domain
- if (url.pathname.length > 1) {
- return '';
- }
-
- return url.host;
- } catch {
- return undefined;
- }
- // If the user writes their full handle including username
- } else if (value.includes('@')) {
- if (value.replace(/^@/, '').split('@').length > 2) {
- return undefined;
- }
- return '';
- }
-
- return value;
-};
-
-const addInputToOptions = (value, options) => {
- value = value.trim();
-
- if (value.includes('.') && isValidDomain(value)) {
- return [value].concat(options.filter((x) => x !== value));
- }
-
- return options;
-};
-
-class LoginForm extends React.PureComponent {
-
- static propTypes = {
- resourceUrl: PropTypes.string,
- intl: PropTypes.object.isRequired,
- };
-
- state = {
- value: localStorage ? (localStorage.getItem(PERSISTENCE_KEY) || '') : '',
- expanded: false,
- selectedOption: -1,
- isLoading: false,
- isSubmitting: false,
- error: false,
- options: [],
- networkOptions: [],
- };
-
- setRef = c => {
- this.input = c;
- };
-
- isValueValid = (value) => {
- let likelyAcct = false;
- let url = null;
-
- if (value.startsWith('/')) {
- return false;
- }
-
- if (value.startsWith('@')) {
- value = value.slice(1);
- likelyAcct = true;
- }
-
- // The user is in the middle of typing something, do not error out
- if (value === '') {
- return true;
- }
-
- if (/^https?:\/\//.test(value) && !likelyAcct) {
- url = value;
- } else {
- url = `https://${value}`;
- }
-
- try {
- new URL(url);
- return true;
- } catch {
- return false;
- }
- };
-
- handleChange = ({ target }) => {
- const error = !this.isValueValid(target.value);
- this.setState(state => ({ error, value: target.value, isLoading: true, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
- };
-
- handleMessage = (event) => {
- const { resourceUrl } = this.props;
-
- if (event.origin !== window.origin || event.source !== this.iframeRef.contentWindow) {
- return;
- }
-
- if (event.data?.type === 'fetchInteractionURL-failure') {
- this.setState({ isSubmitting: false, error: true });
- } else if (event.data?.type === 'fetchInteractionURL-success') {
- if (/^https?:\/\//.test(event.data.template)) {
- try {
- const url = new URL(event.data.template.replace('{uri}', encodeURIComponent(resourceUrl)));
-
- if (localStorage) {
- localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
- }
-
- window.location.href = url;
- } catch (e) {
- console.error(e);
- this.setState({ isSubmitting: false, error: true });
- }
- } else {
- this.setState({ isSubmitting: false, error: true });
- }
- }
- };
-
- componentDidMount () {
- window.addEventListener('message', this.handleMessage);
- }
-
- componentWillUnmount () {
- window.removeEventListener('message', this.handleMessage);
- }
-
- handleSubmit = () => {
- const { value } = this.state;
-
- this.setState({ isSubmitting: true });
-
- this.iframeRef.contentWindow.postMessage({
- type: 'fetchInteractionURL',
- uri_or_domain: value.trim(),
- }, window.origin);
- };
-
- setIFrameRef = (iframe) => {
- this.iframeRef = iframe;
- };
-
- handleFocus = () => {
- this.setState({ expanded: true });
- };
-
- handleBlur = () => {
- this.setState({ expanded: false });
- };
-
- handleKeyDown = (e) => {
- const { options, selectedOption } = this.state;
-
- switch(e.key) {
- case 'ArrowDown':
- e.preventDefault();
-
- if (options.length > 0) {
- this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) });
- }
-
- break;
- case 'ArrowUp':
- e.preventDefault();
-
- if (options.length > 0) {
- this.setState({ selectedOption: Math.max(selectedOption - 1, -1) });
- }
-
- break;
- case 'Enter':
- e.preventDefault();
-
- if (selectedOption === -1) {
- this.handleSubmit();
- } else if (options.length > 0) {
- this.setState({ value: options[selectedOption], error: false }, () => this.handleSubmit());
- }
-
- break;
- }
- };
-
- handleOptionClick = e => {
- const index = Number(e.currentTarget.getAttribute('data-index'));
- const option = this.state.options[index];
-
- e.preventDefault();
- this.setState({ selectedOption: index, value: option, error: false }, () => this.handleSubmit());
- };
-
- _loadOptions = throttle(() => {
- const { value } = this.state;
-
- const domain = valueToDomain(value.trim());
-
- if (typeof domain === 'undefined') {
- this.setState({ options: [], networkOptions: [], isLoading: false, error: true });
- return;
- }
-
- if (domain.length === 0) {
- this.setState({ options: [], networkOptions: [], isLoading: false });
- return;
- }
-
- api().get('/api/v1/peers/search', { params: { q: domain } }).then(({ data }) => {
- if (!data) {
- data = [];
- }
-
- this.setState((state) => ({ networkOptions: data, options: addInputToOptions(state.value, data), isLoading: false }));
- }).catch(() => {
- this.setState({ isLoading: false });
- });
- }, 200, { leading: true, trailing: true });
-
- render () {
- const { intl } = this.props;
- const { value, expanded, options, selectedOption, error, isSubmitting } = this.state;
- const domain = (valueToDomain(value) || '').trim();
- const domainRegExp = new RegExp(`(${escapeRegExp(domain)})`, 'gi');
- const hasPopOut = domain.length > 0 && options.length > 0;
-
- return (
-
-
-
-
-
-
-
-
-
-
- {hasPopOut && (
-
-
- {options.map((option, i) => (
-
- {option.split(domainRegExp).map((part, i) => (
- part.toLowerCase() === domain.toLowerCase() ? (
-
- {part}
-
- ) : (
-
- {part}
-
- )
- ))}
-
- ))}
-
-
- )}
-
- );
- }
-
-}
-
-const IntlLoginForm = injectIntl(LoginForm);
-
-class InteractionModal extends React.PureComponent {
-
- static propTypes = {
- displayNameHtml: PropTypes.string,
- url: PropTypes.string,
- type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow', 'vote']),
- onSignupClick: PropTypes.func.isRequired,
- signupUrl: PropTypes.string.isRequired,
- };
-
- handleSignupClick = () => {
- this.props.onSignupClick();
- };
-
- render () {
- const { url, type, displayNameHtml, signupUrl } = this.props;
-
- const name = ;
-
- let title, actionDescription, icon;
-
- switch(type) {
- case 'reply':
- icon = ;
- title = ;
- actionDescription = ;
- break;
- case 'reblog':
- icon = ;
- title = ;
- actionDescription = ;
- break;
- case 'favourite':
- icon = ;
- title = ;
- actionDescription = ;
- break;
- case 'follow':
- icon = ;
- title = ;
- actionDescription = ;
- break;
- case 'vote':
- icon = ;
- title = ;
- actionDescription = ;
- break;
- }
-
- let signupButton;
-
- if (sso_redirect) {
- signupButton = (
-
-
-
- );
- } else if (registrationsOpen) {
- signupButton = (
-
-
-
- );
- } else {
- signupButton = (
-
-
-
- );
- }
-
- return (
-
-
-
{icon} {title}
-
{actionDescription}
-
-
-
-
-
-
{signupButton}
-
- );
- }
-
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(InteractionModal);
diff --git a/app/javascript/mastodon/features/interaction_modal/index.tsx b/app/javascript/mastodon/features/interaction_modal/index.tsx
new file mode 100644
index 0000000000..40d498d1ae
--- /dev/null
+++ b/app/javascript/mastodon/features/interaction_modal/index.tsx
@@ -0,0 +1,581 @@
+import { useCallback, useEffect, useState, useRef } from 'react';
+
+import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { escapeRegExp } from 'lodash';
+import { useDebouncedCallback } from 'use-debounce';
+
+import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
+import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
+import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
+import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
+import StarIcon from '@/material-icons/400-24px/star.svg?react';
+import { openModal, closeModal } from 'mastodon/actions/modal';
+import { apiRequest } from 'mastodon/api';
+import { Button } from 'mastodon/components/button';
+import { Icon } from 'mastodon/components/icon';
+import {
+ domain as localDomain,
+ registrationsOpen,
+ sso_redirect,
+} from 'mastodon/initial_state';
+import { useAppSelector, useAppDispatch } from 'mastodon/store';
+
+const messages = defineMessages({
+ loginPrompt: {
+ id: 'interaction_modal.username_prompt',
+ defaultMessage: 'E.g. {example}',
+ },
+});
+
+interface LoginFormMessage {
+ type:
+ | 'fetchInteractionURL'
+ | 'fetchInteractionURL-failure'
+ | 'fetchInteractionURL-success';
+ uri_or_domain: string;
+ template?: string;
+}
+
+const PERSISTENCE_KEY = 'mastodon_home';
+
+const EXAMPLE_VALUE = 'username@mastodon.social';
+
+const isValidDomain = (value: string) => {
+ const url = new URL('https:///path');
+ url.hostname = value;
+ return url.hostname === value;
+};
+
+const valueToDomain = (value: string): string | null => {
+ // If the user starts typing an URL
+ if (/^https?:\/\//.test(value)) {
+ try {
+ const url = new URL(value);
+
+ return url.host;
+ } catch {
+ return null;
+ }
+ // If the user writes their full handle including username
+ } else if (value.includes('@')) {
+ const [_, domain, ...other] = value.replace(/^@/, '').split('@');
+
+ if (!domain || other.length > 0) {
+ return null;
+ }
+
+ return valueToDomain(domain);
+ }
+
+ return value;
+};
+
+const addInputToOptions = (value: string, options: string[]) => {
+ value = value.trim();
+
+ if (value.includes('.') && isValidDomain(value)) {
+ return [value].concat(options.filter((x) => x !== value));
+ }
+
+ return options;
+};
+
+const isValueValid = (value: string) => {
+ let likelyAcct = false;
+ let url = null;
+
+ if (value.startsWith('/')) {
+ return false;
+ }
+
+ if (value.startsWith('@')) {
+ value = value.slice(1);
+ likelyAcct = true;
+ }
+
+ // The user is in the middle of typing something, do not error out
+ if (value === '') {
+ return true;
+ }
+
+ if (/^https?:\/\//.test(value) && !likelyAcct) {
+ url = value;
+ } else {
+ url = `https://${value}`;
+ }
+
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+const sendToFrame = (frame: HTMLIFrameElement | null, value: string): void => {
+ if (valueToDomain(value.trim()) === localDomain) {
+ window.location.href = '/auth/sign_in';
+ return;
+ }
+
+ frame?.contentWindow?.postMessage(
+ {
+ type: 'fetchInteractionURL',
+ uri_or_domain: value.trim(),
+ },
+ window.origin,
+ );
+};
+
+const LoginForm: React.FC<{
+ resourceUrl: string;
+}> = ({ resourceUrl }) => {
+ const intl = useIntl();
+ const [value, setValue] = useState(
+ localStorage.getItem(PERSISTENCE_KEY) ?? '',
+ );
+ const [expanded, setExpanded] = useState(false);
+ const [selectedOption, setSelectedOption] = useState(-1);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState(false);
+ const [options, setOptions] = useState([]);
+ const [networkOptions, setNetworkOptions] = useState([]);
+ const [valueChanged, setValueChanged] = useState(false);
+
+ const inputRef = useRef(null);
+ const iframeRef = useRef(null);
+ const searchRequestRef = useRef(null);
+
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent) => {
+ if (
+ event.origin !== window.origin ||
+ event.source !== iframeRef.current?.contentWindow
+ ) {
+ return;
+ }
+
+ if (event.data.type === 'fetchInteractionURL-failure') {
+ setIsSubmitting(false);
+ setError(true);
+ } else if (event.data.type === 'fetchInteractionURL-success') {
+ if (event.data.template && /^https?:\/\//.test(event.data.template)) {
+ try {
+ const url = new URL(
+ event.data.template.replace(
+ '{uri}',
+ encodeURIComponent(resourceUrl),
+ ),
+ );
+
+ localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
+
+ window.location.href = url.toString();
+ } catch {
+ setIsSubmitting(false);
+ setError(true);
+ }
+ } else {
+ setIsSubmitting(false);
+ setError(true);
+ }
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+
+ return () => {
+ window.removeEventListener('message', handleMessage);
+ };
+ }, [resourceUrl, setIsSubmitting, setError]);
+
+ const handleSearch = useDebouncedCallback(
+ (value: string) => {
+ if (searchRequestRef.current) {
+ searchRequestRef.current.abort();
+ }
+
+ const domain = valueToDomain(value.trim());
+
+ if (domain === null || domain.length === 0) {
+ setOptions([]);
+ setNetworkOptions([]);
+ return;
+ }
+
+ searchRequestRef.current = new AbortController();
+
+ void apiRequest('GET', 'v1/peers/search', {
+ signal: searchRequestRef.current.signal,
+ params: {
+ q: domain,
+ },
+ })
+ .then((data) => {
+ setNetworkOptions(data ?? []);
+ setOptions(addInputToOptions(value, data ?? []));
+ return '';
+ })
+ .catch(() => {
+ // Nothing
+ });
+ },
+ 500,
+ { leading: true, trailing: true },
+ );
+
+ const handleChange = useCallback(
+ ({ target: { value } }: React.ChangeEvent) => {
+ setValue(value);
+ setValueChanged(true);
+ setError(!isValueValid(value));
+ setOptions(addInputToOptions(value, networkOptions));
+ handleSearch(value);
+ },
+ [
+ setError,
+ setValue,
+ setValueChanged,
+ setOptions,
+ networkOptions,
+ handleSearch,
+ ],
+ );
+
+ const handleSubmit = useCallback(() => {
+ setIsSubmitting(true);
+ sendToFrame(iframeRef.current, value);
+ }, [setIsSubmitting, value]);
+
+ const handleFocus = useCallback(() => {
+ setExpanded(true);
+ }, [setExpanded]);
+
+ const handleBlur = useCallback(() => {
+ setExpanded(false);
+ }, [setExpanded]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ const selectedOptionValue = options[selectedOption];
+
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+
+ if (options.length > 0) {
+ setSelectedOption((selectedOption) =>
+ Math.min(selectedOption + 1, options.length - 1),
+ );
+ }
+
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+
+ if (options.length > 0) {
+ setSelectedOption((selectedOption) =>
+ Math.max(selectedOption - 1, -1),
+ );
+ }
+
+ break;
+ case 'Enter':
+ e.preventDefault();
+
+ if (selectedOption === -1) {
+ handleSubmit();
+ } else if (options.length > 0 && selectedOptionValue) {
+ setError(false);
+ setValue(selectedOptionValue);
+ setIsSubmitting(true);
+ sendToFrame(iframeRef.current, selectedOptionValue);
+ }
+
+ break;
+ }
+ },
+ [
+ handleSubmit,
+ setSelectedOption,
+ setError,
+ setValue,
+ selectedOption,
+ options,
+ ],
+ );
+
+ const handleOptionClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+
+ const index = Number(e.currentTarget.getAttribute('data-index'));
+ const option = options[index];
+
+ if (!option) {
+ return;
+ }
+
+ setSelectedOption(index);
+ setValue(option);
+ setError(false);
+ setIsSubmitting(true);
+ sendToFrame(iframeRef.current, option);
+ },
+ [options, setSelectedOption, setValue, setError],
+ );
+
+ const domain = (valueToDomain(value) ?? '').trim();
+ const domainRegExp = new RegExp(`(${escapeRegExp(domain)})`, 'gi');
+ const hasPopOut = valueChanged && domain.length > 0 && options.length > 0;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {hasPopOut && (
+
+
+ {options.map((option, i) => (
+
+ {option
+ .split(domainRegExp)
+ .map((part, i) =>
+ part.toLowerCase() === domain.toLowerCase() ? (
+ {part}
+ ) : (
+ {part}
+ ),
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+const InteractionModal: React.FC<{
+ accountId: string;
+ url: string;
+ type: 'reply' | 'reblog' | 'favourite' | 'follow' | 'vote';
+}> = ({ accountId, url, type }) => {
+ const dispatch = useAppDispatch();
+ const displayNameHtml = useAppSelector(
+ (state) => state.accounts.get(accountId)?.display_name_html ?? '',
+ );
+ const signupUrl = useAppSelector(
+ (state) =>
+ (state.server.getIn(['server', 'registrations', 'url'], null) ||
+ '/auth/sign_up') as string,
+ );
+ const name = ;
+
+ const handleSignupClick = useCallback(() => {
+ dispatch(
+ closeModal({
+ modalType: undefined,
+ ignoreFocus: false,
+ }),
+ );
+
+ dispatch(
+ openModal({
+ modalType: 'CLOSED_REGISTRATIONS',
+ modalProps: {},
+ }),
+ );
+ }, [dispatch]);
+
+ let title: React.ReactNode,
+ icon: React.ReactNode,
+ actionPrompt: React.ReactNode;
+
+ switch (type) {
+ case 'reply':
+ icon = ;
+ title = (
+
+ );
+ actionPrompt = (
+
+ );
+ break;
+ case 'reblog':
+ icon = ;
+ title = (
+
+ );
+ actionPrompt = (
+
+ );
+ break;
+ case 'favourite':
+ icon = ;
+ title = (
+
+ );
+ actionPrompt = (
+
+ );
+ break;
+ case 'follow':
+ icon = ;
+ title = (
+
+ );
+ actionPrompt = (
+
+ );
+ break;
+ case 'vote':
+ icon = ;
+ title = (
+
+ );
+ actionPrompt = (
+
+ );
+ break;
+ }
+
+ let signupButton;
+
+ if (sso_redirect) {
+ signupButton = (
+
+
+
+ );
+ } else if (registrationsOpen) {
+ signupButton = (
+
+
+
+ );
+ } else {
+ signupButton = (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {icon} {title}
+
+
{actionPrompt}
+
+
+
+
+
+ {' '}
+ {signupButton}
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default InteractionModal;
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 8fe837b141..4482753242 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -407,23 +407,21 @@
"ignore_notifications_modal.not_followers_title": "Ignore notifications from people not following you?",
"ignore_notifications_modal.not_following_title": "Ignore notifications from people you don't follow?",
"ignore_notifications_modal.private_mentions_title": "Ignore notifications from unsolicited Private Mentions?",
- "interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
- "interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
- "interaction_modal.description.reblog": "With an account on Mastodon, you can boost this post to share it with your own followers.",
- "interaction_modal.description.reply": "With an account on Mastodon, you can respond to this post.",
- "interaction_modal.description.vote": "With an account on Mastodon, you can vote in this poll.",
- "interaction_modal.login.action": "Take me home",
- "interaction_modal.login.prompt": "Domain of your home server, e.g. mastodon.social",
- "interaction_modal.no_account_yet": "Not on Mastodon?",
+ "interaction_modal.action.favourite": "To continue, you need to favorite from your account.",
+ "interaction_modal.action.follow": "To continue, you need to follow from your account.",
+ "interaction_modal.action.reblog": "To continue, you need to reblog from your account.",
+ "interaction_modal.action.reply": "To continue, you need to reply from your account.",
+ "interaction_modal.action.vote": "To continue, you need to vote from your account.",
+ "interaction_modal.go": "Go",
+ "interaction_modal.no_account_yet": "Don't have an account yet?",
"interaction_modal.on_another_server": "On a different server",
"interaction_modal.on_this_server": "On this server",
- "interaction_modal.sign_in": "You are not logged in to this server. Where is your account hosted?",
- "interaction_modal.sign_in_hint": "Tip: That's the website where you signed up. If you don't remember, look for the welcome e-mail in your inbox. You can also enter your full username! (e.g. @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Favorite {name}'s post",
"interaction_modal.title.follow": "Follow {name}",
"interaction_modal.title.reblog": "Boost {name}'s post",
"interaction_modal.title.reply": "Reply to {name}'s post",
"interaction_modal.title.vote": "Vote in {name}'s poll",
+ "interaction_modal.username_prompt": "E.g. {example}",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 345753f124..f070fa6fbb 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -8955,6 +8955,7 @@ noscript {
}
p {
+ text-align: center;
font-size: 17px;
line-height: 22px;
color: $darker-text-color;
@@ -9030,11 +9031,6 @@ noscript {
border: 1px solid var(--background-border-color);
}
- &.focused &__input {
- border-color: $highlight-text-color;
- background: lighten($ui-base-color, 4%);
- }
-
&.invalid &__input {
border-color: $error-red;
}