summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/components/channel_switch_modal.jsx153
-rw-r--r--webapp/components/navbar.jsx30
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx9
-rw-r--r--webapp/components/suggestion/suggestion_list.jsx10
-rw-r--r--webapp/components/suggestion/switch_channel_provider.jsx51
-rw-r--r--webapp/i18n/en.json4
-rw-r--r--webapp/stores/channel_store.jsx4
-rw-r--r--webapp/utils/post_utils.jsx2
8 files changed, 257 insertions, 6 deletions
diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx
new file mode 100644
index 000000000..4194e7b53
--- /dev/null
+++ b/webapp/components/channel_switch_modal.jsx
@@ -0,0 +1,153 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import SuggestionList from './suggestion/suggestion_list.jsx';
+import SuggestionBox from './suggestion/suggestion_box.jsx';
+import SwitchChannelProvider from './suggestion/switch_channel_provider.jsx';
+import {FormattedMessage} from 'react-intl';
+import {Modal} from 'react-bootstrap';
+import * as Utils from 'utils/utils.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import Constants from 'utils/constants.jsx';
+import * as ChannelActions from 'actions/channel_actions.jsx';
+import React from 'react';
+
+export default class SwitchChannelModal extends React.Component {
+ constructor() {
+ super();
+
+ this.onUserInput = this.onUserInput.bind(this);
+ this.onShow = this.onShow.bind(this);
+ this.onHide = this.onHide.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.suggestionProviders = [new SwitchChannelProvider()];
+
+ this.state = {
+ text: '',
+ error: ''
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.show && !prevProps.show) {
+ const textbox = this.refs.search.getTextbox();
+ textbox.focus();
+ Utils.placeCaretAtEnd(textbox);
+ }
+ }
+
+ onShow() {
+ this.setState({
+ text: '',
+ error: ''
+ });
+ }
+
+ onHide() {
+ this.setState({
+ text: '',
+ error: ''
+ });
+ this.props.onHide();
+ }
+
+ onUserInput(message) {
+ this.setState({text: message});
+ }
+
+ handleKeyDown(e) {
+ this.setState({
+ error: ''
+ });
+ if (e.keyCode === Constants.KeyCodes.ENTER) {
+ this.handleSubmit();
+ }
+ }
+
+ handleSubmit() {
+ const channel = ChannelStore.getByName(this.state.text.trim());
+ if (channel !== null && channel.name === this.state.text.trim() && channel.type !== Constants.DM_CHANNEL) {
+ ChannelActions.goToChannel(channel);
+ this.onHide();
+ } else if (this.state.text !== '') {
+ this.setState({
+ error: Utils.localizeMessage('channel_switch_modal.not_found', 'No matches found.')
+ });
+ }
+ }
+
+ render() {
+ let message = this.state.error;
+ return (
+ <Modal
+ className='modal-browse-channel'
+ ref='modal'
+ show={this.props.show}
+ onHide={this.onHide}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>
+ <span>
+ <FormattedMessage
+ id='channel_switch_modal.title'
+ defaultMessage='Switch Channels'
+ />
+ </span>
+ </Modal.Title>
+ </Modal.Header>
+
+ <Modal.Body>
+ <FormattedMessage
+ id='channel_switch_modal.help'
+ defaultMessage='↑↓ to browse, TAB to select, ↵ to confirm, ESC to dismiss'
+ />
+ <SuggestionBox
+ ref='search'
+ className='form-control focused'
+ type='input'
+ onUserInput={this.onUserInput}
+ value={this.state.text}
+ onKeyDown={this.handleKeyDown}
+ listComponent={SuggestionList}
+ maxLength='64'
+ providers={this.suggestionProviders}
+ preventDefaultSubmit={false}
+ listStyle='bottom'
+ />
+ </Modal.Body>
+ <Modal.Footer>
+ <label className='control-label'>
+ {message}
+ </label>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.onHide}
+ >
+ <FormattedMessage
+ id='edit_channel_header_modal.cancel'
+ defaultMessage='Cancel'
+ />
+ </button>
+ <button
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handleSubmit}
+ >
+ <FormattedMessage
+ id='channel_switch_modal.submit'
+ defaultMessage='Switch'
+ />
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+SwitchChannelModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired
+};
+
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index ee199fc03..cfda38670 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -18,6 +18,8 @@ import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
+import ChannelSwitchModal from './channel_switch_modal.jsx';
+
import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -50,11 +52,15 @@ export default class Navbar extends React.Component {
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
+ this.showChannelSwitchModal = this.showChannelSwitchModal.bind(this);
+ this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
+
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
state.showEditChannelHeaderModal = false;
state.showMembersModal = false;
state.showRenameChannelModal = false;
+ state.showChannelSwitchModal = false;
this.state = state;
}
getStateFromStores() {
@@ -72,10 +78,12 @@ export default class Navbar extends React.Component {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
$('.inner-wrap').click(this.hideSidebars);
+ document.addEventListener('keydown', this.showChannelSwitchModal);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
ChannelStore.removeExtraInfoChangeListener(this.onChange);
+ document.removeEventListener('keydown', this.showChannelSwitchModal);
}
handleSubmit(e) {
e.preventDefault();
@@ -150,6 +158,19 @@ export default class Navbar extends React.Component {
showRenameChannelModal: false
});
}
+ showChannelSwitchModal(e) {
+ if ((e.ctrlKey || e.metaKey) && e.keyCode === Constants.KeyCodes.K) {
+ e.preventDefault();
+ this.setState({
+ showChannelSwitchModal: true
+ });
+ }
+ }
+ hideChannelSwitchModal() {
+ this.setState({
+ showChannelSwitchModal: false
+ });
+ }
createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
if (channel) {
var viewInfoOption = (
@@ -441,6 +462,7 @@ export default class Navbar extends React.Component {
var editChannelPurposeModal = null;
let renameChannelModal = null;
let channelMembersModal = null;
+ let channelSwitchModal = null;
if (channel) {
popoverContent = (
@@ -540,6 +562,13 @@ export default class Navbar extends React.Component {
channel={channel}
/>
);
+
+ channelSwitchModal = (
+ <ChannelSwitchModal
+ show={this.state.showChannelSwitchModal}
+ onHide={this.hideChannelSwitchModal}
+ />
+ );
}
var collapseButtons = this.createCollapseButtons(currentId);
@@ -574,6 +603,7 @@ export default class Navbar extends React.Component {
{editChannelPurposeModal}
{renameChannelModal}
{channelMembersModal}
+ {channelSwitchModal}
</div>
);
}
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index f81cc6765..0ed9449ed 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -176,7 +176,10 @@ export default class SuggestionBox extends React.Component {
return (
<div>
{textbox}
- <SuggestionListComponent suggestionId={this.suggestionId}/>
+ <SuggestionListComponent
+ suggestionId={this.suggestionId}
+ location={this.props.listStyle}
+ />
</div>
);
}
@@ -197,7 +200,8 @@ export default class SuggestionBox extends React.Component {
}
SuggestionBox.defaultProps = {
- type: 'input'
+ type: 'input',
+ listStyle: 'top'
};
SuggestionBox.propTypes = {
@@ -206,6 +210,7 @@ SuggestionBox.propTypes = {
value: React.PropTypes.string.isRequired,
onUserInput: React.PropTypes.func,
providers: React.PropTypes.arrayOf(React.PropTypes.object),
+ listStyle: React.PropTypes.string,
// explicitly name any input event handlers we override and need to manually call
onChange: React.PropTypes.func,
diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx
index 7774f9a7d..134e7a8d4 100644
--- a/webapp/components/suggestion/suggestion_list.jsx
+++ b/webapp/components/suggestion/suggestion_list.jsx
@@ -117,11 +117,14 @@ export default class SuggestionList extends React.Component {
);
}
+ const mainClass = 'suggestion-list suggestion-list--' + this.props.location;
+ const contentClass = 'suggestion-list__content suggestion-list__content--' + this.props.location;
+
return (
- <div className='suggestion-list suggestion-list--top'>
+ <div className={mainClass}>
<div
ref='content'
- className='suggestion-list__content suggestion-list__content--top'
+ className={contentClass}
>
{items}
</div>
@@ -131,5 +134,6 @@ export default class SuggestionList extends React.Component {
}
SuggestionList.propTypes = {
- suggestionId: React.PropTypes.string.isRequired
+ suggestionId: React.PropTypes.string.isRequired,
+ location: React.PropTypes.string
};
diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx
new file mode 100644
index 000000000..b52cd7fe9
--- /dev/null
+++ b/webapp/components/suggestion/switch_channel_provider.jsx
@@ -0,0 +1,51 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import SuggestionStore from 'stores/suggestion_store.jsx';
+import Suggestion from './suggestion.jsx';
+
+class SwitchChannelSuggestion extends Suggestion {
+ render() {
+ const {item, isSelection} = this.props;
+
+ let className = 'mentions__name';
+ if (isSelection) {
+ className += ' suggestion--selected';
+ }
+
+ const displayName = item.display_name + ' (' + item.name + ')';
+
+ return (
+ <div
+ onClick={this.handleClick}
+ className={className}
+ >
+ {displayName}
+ </div>
+ );
+ }
+}
+
+export default class SwitchChannelProvider {
+ handlePretextChanged(suggestionId, channelPrefix) {
+ if (channelPrefix) {
+ const allChannels = ChannelStore.getAll();
+ const channels = [];
+
+ for (const id of Object.keys(allChannels)) {
+ const channel = allChannels[id];
+ if (channel.display_name.toLowerCase().startsWith(channelPrefix.toLowerCase())) {
+ channels.push(channel);
+ }
+ }
+
+ channels.sort((a, b) => a.display_name.localeCompare(b.display_name));
+ const channelNames = channels.map((channel) => channel.name);
+
+ SuggestionStore.addSuggestions(suggestionId, channelNames, channels, SwitchChannelSuggestion, channelPrefix);
+ }
+ }
+}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index e86b893c5..3faacf8c5 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -715,6 +715,10 @@
"channel_notifications.sendDesktop": "Send desktop notifications",
"channel_notifications.unreadInfo": "The channel name is bolded in the sidebar when there are unread messages. Selecting \"Only for mentions\" will bold the channel only when you are mentioned.",
"channel_select.placeholder": "--- Select a channel ---",
+ "channel_switch_modal.title": "Switch Channels",
+ "channel_switch_modal.help": "↑↓ to browse, TAB to select, ↵ to confirm, ESC to dismiss",
+ "channel_switch_modal.submit": "Switch",
+ "channel_switch_modal.not_found": "No matches found.",
"choose_auth_page.emailCreate": "Create new team with email address",
"choose_auth_page.find": "Find my teams",
"choose_auth_page.gitlabCreate": "Create new team with GitLab Account",
diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx
index b34e92530..b65ec330c 100644
--- a/webapp/stores/channel_store.jsx
+++ b/webapp/stores/channel_store.jsx
@@ -36,6 +36,7 @@ class ChannelStoreClass extends EventEmitter {
this.get = this.get.bind(this);
this.getMember = this.getMember.bind(this);
this.getByName = this.getByName.bind(this);
+ this.getByDisplayName = this.getByDisplayName.bind(this);
this.setPostMode = this.setPostMode.bind(this);
this.getPostMode = this.getPostMode.bind(this);
this.setUnreadCount = this.setUnreadCount.bind(this);
@@ -118,6 +119,9 @@ class ChannelStoreClass extends EventEmitter {
getByName(name) {
return this.findFirstBy('name', name);
}
+ getByDisplayName(displayName) {
+ return this.findFirstBy('display_name', displayName);
+ }
getMoreByName(name) {
return this.findFirstMoreBy('name', name);
}
diff --git a/webapp/utils/post_utils.jsx b/webapp/utils/post_utils.jsx
index f5111d72d..73538c26b 100644
--- a/webapp/utils/post_utils.jsx
+++ b/webapp/utils/post_utils.jsx
@@ -29,4 +29,4 @@ export function getProfilePicSrcForPost(post, timestamp) {
}
return src;
-}
+} \ No newline at end of file