about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen <eugen@zeonfederated.com>2017-04-23 04:39:50 +0200
committerGitHub <noreply@github.com>2017-04-23 04:39:50 +0200
commit59b1de0bcf39473106e5380c129d7436be1ad89a (patch)
tree348ac6a815dafa69baea150c2e69be3274fbdc3f
parentdf46864b39b0ba26aedb65d2984b9be08ff5e35a (diff)
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
-rw-r--r--app/assets/javascripts/components/containers/status_container.jsx30
-rw-r--r--app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx23
-rw-r--r--app/assets/javascripts/components/features/compose/components/search.jsx10
-rw-r--r--app/assets/javascripts/components/features/notifications/index.jsx14
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx19
-rw-r--r--app/assets/javascripts/components/features/ui/components/confirmation_modal.jsx50
-rw-r--r--app/assets/javascripts/components/features/ui/components/modal_root.jsx4
-rw-r--r--app/assets/javascripts/components/locales/en.jsx2
-rw-r--r--app/assets/stylesheets/components.scss36
-rw-r--r--config/application.rb3
10 files changed, 166 insertions, 25 deletions
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
index df091de04..ae83d36c9 100644
--- a/app/assets/javascripts/components/containers/status_container.jsx
+++ b/app/assets/javascripts/components/containers/status_container.jsx
@@ -20,6 +20,14 @@ import { initReport } from '../actions/reports';
 import { openModal } from '../actions/modal';
 import { createSelector } from 'reselect'
 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 getStatus = makeGetStatus();
@@ -34,7 +42,7 @@ const makeMapStateToProps = () => {
   return mapStateToProps;
 };
 
