Refactor account header banners (#38921)
This commit is contained in:
parent
bbb3392dbe
commit
758db36ec7
140
app/javascript/mastodon/components/account_header/banners.tsx
Normal file
140
app/javascript/mastodon/components/account_header/banners.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { FC, ReactElement, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
authorizeFollowRequest,
|
||||||
|
rejectFollowRequest,
|
||||||
|
} from '@/mastodon/actions/accounts';
|
||||||
|
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
|
||||||
|
import { useRelationship } from '@/mastodon/hooks/useRelationship';
|
||||||
|
import type { Account } from '@/mastodon/models/account';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
|
||||||
|
import { AvatarOverlay } from '../avatar_overlay';
|
||||||
|
import { Button } from '../button';
|
||||||
|
import { DisplayName } from '../display_name';
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
export const AccountBanners: FC<{ account: Account }> = ({ account }) => {
|
||||||
|
const { suspended, hidden } = useAccountVisibility(account.id);
|
||||||
|
const relationship = useRelationship(account.id);
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let banner: ReactNode = null;
|
||||||
|
|
||||||
|
if (account.memorial) {
|
||||||
|
banner = (
|
||||||
|
<MessageText>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.in_memoriam'
|
||||||
|
defaultMessage='In Memoriam.'
|
||||||
|
/>
|
||||||
|
</MessageText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.moved) {
|
||||||
|
banner = <MovedNote account={account} targetAccountId={account.moved} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!suspended && relationship?.requested_by) {
|
||||||
|
banner = <FollowRequestNote account={account} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!banner) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={classes.bannerWrapper}>{banner}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FollowRequestNote: FC<{ account: Account }> = ({ account }) => {
|
||||||
|
const accountId = account.id;
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleAuthorize = useCallback(() => {
|
||||||
|
dispatch(authorizeFollowRequest(accountId));
|
||||||
|
}, [accountId, dispatch]);
|
||||||
|
const handleReject = useCallback(() => {
|
||||||
|
dispatch(rejectFollowRequest(accountId));
|
||||||
|
}, [accountId, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MessageText>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.requested_follow'
|
||||||
|
defaultMessage='{name} has requested to follow you'
|
||||||
|
values={{ name: <DisplayName account={account} variant='simple' /> }}
|
||||||
|
/>
|
||||||
|
</MessageText>
|
||||||
|
|
||||||
|
<div className={classes.bannerActions}>
|
||||||
|
<Button secondary onClick={handleAuthorize}>
|
||||||
|
<Icon id='check' icon={CheckIcon} />
|
||||||
|
<FormattedMessage
|
||||||
|
id='follow_request.authorize'
|
||||||
|
defaultMessage='Authorize'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button secondary onClick={handleReject}>
|
||||||
|
<Icon id='times' icon={CloseIcon} />
|
||||||
|
<FormattedMessage
|
||||||
|
id='follow_request.reject'
|
||||||
|
defaultMessage='Reject'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MovedNote: React.FC<{
|
||||||
|
account: Account;
|
||||||
|
targetAccountId: string;
|
||||||
|
}> = ({ account: from, targetAccountId }) => {
|
||||||
|
const to = useAppSelector((state) => state.accounts.get(targetAccountId));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MessageText>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.moved_to'
|
||||||
|
defaultMessage='{name} has indicated that their new account is now:'
|
||||||
|
values={{
|
||||||
|
name: <DisplayName account={from} variant='simple' />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MessageText>
|
||||||
|
|
||||||
|
<div className={classes.bannerActions}>
|
||||||
|
<Link to={`/@${to?.acct}`} className={classes.bannerActionsDisplayName}>
|
||||||
|
<AvatarOverlay account={to} friend={from} />
|
||||||
|
<DisplayName account={to} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link to={`/@${to?.acct}`} className='button'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.go_to_profile'
|
||||||
|
defaultMessage='Go to profile'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessageText: React.FC<{ children: ReactElement }> = ({ children }) => (
|
||||||
|
<div className={classes.bannerText}>{children}</div>
|
||||||
|
);
|
||||||
@ -5,7 +5,6 @@ import classNames from 'classnames';
|
|||||||
import { Helmet } from '@unhead/react/helmet';
|
import { Helmet } from '@unhead/react/helmet';
|
||||||
|
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import FollowRequestNoteContainer from '@/mastodon/features/account/containers/follow_request_note_container';
|
|
||||||
import { useLayout } from '@/mastodon/hooks/useLayout';
|
import { useLayout } from '@/mastodon/hooks/useLayout';
|
||||||
import { useVisibility } from '@/mastodon/hooks/useVisibility';
|
import { useVisibility } from '@/mastodon/hooks/useVisibility';
|
||||||
import {
|
import {
|
||||||
@ -22,10 +21,9 @@ import { Avatar } from '../avatar';
|
|||||||
import { AnimateEmojiProvider } from '../emoji/context';
|
import { AnimateEmojiProvider } from '../emoji/context';
|
||||||
import { FamiliarFollowers } from '../familiar_followers';
|
import { FamiliarFollowers } from '../familiar_followers';
|
||||||
|
|
||||||
|
import { AccountBanners } from './banners';
|
||||||
import { AccountButtons } from './buttons';
|
import { AccountButtons } from './buttons';
|
||||||
import { AccountHeaderFields } from './fields';
|
import { AccountHeaderFields } from './fields';
|
||||||
import { MemorialNote } from './memorial_note';
|
|
||||||
import { MovedNote } from './moved_note';
|
|
||||||
import { AccountName } from './name';
|
import { AccountName } from './name';
|
||||||
import { AccountNote } from './note';
|
import { AccountNote } from './note';
|
||||||
import { AccountNumberFields } from './number_fields';
|
import { AccountNumberFields } from './number_fields';
|
||||||
@ -51,9 +49,6 @@ export const AccountHeader: React.FC<{
|
|||||||
}> = ({ accountId, hideTabs }) => {
|
}> = ({ accountId, hideTabs }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
const relationship = useAppSelector((state) =>
|
|
||||||
state.relationships.get(accountId),
|
|
||||||
);
|
|
||||||
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
||||||
|
|
||||||
const handleOpenAvatar = useCallback(
|
const handleOpenAvatar = useCallback(
|
||||||
@ -98,18 +93,11 @@ export const AccountHeader: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!hidden && account.memorial && <MemorialNote />}
|
<AccountBanners account={account} />
|
||||||
{!hidden && account.moved && (
|
|
||||||
<MovedNote accountId={account.id} targetAccountId={account.moved} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AnimateEmojiProvider
|
<AnimateEmojiProvider
|
||||||
className={classNames(!!account.moved && classes.moved)}
|
className={classNames(!!account.moved && classes.moved)}
|
||||||
>
|
>
|
||||||
{!suspendedOrHidden && !account.moved && relationship?.requested_by && (
|
|
||||||
<FollowRequestNoteContainer account={account} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
{!suspendedOrHidden && (
|
{!suspendedOrHidden && (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
export const MemorialNote: React.FC = () => (
|
|
||||||
<div className='account-memorial-banner'>
|
|
||||||
<div className='account-memorial-banner__message'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.in_memoriam'
|
|
||||||
defaultMessage='In Memoriam.'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useAppSelector } from '@/mastodon/store';
|
|
||||||
|
|
||||||
import { AvatarOverlay } from '../avatar_overlay';
|
|
||||||
import { DisplayName } from '../display_name';
|
|
||||||
|
|
||||||
export const MovedNote: React.FC<{
|
|
||||||
accountId: string;
|
|
||||||
targetAccountId: string;
|
|
||||||
}> = ({ accountId, targetAccountId }) => {
|
|
||||||
const from = useAppSelector((state) => state.accounts.get(accountId));
|
|
||||||
const to = useAppSelector((state) => state.accounts.get(targetAccountId));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='moved-account-banner'>
|
|
||||||
<div className='moved-account-banner__message'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.moved_to'
|
|
||||||
defaultMessage='{name} has indicated that their new account is now:'
|
|
||||||
values={{
|
|
||||||
name: <DisplayName account={from} variant='simple' />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='moved-account-banner__action'>
|
|
||||||
<Link to={`/@${to?.acct}`} className='detailed-status__display-name'>
|
|
||||||
<div className='detailed-status__display-avatar'>
|
|
||||||
<AvatarOverlay account={to} friend={from} />
|
|
||||||
</div>
|
|
||||||
<DisplayName account={to} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link to={`/@${to?.acct}`} className='button'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.go_to_profile'
|
|
||||||
defaultMessage='Go to profile'
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -453,13 +453,24 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
|
|||||||
border-bottom: 1px solid var(--color-border-primary);
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Banners
|
||||||
|
|
||||||
|
.bannerWrapper,
|
||||||
.bannerBase {
|
.bannerBase {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerWrapper {
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
padding: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerBase {
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -475,6 +486,13 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bannerText {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.bannerTextAndActions {
|
.bannerTextAndActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -489,6 +507,19 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bannerActions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bannerDisclaimer {
|
.bannerDisclaimer {
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@ -519,6 +550,32 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bannerActionsDisplayName {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 22px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover strong {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong,
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
|
|
||||||
.followButton {
|
.followButton {
|
||||||
|
|||||||
@ -104,8 +104,6 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
|
|
||||||
return '';
|
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@ -133,12 +131,11 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
|
|||||||
className={classNames(classes.bannerBase, classes.bannerBaseCentered)}
|
className={classNames(classes.bannerBase, classes.bannerBaseCentered)}
|
||||||
>
|
>
|
||||||
<div className={classes.bannerTextAndActions}>
|
<div className={classes.bannerTextAndActions}>
|
||||||
<h2>
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='email_subscriptions.submitted.title'
|
||||||
id='email_subscriptions.submitted.title'
|
defaultMessage='One more step'
|
||||||
defaultMessage='One more step'
|
tagName='h2'
|
||||||
/>
|
/>
|
||||||
</h2>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='email_subscriptions.submitted.lead'
|
id='email_subscriptions.submitted.lead'
|
||||||
defaultMessage='Check your inbox for an email to finish signing up for email updates.'
|
defaultMessage='Check your inbox for an email to finish signing up for email updates.'
|
||||||
@ -151,15 +148,14 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className={classes.bannerBase} noValidate>
|
<form onSubmit={handleSubmit} className={classes.bannerBase} noValidate>
|
||||||
<div className={classes.bannerTextAndActions}>
|
<div className={classes.bannerTextAndActions}>
|
||||||
<h2>
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='email_subscriptions.form.title'
|
||||||
id='email_subscriptions.form.title'
|
defaultMessage='Sign up for email updates from {name}'
|
||||||
defaultMessage='Sign up for email updates from {name}'
|
tagName='h2'
|
||||||
values={{
|
values={{
|
||||||
name: <DisplayName account={account} variant='simple' />,
|
name: <DisplayName account={account} variant='simple' />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classes.bannerInputButton}>
|
<div className={classes.bannerInputButton}>
|
||||||
|
|||||||
@ -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);
|
|
||||||
@ -257,7 +257,6 @@
|
|||||||
"account_edit_tags.tag_status_count": "{count, plural, one {# post} other {# posts}}",
|
"account_edit_tags.tag_status_count": "{count, plural, one {# post} other {# posts}}",
|
||||||
"account_list.hidden_notice": "This is only visible to you. To show this list to others, go to <link>{page} > {modal} > {field}</link>.",
|
"account_list.hidden_notice": "This is only visible to you. To show this list to others, go to <link>{page} > {modal} > {field}</link>.",
|
||||||
"account_list.total": "{total, plural, one {# account} other {# accounts}}",
|
"account_list.total": "{total, plural, one {# account} other {# accounts}}",
|
||||||
"account_note.placeholder": "Click to add note",
|
|
||||||
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
||||||
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
||||||
"admin.dashboard.retention.average": "Average",
|
"admin.dashboard.retention.average": "Average",
|
||||||
@ -596,19 +595,6 @@
|
|||||||
"domain_block_modal.you_will_lose_num_followers": "You will lose {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} followers}} and {followingCount, plural, one {{followingCountDisplay} person you follow} other {{followingCountDisplay} people you follow}}.",
|
"domain_block_modal.you_will_lose_num_followers": "You will lose {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} followers}} and {followingCount, plural, one {{followingCountDisplay} person you follow} other {{followingCountDisplay} people you follow}}.",
|
||||||
"domain_block_modal.you_will_lose_relationships": "You will lose all followers and people you follow from this server.",
|
"domain_block_modal.you_will_lose_relationships": "You will lose all followers and people you follow from this server.",
|
||||||
"domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.",
|
"domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.",
|
||||||
"domain_pill.activitypub_lets_connect": "It lets you connect and interact with people not just on Mastodon, but across different social apps too.",
|
|
||||||
"domain_pill.activitypub_like_language": "ActivityPub is like the language Mastodon speaks with other social networks.",
|
|
||||||
"domain_pill.server": "Server",
|
|
||||||
"domain_pill.their_handle": "Their handle:",
|
|
||||||
"domain_pill.their_server": "Their digital home, where all of their posts live.",
|
|
||||||
"domain_pill.their_username": "Their unique identifier on their server. It’s possible to find users with the same username on different servers.",
|
|
||||||
"domain_pill.username": "Username",
|
|
||||||
"domain_pill.whats_in_a_handle": "What's in a handle?",
|
|
||||||
"domain_pill.who_they_are": "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>.",
|
|
||||||
"domain_pill.who_you_are": "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>.",
|
|
||||||
"domain_pill.your_handle": "Your handle:",
|
|
||||||
"domain_pill.your_server": "Your digital home, where all of your posts live. Don’t like this one? Transfer servers at any time and bring your followers, too.",
|
|
||||||
"domain_pill.your_username": "Your unique identifier on this server. It’s possible to find users with the same username on different servers.",
|
|
||||||
"dropdown.empty": "Select an option",
|
"dropdown.empty": "Select an option",
|
||||||
"email_subscriptions.email": "Email",
|
"email_subscriptions.email": "Email",
|
||||||
"email_subscriptions.form.action": "Subscribe",
|
"email_subscriptions.form.action": "Subscribe",
|
||||||
@ -745,7 +731,6 @@
|
|||||||
"footer.terms_of_service": "Terms of service",
|
"footer.terms_of_service": "Terms of service",
|
||||||
"form_error.blank": "Field cannot be blank.",
|
"form_error.blank": "Field cannot be blank.",
|
||||||
"form_field.optional": "(optional)",
|
"form_field.optional": "(optional)",
|
||||||
"generic.saved": "Saved",
|
|
||||||
"getting_started.heading": "Getting started",
|
"getting_started.heading": "Getting started",
|
||||||
"hashtag.admin_moderation": "Open moderation interface for #{name}",
|
"hashtag.admin_moderation": "Open moderation interface for #{name}",
|
||||||
"hashtag.browse": "Browse posts in #{hashtag}",
|
"hashtag.browse": "Browse posts in #{hashtag}",
|
||||||
|
|||||||
@ -8222,47 +8222,6 @@ noscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.moved-account-banner,
|
|
||||||
.follow-request-banner,
|
|
||||||
.account-memorial-banner {
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--color-bg-tertiary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&__message {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
padding: 8px 0;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__action {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailed-status__display-name {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.follow-request-banner .button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-memorial-banner__message {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-inline-form {
|
.column-inline-form {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user