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

@ -3,6 +3,8 @@ import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react'; import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
import { unblockDomain } from 'mastodon/actions/domain_blocks';
import { useAppDispatch } from 'mastodon/store';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
@ -13,17 +15,15 @@ const messages = defineMessages({
}, },
}); });
interface Props { export const Domain: React.FC<{
domain: string; domain: string;
onUnblockDomain: (domain: string) => void; }> = ({ domain }) => {
}
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch();
const handleDomainUnblock = useCallback(() => { const handleDomainUnblock = useCallback(() => {
onUnblockDomain(domain); dispatch(unblockDomain(domain));
}, [domain, onUnblockDomain]); }, [dispatch, domain]);
return ( return (
<div className='domain'> <div className='domain'>

View file

@ -1,13 +1,9 @@
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
import { useIdentity } from '@/mastodon/identity_context'; import { useIdentity } from '@/mastodon/identity_context';
import { import { fetchRelationships, followAccount } from 'mastodon/actions/accounts';
fetchRelationships,
followAccount,
unfollowAccount,
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button'; import { Button } from 'mastodon/components/button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
@ -60,29 +56,14 @@ export const FollowButton: React.FC<{
if (accountId === me) { if (accountId === me) {
return; return;
} else if (relationship.following || relationship.requested) { } else if (account && (relationship.following || relationship.requested)) {
dispatch( dispatch(
openModal({ openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
modalType: 'CONFIRM',
modalProps: {
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.unfollow),
onConfirm: () => {
dispatch(unfollowAccount(accountId));
},
},
}),
); );
} else { } else {
dispatch(followAccount(accountId)); dispatch(followAccount(accountId));
} }
}, [dispatch, intl, accountId, relationship, account, signedIn]); }, [dispatch, accountId, relationship, account, signedIn]);
let label; let label;

View file

@ -1,24 +1,20 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { injectIntl } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { import {
followAccount, followAccount,
unfollowAccount,
blockAccount, blockAccount,
unblockAccount, unblockAccount,
muteAccount, muteAccount,
unmuteAccount, unmuteAccount,
} from '../actions/accounts'; } from '../actions/accounts';
import { openModal } from '../actions/modal';
import { initMuteModal } from '../actions/mutes'; import { initMuteModal } from '../actions/mutes';
import Account from '../components/account'; import Account from '../components/account';
import { makeGetAccount } from '../selectors'; import { makeGetAccount } from '../selectors';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
});
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getAccount = makeGetAccount(); const getAccount = makeGetAccount();
@ -29,18 +25,11 @@ const makeMapStateToProps = () => {
return mapStateToProps; return mapStateToProps;
}; };
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
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 { } else {
dispatch(followAccount(account.get('id'))); dispatch(followAccount(account.get('id')));
} }

View file

@ -1,36 +0,0 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { blockDomain, unblockDomain } from '../actions/domain_blocks';
import { openModal } from '../actions/modal';
import { Domain } from '../components/domain';
const messages = defineMessages({
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => {
const mapStateToProps = () => ({});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onBlockDomain (domain) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
}));
},
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain));

View file

@ -1,4 +1,4 @@
import { defineMessages, injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -46,18 +46,6 @@ import Status from '../components/status';
import { deleteModal } from '../initial_state'; import { deleteModal } from '../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
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?' },
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?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture(); const getPictureInPicture = makeGetPictureInPicture();
@ -71,20 +59,14 @@ const makeMapStateToProps = () => {
return mapStateToProps; return mapStateToProps;
}; };
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ const mapDispatchToProps = (dispatch, { contextType }) => ({
onReply (status) { onReply (status) {
dispatch((_, getState) => { dispatch((_, getState) => {
let state = getState(); let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) { if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status)) },
}));
} else { } else {
dispatch(replyCompose(status)); dispatch(replyCompose(status));
} }
@ -129,14 +111,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), withRedraft)); dispatch(deleteStatus(status.get('id'), withRedraft));
} else { } else {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
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)),
},
}));
} }
}, },
@ -144,14 +119,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch((_, getState) => { dispatch((_, getState) => {
let state = getState(); let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) { if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_EDIT_STATUS', modalProps: { statusId: status.get('id') } }));
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.editMessage),
confirm: intl.formatMessage(messages.editConfirm),
onConfirm: () => dispatch(editStatus(status.get('id'))),
},
}));
} else { } else {
dispatch(editStatus(status.get('id'))); dispatch(editStatus(status.get('id')));
} }

