diff --git a/app/javascript/mastodon/components/domain.tsx b/app/javascript/mastodon/components/domain.tsx index ed5e8e7e4c20ef..aa64f0f8c3dd1d 100644 --- a/app/javascript/mastodon/components/domain.tsx +++ b/app/javascript/mastodon/components/domain.tsx @@ -3,6 +3,8 @@ import { useCallback } from 'react'; import { defineMessages, useIntl } from 'react-intl'; 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'; @@ -13,17 +15,15 @@ const messages = defineMessages({ }, }); -interface Props { +export const Domain: React.FC<{ domain: string; - onUnblockDomain: (domain: string) => void; -} - -export const Domain: React.FC = ({ domain, onUnblockDomain }) => { +}> = ({ domain }) => { const intl = useIntl(); + const dispatch = useAppDispatch(); const handleDomainUnblock = useCallback(() => { - onUnblockDomain(domain); - }, [domain, onUnblockDomain]); + dispatch(unblockDomain(domain)); + }, [dispatch, domain]); return (
diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index ecc4e1ee175f63..222789318e83ba 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -1,13 +1,9 @@ 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 { - fetchRelationships, - followAccount, - unfollowAccount, -} from 'mastodon/actions/accounts'; +import { fetchRelationships, followAccount } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; import { Button } from 'mastodon/components/button'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; @@ -60,29 +56,14 @@ export const FollowButton: React.FC<{ if (accountId === me) { return; - } else if (relationship.following || relationship.requested) { + } else if (account && (relationship.following || relationship.requested)) { dispatch( - openModal({ - modalType: 'CONFIRM', - modalProps: { - message: ( - @{account?.acct} }} - /> - ), - confirm: intl.formatMessage(messages.unfollow), - onConfirm: () => { - dispatch(unfollowAccount(accountId)); - }, - }, - }), + openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }), ); } else { dispatch(followAccount(accountId)); } - }, [dispatch, intl, accountId, relationship, account, signedIn]); + }, [dispatch, accountId, relationship, account, signedIn]); let label; diff --git a/app/javascript/mastodon/containers/account_container.jsx b/app/javascript/mastodon/containers/account_container.jsx index f171fcc2fe5d19..d34962fa4a5f44 100644 --- a/app/javascript/mastodon/containers/account_container.jsx +++ b/app/javascript/mastodon/containers/account_container.jsx @@ -1,24 +1,20 @@ -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; +import { openModal } from 'mastodon/actions/modal'; + import { followAccount, - unfollowAccount, blockAccount, unblockAccount, muteAccount, unmuteAccount, } from '../actions/accounts'; -import { openModal } from '../actions/modal'; import { initMuteModal } from '../actions/mutes'; import Account from '../components/account'; import { makeGetAccount } from '../selectors'; -const messages = defineMessages({ - unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, -}); - const makeMapStateToProps = () => { const getAccount = makeGetAccount(); @@ -29,18 +25,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: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - }, - })); + dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } })); } else { dispatch(followAccount(account.get('id'))); } diff --git a/app/javascript/mastodon/containers/domain_container.jsx b/app/javascript/mastodon/containers/domain_container.jsx deleted file mode 100644 index c719a5775c7c16..00000000000000 --- a/app/javascript/mastodon/containers/domain_container.jsx +++ /dev/null @@ -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: {domain} }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), - }, - })); - }, - - onUnblockDomain (domain) { - dispatch(unblockDomain(domain)); - }, -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain)); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index c803822dc057ff..58c5aac8f803cb 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -1,4 +1,4 @@ -import { defineMessages, injectIntl } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; @@ -46,18 +46,6 @@ import Status from '../components/status'; import { deleteModal } from '../initial_state'; 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 getStatus = makeGetStatus(); const getPictureInPicture = makeGetPictureInPicture(); @@ -71,20 +59,14 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ +const mapDispatchToProps = (dispatch, { contextType }) => ({ 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)); } @@ -129,14 +111,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ 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 } })); } }, @@ -144,14 +119,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ dispatch((_, getState) => { let state = getState(); if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.editMessage), - confirm: intl.formatMessage(messages.editConfirm), - onConfirm: () => dispatch(editStatus(status.get('id'))), - }, - })); + dispatch(openModal({ modalType: 'CONFIRM_EDIT_STATUS', modalProps: { statusId: status.get('id') } })); } else { dispatch(editStatus(status.get('id'))); } diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx index 90298879d7c967..8df06bd920a004 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx @@ -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: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - }, - })); + dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } })); } else { dispatch(followAccount(account.get('id'))); } diff --git a/app/javascript/mastodon/features/compose/components/action_bar.jsx b/app/javascript/mastodon/features/compose/components/action_bar.jsx index be52eac11ea4f2..6c2f27b01bfc97 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/action_bar.jsx @@ -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 = []; diff --git a/app/javascript/mastodon/features/compose/index.jsx b/app/javascript/mastodon/features/compose/index.jsx index 83c741fd19119e..3a96ab49c38dc3 100644 --- a/app/javascript/mastodon/features/compose/index.jsx +++ b/app/javascript/mastodon/features/compose/index.jsx @@ -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; }; diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index 326ea3e5ac4502..6588c8b763aee6 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -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)); diff --git a/app/javascript/mastodon/features/directory/components/account_card.tsx b/app/javascript/mastodon/features/directory/components/account_card.tsx index 7201f6135b440e..2a0470bb728bf9 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.tsx +++ b/app/javascript/mastodon/features/directory/components/account_card.tsx @@ -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'])) { - dispatch( - openModal({ - modalType: 'CONFIRM', - modalProps: { - message: ( - @{account.get('acct')} }} - /> - ), - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => { - dispatch(unfollowAccount(account.get('id'))); - }, - }, - }), - ); - } else if (account.getIn(['relationship', 'requested'])) { + if ( + account.getIn(['relationship', 'following']) || + account.getIn(['relationship', 'requested']) + ) { dispatch( - openModal({ - modalType: 'CONFIRM', - modalProps: { - message: ( - @{account.get('acct')} }} - /> - ), - 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) { diff --git a/app/javascript/mastodon/features/domain_blocks/index.jsx b/app/javascript/mastodon/features/domain_blocks/index.jsx index 964eada9c1e5b9..3656596806127d 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.jsx +++ b/app/javascript/mastodon/features/domain_blocks/index.jsx @@ -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 => - , + , )} diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx index f640e503c270e4..59d9f869778de9 100644 --- a/app/javascript/mastodon/features/list_timeline/index.jsx +++ b/app/javascript/mastodon/features/list_timeline/index.jsx @@ -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 }) => { diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index 2434c3982d582b..21dbb2aa901dd9 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -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 () { diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx index d5226eb3461987..300c8dd5b3ef05 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx @@ -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(); } diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js index 5ab1013700adbd..0e73697fef70c5 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -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 } })); } }, diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index e9df3697a2ba9f..7f3044c5c9be0b 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -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) => { diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modal.jsx b/app/javascript/mastodon/features/ui/components/confirmation_modal.jsx deleted file mode 100644 index 5080c0bf85836d..00000000000000 --- a/app/javascript/mastodon/features/ui/components/confirmation_modal.jsx +++ /dev/null @@ -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 ( -
-
- {message} -
- -
- - {secondary !== undefined && ( -
-
- ); - } - -} - -export default injectIntl(ConfirmationModal); diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/clear_notifications.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/clear_notifications.tsx new file mode 100644 index 00000000000000..312ec600a649e9 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/clear_notifications.tsx @@ -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 ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx new file mode 100644 index 00000000000000..c3a0c0aa76d297 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx @@ -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 ( +
+
+
+

{title}

+

{message}

+
+
+ +
+
+ {secondary && ( + <> + + +
+ + )} + + + + +
+
+
+ ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_list.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_list.tsx new file mode 100644 index 00000000000000..8fd9d8da01207f --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_list.tsx @@ -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 ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_status.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_status.tsx new file mode 100644 index 00000000000000..39e80cf7410455 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/delete_status.tsx @@ -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 ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/edit_status.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/edit_status.tsx new file mode 100644 index 00000000000000..fb958518c2537f --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/edit_status.tsx @@ -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 ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts new file mode 100644 index 00000000000000..912c99a393a1f0 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts @@ -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'; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/log_out.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/log_out.tsx new file mode 100644 index 00000000000000..48d24182ed6d45 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/log_out.tsx @@ -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 = ({ + onClose, +}) => { + const intl = useIntl(); + + const onConfirm = useCallback(() => { + logOut(); + }, []); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/reply.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/reply.tsx new file mode 100644 index 00000000000000..cccd62e4b41b7a --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/reply.tsx @@ -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 ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/unfollow.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/unfollow.tsx new file mode 100644 index 00000000000000..58e39da07bf95a --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/unfollow.tsx @@ -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 ( + @{account.acct} }} + /> + } + confirm={intl.formatMessage(messages.unfollowConfirm)} + onConfirm={onConfirm} + onClose={onClose} + /> + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx b/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx index 6a71bb2465ea54..3d1380f66699bd 100644 --- a/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx +++ b/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx @@ -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' })); }, }); diff --git a/app/javascript/mastodon/features/ui/components/link_footer.jsx b/app/javascript/mastodon/features/ui/components/link_footer.jsx index 08af6fa4442658..49b21c2e487aef 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.jsx +++ b/app/javascript/mastodon/features/ui/components/link_footer.jsx @@ -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' })); + }, }); diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 404b53c7429ae6..3e900a06670a26 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -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, diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 60bdfd1d445471..de8f2ebfcfb33c 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -169,27 +169,30 @@ "compose_form.spoiler_placeholder": "Content warning (optional)", "confirmation_modal.cancel": "Cancel", "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.message": "Are you sure you want to delete this post?", + "confirmations.delete.title": "Delete post?", "confirmations.delete_list.confirm": "Delete", "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.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.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.message": "Are you sure you want to log out?", + "confirmations.logout.title": "Log out?", "confirmations.mute.confirm": "Mute", "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.title": "Delete & redraft post?", "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.title": "Overwrite post?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "confirmations.unfollow.title": "Unfollow user?", "conversation.delete": "Delete conversation", "conversation.mark_as_read": "Mark as read", "conversation.open": "View conversation", @@ -507,6 +510,7 @@ "notification_requests.title": "Filtered notifications", "notifications.clear": "Clear 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.sign_up": "New sign-ups:", "notifications.column_settings.alert": "Desktop notifications", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 58c9b53a2cfdd5..c575a033b78095 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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 { display: flex; flex-direction: column; @@ -6140,11 +6159,8 @@ a.status-card { } .boost-modal, -.confirmation-modal, .report-modal, .actions-modal, -.mute-modal, -.block-modal, .compare-history-modal { background: lighten($ui-secondary-color, 8%); color: $inverted-text-color; @@ -6166,10 +6182,7 @@ a.status-card { } } -.boost-modal__action-bar, -.confirmation-modal__action-bar, -.mute-modal__action-bar, -.block-modal__action-bar { +.boost-modal__action-bar { display: flex; justify-content: space-between; 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 { width: 90vw; 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 { padding: 30px; font-size: 16px; @@ -6657,31 +6632,10 @@ a.status-card { } } -.confirmation-modal__container, .report-modal__target { 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 { padding: 15px;