Refactor account header banners (#38921)

This commit is contained in:
Echo authored and GitHub committed 2026-05-13 08:38:17 +00:00
1 parent bbb3392dbe
commit 758db36ec7
12 files changed
+215 -547

No files matched your search

@@ -1,131 +0,0 @@
import type { ChangeEventHandler, KeyboardEventHandler } from 'react';
import { useState, useRef, useCallback, useId } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import Textarea from 'react-textarea-autosize';
import { submitAccountNote } from '@/mastodon/actions/account_notes';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
const messages = defineMessages({
placeholder: {
id: 'account_note.placeholder',
defaultMessage: 'Click to add a note',
},
});
const AccountNoteUI: React.FC<{
initialValue: string | undefined;
onSubmit: (newNote: string) => void;
wasSaved: boolean;
}> = ({ initialValue, onSubmit, wasSaved }) => {
const intl = useIntl();
const uniqueId = useId();
const [value, setValue] = useState(initialValue ?? '');
const isLoading = initialValue === undefined;
const canSubmitOnBlurRef = useRef(true);
const handleChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
(e) => {
setValue(e.target.value);
},
[],
);
const handleKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
(e) => {
if (e.key === 'Escape') {
e.preventDefault();
setValue(initialValue ?? '');
canSubmitOnBlurRef.current = false;
e.currentTarget.blur();
} else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
onSubmit(value);
canSubmitOnBlurRef.current = false;
e.currentTarget.blur();
}
},
[initialValue, onSubmit, value],
);
const handleBlur = useCallback(() => {
if (initialValue !== value && canSubmitOnBlurRef.current) {
onSubmit(value);
}
canSubmitOnBlurRef.current = true;
}, [initialValue, onSubmit, value]);
return (
<div className='account__header__account-note'>
<label htmlFor={`account-note-${uniqueId}`}>
<FormattedMessage
id='account.account_note_header'
defaultMessage='Personal note'
/>{' '}
<span
aria-live='polite'
role='status'
className='inline-alert'
style={{ opacity: wasSaved ? 1 : 0 }}
>
{wasSaved && (
<FormattedMessage id='generic.saved' defaultMessage='Saved' />
)}
</span>
</label>
{isLoading ? (
<div className='account__header__account-note__loading-indicator-wrapper'>
<LoadingIndicator />
</div>
) : (
<Textarea
id={`account-note-${uniqueId}`}
className='account__header__account-note__content'
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
/>
)}
</div>
);
};
export const AccountNote: React.FC<{
accountId: string;
}> = ({ accountId }) => {
const dispatch = useAppDispatch();
const initialValue = useAppSelector((state) =>
state.relationships.get(accountId)?.get('note'),
);
const [wasSaved, setWasSaved] = useState(false);
const handleSubmit = useCallback(
(note: string) => {
setWasSaved(true);
void dispatch(submitAccountNote({ accountId, note }));
setTimeout(() => {
setWasSaved(false);
}, 2000);
},
[dispatch, accountId],
);
return (
<AccountNoteUI
key={`${accountId}-${initialValue}`}
initialValue={initialValue}
wasSaved={wasSaved}
onSubmit={handleSubmit}
/>
);
};
@@ -1,210 +0,0 @@
import type { ReactNode } from 'react';
import { useState, useRef, useCallback, useId } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import BadgeIcon from '@/material-icons/400-24px/badge.svg?react';
import GlobeIcon from '@/material-icons/400-24px/globe.svg?react';
import { Icon } from 'mastodon/components/icon';
export const DomainPill: React.FC<{
domain: string;
username: string;
isSelf: boolean;
children?: ReactNode;
className?: string;
}> = ({ domain, username, isSelf, children, className }) => {
const accessibilityId = useId();
const [open, setOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
const triggerRef = useRef(null);
const handleClick = useCallback(() => {
setOpen(!open);
}, [open, setOpen]);
const handleExpandClick = useCallback(() => {
setExpanded(!expanded);
}, [expanded, setExpanded]);
return (
<>
<button
className={classNames('account__domain-pill', className, {
active: open,
})}
ref={triggerRef}
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
type='button'
>
{children ?? domain}
</button>
<Overlay
show={open}
rootClose
onHide={handleClick}
offset={[5, 5]}
target={triggerRef}
>
{({ props }) => (
<div
{...props}
role='region'
id={accessibilityId}
className='account__domain-pill__popout dropdown-animation'
>
<div className='account__domain-pill__popout__header'>
<div className='account__domain-pill__popout__header__icon'>
<Icon id='' icon={BadgeIcon} />
</div>
<h3>
<FormattedMessage
id='domain_pill.whats_in_a_handle'
defaultMessage="What's in a handle?"
/>
</h3>
</div>
<div className='account__domain-pill__popout__handle'>
<div className='account__domain-pill__popout__handle__label'>
{isSelf ? (
<FormattedMessage
id='domain_pill.your_handle'
defaultMessage='Your handle:'
/>
) : (
<FormattedMessage
id='domain_pill.their_handle'
defaultMessage='Their handle:'
/>
)}
</div>
<div className='account__domain-pill__popout__handle__handle'>
@{username}@{domain}
</div>
</div>
<div className='account__domain-pill__popout__parts'>
<div>
<div className='account__domain-pill__popout__parts__icon'>
<Icon id='' icon={AlternateEmailIcon} />
</div>
<div>
<h6>
<FormattedMessage
id='domain_pill.username'
defaultMessage='Username'
/>
</h6>
<p>
{isSelf ? (
<FormattedMessage
id='domain_pill.your_username'
defaultMessage='Your unique identifier on this server. It’s possible to find users with the same username on different servers.'
/>
) : (
<FormattedMessage
id='domain_pill.their_username'
defaultMessage='Their unique identifier on their server. It’s possible to find users with the same username on different servers.'
/>
)}
</p>
</div>
</div>
<div>
<div className='account__domain-pill__popout__parts__icon'>
<Icon id='' icon={GlobeIcon} />
</div>
<div>
<h6>
<FormattedMessage
id='domain_pill.server'
defaultMessage='Server'
/>
</h6>
<p>
{isSelf ? (
<FormattedMessage
id='domain_pill.your_server'
defaultMessage='Your digital home, where all of your posts live. Don’t like this one? Transfer servers at any time and bring your followers, too.'
/>
) : (
<FormattedMessage
id='domain_pill.their_server'
defaultMessage='Their digital home, where all of their posts live.'
/>
)}
</p>
</div>
</div>
</div>
<p>
{isSelf ? (
<FormattedMessage
id='domain_pill.who_you_are'
defaultMessage='Because your handle says who you are and where you are, people can interact with you across the social web of <button>ActivityPub-powered platforms</button>.'
values={{
button: (x) => (
<button
onClick={handleExpandClick}
className='link-button'
type='button'
>
{x}
</button>
),
}}
/>
) : (
<FormattedMessage
id='domain_pill.who_they_are'
defaultMessage='Since handles say who someone is and where they are, you can interact with people across the social web of <button>ActivityPub-powered platforms</button>.'
values={{
button: (x) => (
<button
onClick={handleExpandClick}
className='link-button'
type='button'
>
{x}
</button>
),
}}
/>
)}
</p>
{expanded && (
<>
<p>
<FormattedMessage
id='domain_pill.activitypub_like_language'
defaultMessage='ActivityPub is like the language Mastodon speaks with other social networks.'
/>
</p>
<p>
<FormattedMessage
id='domain_pill.activitypub_lets_connect'
defaultMessage='It lets you connect and interact with people not just on Mastodon, but across different social apps too.'
/>
</p>
</>
)}
</div>
)}
</Overlay>
</>
);
};
@@ -1,41 +0,0 @@
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Icon } from 'mastodon/components/icon';
import { DisplayName } from '@/mastodon/components/display_name';
export default class FollowRequestNote extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.record.isRequired,
};
render () {
const { account, onAuthorize, onReject } = this.props;
return (
<div className='follow-request-banner'>
<div className='follow-request-banner__message'>
<FormattedMessage id='account.requested_follow' defaultMessage='{name} has requested to follow you' values={{ name: <DisplayName account={account} variant='simple' /> }} />
</div>
<div className='follow-request-banner__action'>
<button type='button' className='button button-secondary button--confirmation' onClick={onAuthorize}>
<Icon id='check' icon={CheckIcon} />
<FormattedMessage id='follow_request.authorize' defaultMessage='Authorize' />
</button>
<button type='button' className='button button-secondary button--destructive' onClick={onReject}>
<Icon id='times' icon={CloseIcon} />
<FormattedMessage id='follow_request.reject' defaultMessage='Reject' />
</button>
</div>
</div>
);
}
}
@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts';
import FollowRequestNote from '../components/follow_request_note';
const mapDispatchToProps = (dispatch, { account }) => ({
onAuthorize () {
dispatch(authorizeFollowRequest(account.get('id')));
},
onReject () {
dispatch(rejectFollowRequest(account.get('id')));
},
});
export default connect(null, mapDispatchToProps)(FollowRequestNote);