Add a confirmation modal: (#2279)

- Deleting a toot
- Muting, blocking someone
- Clearing notifications

Remove source map generation from development environment, as it is a huge
performance sink hole with little gains
This commit is contained in:
Eugen 2017-04-23 04:39:50 +02:00 committed by GitHub
parent df46864b39
commit 59b1de0bcf
10 changed files with 166 additions and 25 deletions

View File

@ -20,6 +20,14 @@ import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { isMobile } from '../is_mobile' import { isMobile } from '../is_mobile'
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
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?' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
});
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
@ -34,7 +42,7 @@ const makeMapStateToProps = () => {
return mapStateToProps; return mapStateToProps;
}; };
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) { onReply (status, router) {
dispatch(replyCompose(status, router)); dispatch(replyCompose(status, router));
@ -65,7 +73,11 @@ const mapDispatchToProps = (dispatch) => ({
}, },
onDelete (status) { onDelete (status) {
dispatch(deleteStatus(status.get('id'))); dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id')))
}));
}, },
onMention (account, router) { onMention (account, router) {
@ -81,7 +93,11 @@ const mapDispatchToProps = (dispatch) => ({
}, },
onBlock (account) { onBlock (account) {
dispatch(blockAccount(account.get('id'))); dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id')))
}));
}, },
onReport (status) { onReport (status) {
@ -89,9 +105,13 @@ const mapDispatchToProps = (dispatch) => ({
}, },
onMute (account) { onMute (account) {
dispatch(muteAccount(account.get('id'))); dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.muteConfirm),
onConfirm: () => dispatch(muteAccount(account.get('id')))
}));
}, },
}); });
export default connect(makeMapStateToProps, mapDispatchToProps)(Status); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

View File

@ -11,6 +11,13 @@ import {
} from '../../../actions/accounts'; } from '../../../actions/accounts';
import { mentionCompose } from '../../../actions/compose'; import { mentionCompose } from '../../../actions/compose';
import { initReport } from '../../../actions/reports'; import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }
});
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getAccount = makeGetAccount(); const getAccount = makeGetAccount();
@ -23,7 +30,7 @@ const makeMapStateToProps = () => {
return mapStateToProps; return mapStateToProps;
}; };
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'following'])) {
dispatch(unfollowAccount(account.get('id'))); dispatch(unfollowAccount(account.get('id')));
@ -36,7 +43,11 @@ const mapDispatchToProps = dispatch => ({
if (account.getIn(['relationship', 'blocking'])) { if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id'))); dispatch(unblockAccount(account.get('id')));
} else { } else {
dispatch(blockAccount(account.get('id'))); dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id')))
}));
} }
}, },
@ -52,9 +63,13 @@ const mapDispatchToProps = dispatch => ({
if (account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id'))); dispatch(unmuteAccount(account.get('id')));
} else { } else {
dispatch(muteAccount(account.get('id'))); dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.muteConfirm),
onConfirm: () => dispatch(muteAccount(account.get('id')))
}));
} }
} }
}); });
export default connect(makeMapStateToProps, mapDispatchToProps)(Header); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

View File

