Skip to content

Commit 101c9d4

Browse files
ClearlyClairehiyuki2578
authored andcommitted
Add confirmation modal when logging out from the web app (mastodon#11671)
* [WiP] Add a confirmation modal before logging out from the web interface * Add confirmation to logout link in getting started footer * Please CodeClimate
1 parent ddcc633 commit 101c9d4

6 files changed

Lines changed: 146 additions & 33 deletions

File tree

app/javascript/mastodon/features/compose/components/action_bar.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ class ActionBar extends React.PureComponent {
2323

2424
static propTypes = {
2525
account: ImmutablePropTypes.map.isRequired,
26+
onLogout: PropTypes.func.isRequired,
2627
intl: PropTypes.object.isRequired,
2728
};
2829

30+
handleLogout = () => {
31+
this.props.onLogout();
32+
}
33+
2934
render () {
3035
const { intl } = this.props;
3136

@@ -44,7 +49,7 @@ class ActionBar extends React.PureComponent {
4449
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
4550
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
4651
menu.push(null);
47-
menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', target: null, method: 'delete' });
52+
menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout });
4853

4954
return (
5055
<div className='compose__action-bar'>

app/javascript/mastodon/features/compose/components/navigation_bar.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default class NavigationBar extends ImmutablePureComponent {
1212

1313
static propTypes = {
1414
account: ImmutablePropTypes.map.isRequired,
15+
onLogout: PropTypes.func.isRequired,
1516
onClose: PropTypes.func,
1617
};
1718

@@ -33,7 +34,7 @@ export default class NavigationBar extends ImmutablePureComponent {
3334

3435
<div className='navigation-bar__actions'>
3536
<IconButton className='close' title='' icon='close' onClick={this.props.onClose} />
36-
<ActionBar account={this.props.account} />
37+
<ActionBar account={this.props.account} onLogout={this.props.onLogout} />
3738
</div>
3839
</div>
3940
);
Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
import { connect } from 'react-redux';
2+
import { defineMessages, injectIntl } from 'react-intl';
23
import NavigationBar from '../components/navigation_bar';
4+
import { logOut } from 'mastodon/utils/log_out';
5+
import { openModal } from 'mastodon/actions/modal';
36
import { me } from '../../../initial_state';
47

8+
const messages = defineMessages({
9+
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
10+
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
11+
});
12+
513
const mapStateToProps = state => {
614
return {
715
account: state.getIn(['accounts', me]),
816
};
917
};
1018

11-
export default connect(mapStateToProps)(NavigationBar);
19+
const mapDispatchToProps = (dispatch, { intl }) => ({
20+
onLogout () {
21+
dispatch(openModal('CONFIRM', {
22+
message: intl.formatMessage(messages.logoutMessage),
23+
confirm: intl.formatMessage(messages.logoutConfirm),
24+
onConfirm: () => logOut(),
25+
}));
26+
},
27+
});
28+
29+
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));

app/javascript/mastodon/features/compose/index.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import Motion from '../ui/util/optional_motion';
1212
import spring from 'react-motion/lib/spring';
1313
import SearchResultsContainer from './containers/search_results_container';
1414
import { changeComposing } from '../../actions/compose';
15+
import { openModal } from 'mastodon/actions/modal';
1516
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
1617
import { mascot } from '../../initial_state';
1718
import Icon from 'mastodon/components/icon';
19+
import { logOut } from 'mastodon/utils/log_out';
1820

1921
const messages = defineMessages({
2022
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -25,6 +27,8 @@ const messages = defineMessages({
2527
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
2628
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
2729
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
30+
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
31+
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
2832
});
2933

3034
const mapStateToProps = (state, ownProps) => ({
@@ -61,6 +65,21 @@ class Compose extends React.PureComponent {
6165
}
6266
}
6367

68+
handleLogoutClick = e => {
69+
const { dispatch, intl } = this.props;
70+
71+
e.preventDefault();
72+
e.stopPropagation();
73+
74+
dispatch(openModal('CONFIRM', {
75+
message: intl.formatMessage(messages.logoutMessage),
76+
confirm: intl.formatMessage(messages.logoutConfirm),
77+
onConfirm: () => logOut(),
78+
}));
79+
80+
return false;
81+
}
82+
6483
onFocus = () => {
6584
this.props.dispatch(changeComposing(true));
6685
}
@@ -92,7 +111,7 @@ class Compose extends React.PureComponent {
92111
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
93112
)}
94113
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
95-
<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>
114+
<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>
96115
</nav>
97116
);
98117
}
Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,72 @@
1+
import { connect } from 'react-redux';
12
import React from 'react';
23
import PropTypes from 'prop-types';
3-
import { FormattedMessage } from 'react-intl';
4+
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
45
import { Link } from 'react-router-dom';
56
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
7+
import { logOut } from 'mastodon/utils/log_out';
8+
import { openModal } from 'mastodon/actions/modal';
69

7-
const LinkFooter = ({ withHotkeys }) => (
8-
<div className='getting-started__footer'>
9-
<ul>
10-
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
11-
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
12-
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
13-
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
14-
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
15-
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
16-
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
17-
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
18-
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
19-
</ul>
20-
21-
<p>
22-
<FormattedMessage
23-
id='getting_started.open_source_notice'
24-
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
25-
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
26-
/>
27-
</p>
28-
</div>
29-
);
30-
31-
LinkFooter.propTypes = {
32-
withHotkeys: PropTypes.bool,
33-
};
10+
const messages = defineMessages({
11+
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
12+
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
13+
});
14+
15+
const mapDispatchToProps = (dispatch, { intl }) => ({
16+
onLogout () {
17+
dispatch(openModal('CONFIRM', {
18+
message: intl.formatMessage(messages.logoutMessage),
19+
confirm: intl.formatMessage(messages.logoutConfirm),
20+
onConfirm: () => logOut(),
21+
}));
22+
},
23+
});
24+
25+
export default @injectIntl
26+
@connect(null, mapDispatchToProps)
27+
class LinkFooter extends React.PureComponent {
28+
29+
static propTypes = {
30+
withHotkeys: PropTypes.bool,
31+
onLogout: PropTypes.func.isRequired,
32+
intl: PropTypes.object.isRequired,
33+
};
34+
35+
handleLogoutClick = e => {
36+
e.preventDefault();
37+
e.stopPropagation();
38+
39+
this.props.onLogout();
3440

35-
export default LinkFooter;
41+
return false;
42+
}
43+
44+
render () {
45+
const { withHotkeys } = this.props;
46+
47+
return (
48+
<div className='getting-started__footer'>
49+
<ul>
50+
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
51+
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
52+
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
53+
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
54+
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
55+
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
56+
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
57+
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
58+
<li><a href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
59+
</ul>
60+
61+
<p>
62+
<FormattedMessage
63+
id='getting_started.open_source_notice'
64+
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
65+
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
66+
/>
67+
</p>
68+
</div>
69+
);
70+
}
71+
72+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Rails from 'rails-ujs';
2+
3+
export const logOut = () => {
4+
const form = document.createElement('form');
5+
6+
const methodInput = document.createElement('input');
7+
methodInput.setAttribute('name', '_method');
8+
methodInput.setAttribute('value', 'delete');
9+
methodInput.setAttribute('type', 'hidden');
10+
form.appendChild(methodInput);
11+
12+
const csrfToken = Rails.csrfToken();
13+
const csrfParam = Rails.csrfParam();
14+
15+
if (csrfParam && csrfToken) {
16+
const csrfInput = document.createElement('input');
17+
csrfInput.setAttribute('name', csrfParam);
18+
csrfInput.setAttribute('value', csrfToken);
19+
csrfInput.setAttribute('type', 'hidden');
20+
form.appendChild(csrfInput);
21+
}
22+
23+
const submitButton = document.createElement('input');
24+
submitButton.setAttribute('type', 'submit');
25+
form.appendChild(submitButton);
26+
27+
form.method = 'post';
28+
form.action = '/auth/sign_out';
29+
form.style.display = 'none';
30+
31+
document.body.appendChild(form);
32+
submitButton.click();
33+
};

0 commit comments

Comments
 (0)