diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx new file mode 100644 index 0000000000..4194e7b53f --- /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 ( + + + + + + + + + + + + + + + + + + + + ); + } +} + +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 ee199fc036..cfda38670d 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 = ( + + ); } var collapseButtons = this.createCollapseButtons(currentId); @@ -574,6 +603,7 @@ export default class Navbar extends React.Component { {editChannelPurposeModal} {renameChannelModal} {channelMembersModal} + {channelSwitchModal} ); } diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx index f81cc67654..0ed9449ed2 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 (
{textbox} - +
); } @@ -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 7774f9a7dd..134e7a8d4d 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 ( -
+
{items}
@@ -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 0000000000..b52cd7fe96 --- /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 ( +
+ {displayName} +
+ ); + } +} + +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 e86b893c51..3faacf8c51 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 b34e925304..b65ec330c4 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 f5111d72da..73538c26b4 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