View file

@ -1,4 +1,4 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { injectIntl } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -6,7 +6,6 @@ import { openURL } from 'mastodon/actions/search';
import { import {
followAccount, followAccount,
unfollowAccount,
unblockAccount, unblockAccount,
unmuteAccount, unmuteAccount,
pinAccount, pinAccount,
@ -24,11 +23,6 @@ import { initReport } from '../../../actions/reports';
import { makeGetAccount, getAccountHidden } from '../../../selectors'; import { makeGetAccount, getAccountHidden } from '../../../selectors';
import Header from '../components/header'; 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 makeMapStateToProps = () => {
const getAccount = makeGetAccount(); const getAccount = makeGetAccount();
@ -41,18 +35,11 @@ const makeMapStateToProps = () => {
return mapStateToProps; return mapStateToProps;
}; };
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
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 { } else {
dispatch(followAccount(account.get('id'))); 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 MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@ -23,8 +22,6 @@ const messages = defineMessages({
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, 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 = () => { export const ActionBar = () => {
@ -32,16 +29,8 @@ export const ActionBar = () => {
const intl = useIntl(); const intl = useIntl();
const handleLogoutClick = useCallback(() => { const handleLogoutClick = useCallback(() => {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
modalType: 'CONFIRM', }, [dispatch]);
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
}, [dispatch, intl]);
let menu = []; 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 { openModal } from 'mastodon/actions/modal';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { logOut } from 'mastodon/utils/log_out';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose'; import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
@ -42,8 +41,6 @@ const messages = defineMessages({
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, 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) => ({ const mapStateToProps = (state, ownProps) => ({
@ -72,20 +69,12 @@ class Compose extends PureComponent {
} }
handleLogoutClick = e => { handleLogoutClick = e => {
const { dispatch, intl } = this.props; const { dispatch } = this.props;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
return false; return false;
}; };

View file

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

View file

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

View file

@ -11,16 +11,15 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react'; 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 { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
import { LoadingIndicator } from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import DomainContainer from '../../containers/domain_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' }, heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -70,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{domains.map(domain => {domains.map(domain =>
<DomainContainer key={domain} domain={domain} />, <Domain key={domain} domain={domain} />,
)} )}
</ScrollableList> </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 EditIcon from '@/material-icons/400-24px/edit.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; 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 { openModal } from 'mastodon/actions/modal';
import { connectListStream } from 'mastodon/actions/streaming'; import { connectListStream } from 'mastodon/actions/streaming';
import { expandListTimeline } from 'mastodon/actions/timelines'; 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'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({ 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' }, followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' }, none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' }, list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
@ -125,25 +123,10 @@ class ListTimeline extends PureComponent {
}; };
handleDeleteClick = () => { handleDeleteClick = () => {
const { dispatch, columnId, intl } = this.props; const { dispatch, columnId } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_DELETE_LIST', modalProps: { listId: id, columnId } }));
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');
}
},
},
}));
}; };
handleRepliesPolicyChange = ({ target }) => { handleRepliesPolicyChange = ({ target }) => {

View file

@ -2,11 +2,10 @@ import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { initializeNotifications } from 'mastodon/actions/notifications_migration'; import { initializeNotifications } from 'mastodon/actions/notifications_migration';
import { showAlert } from '../../../actions/alerts'; import { showAlert } from '../../../actions/alerts';
import { openModal } from '../../../actions/modal';
import { clearNotifications } from '../../../actions/notification_groups';
import { updateNotificationsPolicy } from '../../../actions/notification_policies'; import { updateNotificationsPolicy } from '../../../actions/notification_policies';
import { setFilter, requestBrowserPermission } from '../../../actions/notifications'; import { setFilter, requestBrowserPermission } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
@ -14,8 +13,6 @@ import { changeSetting } from '../../../actions/settings';
import ColumnSettings from '../components/column_settings'; import ColumnSettings from '../components/column_settings';
const messages = defineMessages({ 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' }, 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, notificationPolicy: state.notificationPolicy,
}); });
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch) => ({
onChange (path, checked) { onChange (path, checked) {
if (path[0] === 'push') { if (path[0] === 'push') {
@ -70,14 +67,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onClear () { onClear () {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_CLEAR_NOTIFICATIONS' }));
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.clearMessage),
confirm: intl.formatMessage(messages.clearConfirm),
onConfirm: () => dispatch(clearNotifications()),
},
}));
}, },
onRequestNotificationPermission () { onRequestNotificationPermission () {

View file

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

View file

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

View file

@ -72,17 +72,10 @@ import DetailedStatus from './components/detailed_status';
const messages = defineMessages({ 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' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less 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}}' }, 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' }, 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 = () => { const makeMapStateToProps = () => {
@ -264,19 +257,12 @@ class Status extends ImmutablePureComponent {
}; };
handleReplyClick = (status) => { handleReplyClick = (status) => {
const { askReplyConfirmation, dispatch, intl } = this.props; const { askReplyConfirmation, dispatch } = this.props;
const { signedIn } = this.props.identity; const { signedIn } = this.props.identity;
if (signedIn) { if (signedIn) {
if (askReplyConfirmation) { if (askReplyConfirmation) {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status)),
},
}));
} else { } else {
dispatch(replyCompose(status)); dispatch(replyCompose(status));
} }
@ -319,24 +305,23 @@ class Status extends ImmutablePureComponent {
}; };
handleDeleteClick = (status, withRedraft = false) => { handleDeleteClick = (status, withRedraft = false) => {
const { dispatch, intl } = this.props; const { dispatch } = this.props;
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), withRedraft)); dispatch(deleteStatus(status.get('id'), withRedraft));
} else { } else {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
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)),
},
}));
} }
}; };
handleEditClick = (status) => { 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) => { 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 PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -9,29 +9,15 @@ import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { disabledAccountId, movedToAccountId, domain } from 'mastodon/initial_state'; 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) => ({ const mapStateToProps = (state) => ({
disabledAcct: state.getIn(['accounts', disabledAccountId, 'acct']), disabledAcct: state.getIn(['accounts', disabledAccountId, 'acct']),
movedToAcct: movedToAccountId ? state.getIn(['accounts', movedToAccountId, 'acct']) : undefined, movedToAcct: movedToAccountId ? state.getIn(['accounts', movedToAccountId, 'acct']) : undefined,
}); });
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch) => ({
onLogout () { onLogout () {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
}, },
}); });