-const mapDispatchToProps = (dispatch) => ({
+const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onReply (status, router) {
     dispatch(replyCompose(status, router));
@@ -65,7 +73,11 @@ const mapDispatchToProps = (dispatch) => ({
   },
 
   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) {
@@ -81,7 +93,11 @@ const mapDispatchToProps = (dispatch) => ({
   },
 
   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) {
@@ -89,9 +105,13 @@ const mapDispatchToProps = (dispatch) => ({
   },
 
   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));
diff --git a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
index 8472d25a5..f924e7f5e 100644
--- a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
+++ b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
@@ -11,6 +11,13 @@ import {
 } from '../../../actions/accounts';
 import { mentionCompose } from '../../../actions/compose';
 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 getAccount = makeGetAccount();
@@ -23,7 +30,7 @@ const makeMapStateToProps = () => {
   return mapStateToProps;
 };
 
-const mapDispatchToProps = dispatch => ({
+const mapDispatchToProps = (dispatch, { intl }) => ({
   onFollow (account) {
     if (account.getIn(['relationship', 'following'])) {
       dispatch(unfollowAccount(account.get('id')));
@@ -36,7 +43,11 @@ const mapDispatchToProps = dispatch => ({
     if (account.getIn(['relationship', 'blocking'])) {
       dispatch(unblockAccount(account.get('id')));
     } 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'])) {
       dispatch(unmuteAccount(account.get('id')));
     } 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));
diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx
index 7b025341b..f62248a33 100644
--- a/app/assets/javascripts/components/features/compose/components/search.jsx
+++ b/app/assets/javascripts/components/features/compose/components/search.jsx
@@ -13,6 +13,7 @@ class Search extends React.PureComponent {
     this.handleChange = this.handleChange.bind(this);
     this.handleKeyDown = this.handleKeyDown.bind(this);
     this.handleFocus = this.handleFocus.bind(this);
+    this.handleClear = this.handleClear.bind(this);
   }
 
   handleChange (e) {
@@ -21,7 +22,10 @@ class Search extends React.PureComponent {
 
   handleClear (e) {
     e.preventDefault();
-    this.props.onClear();
+
+    if (this.props.value.length > 0 || this.props.submitted) {
+      this.props.onClear();
+    }
   }
 
   handleKeyDown (e) {
@@ -55,9 +59,9 @@ class Search extends React.PureComponent {
           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 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>
     );
diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx
index 7b9b6d9e4..14c00b9ce 100644
--- a/app/assets/javascripts/components/features/notifications/index.jsx
+++ b/app/assets/javascripts/components/features/notifications/index.jsx
@@ -11,10 +11,12 @@ import { createSelector } from 'reselect';
 import Immutable from 'immutable';
 import LoadMore from '../../components/load_more';
 import ClearColumnButton from './components/clear_column_button';
+import { openModal } from '../../actions/modal';
 
 const messages = defineMessages({
   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([
@@ -64,9 +66,13 @@ class Notifications extends React.PureComponent {
   }
 
   handleClear () {
-    if (window.confirm(this.props.intl.formatMessage(messages.confirm))) {
-      this.props.dispatch(clearNotifications());
-    }
+    const { dispatch, intl } = this.props;
+
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.clearMessage),
+      confirm: intl.formatMessage(messages.clearConfirm),
+      onConfirm: () => dispatch(clearNotifications())
+    }));
   }
 
   setRef (c) {
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 60f5415d6..595df251c 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -30,6 +30,12 @@ import ColumnBackButton from '../../components/column_back_button';
 import StatusContainer from '../../containers/status_container';
 import { openModal } from '../../actions/modal';
 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 getStatus = makeGetStatus();
@@ -100,7 +106,13 @@ class Status extends React.PureComponent {
   }
 
   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) {
@@ -178,7 +190,8 @@ Status.propTypes = {
   descendantsIds: ImmutablePropTypes.list,
   me: PropTypes.number,
   boostModal: PropTypes.bool,
-  autoPlayGif: PropTypes.bool
+  autoPlayGif: PropTypes.bool,
+  intl: PropTypes.object.isRequired
 };
 
-export default connect(makeMapStateToProps)(Status);
+export default injectIntl(connect(makeMapStateToProps)(Status));
diff --git a/app/assets/javascripts/components/features/ui/components/confirmation_modal.jsx b/app/assets/javascripts/components/features/ui/components/confirmation_modal.jsx
new file mode 100644
index 000000000..914c12f82
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/confirmation_modal.jsx
@@ -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);
diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
index f9e173222..cfaa8a598 100644
--- a/app/assets/javascripts/components/features/ui/components/modal_root.jsx
+++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
@@ -3,13 +3,15 @@ import MediaModal from './media_modal';
 import OnboardingModal from './onboarding_modal';
 import VideoModal from './video_modal';
 import BoostModal from './boost_modal';
+import ConfirmationModal from './confirmation_modal';
 import { TransitionMotion, spring } from 'react-motion';
 
 const MODAL_COMPONENTS = {
   'MEDIA': MediaModal,
   'ONBOARDING': OnboardingModal,
   'VIDEO': VideoModal,
-  'BOOST': BoostModal
+  'BOOST': BoostModal,
+  'CONFIRM': ConfirmationModal
 };
 
 class ModalRoot extends React.PureComponent {
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index c3155c342..420cfe00f 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -85,7 +85,7 @@ const en = {
   "notification.follow": "{name} followed you",
   "notification.mention": "{name} mentioned you",
   "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.column_settings.alert": "Desktop notifications",
   "notifications.column_settings.favourite": "Favourites:",
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 4e7309a02..ac9a264ee 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -2773,7 +2773,7 @@ button.icon-button.active i.fa-retweet {
   margin-left: 10px;
 }
 
-.boost-modal {
+.boost-modal, .confirmation-modal {
   background: lighten($color2, 8%);
   color: $color1;
   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;
   background: $color2;
   padding: 10px;
@@ -2835,6 +2835,38 @@ button.icon-button.active i.fa-retweet {
   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 {
   background-color: $color4;
   height: 3px;
diff --git a/config/application.rb b/config/application.rb
index 3053faa90..e51157292 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -53,7 +53,7 @@ module Mastodon
       :'zh-TW',
     ]
 
-    config.i18n.default_locale    = :en
+    config.i18n.default_locale = :en
 
     # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
     # config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
@@ -72,7 +72,6 @@ module Mastodon
     config.middleware.use Rack::Attack
     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.evaluate_node_modules = true