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:
parent
df46864b39
commit
59b1de0bcf
10 changed files with 166 additions and 25 deletions
|
@ -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));
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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,7 +22,10 @@ class Search extends React.PureComponent {
|
||||||
|
|
||||||
handleClear (e) {
|
handleClear (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onClear();
|
|
||||||
|
if (this.props.value.length > 0 || this.props.submitted) {
|
||||||
|
this.props.onClear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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);
|
|
@ -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 {
|
||||||
|
|
|
@ -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:",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -53,7 +53,7 @@ module Mastodon
|
||||||
:'zh-TW',
|
:'zh-TW',
|
||||||
]
|
]
|
||||||
|
|
||||||
config.i18n.default_locale = :en
|
config.i18n.default_locale = :en
|
||||||
|
|
||||||
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
||||||
# config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
|
# config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue