Merge commit '7af5ba271d6f04831f000e9b815914375e42abeb' into glitch-soc/merge-upstream
This commit is contained in:
commit
5a45a3203a
96 files changed
+959
-505
No files matched your search
@@ -44,8 +44,7 @@ export const DomainPill: React.FC<{
|
||||
aria-controls={accessibilityId}
|
||||
type='button'
|
||||
>
|
||||
{domain}
|
||||
{children}
|
||||
{children ?? domain}
|
||||
</button>
|
||||
|
||||
<Overlay
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AccountHeaderFields } from './fields';
|
||||
import { AccountInfo } from './info';
|
||||
import { MemorialNote } from './memorial_note';
|
||||
import { MovedNote } from './moved_note';
|
||||
import { AccountNote as AccountNoteRedesign } from './note';
|
||||
import { AccountNumberFields } from './number_fields';
|
||||
import redesignClasses from './redesign.module.scss';
|
||||
import { AccountTabs } from './tabs';
|
||||
@@ -142,12 +143,7 @@ export const AccountHeader: React.FC<{
|
||||
isRedesignEnabled() && redesignClasses.nameWrapper,
|
||||
)}
|
||||
>
|
||||
<AccountName
|
||||
accountId={accountId}
|
||||
className={classNames(
|
||||
isRedesignEnabled() && redesignClasses.name,
|
||||
)}
|
||||
/>
|
||||
<AccountName accountId={accountId} />
|
||||
{isRedesignEnabled() && <AccountButtons accountId={accountId} />}
|
||||
</div>
|
||||
|
||||
@@ -166,7 +162,9 @@ export const AccountHeader: React.FC<{
|
||||
{!suspendedOrHidden && (
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{me && account.id !== me && (
|
||||
{me && account.id !== me && isRedesignEnabled() ? (
|
||||
<AccountNoteRedesign accountId={accountId} />
|
||||
) : (
|
||||
<AccountNote accountId={accountId} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DisplayName } from '@/mastodon/components/display_name';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import HelpIcon from '@/material-icons/400-24px/help.svg?react';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
|
||||
import { DomainPill } from '../../account/components/domain_pill';
|
||||
@@ -14,10 +14,7 @@ import { isRedesignEnabled } from '../common';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
|
||||
export const AccountName: FC<{ accountId: string; className?: string }> = ({
|
||||
accountId,
|
||||
className,
|
||||
}) => {
|
||||
export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const account = useAccount(accountId);
|
||||
const me = useAppSelector((state) => state.meta.get('me') as string);
|
||||
@@ -31,38 +28,52 @@ export const AccountName: FC<{ accountId: string; className?: string }> = ({
|
||||
|
||||
const [username = '', domain = localDomain] = account.acct.split('@');
|
||||
|
||||
return (
|
||||
<h1 className={className}>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
<small>
|
||||
<span>
|
||||
@{username}
|
||||
{isRedesignEnabled() && '@'}
|
||||
<span className='invisible'>
|
||||
{!isRedesignEnabled() && '@'}
|
||||
{domain}
|
||||
if (!isRedesignEnabled()) {
|
||||
return (
|
||||
<h1>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
<small>
|
||||
<span>
|
||||
@{username}
|
||||
<span className='invisible'>@{domain}</span>
|
||||
</span>
|
||||
</span>
|
||||
<DomainPill
|
||||
username={username}
|
||||
domain={domain}
|
||||
isSelf={me === account.id}
|
||||
/>
|
||||
{account.locked && (
|
||||
<Icon
|
||||
id='lock'
|
||||
icon={LockIcon}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'account.locked_info',
|
||||
defaultMessage:
|
||||
'This account privacy status is set to locked. The owner manually reviews who can follow them.',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</small>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.name}>
|
||||
<h1>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
</h1>
|
||||
<p className={classes.username}>
|
||||
@{username}@{domain}
|
||||
<DomainPill
|
||||
username={username}
|
||||
domain={domain}
|
||||
isSelf={me === account.id}
|
||||
className={(isRedesignEnabled() && classes.domainPill) || ''}
|
||||
className={classes.domainPill}
|
||||
>
|
||||
{isRedesignEnabled() && <Icon id='info' icon={InfoIcon} />}
|
||||
<Icon id='help' icon={HelpIcon} />
|
||||
</DomainPill>
|
||||
{!isRedesignEnabled() && account.locked && (
|
||||
<Icon
|
||||
id='lock'
|
||||
icon={LockIcon}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'account.locked_info',
|
||||
defaultMessage:
|
||||
'This account privacy status is set to locked. The owner manually reviews who can follow them.',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</small>
|
||||
</h1>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -37,7 +37,12 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
let icon: ReactNode = undefined;
|
||||
if (isAdminBadge(role)) {
|
||||
icon = (
|
||||
<Icon icon={IconAdmin} id='badge-admin' className={classes.badgeIcon} />
|
||||
<Icon
|
||||
icon={IconAdmin}
|
||||
id='badge-admin'
|
||||
className={classes.badgeIcon}
|
||||
noFill
|
||||
/>
|
||||
);
|
||||
}
|
||||
badges.push(
|
||||
|
||||
@@ -76,6 +76,7 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
|
||||
id='verified'
|
||||
icon={IconVerified}
|
||||
className={classes.fieldIconVerified}
|
||||
noFill
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -81,6 +81,7 @@ export const AccountFieldsModal: FC<{
|
||||
id='verified'
|
||||
icon={IconVerified}
|
||||
className={classes.fieldIconVerified}
|
||||
noFill
|
||||
/>
|
||||
)}
|
||||
</dd>
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
|
||||
import { isRedesignEnabled } from '../common';
|
||||
|
||||
const messages = defineMessages({
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
|
||||
@@ -55,6 +57,14 @@ const messages = defineMessages({
|
||||
id: 'account.show_reblogs',
|
||||
defaultMessage: 'Show boosts from @{name}',
|
||||
},
|
||||
addNote: {
|
||||
id: 'account.add_note',
|
||||
defaultMessage: 'Add a personal note',
|
||||
},
|
||||
editNote: {
|
||||
id: 'account.edit_note',
|
||||
defaultMessage: 'Edit personal note',
|
||||
},
|
||||
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
|
||||
unendorse: {
|
||||
id: 'account.unendorse',
|
||||
@@ -187,7 +197,30 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
});
|
||||
arr.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRedesignEnabled()) {
|
||||
arr.push({
|
||||
text: intl.formatMessage(
|
||||
relationship?.note ? messages.editNote : messages.addNote,
|
||||
),
|
||||
action: () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'ACCOUNT_NOTE',
|
||||
modalProps: {
|
||||
accountId: account.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
if (!relationship?.following) {
|
||||
arr.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (relationship?.following) {
|
||||
arr.push({
|
||||
text: intl.formatMessage(
|
||||
relationship.endorsed ? messages.unendorse : messages.endorse,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchRelationships } from '@/mastodon/actions/accounts';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { Callout } from '@/mastodon/components/callout';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import EditIcon from '@/material-icons/400-24px/edit_square.svg?react';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'account.note.title',
|
||||
defaultMessage: 'Personal note (visible only to you)',
|
||||
},
|
||||
editButton: {
|
||||
id: 'account.note.edit_button',
|
||||
defaultMessage: 'Edit',
|
||||
},
|
||||
});
|
||||
|
||||
export const AccountNote: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const relationship = useAppSelector((state) =>
|
||||
state.relationships.get(accountId),
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
if (!relationship) {
|
||||
dispatch(fetchRelationships([accountId]));
|
||||
}
|
||||
}, [accountId, dispatch, relationship]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'ACCOUNT_NOTE',
|
||||
modalProps: { accountId },
|
||||
}),
|
||||
);
|
||||
}, [accountId, dispatch]);
|
||||
|
||||
if (!relationship?.note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Callout
|
||||
icon={false}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
className={classes.note}
|
||||
extraContent={
|
||||
<IconButton
|
||||
icon='edit'
|
||||
iconComponent={EditIcon}
|
||||
title={intl.formatMessage(messages.editButton)}
|
||||
className={classes.noteEditButton}
|
||||
onClick={handleEdit}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{relationship.note}
|
||||
</Callout>
|
||||
);
|
||||
};
|
||||
@@ -7,20 +7,16 @@
|
||||
flex-grow: 1;
|
||||
font-size: 22px;
|
||||
white-space: initial;
|
||||
text-overflow: initial;
|
||||
line-height: normal;
|
||||
|
||||
:global(.icon-info) {
|
||||
margin-left: 2px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides .account__header__tabs__name h1 small
|
||||
h1.name > small {
|
||||
gap: 0;
|
||||
.username {
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
align-items: center;
|
||||
user-select: all;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.domainPill {
|
||||
@@ -32,10 +28,20 @@ h1.name > small {
|
||||
color: inherit;
|
||||
font-size: 1em;
|
||||
font-weight: initial;
|
||||
margin-left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: color 0.2s ease-in-out;
|
||||
|
||||
> svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:global(.active) {
|
||||
background: none;
|
||||
color: inherit;
|
||||
color: var(--color-text-brand-soft);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +59,18 @@ h1.name > small {
|
||||
|
||||
svg.badgeIcon {
|
||||
opacity: 1;
|
||||
fill: revert-layer;
|
||||
}
|
||||
|
||||
path {
|
||||
fill: revert-layer;
|
||||
.note {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.noteEditButton {
|
||||
color: inherit;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,11 +104,6 @@ svg.badgeIcon {
|
||||
.fieldIconVerified {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
||||
// Need to override .icon path.
|
||||
path {
|
||||
fill: revert-layer;
|
||||
}
|
||||
}
|
||||
|
||||
.fieldNumbersWrapper {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.noteCallout {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.noteInput {
|
||||
min-height: 70px;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
appearance: none;
|
||||
resize: none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.noteInput:focus-visible {
|
||||
outline: var(--outline-focus-default);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { ChangeEventHandler, FC } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { submitAccountNote } from '@/mastodon/actions/account_notes';
|
||||
import { fetchRelationships } from '@/mastodon/actions/accounts';
|
||||
import { Callout } from '@/mastodon/components/callout';
|
||||
import { TextAreaField } from '@/mastodon/components/form_fields';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import type { Relationship } from '@/mastodon/models/relationship';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
|
||||
|
||||
import classes from './modals.module.css';
|
||||
|
||||
const messages = defineMessages({
|
||||
newTitle: {
|
||||
id: 'account.node_modal.title',
|
||||
defaultMessage: 'Add a personal note',
|
||||
},
|
||||
editTitle: {
|
||||
id: 'account.node_modal.edit_title',
|
||||
defaultMessage: 'Edit personal note',
|
||||
},
|
||||
save: {
|
||||
id: 'account.node_modal.save',
|
||||
defaultMessage: 'Save',
|
||||
},
|
||||
fieldLabel: {
|
||||
id: 'account.node_modal.field_label',
|
||||
defaultMessage: 'Personal Note',
|
||||
},
|
||||
errorUnknown: {
|
||||
id: 'account.node_modal.error_unknown',
|
||||
defaultMessage: 'Could not save the note',
|
||||
},
|
||||
});
|
||||
|
||||
export const AccountNoteModal: FC<{
|
||||
accountId: string;
|
||||
onClose: () => void;
|
||||
}> = ({ accountId, onClose }) => {
|
||||
const relationship = useAppSelector((state) =>
|
||||
state.relationships.get(accountId),
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
if (!relationship) {
|
||||
dispatch(fetchRelationships([accountId]));
|
||||
}
|
||||
}, [accountId, dispatch, relationship]);
|
||||
|
||||
if (!relationship) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InnerNodeModal
|
||||
relationship={relationship}
|
||||
accountId={accountId}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const InnerNodeModal: FC<{
|
||||
relationship: Relationship;
|
||||
accountId: string;
|
||||
onClose: () => void;
|
||||
}> = ({ relationship, accountId, onClose }) => {
|
||||
// Set up the state.
|
||||
const initialContents = relationship.note;
|
||||
const [note, setNote] = useState(initialContents);
|
||||
const [errorText, setErrorText] = useState('');
|
||||
const [state, setState] = useState<'idle' | 'saving' | 'error'>('idle');
|
||||
const isDirty = note !== initialContents;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
(e) => {
|
||||
if (state !== 'saving') {
|
||||
setNote(e.target.value);
|
||||
}
|
||||
},
|
||||
[state],
|
||||
);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
// Create an abort controller to cancel the request if the modal is closed.
|
||||
const abortController = useRef(new AbortController());
|
||||
const dispatch = useAppDispatch();
|
||||
const handleSave = useCallback(() => {
|
||||
if (state === 'saving' || !isDirty) {
|
||||
return;
|
||||
}
|
||||
setState('saving');
|
||||
dispatch(
|
||||
submitAccountNote(
|
||||
{ accountId, note },
|
||||
{ signal: abortController.current.signal },
|
||||
),
|
||||
)
|
||||
.then(() => {
|
||||
setState('idle');
|
||||
onClose();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setState('error');
|
||||
if (err instanceof Error) {
|
||||
setErrorText(err.message);
|
||||
} else {
|
||||
setErrorText(intl.formatMessage(messages.errorUnknown));
|
||||
}
|
||||
});
|
||||
}, [accountId, dispatch, intl, isDirty, note, onClose, state]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
abortController.current.abort();
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
title={
|
||||
initialContents
|
||||
? intl.formatMessage(messages.editTitle)
|
||||
: intl.formatMessage(messages.newTitle)
|
||||
}
|
||||
extraContent={
|
||||
<>
|
||||
<Callout className={classes.noteCallout}>
|
||||
<FormattedMessage
|
||||
id='account.node_modal.callout'
|
||||
defaultMessage='Personal notes are visible only to you.'
|
||||
/>
|
||||
</Callout>
|
||||
<TextAreaField
|
||||
value={note}
|
||||
onChange={handleChange}
|
||||
label={intl.formatMessage(messages.fieldLabel)}
|
||||
className={classes.noteInput}
|
||||
hasError={state === 'error'}
|
||||
hint={errorText}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus -- We want to focus here as it's a modal.
|
||||
autoFocus
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onClose={handleCancel}
|
||||
confirm={intl.formatMessage(messages.save)}
|
||||
onConfirm={handleSave}
|
||||
updating={state === 'saving'}
|
||||
disabled={!isDirty}
|
||||
closeWhenConfirm={false}
|
||||
noFocusButton
|
||||
/>
|
||||
);
|
||||
};
|
||||
+15
-2
@@ -19,6 +19,9 @@ export const ConfirmationModal: React.FC<
|
||||
onConfirm: () => void;
|
||||
closeWhenConfirm?: boolean;
|
||||
extraContent?: React.ReactNode;
|
||||
updating?: boolean;
|
||||
disabled?: boolean;
|
||||
noFocusButton?: boolean;
|
||||
} & BaseConfirmationModalProps
|
||||
> = ({
|
||||
title,
|
||||
@@ -31,6 +34,9 @@ export const ConfirmationModal: React.FC<
|
||||
onSecondary,
|
||||
closeWhenConfirm = true,
|
||||
extraContent,
|
||||
updating,
|
||||
disabled,
|
||||
noFocusButton = false,
|
||||
}) => {
|
||||
const handleClick = useCallback(() => {
|
||||
if (closeWhenConfirm) {
|
||||
@@ -74,16 +80,23 @@ export const ConfirmationModal: React.FC<
|
||||
onClick={handleSecondary}
|
||||
className='link-button'
|
||||
type='button'
|
||||
disabled={disabled}
|
||||
>
|
||||
{secondary}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
|
||||
<Button onClick={handleClick} autoFocus>
|
||||
{/* eslint-disable jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
loading={updating}
|
||||
disabled={disabled}
|
||||
autoFocus={!noFocusButton}
|
||||
>
|
||||
{confirm}
|
||||
</Button>
|
||||
{/* eslint-enable */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,6 +85,7 @@ export const MODAL_COMPONENTS = {
|
||||
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
|
||||
'ANNUAL_REPORT': AnnualReportModal,
|
||||
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
|
||||
'ACCOUNT_NOTE': () => import('@/mastodon/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
|
||||
'ACCOUNT_FIELDS': () => import('mastodon/features/account_timeline/components/fields_modal.tsx').then(module => ({ default: module.AccountFieldsModal })),
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user