diff options
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 |