diff options
Diffstat (limited to 'webapp')
-rw-r--r-- | webapp/components/channel_switch_modal.jsx | 153 | ||||
-rw-r--r-- | webapp/components/navbar.jsx | 30 | ||||
-rw-r--r-- | webapp/components/suggestion/suggestion_box.jsx | 9 | ||||
-rw-r--r-- | webapp/components/suggestion/suggestion_list.jsx | 10 | ||||
-rw-r--r-- | webapp/components/suggestion/switch_channel_provider.jsx | 51 | ||||
-rw-r--r-- | webapp/i18n/en.json | 4 | ||||
-rw-r--r-- | webapp/stores/channel_store.jsx | 4 | ||||
-rw-r--r-- | webapp/utils/post_utils.jsx | 2 |
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 |