View file

@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -11,24 +11,11 @@ import { openModal } from 'mastodon/actions/modal';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state'; import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state';
import { PERMISSION_INVITE_USERS } from 'mastodon/permissions'; import { PERMISSION_INVITE_USERS } from 'mastodon/permissions';
import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({ const mapDispatchToProps = (dispatch) => ({
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 }) => ({
onLogout () { onLogout () {
dispatch(openModal({ dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
}, },
}); });

View file

@ -26,7 +26,16 @@ import ActionsModal from './actions_modal';
import AudioModal from './audio_modal'; import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal'; import { BoostModal } from './boost_modal';
import BundleModalError from './bundle_modal_error'; 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 FocalPointModal from './focal_point_modal';
import ImageModal from './image_modal'; import ImageModal from './image_modal';
import MediaModal from './media_modal'; import MediaModal from './media_modal';
@ -40,6 +49,13 @@ export const MODAL_COMPONENTS = {
'IMAGE': () => Promise.resolve({ default: ImageModal }), 'IMAGE': () => Promise.resolve({ default: ImageModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }), 'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), '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, 'MUTE': MuteModal,
'BLOCK': BlockModal, 'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal, 'DOMAIN_BLOCK': DomainBlockModal,

View file

@ -169,27 +169,30 @@
"compose_form.spoiler_placeholder": "Content warning (optional)", "compose_form.spoiler_placeholder": "Content warning (optional)",
"confirmation_modal.cancel": "Cancel", "confirmation_modal.cancel": "Cancel",
"confirmations.block.confirm": "Block", "confirmations.block.confirm": "Block",
"confirmations.cancel_follow_request.confirm": "Withdraw request",
"confirmations.cancel_follow_request.message": "Are you sure you want to withdraw your request to follow {name}?",
"confirmations.delete.confirm": "Delete", "confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this post?", "confirmations.delete.message": "Are you sure you want to delete this post?",
"confirmations.delete.title": "Delete post?",
"confirmations.delete_list.confirm": "Delete", "confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.title": "Delete list?",
"confirmations.discard_edit_media.confirm": "Discard", "confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?", "confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Block server",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"confirmations.edit.confirm": "Edit", "confirmations.edit.confirm": "Edit",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.edit.title": "Overwrite post?",
"confirmations.logout.confirm": "Log out", "confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?", "confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.logout.title": "Log out?",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Mute",
"confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.", "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.",
"confirmations.redraft.title": "Delete & redraft post?",
"confirmations.reply.confirm": "Reply", "confirmations.reply.confirm": "Reply",
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.reply.title": "Overwrite post?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"confirmations.unfollow.title": "Unfollow user?",
"conversation.delete": "Delete conversation", "conversation.delete": "Delete conversation",
"conversation.mark_as_read": "Mark as read", "conversation.mark_as_read": "Mark as read",
"conversation.open": "View conversation", "conversation.open": "View conversation",
@ -507,6 +510,7 @@
"notification_requests.title": "Filtered notifications", "notification_requests.title": "Filtered notifications",
"notifications.clear": "Clear notifications", "notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.clear_title": "Clear notifications?",
"notifications.column_settings.admin.report": "New reports:", "notifications.column_settings.admin.report": "New reports:",
"notifications.column_settings.admin.sign_up": "New sign-ups:", "notifications.column_settings.admin.sign_up": "New sign-ups:",
"notifications.column_settings.alert": "Desktop notifications", "notifications.column_settings.alert": "Desktop notifications",

View file

@ -6055,6 +6055,25 @@ a.status-card {
} }
} }
&__confirmation {
font-size: 14px;
line-height: 20px;
color: $darker-text-color;
h1 {
font-size: 16px;
line-height: 24px;
color: $primary-text-color;
font-weight: 500;
margin-bottom: 8px;
}
strong {
font-weight: 700;
color: $primary-text-color;
}
}
&__bullet-points { &__bullet-points {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -6140,11 +6159,8 @@ a.status-card {
} }
.boost-modal, .boost-modal,
.confirmation-modal,
.report-modal, .report-modal,
.actions-modal, .actions-modal,
.mute-modal,
.block-modal,
.compare-history-modal { .compare-history-modal {
background: lighten($ui-secondary-color, 8%); background: lighten($ui-secondary-color, 8%);
color: $inverted-text-color; color: $inverted-text-color;
@ -6166,10 +6182,7 @@ a.status-card {
} }
} }
.boost-modal__action-bar, .boost-modal__action-bar {
.confirmation-modal__action-bar,
.mute-modal__action-bar,
.block-modal__action-bar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -6192,16 +6205,6 @@ a.status-card {
} }
} }
.mute-modal,
.block-modal {
line-height: 24px;
}
.mute-modal .react-toggle,
.block-modal .react-toggle {
vertical-align: middle;
}
.report-modal { .report-modal {
width: 90vw; width: 90vw;
max-width: 700px; max-width: 700px;
@ -6596,34 +6599,6 @@ a.status-card {
} }
} }
.confirmation-modal__action-bar,
.mute-modal__action-bar,
.block-modal__action-bar {
.confirmation-modal__secondary-button {
flex-shrink: 1;
}
}
.confirmation-modal__secondary-button,
.confirmation-modal__cancel-button,
.mute-modal__cancel-button,
.block-modal__cancel-button {
background-color: transparent;
color: $lighter-text-color;
font-size: 14px;
font-weight: 500;
&:hover,
&:focus,
&:active {
color: darken($lighter-text-color, 4%);
background-color: transparent;
}
}
.confirmation-modal__container,
.mute-modal__container,
.block-modal__container,
.report-modal__target { .report-modal__target {
padding: 30px; padding: 30px;
font-size: 16px; font-size: 16px;
@ -6657,31 +6632,10 @@ a.status-card {
} }
} }
.confirmation-modal__container,
.report-modal__target { .report-modal__target {
text-align: center; text-align: center;
} }
.block-modal,
.mute-modal {
&__explanation {
margin-top: 20px;
}
.setting-toggle {
margin-top: 20px;
margin-bottom: 24px;
display: flex;
align-items: center;
&__label {
color: $inverted-text-color;
margin: 0;
margin-inline-start: 8px;
}
}
}
.report-modal__target { .report-modal__target {
padding: 15px; padding: 15px;