Add confirmation modal when logging out from the web app (#11671)
* [WiP] Add a confirmation modal before logging out from the web interface * Add confirmation to logout link in getting started footer * Please CodeClimate
This commit is contained in:
		
							parent
							
								
									2b853158dc
								
							
						
					
					
						commit
						fd3d45d38f
					
				
					 6 changed files with 144 additions and 31 deletions
				
			
		| 
						 | 
				
			
			@ -23,9 +23,14 @@ class ActionBar extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    account: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    onLogout: PropTypes.func.isRequired,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleLogout = () => {
 | 
			
		||||
    this.props.onLogout();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl } = this.props;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +49,7 @@ class ActionBar extends React.PureComponent {
 | 
			
		|||
    menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
 | 
			
		||||
    menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
 | 
			
		||||
    menu.push(null);
 | 
			
		||||
    menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', target: null, method: 'delete' });
 | 
			
		||||
    menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout });
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='compose__action-bar'>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ export default class NavigationBar extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    account: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    onLogout: PropTypes.func.isRequired,
 | 
			
		||||
    onClose: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +34,7 @@ export default class NavigationBar extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
        <div className='navigation-bar__actions'>
 | 
			
		||||
          <IconButton className='close' title='' icon='close' onClick={this.props.onClose} />
 | 
			
		||||
          <ActionBar account={this.props.account} />
 | 
			
		||||
          <ActionBar account={this.props.account} onLogout={this.props.onLogout} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,29 @@
 | 
			
		|||
import { connect }   from 'react-redux';
 | 
			
		||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
import NavigationBar from '../components/navigation_bar';
 | 
			
		||||
