From 8818748b9023acd84f42bf887e361d9244521df4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Jul 2024 19:05:54 +0200 Subject: [PATCH] Change design of confirmation modals in web UI (#30884) Co-authored-by: Renaud Chaput --- app/javascript/mastodon/components/domain.tsx | 14 +-- .../mastodon/components/follow_button.tsx | 29 ++----- .../mastodon/containers/account_container.jsx | 21 ++--- .../mastodon/containers/domain_container.jsx | 36 -------- .../mastodon/containers/status_container.jsx | 42 ++------- .../containers/header_container.jsx | 19 +--- .../compose/components/action_bar.jsx | 15 +--- .../mastodon/features/compose/index.jsx | 15 +--- .../components/conversation.jsx | 13 +-- .../directory/components/account_card.tsx | 52 ++--------- .../mastodon/features/domain_blocks/index.jsx | 5 +- .../mastodon/features/list_timeline/index.jsx | 23 +---- .../containers/column_settings_container.js | 16 +--- .../picture_in_picture/components/footer.jsx | 14 +-- .../containers/detailed_status_container.js | 31 +------ .../mastodon/features/status/index.jsx | 37 +++----- .../ui/components/confirmation_modal.jsx | 65 -------------- .../clear_notifications.tsx | 46 ++++++++++ .../confirmation_modal.tsx | 79 +++++++++++++++++ .../confirmation_modals/delete_list.tsx | 58 +++++++++++++ .../confirmation_modals/delete_status.tsx | 67 +++++++++++++++ .../confirmation_modals/edit_status.tsx | 45 ++++++++++ .../components/confirmation_modals/index.ts | 8 ++ .../confirmation_modals/log_out.tsx | 40 +++++++++ .../components/confirmation_modals/reply.tsx | 46 ++++++++++ .../confirmation_modals/unfollow.tsx | 50 +++++++++++ .../ui/components/disabled_account_banner.jsx | 20 +---- .../features/ui/components/link_footer.jsx | 21 +---- .../features/ui/components/modal_root.jsx | 18 +++- app/javascript/mastodon/locales/en.json | 12 ++- .../styles/mastodon/components.scss | 86 +++++-------------- 31 files changed, 554 insertions(+), 489 deletions(-) delete mode 100644 app/javascript/mastodon/containers/domain_container.jsx delete mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modal.jsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/clear_notifications.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/delete_list.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/delete_status.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/edit_status.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/log_out.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/reply.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/unfollow.tsx diff --git a/app/javascript/mastodon/components/domain.tsx b/app/javascript/mastodon/components/domain.tsx index ed5e8e7e4c..aa64f0f8c3 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 ecc4e1ee17..222789318e 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 f171fcc2fe..d34962fa4a 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 c719a5775c..0000000000 --- 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 c803822dc0..58c5aac8f8 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 90298879d7..8df06bd920 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 be52eac11e..6c2f27b01b 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 83c741fd19..3a96ab49c3 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 326ea3e5ac..6588c8b763 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 7201f6135b..2a0470bb72 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'])) { + 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'))); - }, - }, - }), - ); - } else if (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 964eada9c1..3656596806 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 f640e503c2..59d9f86977 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 2434c3982d..21dbb2aa90 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 d5226eb346..300c8dd5b3 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 5ab1013700..0e73697fef 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 e9df3697a2..7f3044c5c9 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 5080c0bf85..0000000000 --- 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 0000000000..312ec600a6 --- /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 0000000000..c3a0c0aa76 --- /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 0000000000..8fd9d8da01 --- /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 0000000000..39e80cf741 --- /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 0000000000..fb958518c2 --- /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 0000000000..912c99a393 --- /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 0000000000..48d24182ed --- /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 0000000000..cccd62e4b4 --- /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 0000000000..58e39da07b --- /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 6a71bb2465..3d1380f666 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 08af6fa444..49b21c2e48 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 404b53c742..3e900a0667 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 60bdfd1d44..de8f2ebfcf 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 58c9b53a2c..c575a033b7 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;