@ -13,6 +13,7 @@ class Search extends React.PureComponent {
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleFocus = this.handleFocus.bind(this); this.handleFocus = this.handleFocus.bind(this);
this.handleClear = this.handleClear.bind(this);
} }
handleChange (e) { handleChange (e) {
@ -21,8 +22,11 @@ class Search extends React.PureComponent {
handleClear (e) { handleClear (e) {
e.preventDefault(); e.preventDefault();
if (this.props.value.length > 0 || this.props.submitted) {
this.props.onClear(); this.props.onClear();
} }
}
handleKeyDown (e) { handleKeyDown (e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
@ -55,9 +59,9 @@ class Search extends React.PureComponent {
onFocus={this.handleFocus} onFocus={this.handleFocus}
/> />
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}> <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> <i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} /> <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
</div> </div>
</div> </div>
); );

View File

@ -11,10 +11,12 @@ import { createSelector } from 'reselect';
import Immutable from 'immutable'; import Immutable from 'immutable';
import LoadMore from '../../components/load_more'; import LoadMore from '../../components/load_more';
import ClearColumnButton from './components/clear_column_button'; import ClearColumnButton from './components/clear_column_button';
import { openModal } from '../../actions/modal';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' }, title: { id: 'column.notifications', defaultMessage: 'Notifications' },
confirm: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to clear all your 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' }
}); });
const getNotifications = createSelector([ const getNotifications = createSelector([
@ -64,9 +66,13 @@ class Notifications extends React.PureComponent {
} }
handleClear () { handleClear () {
if (window.confirm(this.props.intl.formatMessage(messages.confirm))) { const { dispatch, intl } = this.props;
this.props.dispatch(clearNotifications());
} dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.clearMessage),
confirm: intl.formatMessage(messages.clearConfirm),
onConfirm: () => dispatch(clearNotifications())
}));
} }
setRef (c) { setRef (c) {

View File

@ -30,6 +30,12 @@ import ColumnBackButton from '../../components/column_back_button';
import StatusContainer from '../../containers/status_container'; import StatusContainer from '../../containers/status_container';
import { openModal } from '../../actions/modal'; import { openModal } from '../../actions/modal';
import { isMobile } from '../../is_mobile' import { isMobile } from '../../is_mobile'
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
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?' }
});
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
@ -100,7 +106,13 @@ class Status extends React.PureComponent {
} }
handleDeleteClick (status) { handleDeleteClick (status) {
this.props.dispatch(deleteStatus(status.get('id'))); const { dispatch, intl } = this.props;
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id')))
}));
} }
handleMentionClick (account, router) { handleMentionClick (account, router) {
@ -178,7 +190,8 @@ Status.propTypes = {
descendantsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list,
me: PropTypes.number, me: PropTypes.number,
boostModal: PropTypes.bool, boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool autoPlayGif: PropTypes.bool,
intl: PropTypes.object.isRequired
}; };
export default connect(makeMapStateToProps)(Status); export default injectIntl(connect(makeMapStateToProps)(Status));

View File

@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from '../../../components/button';
class ConfirmationModal extends React.PureComponent {
constructor (props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
handleClick () {
this.props.onClose();
this.props.onConfirm();
}
handleCancel (e) {
e.preventDefault();
this.props.onClose();
}
render () {
const { intl, message, confirm, onConfirm, onClose } = this.props;
return (
<div className='modal-root__modal confirmation-modal'>
<div className='confirmation-modal__container'>
{message}
</div>
<div className='confirmation-modal__action-bar'>
<div><a href='#' onClick={this.handleCancel}><FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /></a></div>
<Button text={confirm} onClick={this.handleClick} />
</div>
</div>
);
}
}
ConfirmationModal.propTypes = {
message: PropTypes.node.isRequired,
confirm: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired
};
export default injectIntl(ConfirmationModal);

View File

@ -3,13 +3,15 @@ import MediaModal from './media_modal';
import OnboardingModal from './onboarding_modal'; import OnboardingModal from './onboarding_modal';
import VideoModal from './video_modal'; import VideoModal from './video_modal';
import BoostModal from './boost_modal'; import BoostModal from './boost_modal';
import ConfirmationModal from './confirmation_modal';
import { TransitionMotion, spring } from 'react-motion'; import { TransitionMotion, spring } from 'react-motion';
const MODAL_COMPONENTS = { const MODAL_COMPONENTS = {
'MEDIA': MediaModal, 'MEDIA': MediaModal,
'ONBOARDING': OnboardingModal, 'ONBOARDING': OnboardingModal,
'VIDEO': VideoModal, 'VIDEO': VideoModal,
'BOOST': BoostModal 'BOOST': BoostModal,
'CONFIRM': ConfirmationModal
}; };
class ModalRoot extends React.PureComponent { class ModalRoot extends React.PureComponent {

View File

@ -85,7 +85,7 @@ const en = {
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",
"notification.reblog": "{name} boosted your status", "notification.reblog": "{name} boosted your status",
"notifications.clear_confirmation": "Are you sure you want to clear all your notifications?", "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.clear": "Clear notifications", "notifications.clear": "Clear notifications",
"notifications.column_settings.alert": "Desktop notifications", "notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.favourite": "Favourites:",

View File

@ -2773,7 +2773,7 @@ button.icon-button.active i.fa-retweet {
margin-left: 10px; margin-left: 10px;
} }
.boost-modal { .boost-modal, .confirmation-modal {
background: lighten($color2, 8%); background: lighten($color2, 8%);
color: $color1; color: $color1;
border-radius: 8px; border-radius: 8px;
@ -2808,7 +2808,7 @@ button.icon-button.active i.fa-retweet {
} }
} }
.boost-modal__action-bar { .boost-modal__action-bar, .confirmation-modal__action-bar {
display: flex; display: flex;
background: $color2; background: $color2;
padding: 10px; padding: 10px;
@ -2835,6 +2835,38 @@ button.icon-button.active i.fa-retweet {
font-size: 14px; font-size: 14px;
} }
.confirmation-modal {
max-width: 380px;
}
.confirmation-modal__action-bar {
& > div {
text-align: left;
padding: 0 16px;
}
a {
color: darken($color2, 34%);
text-decoration: none;
font-size: 14px;
font-weight: 500;
&:hover, &:focus, &:active {
color: darken($color2, 38%);
}
}
}
.confirmation-modal__container {
padding: 30px;
font-size: 16px;
text-align: center;
strong {
font-weight: 500;
}
}
.loading-bar { .loading-bar {
background-color: $color4; background-color: $color4;
height: 3px; height: 3px;

View File

@ -72,7 +72,6 @@ module Mastodon
config.middleware.use Rack::Attack config.middleware.use Rack::Attack
config.middleware.use Rack::Deflater config.middleware.use Rack::Deflater
config.browserify_rails.source_map_environments << 'development'
config.browserify_rails.commandline_options = '--transform [ babelify --presets [ es2015 react ] --plugins [ transform-decorators-legacy ] ] --extension=".jsx"' config.browserify_rails.commandline_options = '--transform [ babelify --presets [ es2015 react ] --plugins [ transform-decorators-legacy ] ] --extension=".jsx"'
config.browserify_rails.evaluate_node_modules = true config.browserify_rails.evaluate_node_modules = true