0
0
Fork 0

Change design of confirmation modals in web UI (#30884)

Co-authored-by: Renaud Chaput <renchap@gmail.com>
This commit is contained in:
Eugen Rochko 2024-07-25 19:05:54 +02:00 committed by GitHub
parent ff6d2ec343
commit 8818748b90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 554 additions and 489 deletions

View file

@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { injectIntl, FormattedMessage } from 'react-intl';
import { Button } from '../../../components/button';
class ConfirmationModal extends PureComponent {
static propTypes = {
message: PropTypes.node.isRequired,
confirm: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
secondary: PropTypes.string,
onSecondary: PropTypes.func,
closeWhenConfirm: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
static defaultProps = {
closeWhenConfirm: true,
};
handleClick = () => {
if (this.props.closeWhenConfirm) {
this.props.onClose();
}
this.props.onConfirm();
};
handleSecondary = () => {
this.props.onClose();
this.props.onSecondary();
};
handleCancel = () => {
this.props.onClose();
};
render () {
const { message, confirm, secondary } = this.props;
return (
<div className='modal-root__modal confirmation-modal'>
<div className='confirmation-modal__container'>
{message}
</div>
<div className='confirmation-modal__action-bar'>
<Button onClick={this.handleCancel} className='confirmation-modal__cancel-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button>
{secondary !== undefined && (
<Button text={secondary} onClick={this.handleSecondary} className='confirmation-modal__secondary-button' />
)}
<Button text={confirm} onClick={this.handleClick} autoFocus />
</div>
</div>
);
}
}
export default injectIntl(ConfirmationModal);

View file

@ -0,0 +1,46 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { clearNotifications } from 'mastodon/actions/notification_groups';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
clearTitle: {
id: 'notifications.clear_title',
defaultMessage: 'Clear notifications?',
},
clearMessage: {
id: 'notifications.clear_confirmation',
defaultMessage:
'Are you sure you want to permanently clear all your notifications?',
},
clearConfirm: {
id: 'notifications.clear',
defaultMessage: 'Clear notifications',
},
});
export const ConfirmClearNotificationsModal: React.FC<
BaseConfirmationModalProps
> = ({ onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
void dispatch(clearNotifications());
}, [dispatch]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.clearTitle)}
message={intl.formatMessage(messages.clearMessage)}
confirm={intl.formatMessage(messages.clearConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -0,0 +1,79 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button } from 'mastodon/components/button';
export interface BaseConfirmationModalProps {
onClose: () => void;
}
export const ConfirmationModal: React.FC<
{
title: React.ReactNode;
message: React.ReactNode;
confirm: React.ReactNode;
secondary?: React.ReactNode;
onSecondary?: () => void;
onConfirm: () => void;
closeWhenConfirm?: boolean;
} & BaseConfirmationModalProps
> = ({
title,
message,
confirm,
onClose,
onConfirm,
secondary,
onSecondary,
closeWhenConfirm = true,
}) => {
const handleClick = useCallback(() => {
if (closeWhenConfirm) {
onClose();
}
onConfirm();
}, [onClose, onConfirm, closeWhenConfirm]);
const handleSecondary = useCallback(() => {
onClose();
onSecondary?.();
}, [onClose, onSecondary]);
const handleCancel = useCallback(() => {
onClose();
}, [onClose]);
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__confirmation'>
<h1>{title}</h1>
<p>{message}</p>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
{secondary && (
<>
<Button onClick={handleSecondary}>{secondary}</Button>
<div className='spacer' />
</>
)}
<button onClick={handleCancel} className='link-button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'
/>
</button>
<Button onClick={handleClick}>{confirm}</Button>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,58 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router';
import { removeColumn } from 'mastodon/actions/columns';
import { deleteList } from 'mastodon/actions/lists';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
deleteListTitle: {
id: 'confirmations.delete_list.title',
defaultMessage: 'Delete list?',
},
deleteListMessage: {
id: 'confirmations.delete_list.message',
defaultMessage: 'Are you sure you want to permanently delete this list?',
},
deleteListConfirm: {
id: 'confirmations.delete_list.confirm',
defaultMessage: 'Delete',
},
});
export const ConfirmDeleteListModal: React.FC<
{
listId: string;
columnId: string;
} & BaseConfirmationModalProps
> = ({ listId, columnId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const history = useHistory();
const onConfirm = useCallback(() => {
dispatch(deleteList(listId));
if (columnId) {
dispatch(removeColumn(columnId));
} else {
history.push('/lists');
}
}, [dispatch, history, columnId, listId]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.deleteListTitle)}
message={intl.formatMessage(messages.deleteListMessage)}
confirm={intl.formatMessage(messages.deleteListConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -0,0 +1,67 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { deleteStatus } from 'mastodon/actions/statuses';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
deleteAndRedraftTitle: {
id: 'confirmations.redraft.title',
defaultMessage: 'Delete & redraft post?',
},
deleteAndRedraftMessage: {
id: 'confirmations.redraft.message',
defaultMessage:
'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.',
},
deleteAndRedraftConfirm: {
id: 'confirmations.redraft.confirm',
defaultMessage: 'Delete & redraft',
},
deleteTitle: {
id: 'confirmations.delete.title',
defaultMessage: 'Delete post?',
},
deleteMessage: {
id: 'confirmations.delete.message',
defaultMessage: 'Are you sure you want to delete this status?',
},
deleteConfirm: {
id: 'confirmations.delete.confirm',
defaultMessage: 'Delete',
},
});
export const ConfirmDeleteStatusModal: React.FC<
{
statusId: string;
withRedraft: boolean;
} & BaseConfirmationModalProps
> = ({ statusId, withRedraft, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(deleteStatus(statusId, withRedraft));
}, [dispatch, statusId, withRedraft]);
return (
<ConfirmationModal
title={intl.formatMessage(
withRedraft ? messages.deleteAndRedraftTitle : messages.deleteTitle,
)}
message={intl.formatMessage(
withRedraft ? messages.deleteAndRedraftMessage : messages.deleteMessage,
)}
confirm={intl.formatMessage(
withRedraft ? messages.deleteAndRedraftConfirm : messages.deleteConfirm,
)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -0,0 +1,45 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { editStatus } from 'mastodon/actions/statuses';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
editTitle: {
id: 'confirmations.edit.title',
defaultMessage: 'Overwrite post?',
},
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: {
id: 'confirmations.edit.message',
defaultMessage:
'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?',
},
});
export const ConfirmEditStatusModal: React.FC<
{
statusId: string;
} & BaseConfirmationModalProps
> = ({ statusId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(editStatus(statusId));
}, [dispatch, statusId]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.editTitle)}
message={intl.formatMessage(messages.editMessage)}
confirm={intl.formatMessage(messages.editConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -0,0 +1,8 @@
export { ConfirmationModal } from './confirmation_modal';
export { ConfirmDeleteStatusModal } from './delete_status';
export { ConfirmDeleteListModal } from './delete_list';
export { ConfirmReplyModal } from './reply';
export { ConfirmEditStatusModal } from './edit_status';
export { ConfirmUnfollowModal } from './unfollow';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';

View file

@ -0,0 +1,40 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { logOut } from 'mastodon/utils/log_out';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
logoutTitle: { id: 'confirmations.logout.title', defaultMessage: 'Log out?' },
logoutMessage: {
id: 'confirmations.logout.message',
defaultMessage: 'Are you sure you want to log out?',
},
logoutConfirm: {
id: 'confirmations.logout.confirm',
defaultMessage: 'Log out',
},
});
export const ConfirmLogOutModal: React.FC<BaseConfirmationModalProps> = ({
onClose,
}) => {
const intl = useIntl();
const onConfirm = useCallback(() => {
logOut();
}, []);
return (
<ConfirmationModal
title={intl.formatMessage(messages.logoutTitle)}
message={intl.formatMessage(messages.logoutMessage)}
confirm={intl.formatMessage(messages.logoutConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -0,0 +1,46 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { replyCompose } from 'mastodon/actions/compose';
import type { Status } from 'mastodon/models/status';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
replyTitle: {
id: 'confirmations.reply.title',
defaultMessage: 'Overwrite post?',
},
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: {
id: 'confirmations.reply.message',
defaultMessage:
'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?',
},
});
export const ConfirmReplyModal: React.FC<
{
status: Status;
} & BaseConfirmationModalProps
> = ({ status, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(replyCompose(status));
}, [dispatch, status]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.replyTitle)}
message={intl.formatMessage(messages.replyMessage)}
confirm={intl.formatMessage(messages.replyConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -0,0 +1,50 @@
import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { unfollowAccount } from 'mastodon/actions/accounts';
import type { Account } from 'mastodon/models/account';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
unfollowTitle: {
id: 'confirmations.unfollow.title',
defaultMessage: 'Unfollow user?',
},
unfollowConfirm: {
id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow',
},
});
export const ConfirmUnfollowModal: React.FC<
{
account: Account;
} & BaseConfirmationModalProps
> = ({ account, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(unfollowAccount(account.id));
}, [dispatch, account.id]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.unfollowTitle)}
message={
<FormattedMessage
id='confirmations.unfollow.message'
defaultMessage='Are you sure you want to unfollow {name}?'
values={{ name: <strong>@{account.acct}</strong> }}
/>
}
confirm={intl.formatMessage(messages.unfollowConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
@ -9,29 +9,15 @@ import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { disabledAccountId, movedToAccountId, domain } from 'mastodon/initial_state';
import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
const mapStateToProps = (state) => ({
disabledAcct: state.getIn(['accounts', disabledAccountId, 'acct']),
movedToAcct: movedToAccountId ? state.getIn(['accounts', movedToAccountId, 'acct']) : undefined,
});
const mapDispatchToProps = (dispatch, { intl }) => ({
const mapDispatchToProps = (dispatch) => ({
onLogout () {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
},
});

View file

@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
@ -11,24 +11,11 @@ import { openModal } from 'mastodon/actions/modal';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state';
import { PERMISSION_INVITE_USERS } from 'mastodon/permissions';
import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
const mapDispatchToProps = (dispatch, { intl }) => ({
const mapDispatchToProps = (dispatch) => ({
onLogout () {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
},
});

View file

@ -26,7 +26,16 @@ import ActionsModal from './actions_modal';
import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal';
import BundleModalError from './bundle_modal_error';
import ConfirmationModal from './confirmation_modal';
import {
ConfirmationModal,
ConfirmDeleteStatusModal,
ConfirmDeleteListModal,
ConfirmReplyModal,
ConfirmEditStatusModal,
ConfirmUnfollowModal,
ConfirmClearNotificationsModal,
ConfirmLogOutModal,
} from './confirmation_modals';
import FocalPointModal from './focal_point_modal';
import ImageModal from './image_modal';
import MediaModal from './media_modal';
@ -40,6 +49,13 @@ export const MODAL_COMPONENTS = {
'IMAGE': () => Promise.resolve({ default: ImageModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'CONFIRM_DELETE_STATUS': () => Promise.resolve({ default: ConfirmDeleteStatusModal }),
'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }),
'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }),
'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }),
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'MUTE': MuteModal,
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,