summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/command_leave_test.go66
-rw-r--r--app/command_leave.go55
-rw-r--r--i18n/en.json24
-rw-r--r--webapp/actions/channel_actions.jsx33
-rw-r--r--webapp/actions/global_actions.jsx11
-rw-r--r--webapp/components/channel_header.jsx59
-rw-r--r--webapp/components/modals/leave_private_channel_modal.jsx120
-rw-r--r--webapp/components/navbar.jsx62
-rw-r--r--webapp/components/needs_team/needs_team.jsx2
-rw-r--r--webapp/stores/modal_store.jsx1
-rw-r--r--webapp/utils/constants.jsx1
11 files changed, 315 insertions, 119 deletions
diff --git a/api/command_leave_test.go b/api/command_leave_test.go
new file mode 100644
index 000000000..ae098b8a2
--- /dev/null
+++ b/api/command_leave_test.go
@@ -0,0 +1,66 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/mattermost/platform/model"
+)
+
+func TestLeaveCommands(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+ team := th.BasicTeam
+ user2 := th.BasicUser2
+
+ channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+ Client.Must(Client.JoinChannel(channel1.Id))
+
+ channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
+ channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
+ Client.Must(Client.JoinChannel(channel2.Id))
+ Client.Must(Client.AddChannelMember(channel2.Id, user2.Id))
+
+ channel3 := Client.Must(Client.CreateDirectChannel(user2.Id)).Data.(*model.Channel)
+
+ rs1 := Client.Must(Client.Command(channel1.Id, "/leave")).Data.(*model.CommandResponse)
+ if !strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+model.DEFAULT_CHANNEL) {
+ t.Fatal("failed to leave open channel 1")
+ }
+
+ rs2 := Client.Must(Client.Command(channel2.Id, "/leave")).Data.(*model.CommandResponse)
+ if !strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+model.DEFAULT_CHANNEL) {
+ t.Fatal("failed to leave private channel 1")
+ }
+
+ rs3 := Client.Must(Client.Command(channel3.Id, "/leave")).Data.(*model.CommandResponse)
+ if strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+model.DEFAULT_CHANNEL) {
+ t.Fatal("should not have left direct message channel")
+ }
+
+ cdata := Client.Must(Client.GetChannels("")).Data.(*model.ChannelList)
+
+ found := false
+ for _, c := range *cdata {
+ if c.Id == channel1.Id || c.Id == channel2.Id {
+ found = true
+ }
+ }
+
+ if found {
+ t.Fatal("did not leave right channels")
+ }
+
+ for _, c := range *cdata {
+ if c.Name == model.DEFAULT_CHANNEL {
+ if _, err := Client.LeaveChannel(c.Id); err == nil {
+ t.Fatal("should have errored on leaving default channel")
+ }
+ break
+ }
+ }
+}
diff --git a/app/command_leave.go b/app/command_leave.go
new file mode 100644
index 000000000..963172e86
--- /dev/null
+++ b/app/command_leave.go
@@ -0,0 +1,55 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+ goi18n "github.com/nicksnyder/go-i18n/i18n"
+)
+
+type LeaveProvider struct {
+}
+
+const (
+ CMD_LEAVE = "leave"
+)
+
+func init() {
+ RegisterCommandProvider(&LeaveProvider{})
+}
+
+func (me *LeaveProvider) GetTrigger() string {
+ return CMD_LEAVE
+}
+
+func (me *LeaveProvider) GetCommand(T goi18n.TranslateFunc) *model.Command {
+ return &model.Command{
+ Trigger: CMD_LEAVE,
+ AutoComplete: true,
+ AutoCompleteDesc: T("api.command_leave.desc"),
+ DisplayName: T("api.command_leave.name"),
+ }
+}
+
+func (me *LeaveProvider) DoCommand(args *model.CommandArgs, message string) *model.CommandResponse {
+ var channel *model.Channel
+ var noChannelErr *model.AppError
+ if channel, noChannelErr = GetChannel(args.ChannelId); noChannelErr != nil {
+ return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+ if channel.Name == model.DEFAULT_CHANNEL {
+ return &model.CommandResponse{Text: args.T("api.channel.leave.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+ err := LeaveChannel(args.ChannelId, args.UserId)
+ if err != nil {
+ return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ team, err := GetTeam(args.TeamId)
+ if err != nil {
+ return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + model.DEFAULT_CHANNEL, Text: args.T("api.command_leave.success"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+}
diff --git a/i18n/en.json b/i18n/en.json
index c4bc9652a..abbf73699 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -692,6 +692,30 @@
"translation": "Joined channel."
},
{
+ "id": "api.command_leave.desc",
+ "translation": "Leave the current channel"
+ },
+ {
+ "id": "api.command_leave.fail.app_error",
+ "translation": "An error occurred while leaving the channel."
+ },
+ {
+ "id": "api.command_leave.list.app_error",
+ "translation": "An error occurred while listing channels."
+ },
+ {
+ "id": "api.command_leave.missing.app_error",
+ "translation": "We couldn't find the channel."
+ },
+ {
+ "id": "api.command_leave.name",
+ "translation": "leave"
+ },
+ {
+ "id": "api.command_leave.success",
+ "translation": "Left the channel."
+ },
+ {
"id": "api.command_logout.desc",
"translation": "Logout of Mattermost"
},
diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx
index 39dc37591..78df1ff17 100644
--- a/webapp/actions/channel_actions.jsx
+++ b/webapp/actions/channel_actions.jsx
@@ -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;
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index f464483cf..a163db126 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -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: {}
};
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 10e568794..bee06c700 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -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}
diff --git a/webapp/components/modals/leave_private_channel_modal.jsx b/webapp/components/modals/leave_private_channel_modal.jsx
new file mode 100644
index 000000000..9b8e6df83
--- /dev/null
+++ b/webapp/components/modals/leave_private_channel_modal.jsx
@@ -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);
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 81959f352..0217dc15c 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -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}
diff --git a/webapp/components/needs_team/needs_team.jsx b/webapp/components/needs_team/needs_team.jsx
index e86e4fb11..7b8630f40 100644
--- a/webapp/components/needs_team/needs_team.jsx
+++ b/webapp/components/needs_team/needs_team.jsx
@@ -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>
);
diff --git a/webapp/stores/modal_store.jsx b/webapp/stores/modal_store.jsx
index 34bab780c..66be37b25 100644
--- a/webapp/stores/modal_store.jsx
+++ b/webapp/stores/modal_store.jsx
@@ -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;
}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index e9246fdaf..60151fc8e 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -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,