PLT-6484 Add /leave command to leave a channel (#6402)

* PLT-6484 Add /leave command to leave a channel

* Text changes requeted on review.

* PLT-6484 Display the right error message when trying to /leave town-square

* PLT-6484 Be able to execute /leave command in direct and group message channels with the same effect as clicking x

* PLT-6484 Refactor to create new leave_private_channel_modal.jsx

* PLT-6484 Remove previous leave private channel logic to use new leave_private_channel_modal.jsx

* Remove dot in command description. Change localized error when leaving Town square.

* disable /leave command in reply threads on the right-hand sidebar, since it is not obvious which channel you should leave
Этот коммит содержится в:
David Meza
2017-08-03 07:59:42 -05:00
коммит произвёл Joram Wilander
родитель 19804c4e40
Коммит b54d134299
11 изменённых файлов: 315 добавлений и 119 удалений

Просмотреть файл

@@ -72,6 +72,39 @@ export function executeCommand(message, args, success, error) {
msg = '/shortcuts';
}
break;
case '/leave': {
// /leave command not supported in reply threads.
if (args.channel_id && (args.root_id || args.parent_id)) {
GlobalActions.sendEphemeralPost('/leave is not supported in reply threads. Use it in the center channel instead.', args.channel_id, args.parent_id);
return;
}
const channel = ChannelStore.getCurrent();
if (channel.type === Constants.PRIVATE_CHANNEL) {
GlobalActions.showLeavePrivateChannelModal(channel);
return;
} else if (
channel.type === Constants.DM_CHANNEL ||
channel.type === Constants.GM_CHANNEL
) {
let name;
let category;
if (channel.type === Constants.DM_CHANNEL) {
name = Utils.getUserIdFromChannelName(channel);
category = Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW;
} else {
name = channel.id;
category = Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW;
}
const currentUserId = UserStore.getCurrentId();
savePreferences(currentUserId, [{category, name, user_id: currentUserId, value: 'false'}])(dispatch, getState);
if (ChannelUtils.isFavoriteChannel(channel)) {
unmarkFavorite(channel.id);
}
browserHistory.push(`${TeamStore.getCurrentTeamRelativeUrl()}/channels/town-square`);
return;
}
break;
}
case '/settings':
GlobalActions.showAccountSettingsModal();
return;

Просмотреть файл

@@ -281,6 +281,13 @@ export function showLeaveTeamModal() {
});
}
export function showLeavePrivateChannelModal(channel) {
AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_LEAVE_PRIVATE_CHANNEL_MODAL,
value: channel
});
}
export function emitSuggestionPretextChanged(suggestionId, pretext) {
AppDispatcher.handleViewAction({
type: ActionTypes.SUGGESTION_PRETEXT_CHANGED,
@@ -351,7 +358,7 @@ export function emitPreferencesDeletedEvent(preferences) {
});
}
export function sendEphemeralPost(message, channelId) {
export function sendEphemeralPost(message, channelId, parentId) {
const timestamp = Utils.getTimestamp();
const post = {
id: Utils.generateId(),
@@ -361,6 +368,8 @@ export function sendEphemeralPost(message, channelId) {
type: Constants.PostTypes.EPHEMERAL,
create_at: timestamp,
update_at: timestamp,
root_id: parentId,
parent_id: parentId,
props: {}
};

Просмотреть файл

@@ -14,7 +14,6 @@ import ChannelMembersModal from './channel_members_modal.jsx';
import ChannelNotificationsModal from './channel_notifications_modal.jsx';
import DeleteChannelModal from './delete_channel_modal.jsx';
import RenameChannelModal from './rename_channel_modal.jsx';
import ConfirmModal from './confirm_modal.jsx';
import ToggleModalButton from './toggle_modal_button.jsx';
import ChannelStore from 'stores/channel_store.jsx';
@@ -60,8 +59,6 @@ export default class ChannelHeader extends React.Component {
this.initWebrtc = this.initWebrtc.bind(this);
this.onBusy = this.onBusy.bind(this);
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
this.createLeaveChannelModal = this.createLeaveChannelModal.bind(this);
this.hideLeaveChannelModal = this.hideLeaveChannelModal.bind(this);
const state = this.getStateFromStores();
state.showEditChannelHeaderModal = false;
@@ -91,7 +88,6 @@ export default class ChannelHeader extends React.Component {
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
isBusy: WebrtcStore.isBusy(),
isFavorite: channel && ChannelUtils.isFavoriteChannel(channel),
showLeaveChannelModal: false,
pinsOpen: SearchStore.getIsPinnedPosts()
};
}
@@ -144,9 +140,7 @@ export default class ChannelHeader extends React.Component {
handleLeave() {
if (this.state.channel.type === Constants.PRIVATE_CHANNEL) {
this.setState({
showLeaveChannelModal: true
});
GlobalActions.showLeavePrivateChannelModal(this.state.channel);
} else {
ChannelActions.leaveChannel(this.state.channel.id);
}
@@ -233,54 +227,6 @@ export default class ChannelHeader extends React.Component {
});
}
hideLeaveChannelModal() {
this.setState({
showLeaveChannelModal: false
});
}
createLeaveChannelModal() {
const title = (
<FormattedMessage
id='leave_private_channel_modal.title'
defaultMessage='Leave Private Channel {channel}'
values={{
channel: <b>{this.state.channel.display_name}</b>
}}
/>
);
const message = (
<FormattedMessage
id='leave_private_channel_modal.message'
defaultMessage='Are you sure you wish to leave the private channel {channel}? You must be re-invited in order to re-join this channel in the future.'
values={{
channel: <b>{this.state.channel.display_name}</b>
}}
/>
);
const buttonClass = 'btn btn-danger';
const button = (
<FormattedMessage
id='leave_private_channel_modal.leave'
defaultMessage='Yes, leave channel'
/>
);
return (
<ConfirmModal
show={this.state.showLeaveChannelModal}
title={title}
message={message}
confirmButtonClass={buttonClass}
confirmButtonText={button}
onConfirm={() => ChannelActions.leaveChannel(this.state.channel.id)}
onCancel={this.hideLeaveChannelModal}
/>
);
}
render() {
const flagIcon = Constants.FLAG_ICON_SVG;
const pinIcon = Constants.PIN_ICON_SVG;
@@ -885,8 +831,6 @@ export default class ChannelHeader extends React.Component {
);
}
const leaveChannelModal = this.createLeaveChannelModal();
let pinnedIconClass = 'channel-header__icon';
if (this.state.pinsOpen) {
pinnedIconClass += ' active';
@@ -1001,7 +945,6 @@ export default class ChannelHeader extends React.Component {
{editHeaderModal}
{editPurposeModal}
{channelMembersModal}
{leaveChannelModal}
<RenameChannelModal
show={this.state.showRenameChannelModal}
onHide={this.hideRenameChannelModal}

Просмотреть файл

@@ -0,0 +1,120 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ConfirmModal from 'components/confirm_modal.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import ModalStore from 'stores/modal_store.jsx';
import Constants from 'utils/constants.jsx';
import {intlShape, injectIntl, FormattedMessage} from 'react-intl';
import React from 'react';
class LeavePrivateChannelModal extends React.Component {
constructor(props) {
super(props);
this.handleToggle = this.handleToggle.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleHide = this.handleHide.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.state = {
show: false,
channel: null
};
this.mounted = false;
}
componentDidMount() {
this.mounted = true;
ModalStore.addModalListener(Constants.ActionTypes.TOGGLE_LEAVE_PRIVATE_CHANNEL_MODAL, this.handleToggle);
}
componentWillUnmount() {
this.mounted = false;
ModalStore.removeModalListener(Constants.ActionTypes.TOGGLE_LEAVE_PRIVATE_CHANNEL_MODAL, this.handleToggle);
}
handleKeyPress(e) {
if (e.key === 'Enter' && this.state.show) {
this.handleSubmit();
}
}
handleSubmit() {
const channelId = this.state.channel.id;
this.setState({
show: false,
channel: null
});
ChannelActions.leaveChannel(channelId);
}
handleToggle(value) {
this.setState({
channel: value,
show: value !== null
});
}
handleHide() {
this.setState({
show: false
});
}
render() {
let title = '';
let message = '';
if (this.state.channel) {
title = (
<FormattedMessage
id='leave_private_channel_modal.title'
defaultMessage='Leave Private Channel {channel}'
values={{
channel: <b>{this.state.channel.display_name}</b>
}}
/>
);
message = (
<FormattedMessage
id='leave_private_channel_modal.message'
defaultMessage='Are you sure you wish to leave the private channel {channel}? You must be re-invited in order to re-join this channel in the future.'
values={{
channel: <b>{this.state.channel.display_name}</b>
}}
/>
);
}
const buttonClass = 'btn btn-danger';
const button = (
<FormattedMessage
id='leave_private_channel_modal.leave'
defaultMessage='Yes, leave channel'
/>
);
return (
<ConfirmModal
show={this.state.show}
title={title}
message={message}
confirmButtonClass={buttonClass}
confirmButtonText={button}
onConfirm={this.handleSubmit}
onCancel={this.handleHide}
/>
);
}
}
LeavePrivateChannelModal.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(LeavePrivateChannelModal);

Просмотреть файл

@@ -12,7 +12,6 @@ import ChannelMembersModal from './channel_members_modal.jsx';
import ChannelNotificationsModal from './channel_notifications_modal.jsx';
import DeleteChannelModal from './delete_channel_modal.jsx';
import RenameChannelModal from './rename_channel_modal.jsx';
import ConfirmModal from './confirm_modal.jsx';
import ToggleModalButton from './toggle_modal_button.jsx';
import StatusIcon from './status_icon.jsx';
@@ -75,9 +74,6 @@ export default class Navbar extends React.Component {
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
this.getPinnedPosts = this.getPinnedPosts.bind(this);
this.createLeaveChannelModal = this.createLeaveChannelModal.bind(this);
this.hideLeaveChannelModal = this.hideLeaveChannelModal.bind(this);
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
state.showEditChannelHeaderModal = false;
@@ -97,8 +93,7 @@ export default class Navbar extends React.Component {
users: [],
userCount: ChannelStore.getCurrentStats().member_count,
currentUser: UserStore.getCurrentUser(),
isFavorite: channel && ChannelUtils.isFavoriteChannel(channel),
showLeaveChannelModal: false
isFavorite: channel && ChannelUtils.isFavoriteChannel(channel)
};
}
@@ -139,9 +134,7 @@ export default class Navbar extends React.Component {
handleLeave() {
if (this.state.channel.type === Constants.PRIVATE_CHANNEL) {
this.setState({
showLeaveChannelModal: true
});
GlobalActions.showLeavePrivateChannelModal(this.state.channel);
} else {
ChannelActions.leaveChannel(this.state.channel.id);
}
@@ -739,54 +732,6 @@ export default class Navbar extends React.Component {
return buttons;
}
hideLeaveChannelModal() {
this.setState({
showLeaveChannelModal: false
});
}
createLeaveChannelModal() {
const title = (
<FormattedMessage
id='leave_private_channel_modal.title'
defaultMessage='Leave Private Channel {channel}'
values={{
channel: <b>{this.state.channel.display_name}</b>
}}
/>
);
const message = (
<FormattedMessage
id='leave_private_channel_modal.message'
defaultMessage='Are you sure you wish to leave the private channel {channel}? You must be re-invited in order to re-join this channel in the future.'
values={{
channel: <b>{this.state.channel.display_name}</b>
}}
/>
);
const buttonClass = 'btn btn-danger';
const button = (
<FormattedMessage
id='leave_private_channel_modal.leave'
defaultMessage='Yes, leave channel'
/>
);
return (
<ConfirmModal
show={this.state.showLeaveChannelModal}
title={title}
message={message}
confirmButtonClass={buttonClass}
confirmButtonText={button}
onConfirm={() => ChannelActions.leaveChannel(this.state.channel.id)}
onCancel={this.hideLeaveChannelModal}
/>
);
}
getTeammateStatus() {
const channel = this.state.channel;
@@ -961,8 +906,6 @@ export default class Navbar extends React.Component {
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isSystemAdmin, isTeamAdmin, isChannelAdmin, isDirect, isGroup, popoverContent);
const leaveChannelModal = this.createLeaveChannelModal();
return (
<div>
<nav
@@ -979,7 +922,6 @@ export default class Navbar extends React.Component {
</nav>
{editChannelHeaderModal}
{editChannelPurposeModal}
{leaveChannelModal}
{renameChannelModal}
{channelMembersModal}
{quickSwitchModal}

Просмотреть файл

@@ -45,6 +45,7 @@ import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx';
import InviteMemberModal from 'components/invite_member_modal.jsx';
import LeaveTeamModal from 'components/leave_team_modal.jsx';
import ResetStatusModal from 'components/reset_status_modal';
import LeavePrivateChannelModal from 'components/modals/leave_private_channel_modal.jsx';
import iNoBounce from 'inobounce';
import * as UserAgent from 'utils/user_agent.jsx';
@@ -231,6 +232,7 @@ export default class NeedsTeam extends React.Component {
<DeletePostModal/>
<RemovedFromChannelModal/>
<ResetStatusModal/>
<LeavePrivateChannelModal/>
</div>
</div>
);

Просмотреть файл

@@ -44,6 +44,7 @@ class ModalStoreClass extends EventEmitter {
case ActionTypes.TOGGLE_CHANNEL_HEADER_UPDATE_MODAL:
case ActionTypes.TOGGLE_CHANNEL_PURPOSE_UPDATE_MODAL:
case ActionTypes.TOGGLE_CHANNEL_NAME_UPDATE_MODAL:
case ActionTypes.TOGGLE_LEAVE_PRIVATE_CHANNEL_MODAL:
this.emit(type, value, args);
break;
}

Просмотреть файл

@@ -180,6 +180,7 @@ export const ActionTypes = keyMirror({
TOGGLE_CHANNEL_HEADER_UPDATE_MODAL: null,
TOGGLE_CHANNEL_PURPOSE_UPDATE_MODAL: null,
TOGGLE_CHANNEL_NAME_UPDATE_MODAL: null,
TOGGLE_LEAVE_PRIVATE_CHANNEL_MODAL: null,
SUGGESTION_PRETEXT_CHANGED: null,
SUGGESTION_RECEIVED_SUGGESTIONS: null,