import { logOut } from 'mastodon/utils/log_out';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import { me } from '../../../initial_state';
 | 
			
		||||
 | 
			
		||||
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 => {
 | 
			
		||||
  return {
 | 
			
		||||
    account: state.getIn(['accounts', me]),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(NavigationBar);
 | 
			
		||||
const mapDispatchToProps = (dispatch, { intl }) => ({
 | 
			
		||||
  onLogout () {
 | 
			
		||||
    dispatch(openModal('CONFIRM', {
 | 
			
		||||
      message: intl.formatMessage(messages.logoutMessage),
 | 
			
		||||
      confirm: intl.formatMessage(messages.logoutConfirm),
 | 
			
		||||
      onConfirm: () => logOut(),
 | 
			
		||||
    }));
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,9 +12,11 @@ import Motion from '../ui/util/optional_motion';
 | 
			
		|||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
import SearchResultsContainer from './containers/search_results_container';
 | 
			
		||||
import { changeComposing } from '../../actions/compose';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
 | 
			
		||||
import { mascot } from '../../initial_state';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import { logOut } from 'mastodon/utils/log_out';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +27,8 @@ 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 toot' },
 | 
			
		||||
  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) => ({
 | 
			
		||||
| 
						 | 
				
			
			@ -61,6 +65,21 @@ class Compose extends React.PureComponent {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLogoutClick = e => {
 | 
			
		||||
    const { dispatch, intl } = this.props;
 | 
			
		||||
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
    dispatch(openModal('CONFIRM', {
 | 
			
		||||
      message: intl.formatMessage(messages.logoutMessage),
 | 
			
		||||
      confirm: intl.formatMessage(messages.logoutConfirm),
 | 
			
		||||
      onConfirm: () => logOut(),
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onFocus = () => {
 | 
			
		||||
    this.props.dispatch(changeComposing(true));
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +111,7 @@ class Compose extends React.PureComponent {
 | 
			
		|||
            <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
 | 
			
		||||
          )}
 | 
			
		||||
          <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
 | 
			
		||||
          <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a>
 | 
			
		||||
          <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a>
 | 
			
		||||
        </nav>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +1,72 @@
 | 
			
		|||
import { connect } from 'react-redux';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
 | 
			
		||||
import { logOut } from 'mastodon/utils/log_out';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
 | 
			
		||||
const LinkFooter = ({ withHotkeys }) => (
 | 
			
		||||
  <div className='getting-started__footer'>
 | 
			
		||||
    <ul>
 | 
			
		||||
      {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
 | 
			
		||||
      {withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
 | 
			
		||||
      <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
 | 
			
		||||
      <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
 | 
			
		||||
      <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
 | 
			
		||||
      <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
 | 
			
		||||
      <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
 | 
			
		||||
      <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
 | 
			
		||||
      <li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
 | 
			
		||||
    </ul>
 | 
			
		||||
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' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
    <p>
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
        id='getting_started.open_source_notice'
 | 
			
		||||
        defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
 | 
			
		||||
        values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
 | 
			
		||||
      />
 | 
			
		||||
    </p>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
const mapDispatchToProps = (dispatch, { intl }) => ({
 | 
			
		||||
  onLogout () {
 | 
			
		||||
    dispatch(openModal('CONFIRM', {
 | 
			
		||||
      message: intl.formatMessage(messages.logoutMessage),
 | 
			
		||||
      confirm: intl.formatMessage(messages.logoutConfirm),
 | 
			
		||||
      onConfirm: () => logOut(),
 | 
			
		||||
    }));
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @injectIntl
 | 
			
		||||
@connect(null, mapDispatchToProps)
 | 
			
		||||
class LinkFooter extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    withHotkeys: PropTypes.bool,
 | 
			
		||||
    onLogout: PropTypes.func.isRequired,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleLogoutClick = e => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
    this.props.onLogout();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { withHotkeys } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='getting-started__footer'>
 | 
			
		||||
        <ul>
 | 
			
		||||
          {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
 | 
			
		||||
          {withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
 | 
			
		||||
          <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
 | 
			
		||||
          <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
 | 
			
		||||
          <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
 | 
			
		||||
          <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
 | 
			
		||||
          <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
 | 
			
		||||
          <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
 | 
			
		||||
          <li><a href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
 | 
			
		||||
        <p>
 | 
			
		||||
          <FormattedMessage
 | 
			
		||||
            id='getting_started.open_source_notice'
 | 
			
		||||
            defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
 | 
			
		||||
            values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
 | 
			
		||||
          />
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
LinkFooter.propTypes = {
 | 
			
		||||
  withHotkeys: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default LinkFooter;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										33
									
								
								app/javascript/mastodon/utils/log_out.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/javascript/mastodon/utils/log_out.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
import Rails from 'rails-ujs';
 | 
			
		||||
 | 
			
		||||
export const logOut = () => {
 | 
			
		||||
  const form = document.createElement('form');
 | 
			
		||||
 | 
			
		||||
  const methodInput = document.createElement('input');
 | 
			
		||||
  methodInput.setAttribute('name', '_method');
 | 
			
		||||
  methodInput.setAttribute('value', 'delete');
 | 
			
		||||
  methodInput.setAttribute('type', 'hidden');
 | 
			
		||||
  form.appendChild(methodInput);
 | 
			
		||||
 | 
			
		||||
  const csrfToken = Rails.csrfToken();
 | 
			
		||||
  const csrfParam = Rails.csrfParam();
 | 
			
		||||
 | 
			
		||||
  if (csrfParam && csrfToken) {
 | 
			
		||||
    const csrfInput = document.createElement('input');
 | 
			
		||||
    csrfInput.setAttribute('name', csrfParam);
 | 
			
		||||
    csrfInput.setAttribute('value', csrfToken);
 | 
			
		||||
    csrfInput.setAttribute('type', 'hidden');
 | 
			
		||||
    form.appendChild(csrfInput);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const submitButton = document.createElement('input');
 | 
			
		||||
  submitButton.setAttribute('type', 'submit');
 | 
			
		||||
  form.appendChild(submitButton);
 | 
			
		||||
 | 
			
		||||
  form.method = 'post';
 | 
			
		||||
  form.action = '/auth/sign_out';
 | 
			
		||||
  form.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
  document.body.appendChild(form);
 | 
			
		||||
  submitButton.click();
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue