Profile redesign: Account notes (#37593)
This commit is contained in:
parent
c0616bcab6
commit
34f0482ad4
@ -36,6 +36,7 @@ export interface CalloutProps {
|
||||
secondaryLabel?: string;
|
||||
onClose?: () => void;
|
||||
id?: string;
|
||||
extraContent?: ReactNode;
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
@ -59,6 +60,7 @@ export const Callout: FC<CalloutProps> = ({
|
||||
onSecondary: secondaryAction,
|
||||
secondaryLabel,
|
||||
onClose,
|
||||
extraContent,
|
||||
id,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
@ -105,6 +107,8 @@ export const Callout: FC<CalloutProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{extraContent}
|
||||
|
||||
{onClose && (
|
||||
<IconButton
|
||||
icon='close'
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
border-radius: 9999px;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.content {
|
||||
@ -28,16 +29,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.icon + .content,
|
||||
.wrapper:has(.close) .content {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,6 +59,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.action {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
color: inherit;
|
||||
|
||||
|
||||
@ -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';
|
||||
@ -161,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} />
|
||||
)}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -61,6 +61,19 @@ svg.badgeIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.noteEditButton {
|
||||
color: inherit;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.fieldList {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@ -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
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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 })),
|
||||
};
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
"about.powered_by": "Decentralized social media powered by {mastodon}",
|
||||
"about.rules": "Server rules",
|
||||
"account.account_note_header": "Personal note",
|
||||
"account.add_note": "Add a personal note",
|
||||
"account.add_or_remove_from_list": "Add or Remove from lists",
|
||||
"account.badges.bot": "Automated",
|
||||
"account.badges.group": "Group",
|
||||
@ -27,6 +28,7 @@
|
||||
"account.direct": "Privately mention @{name}",
|
||||
"account.disable_notifications": "Stop notifying me when @{name} posts",
|
||||
"account.domain_blocking": "Blocking domain",
|
||||
"account.edit_note": "Edit personal note",
|
||||
"account.edit_profile": "Edit profile",
|
||||
"account.edit_profile_short": "Edit",
|
||||
"account.enable_notifications": "Notify me when @{name} posts",
|
||||
@ -72,6 +74,14 @@
|
||||
"account.muting": "Muting",
|
||||
"account.mutual": "You follow each other",
|
||||
"account.no_bio": "No description provided.",
|
||||
"account.node_modal.callout": "Personal notes are visible only to you.",
|
||||
"account.node_modal.edit_title": "Edit personal note",
|
||||
"account.node_modal.error_unknown": "Could not save the note",
|
||||
"account.node_modal.field_label": "Personal Note",
|
||||
"account.node_modal.save": "Save",
|
||||
"account.node_modal.title": "Add a personal note",
|
||||
"account.note.edit_button": "Edit",
|
||||
"account.note.title": "Personal note (visible only to you)",
|
||||
"account.open_original_page": "Open original page",
|
||||
"account.posts": "Posts",
|
||||
"account.posts_with_replies": "Posts and replies",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user