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,4 +1,4 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
@ -6,7 +6,6 @@ import { openURL } from 'mastodon/actions/search';
import {
followAccount,
unfollowAccount,
unblockAccount,
unmuteAccount,
pinAccount,
@ -24,11 +23,6 @@ import { initReport } from '../../../actions/reports';
import { makeGetAccount, getAccountHidden } from '../../../selectors';
import Header from '../components/header';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
@ -41,18 +35,11 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
const mapDispatchToProps = (dispatch) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
} else {
dispatch(followAccount(account.get('id')));
}

View file

@ -7,7 +7,6 @@ import { useDispatch } from 'react-redux';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'mastodon/actions/modal';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@ -23,8 +22,6 @@ const messages = defineMessages({
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
export const ActionBar = () => {
@ -32,16 +29,8 @@ export const ActionBar = () => {
const intl = useIntl();
const handleLogoutClick = useCallback(() => {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
}, [dispatch, intl]);
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
}, [dispatch]);
let menu = [];

View file

@ -21,7 +21,6 @@ import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
import { openModal } from 'mastodon/actions/modal';
import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon';
import { logOut } from 'mastodon/utils/log_out';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
@ -42,8 +41,6 @@ const messages = defineMessages({
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
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, ownProps) => ({
@ -72,20 +69,12 @@ class Compose extends PureComponent {
}
handleLogoutClick = e => {
const { dispatch, intl } = this.props;
const { dispatch } = this.props;
e.preventDefault();
e.stopPropagation();
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' }));
return false;
};

View file

@ -36,8 +36,6 @@ const messages = defineMessages({
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
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?' },
});
const getAccounts = createSelector(
@ -103,19 +101,12 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(lastStatus)),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status: lastStatus } }));
} else {
dispatch(replyCompose(lastStatus));
}
});
}, [dispatch, lastStatus, intl]);
}, [dispatch, lastStatus]);
const handleDelete = useCallback(() => {
dispatch(deleteConversation(id));

View file

@ -8,7 +8,6 @@ import { Link } from 'react-router-dom';
import {
followAccount,
unfollowAccount,
unblockAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
@ -29,20 +28,12 @@ const messages = defineMessages({
id: 'account.cancel_follow_request',
defaultMessage: 'Withdraw follow request',
},
cancelFollowRequestConfirm: {
id: 'confirmations.cancel_follow_request.confirm',
defaultMessage: 'Withdraw request',
},
requested: {
id: 'account.requested',
defaultMessage: 'Awaiting approval. Click to cancel follow request',
},
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
unfollowConfirm: {
id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow',
},
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});
@ -89,48 +80,17 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
const handleFollow = useCallback(() => {
if (!account) return;
if (account.getIn(['relationship', 'following'])) {
if (
account.getIn(['relationship', 'following']) ||
account.getIn(['relationship', 'requested'])
) {
dispatch(
openModal({
modalType: 'CONFIRM',
modalProps: {
message: (
<FormattedMessage
id='confirmations.unfollow.message'
defaultMessage='Are you sure you want to unfollow {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
),
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => {
dispatch(unfollowAccount(account.get('id')));
},
},
}),
);
} else if (account.getIn(['relationship', 'requested'])) {
dispatch(
openModal({
modalType: 'CONFIRM',
modalProps: {
message: (
<FormattedMessage
id='confirmations.cancel_follow_request.message'
defaultMessage='Are you sure you want to withdraw your request to follow {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
),
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
onConfirm: () => {
dispatch(unfollowAccount(account.get('id')));
},
},
}),
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
);
} else {
dispatch(followAccount(account.get('id')));
}
}, [account, dispatch, intl]);
}, [account, dispatch]);
const handleBlock = useCallback(() => {
if (account?.relationship?.blocking) {

View file

@ -11,16 +11,15 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
import { Domain } from 'mastodon/components/domain';
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import DomainContainer from '../../containers/domain_container';
import Column from '../ui/components/column';
const messages = defineMessages({
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
});
const mapStateToProps = state => ({
@ -70,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{domains.map(domain =>
<DomainContainer key={domain} domain={domain} />,
<Domain key={domain} domain={domain} />,
)}
</ScrollableList>

View file

@ -15,7 +15,7 @@ import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
import { fetchList, deleteList, updateList } from 'mastodon/actions/lists';
import { fetchList, updateList } from 'mastodon/actions/lists';
import { openModal } from 'mastodon/actions/modal';
import { connectListStream } from 'mastodon/actions/streaming';
import { expandListTimeline } from 'mastodon/actions/timelines';
@ -29,8 +29,6 @@ import StatusListContainer from 'mastodon/features/ui/containers/status_list_con
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
@ -125,25 +123,10 @@ class ListTimeline extends PureComponent {
};
handleDeleteClick = () => {
const { dispatch, columnId, intl } = this.props;
const { dispatch, columnId } = this.props;
const { id } = this.props.params;
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => {
dispatch(deleteList(id));
if (columnId) {
dispatch(removeColumn(columnId));
} else {
this.props.history.push('/lists');
}
},
},
}));
dispatch(openModal({ modalType: 'CONFIRM_DELETE_LIST', modalProps: { listId: id, columnId } }));
};
handleRepliesPolicyChange = ({ target }) => {

View file

@ -2,11 +2,10 @@ import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { initializeNotifications } from 'mastodon/actions/notifications_migration';
import { showAlert } from '../../../actions/alerts';
import { openModal } from '../../../actions/modal';
import { clearNotifications } from '../../../actions/notification_groups';
import { updateNotificationsPolicy } from '../../../actions/notification_policies';
import { setFilter, requestBrowserPermission } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
@ -14,8 +13,6 @@ import { changeSetting } from '../../../actions/settings';
import ColumnSettings from '../components/column_settings';
const messages = defineMessages({
clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' },
});
@ -31,7 +28,7 @@ const mapStateToProps = state => ({
notificationPolicy: state.notificationPolicy,
});
const mapDispatchToProps = (dispatch, { intl }) => ({
const mapDispatchToProps = (dispatch) => ({
onChange (path, checked) {
if (path[0] === 'push') {
@ -70,14 +67,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onClear () {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.clearMessage),
confirm: intl.formatMessage(messages.clearConfirm),
onConfirm: () => dispatch(clearNotifications()),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_CLEAR_NOTIFICATIONS' }));
},
onRequestNotificationPermission () {

View file

@ -31,8 +31,6 @@ const messages = defineMessages({
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
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?' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
});
@ -71,19 +69,13 @@ class Footer extends ImmutablePureComponent {
};
handleReplyClick = () => {
const { dispatch, askReplyConfirmation, status, intl } = this.props;
const { dispatch, askReplyConfirmation, status, onClose } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
if (askReplyConfirmation) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: this._performReply,
},
}));
onClose(true);
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
} else {
this._performReply();
}

View file

@ -1,4 +1,4 @@
import { defineMessages, injectIntl } from 'react-intl';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
@ -28,15 +28,6 @@ import { deleteModal } from '../../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
import DetailedStatus from '../components/detailed_status';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { 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.' },
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?' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
@ -50,20 +41,13 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
const mapDispatchToProps = (dispatch) => ({
onReply (status) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status)),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
} else {
dispatch(replyCompose(status));
}
@ -100,14 +84,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
}
},

View file

@ -72,17 +72,10 @@ import DetailedStatus from './components/detailed_status';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { 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.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
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?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => {
@ -264,19 +257,12 @@ class Status extends ImmutablePureComponent {
};
handleReplyClick = (status) => {
const { askReplyConfirmation, dispatch, intl } = this.props;
const { askReplyConfirmation, dispatch } = this.props;
const { signedIn } = this.props.identity;
if (signedIn) {
if (askReplyConfirmation) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status)),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
} else {
dispatch(replyCompose(status));
}
@ -319,24 +305,23 @@ class Status extends ImmutablePureComponent {
};
handleDeleteClick = (status, withRedraft = false) => {
const { dispatch, intl } = this.props;
const { dispatch } = this.props;
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
}
};
handleEditClick = (status) => {
this.props.dispatch(editStatus(status.get('id')));
const { dispatch, askReplyConfirmation } = this.props;
if (askReplyConfirmation) {
dispatch(openModal({ modalType: 'CONFIRM_EDIT_STATUS', modalProps: { statusId: status.get('id') } }));
} else {
dispatch(editStatus(status.get('id')));
}
};
handleDirectClick = (account) => {

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,