diff options
Diffstat (limited to 'webapp')
21 files changed, 894 insertions, 344 deletions
diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx index b1b0f0276..bad8dffca 100644 --- a/webapp/components/channel_select.jsx +++ b/webapp/components/channel_select.jsx @@ -1,9 +1,8 @@ -import PropTypes from 'prop-types'; - // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import React from 'react'; +import PropTypes from 'prop-types'; import Constants from 'utils/constants.jsx'; import ChannelStore from 'stores/channel_store.jsx'; diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx deleted file mode 100644 index 0f1fc4252..000000000 --- a/webapp/components/channel_switch_modal.jsx +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) 2016-present 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 {goToChannel, openDirectChannelToUser} from 'actions/channel_actions.jsx'; - -import ChannelStore from 'stores/channel_store.jsx'; -import UserStore from 'stores/user_store.jsx'; - -import Constants from 'utils/constants.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import PropTypes from 'prop-types'; - -import React from 'react'; -import $ from 'jquery'; - -export default class SwitchChannelModal extends React.Component { - constructor() { - super(); - - this.onChange = this.onChange.bind(this); - this.onItemSelected = this.onItemSelected.bind(this); - this.onShow = this.onShow.bind(this); - this.onHide = this.onHide.bind(this); - this.onExited = this.onExited.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.switchToChannel = this.switchToChannel.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(); - } - - onExited() { - this.selected = null; - setTimeout(() => { - $('#post_textbox').get(0).focus(); - }); - } - - onChange(e) { - this.setState({text: e.target.value}); - this.selected = null; - } - - onItemSelected(item) { - this.selected = item; - } - - handleKeyDown(e) { - this.setState({ - error: '' - }); - if (e.keyCode === Constants.KeyCodes.ENTER) { - this.handleSubmit(); - } - } - - handleSubmit() { - let channel = null; - - if (!this.selected) { - if (this.state.text !== '') { - this.setState({ - error: Utils.localizeMessage('channel_switch_modal.not_found', 'No matches found.') - }); - } - return; - } - - if (this.selected.type === Constants.DM_CHANNEL) { - const user = UserStore.getProfileByUsername(this.selected.name); - - if (user) { - openDirectChannelToUser( - user.id, - (ch) => { - channel = ch; - this.switchToChannel(channel); - }, - () => { - channel = null; - this.switchToChannel(channel); - } - ); - } - } else { - channel = ChannelStore.get(this.selected.id); - this.switchToChannel(channel); - } - } - - switchToChannel(channel) { - if (channel !== null) { - goToChannel(channel); - this.onHide(); - } else if (this.state.text !== '') { - this.setState({ - error: Utils.localizeMessage('channel_switch_modal.failed_to_open', 'Failed to open channel.') - }); - } - } - - render() { - const message = this.state.error; - return ( - <Modal - dialogClassName='channel-switch-modal modal--overflow' - ref='modal' - show={this.props.show} - onHide={this.onHide} - onExited={this.onExited} - > - <Modal.Header closeButton={true}> - <Modal.Title> - <span> - <FormattedMessage - id='channel_switch_modal.title' - defaultMessage='Switch Channels' - /> - </span> - </Modal.Title> - </Modal.Header> - - <Modal.Body> - <div className='modal__hint'> - <FormattedMessage - id='channel_switch_modal.help' - defaultMessage='Type channel name. Use ↑↓ to browse, TAB to select, ↵ to confirm, ESC to dismiss' - /> - </div> - <SuggestionBox - ref='search' - className='form-control focused' - type='input' - onChange={this.onChange} - value={this.state.text} - onKeyDown={this.handleKeyDown} - onItemSelected={this.onItemSelected} - listComponent={SuggestionList} - maxLength='64' - providers={this.suggestionProviders} - listStyle='bottom' - /> - </Modal.Body> - <Modal.Footer> - <div className='modal__error'> - {message} - </div> - <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: PropTypes.bool.isRequired, - onHide: PropTypes.func.isRequired -}; - diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index d06b2dd57..948649b45 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -21,8 +21,9 @@ import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import SearchStore from 'stores/search_store.jsx'; +import ModalStore from 'stores/modal_store.jsx'; -import ChannelSwitchModal from './channel_switch_modal.jsx'; +import QuickSwitchModal from 'components/quick_switch_modal'; import * as Utils from 'utils/utils.jsx'; import * as ChannelUtils from 'utils/channel_utils.jsx'; @@ -44,6 +45,8 @@ import {Link} from 'react-router/es6'; import PropTypes from 'prop-types'; import React from 'react'; +import store from 'stores/redux_store.jsx'; +import {getMyTeams} from 'mattermost-redux/selectors/entities/teams'; export default class Navbar extends React.Component { constructor(props) { @@ -64,8 +67,9 @@ export default class Navbar extends React.Component { this.showMembersModal = this.showMembersModal.bind(this); this.hideMembersModal = this.hideMembersModal.bind(this); - this.showChannelSwitchModal = this.showChannelSwitchModal.bind(this); - this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this); + this.toggleQuickSwitchModal = this.toggleQuickSwitchModal.bind(this); + this.hideQuickSwitchModal = this.hideQuickSwitchModal.bind(this); + this.handleQuickSwitchKeyPress = this.handleQuickSwitchKeyPress.bind(this); this.openDirectMessageModal = this.openDirectMessageModal.bind(this); this.getPinnedPosts = this.getPinnedPosts.bind(this); @@ -78,7 +82,8 @@ export default class Navbar extends React.Component { state.showEditChannelHeaderModal = false; state.showMembersModal = false; state.showRenameChannelModal = false; - state.showChannelSwitchModal = false; + state.showQuickSwitchModal = false; + state.quickSwitchMode = 'channel'; this.state = state; } @@ -106,8 +111,9 @@ export default class Navbar extends React.Component { UserStore.addStatusesChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); PreferenceStore.addChangeListener(this.onChange); + ModalStore.addModalListener(ActionTypes.TOGGLE_QUICK_SWITCH_MODAL, this.toggleQuickSwitchModal); $('.inner-wrap').click(this.hideSidebars); - document.addEventListener('keydown', this.showChannelSwitchModal); + document.addEventListener('keydown', this.handleQuickSwitchKeyPress); } componentWillUnmount() { @@ -116,7 +122,8 @@ export default class Navbar extends React.Component { UserStore.removeStatusesChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange); PreferenceStore.removeChangeListener(this.onChange); - document.removeEventListener('keydown', this.showChannelSwitchModal); + ModalStore.removeModalListener(ActionTypes.TOGGLE_QUICK_SWITCH_MODAL, this.toggleQuickSwitchModal); + document.removeEventListener('keydown', this.handleQuickSwitchKeyPress); } handleSubmit(e) { @@ -212,16 +219,32 @@ export default class Navbar extends React.Component { this.setState({showMembersModal: false}); } - showChannelSwitchModal(e) { - if (Utils.cmdOrCtrlPressed(e) && e.keyCode === Constants.KeyCodes.K) { + handleQuickSwitchKeyPress(e) { + if (Utils.cmdOrCtrlPressed(e, true) && e.keyCode === Constants.KeyCodes.K) { e.preventDefault(); - this.setState({showChannelSwitchModal: !this.state.showChannelSwitchModal}); + if (e.altKey) { + if (getMyTeams(store.getState()).length <= 1) { + return; + } + this.toggleQuickSwitchModal('team'); + } else { + this.toggleQuickSwitchModal('channel'); + } + } + } + + toggleQuickSwitchModal(mode = 'channel') { + if (this.state.showQuickSwitchModal) { + this.setState({showQuickSwitchModal: false, quickSwitchMode: 'channel'}); + } else { + this.setState({showQuickSwitchModal: true, quickSwitchMode: mode}); } } - hideChannelSwitchModal() { + hideQuickSwitchModal() { this.setState({ - showChannelSwitchModal: false + showQuickSwitchModal: false, + quickSwitchMode: 'channel' }); } @@ -770,7 +793,7 @@ export default class Navbar extends React.Component { var editChannelPurposeModal = null; let renameChannelModal = null; let channelMembersModal = null; - let channelSwitchModal = null; + let quickSwitchModal = null; if (channel) { popoverContent = ( @@ -883,10 +906,11 @@ export default class Navbar extends React.Component { ); } - channelSwitchModal = ( - <ChannelSwitchModal - show={this.state.showChannelSwitchModal} - onHide={this.hideChannelSwitchModal} + quickSwitchModal = ( + <QuickSwitchModal + show={this.state.showQuickSwitchModal} + onHide={this.hideQuickSwitchModal} + initialMode={this.state.quickSwitchMode} /> ); } @@ -926,7 +950,7 @@ export default class Navbar extends React.Component { {leaveChannelModal} {renameChannelModal} {channelMembersModal} - {channelSwitchModal} + {quickSwitchModal} </div> ); } diff --git a/webapp/components/quick_switch_modal/index.js b/webapp/components/quick_switch_modal/index.js new file mode 100644 index 000000000..7826fd8f5 --- /dev/null +++ b/webapp/components/quick_switch_modal/index.js @@ -0,0 +1,16 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {getMyTeams} from 'mattermost-redux/selectors/entities/teams'; + +import QuickSwitchModal from './quick_switch_modal.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps, + showTeamSwitcher: getMyTeams(state).length > 1 + }; +} + +export default connect(mapStateToProps)(QuickSwitchModal); diff --git a/webapp/components/quick_switch_modal/quick_switch_modal.jsx b/webapp/components/quick_switch_modal/quick_switch_modal.jsx new file mode 100644 index 000000000..c3095caf9 --- /dev/null +++ b/webapp/components/quick_switch_modal/quick_switch_modal.jsx @@ -0,0 +1,322 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import SuggestionList from 'components/suggestion/suggestion_list.jsx'; +import SuggestionBox from 'components/suggestion/suggestion_box.jsx'; +import SwitchChannelProvider from 'components/suggestion/switch_channel_provider.jsx'; +import SwitchTeamProvider from 'components/suggestion/switch_team_provider.jsx'; + +import {goToChannel, openDirectChannelToUser} from 'actions/channel_actions.jsx'; + +import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import {browserHistory} from 'react-router/es6'; +import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; + +// Redux actions +import store from 'stores/redux_store.jsx'; +const getState = store.getState; + +import {getChannel} from 'mattermost-redux/selectors/entities/channels'; +import {getUserByUsername} from 'mattermost-redux/selectors/entities/users'; + +const CHANNEL_MODE = 'channel'; +const TEAM_MODE = 'team'; + +export default class QuickSwitchModal extends React.PureComponent { + static propTypes = { + + /** + * The mode to start in when showing the modal, either 'channel' or 'team' + */ + initialMode: PropTypes.string.isRequired, + + /** + * Set to show the modal + */ + show: PropTypes.bool.isRequired, + + /** + * The function called to hide the modal + */ + onHide: PropTypes.func.isRequired, + + /** + * Set to show team switcher + */ + showTeamSwitcher: PropTypes.bool + } + + static defaultProps = { + initialMode: CHANNEL_MODE + } + + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + this.onShow = this.onShow.bind(this); + this.onHide = this.onHide.bind(this); + this.onExited = this.onExited.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.switchToChannel = this.switchToChannel.bind(this); + this.switchMode = this.switchMode.bind(this); + this.focusTextbox = this.focusTextbox.bind(this); + + this.enableChannelProvider = this.enableChannelProvider.bind(this); + this.enableTeamProvider = this.enableTeamProvider.bind(this); + this.channelProviders = [new SwitchChannelProvider()]; + this.teamProviders = [new SwitchTeamProvider()]; + + this.state = { + text: '', + mode: props.initialMode + }; + } + + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.focusTextbox(); + } + } + + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + this.setState({mode: nextProps.initialMode, text: ''}); + } + } + + focusTextbox() { + if (this.refs.switchbox == null) { + return; + } + + const textbox = this.refs.switchbox.getTextbox(); + textbox.focus(); + Utils.placeCaretAtEnd(textbox); + } + + onShow() { + this.setState({ + text: '' + }); + } + + onHide() { + this.setState({ + text: '' + }); + this.props.onHide(); + } + + onExited() { + setTimeout(() => { + document.querySelector('#post_textbox').focus(); + }); + } + + onChange(e) { + this.setState({text: e.target.value}); + } + + handleKeyDown(e) { + if (e.keyCode === Constants.KeyCodes.TAB) { + e.preventDefault(); + this.switchMode(); + } + } + + handleSubmit(selected) { + let channel = null; + + if (!selected) { + return; + } + + if (this.state.mode === CHANNEL_MODE) { + const selectedChannel = selected.channel; + if (selectedChannel.type === Constants.DM_CHANNEL) { + const user = getUserByUsername(getState(), selectedChannel.name); + + if (user) { + openDirectChannelToUser( + user.id, + (ch) => { + channel = ch; + this.switchToChannel(channel); + }, + () => { + channel = null; + this.switchToChannel(channel); + } + ); + } + } else { + channel = getChannel(getState(), selectedChannel.id); + this.switchToChannel(channel); + } + } else { + browserHistory.push('/' + selected.name); + this.onHide(); + } + } + + switchToChannel(channel) { + if (channel != null) { + goToChannel(channel); + this.onHide(); + } + } + + enableChannelProvider() { + this.channelProviders[0].disableDispatches = false; + this.teamProviders[0].disableDispatches = true; + } + + enableTeamProvider() { + this.teamProviders[0].disableDispatches = false; + this.channelProviders[0].disableDispatches = true; + } + + switchMode() { + if (this.state.mode === CHANNEL_MODE && this.props.showTeamSwitcher) { + this.enableTeamProvider(); + this.setState({mode: TEAM_MODE}); + } else if (this.state.mode === TEAM_MODE) { + this.enableChannelProvider(); + this.setState({mode: CHANNEL_MODE}); + } + } + + render() { + let providers = this.channelProviders; + let header; + let renderDividers = true; + + let channelShortcut = 'quick_switch_modal.channelsShortcut.windows'; + if (Utils.isMac()) { + channelShortcut = 'quick_switch_modal.channelsShortcut.mac'; + } + + let teamShortcut = 'quick_switch_modal.teamsShortcut.windows'; + if (Utils.isMac()) { + teamShortcut = 'quick_switch_modal.teamsShortcut.mac'; + } + + if (this.props.showTeamSwitcher) { + let channelsActiveClass = ''; + let teamsActiveClass = ''; + if (this.state.mode === TEAM_MODE) { + providers = this.teamProviders; + renderDividers = false; + teamsActiveClass = 'active'; + } else { + channelsActiveClass = 'active'; + } + + header = ( + <div className='nav nav-tabs'> + <li className={channelsActiveClass}> + <a + href='#' + onClick={(e) => { + e.preventDefault(); + this.enableChannelProvider(); + this.setState({mode: 'channel'}); + this.focusTextbox(); + }} + > + <FormattedMessage + id='quick_switch_modal.channels' + defaultMessage='Channels' + /> + <span className='small'> + <FormattedMessage + id={channelShortcut} + defaultMessage='CTRL+K' + /> + </span> + </a> + </li> + <li className={teamsActiveClass}> + <a + href='#' + onClick={(e) => { + e.preventDefault(); + this.enableTeamProvider(); + this.setState({mode: 'team'}); + this.focusTextbox(); + }} + > + <FormattedMessage + id='quick_switch_modal.teams' + defaultMessage='Teams' + /> + <span className='small'> + <FormattedMessage + id={teamShortcut} + defaultMessage='CTRL+ALT+K' + /> + </span> + </a> + </li> + </div> + ); + } + + let help; + if (this.props.showTeamSwitcher) { + help = ( + <FormattedMessage + id='quick_switch_modal.help' + defaultMessage='Use TAB to toggle between teams/channels, ↑↓ to browse, ↵ to confirm, ESC to dismiss' + /> + ); + } else { + help = ( + <FormattedMessage + id='quick_switch_modal.help_no_team' + defaultMessage='Type a channel name. Use ↑↓ to browse, ↵ to confirm, ESC to dismiss' + /> + ); + } + + return ( + <Modal + dialogClassName='channel-switch-modal modal--overflow' + ref='modal' + show={this.props.show} + onHide={this.onHide} + onExited={this.onExited} + > + <Modal.Header closeButton={true}/> + <Modal.Body> + {header} + <div className='modal__hint'> + {help} + </div> + <SuggestionBox + ref='switchbox' + className='form-control focused' + type='input' + onChange={this.onChange} + value={this.state.text} + onKeyDown={this.handleKeyDown} + onItemSelected={this.handleSubmit} + listComponent={SuggestionList} + maxLength='64' + providers={providers} + listStyle='bottom' + completeOnTab={false} + renderDividers={renderDividers} + /> + </Modal.Body> + </Modal> + ); + } +} diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index f8b3616f3..a802c2f4f 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -17,6 +17,7 @@ import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import ModalStore from 'stores/modal_store.jsx'; +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import {sortTeamsByDisplayName} from 'utils/team_utils.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -424,6 +425,13 @@ export default class Sidebar extends React.Component { } } + openQuickSwitcher(e) { + e.preventDefault(); + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_QUICK_SWITCH_MODAL + }); + } + createTutorialTip() { const screens = []; @@ -790,6 +798,11 @@ export default class Sidebar extends React.Component { ); } + let quickSwitchText = 'sidebar.switch_channels'; + if (Utils.isMac()) { + quickSwitchText += '.mac'; + } + return ( <div className='sidebar--left' @@ -890,6 +903,18 @@ export default class Sidebar extends React.Component { {directMessageMore} </ul> </div> + <div style={{height: '20px', width: '100%'}}> + <a + href='#' + className='sidebar__switcher' + onClick={this.openQuickSwitcher} + > + <FormattedMessage + id={quickSwitchText} + defaultMessage='Switch Channels (CTRL + K)' + /> + </a> + </div> </div> ); } diff --git a/webapp/components/suggestion/provider.jsx b/webapp/components/suggestion/provider.jsx index 39bb135a8..a5b54fb26 100644 --- a/webapp/components/suggestion/provider.jsx +++ b/webapp/components/suggestion/provider.jsx @@ -7,6 +7,7 @@ export default class Provider { constructor() { this.latestPrefix = ''; this.latestComplete = true; + this.disableDispatches = false; } handlePretextChanged(suggestionId, pretext) { // eslint-disable-line no-unused-vars @@ -22,6 +23,10 @@ export default class Provider { } shouldCancelDispatch(prefix) { + if (this.disableDispatches) { + return true; + } + if (prefix === this.latestPrefix) { this.latestComplete = true; } else if (this.latestComplete) { diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx index 1915b22b7..e1de927b9 100644 --- a/webapp/components/suggestion/suggestion_box.jsx +++ b/webapp/components/suggestion/suggestion_box.jsx @@ -15,6 +15,71 @@ import PropTypes from 'prop-types'; import React from 'react'; export default class SuggestionBox extends React.Component { + static propTypes = { + + /** + * The list component to render, usually SuggestionList + */ + listComponent: PropTypes.func.isRequired, + + /** + * The HTML input box type + */ + type: PropTypes.oneOf(['input', 'textarea', 'search']).isRequired, + + /** + * The value of in the input + */ + value: PropTypes.string.isRequired, + + /** + * Array of suggestion providers + */ + providers: PropTypes.arrayOf(PropTypes.object), + + /** + * Where the list will be displayed relative to the input box, defaults to 'top' + */ + listStyle: PropTypes.string, + + /** + * Set to true to draw dividers between types of list items, defaults to false + */ + renderDividers: PropTypes.bool, + + /** + * Set to allow TAB to select an item in the list, defaults to true + */ + completeOnTab: PropTypes.bool, + + /** + * Function called when input box loses focus + */ + onBlur: PropTypes.func, + + /** + * Function called when input box value changes + */ + onChange: PropTypes.func, + + /** + * Function called when a key is pressed and the input box is in focus + */ + onKeyDown: PropTypes.func, + + /** + * Function called when an item is selected + */ + onItemSelected: PropTypes.func + } + + static defaultProps = { + type: 'input', + listStyle: 'top', + renderDividers: false, + completeOnTab: true + } + constructor(props) { super(props); @@ -46,6 +111,14 @@ export default class SuggestionBox extends React.Component { SuggestionStore.unregisterSuggestionBox(this.suggestionId); } + componentDidUpdate(prevProps) { + if (this.props.providers !== prevProps.providers) { + const textbox = this.getTextbox(); + const pretext = textbox.value.substring(0, textbox.selectionEnd); + GlobalActions.emitSuggestionPretextChanged(this.suggestionId, pretext); + } + } + getTextbox() { if (this.props.type === 'textarea') { return this.refs.textbox.getDOMNode(); @@ -171,7 +244,7 @@ export default class SuggestionBox extends React.Component { } else if (e.which === KeyCodes.DOWN) { GlobalActions.emitSelectNextSuggestion(this.suggestionId); e.preventDefault(); - } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB) { + } else if (e.which === KeyCodes.ENTER || (this.props.completeOnTab && e.which === KeyCodes.TAB)) { this.handleCompleteWord(SuggestionStore.getSelection(this.suggestionId), SuggestionStore.getSelectedMatchedPretext(this.suggestionId)); this.props.onKeyDown(e); e.preventDefault(); @@ -281,23 +354,3 @@ export default class SuggestionBox extends React.Component { return ''; } } - -SuggestionBox.defaultProps = { - type: 'input', - listStyle: 'top' -}; - -SuggestionBox.propTypes = { - listComponent: PropTypes.func.isRequired, - type: PropTypes.oneOf(['input', 'textarea', 'search']).isRequired, - value: PropTypes.string.isRequired, - providers: PropTypes.arrayOf(PropTypes.object), - listStyle: PropTypes.string, - renderDividers: PropTypes.bool, - - // explicitly name any input event handlers we override and need to manually call - onBlur: PropTypes.func, - onChange: PropTypes.func, - onKeyDown: PropTypes.func, - onItemSelected: PropTypes.func -}; diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx index 59f0d02f8..64e8713c5 100644 --- a/webapp/components/suggestion/suggestion_list.jsx +++ b/webapp/components/suggestion/suggestion_list.jsx @@ -1,14 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import SuggestionStore from 'stores/suggestion_store.jsx'; + import $ from 'jquery'; -import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; import {FormattedMessage} from 'react-intl'; -import SuggestionStore from 'stores/suggestion_store.jsx'; - export default class SuggestionList extends React.Component { static propTypes = { suggestionId: PropTypes.string.isRequired, @@ -111,6 +111,17 @@ export default class SuggestionList extends React.Component { ); } + renderLoading(type) { + return ( + <div + key={type + '-loading'} + className='suggestion-loader' + > + <i className='fa fa-spinner fa-pulse fa-fw margin-bottom'/> + </div> + ); + } + render() { if (this.state.items.length === 0) { return null; @@ -131,6 +142,11 @@ export default class SuggestionList extends React.Component { lastType = item.type; } + if (item.loading) { + items.push(this.renderLoading(item.type)); + continue; + } + items.push( <Component key={term} diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx index 89af74c6d..9790de38e 100644 --- a/webapp/components/suggestion/switch_channel_provider.jsx +++ b/webapp/components/suggestion/switch_channel_provider.jsx @@ -4,10 +4,6 @@ import Suggestion from './suggestion.jsx'; import Provider from './provider.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; -import UserStore from 'stores/user_store.jsx'; - -import {autocompleteUsers} from 'actions/user_actions.jsx'; import Client from 'client/web_client.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import {Constants, ActionTypes} from 'utils/constants.jsx'; @@ -16,30 +12,44 @@ import {sortChannelsByDisplayName, getChannelDisplayName} from 'utils/channel_ut import React from 'react'; +import store from 'stores/redux_store.jsx'; +const getState = store.getState; +const dispatch = store.dispatch; + +import {searchChannels} from 'mattermost-redux/actions/channels'; +import {autocompleteUsers} from 'mattermost-redux/actions/users'; + +import {getCurrentUserId, searchProfiles} from 'mattermost-redux/selectors/entities/users'; +import {getChannelsInCurrentTeam, getMyChannelMemberships, getGroupChannels} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getBool} from 'mattermost-redux/selectors/entities/preferences'; +import {Preferences} from 'mattermost-redux/constants'; + class SwitchChannelSuggestion extends Suggestion { render() { const {item, isSelection} = this.props; + const channel = item.channel; let className = 'mentions__name'; if (isSelection) { className += ' suggestion--selected'; } - let displayName = item.display_name; + let displayName = channel.display_name; let icon = null; - if (item.type === Constants.OPEN_CHANNEL) { + if (channel.type === Constants.OPEN_CHANNEL) { icon = <div className='status'><i className='fa fa-globe'/></div>; - } else if (item.type === Constants.PRIVATE_CHANNEL) { + } else if (channel.type === Constants.PRIVATE_CHANNEL) { icon = <div className='status'><i className='fa fa-lock'/></div>; - } else if (item.type === Constants.GM_CHANNEL) { - displayName = getChannelDisplayName(item); + } else if (channel.type === Constants.GM_CHANNEL) { + displayName = getChannelDisplayName(channel); icon = <div className='status status--group'>{'G'}</div>; } else { icon = ( <div className='pull-left'> <img className='mention__image' - src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.last_picture_update} + src={Client.getUsersRoute() + '/' + channel.id + '/image?time=' + channel.last_picture_update} /> </div> ); @@ -57,83 +67,179 @@ class SwitchChannelSuggestion extends Suggestion { } } +let prefix = ''; + +function quickSwitchSorter(wrappedA, wrappedB) { + if (wrappedA.type === Constants.MENTION_CHANNELS && wrappedB.type === Constants.MENTION_MORE_CHANNELS) { + return -1; + } else if (wrappedB.type === Constants.MENTION_CHANNELS && wrappedA.type === Constants.MENTION_MORE_CHANNELS) { + return 1; + } + + const a = wrappedA.channel; + const b = wrappedB.channel; + + let aDisplayName = getChannelDisplayName(a).toLowerCase(); + let bDisplayName = getChannelDisplayName(b).toLowerCase(); + + if (a.type === Constants.DM_CHANNEL) { + aDisplayName = aDisplayName.substring(1); + } + + if (b.type === Constants.DM_CHANNEL) { + bDisplayName = bDisplayName.substring(1); + } + + const aStartsWith = aDisplayName.startsWith(prefix); + const bStartsWith = bDisplayName.startsWith(prefix); + if (aStartsWith && bStartsWith) { + return sortChannelsByDisplayName(a, b); + } else if (!aStartsWith && !bStartsWith) { + return sortChannelsByDisplayName(a, b); + } else if (aStartsWith) { + return -1; + } + + return 1; +} + export default class SwitchChannelProvider extends Provider { handlePretextChanged(suggestionId, channelPrefix) { if (channelPrefix) { + prefix = channelPrefix; this.startNewRequest(suggestionId, channelPrefix); - const allChannels = ChannelStore.getAll(); - const channels = []; + // Dispatch suggestions for local data + const channels = getChannelsInCurrentTeam(getState()).concat(getGroupChannels(getState())); + const users = Object.assign([], searchProfiles(getState(), channelPrefix, true), true); + this.formatChannelsAndDispatch(channelPrefix, suggestionId, channels, users, true); - autocompleteUsers( - channelPrefix, - (data) => { - const users = Object.assign([], data.users); + // Fetch data from the server and dispatch + this.fetchUsersAndChannels(channelPrefix, suggestionId); - if (this.shouldCancelDispatch(channelPrefix)) { - return; - } + return true; + } - const currentId = UserStore.getCurrentId(); + return false; + } - for (const id of Object.keys(allChannels)) { - const channel = allChannels[id]; - if (channel.display_name.toLowerCase().indexOf(channelPrefix.toLowerCase()) !== -1) { - const newChannel = Object.assign({}, channel); - if (newChannel.type === Constants.GM_CHANNEL) { - newChannel.name = getChannelDisplayName(newChannel); - } - channels.push(newChannel); - } - } + async fetchUsersAndChannels(channelPrefix, suggestionId) { + const usersAsync = autocompleteUsers(channelPrefix)(dispatch, getState); + const channelsAsync = searchChannels(getCurrentTeamId(getState()), channelPrefix)(dispatch, getState); + await usersAsync; + await channelsAsync; - const userMap = {}; - for (let i = 0; i < users.length; i++) { - const user = users[i]; - let displayName = `@${user.username} `; + if (this.shouldCancelDispatch(channelPrefix)) { + return; + } - if (user.id === currentId) { - continue; - } + const users = Object.assign([], searchProfiles(getState(), channelPrefix, true)); + const channels = getChannelsInCurrentTeam(getState()).concat(getGroupChannels(getState())); + this.formatChannelsAndDispatch(channelPrefix, suggestionId, channels, users); + } - if ((user.first_name || user.last_name) && user.nickname) { - displayName += `- ${Utils.getFullName(user)} (${user.nickname})`; - } else if (user.nickname) { - displayName += `- (${user.nickname})`; - } else if (user.first_name || user.last_name) { - displayName += `- ${Utils.getFullName(user)}`; - } + formatChannelsAndDispatch(channelPrefix, suggestionId, allChannels, users, skipNotInChannel = false) { + const channels = []; + const members = getMyChannelMemberships(getState()); + + if (this.shouldCancelDispatch(channelPrefix)) { + return; + } - const newChannel = { - display_name: displayName, - name: user.username, - id: user.id, - update_at: user.update_at, - type: Constants.DM_CHANNEL - }; - channels.push(newChannel); - userMap[user.id] = user; + const currentId = getCurrentUserId(getState()); + + for (const id of Object.keys(allChannels)) { + const channel = allChannels[id]; + const member = members[channel.id]; + + if (channel.display_name.toLowerCase().indexOf(channelPrefix.toLowerCase()) !== -1) { + const newChannel = Object.assign({}, channel); + const wrappedChannel = {channel: newChannel, name: newChannel.name}; + if (newChannel.type === Constants.GM_CHANNEL) { + newChannel.name = getChannelDisplayName(newChannel); + wrappedChannel.name = newChannel.name; + const isGMVisible = getBool(getState(), Preferences.CATEGORY_GROUP_CHANNEL_SHOW, newChannel.id, false); + if (isGMVisible) { + wrappedChannel.type = Constants.MENTION_CHANNELS; + } else { + wrappedChannel.type = Constants.MENTION_MORE_CHANNELS; + if (skipNotInChannel) { + continue; + } } + } else if (member) { + wrappedChannel.type = Constants.MENTION_CHANNELS; + } else { + wrappedChannel.type = Constants.MENTION_MORE_CHANNELS; + if (skipNotInChannel || !newChannel.display_name.startsWith(channelPrefix)) { + continue; + } + } - const channelNames = channels. - sort(sortChannelsByDisplayName). - map((channel) => channel.name); - - AppDispatcher.handleServerAction({ - type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, - id: suggestionId, - matchedPretext: channelPrefix, - terms: channelNames, - items: channels, - component: SwitchChannelSuggestion - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_PROFILES, - profiles: userMap - }); + channels.push(wrappedChannel); + } + } + + for (let i = 0; i < users.length; i++) { + const user = users[i]; + const isDMVisible = getBool(getState(), Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, false); + let displayName = `@${user.username} `; + + if (user.id === currentId) { + continue; + } + + if ((user.first_name || user.last_name) && user.nickname) { + displayName += `- ${Utils.getFullName(user)} (${user.nickname})`; + } else if (user.nickname) { + displayName += `- (${user.nickname})`; + } else if (user.first_name || user.last_name) { + displayName += `- ${Utils.getFullName(user)}`; + } + + const wrappedChannel = { + channel: { + display_name: displayName, + name: user.username, + id: user.id, + update_at: user.update_at, + type: Constants.DM_CHANNEL + }, + name: user.username + }; + + if (isDMVisible) { + wrappedChannel.type = Constants.MENTION_CHANNELS; + } else { + wrappedChannel.type = Constants.MENTION_MORE_CHANNELS; + if (skipNotInChannel) { + continue; } - ); + } + + channels.push(wrappedChannel); } + + const channelNames = channels. + sort(quickSwitchSorter). + map((wrappedChannel) => wrappedChannel.channel.name); + + if (skipNotInChannel) { + channels.push({ + type: Constants.MENTION_MORE_CHANNELS, + loading: true + }); + } + + setTimeout(() => { + AppDispatcher.handleServerAction({ + type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, + id: suggestionId, + matchedPretext: channelPrefix, + terms: channelNames, + items: channels, + component: SwitchChannelSuggestion + }); + }, 0); } } diff --git a/webapp/components/suggestion/switch_team_provider.jsx b/webapp/components/suggestion/switch_team_provider.jsx new file mode 100644 index 000000000..ff2a8f24b --- /dev/null +++ b/webapp/components/suggestion/switch_team_provider.jsx @@ -0,0 +1,96 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Suggestion from './suggestion.jsx'; +import Provider from './provider.jsx'; + +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; +import {ActionTypes} from 'utils/constants.jsx'; +import LocalizationStore from 'stores/localization_store.jsx'; + +import React from 'react'; + +// Redux actions +import store from 'stores/redux_store.jsx'; +const getState = store.getState; + +import * as Selectors from 'mattermost-redux/selectors/entities/teams'; + +class SwitchTeamSuggestion extends Suggestion { + render() { + const {item, isSelection} = this.props; + + let className = 'mentions__name'; + if (isSelection) { + className += ' suggestion--selected'; + } + + return ( + <div + onClick={this.handleClick} + className={className} + > + <div className='status'><i className='fa fa-group'/></div> + {item.display_name} + </div> + ); + } +} + +let prefix = ''; + +function quickSwitchSorter(a, b) { + const aDisplayName = a.display_name.toLowerCase(); + const bDisplayName = b.display_name.toLowerCase(); + const aStartsWith = aDisplayName.startsWith(prefix); + const bStartsWith = bDisplayName.startsWith(prefix); + + if (aStartsWith && bStartsWith) { + const locale = LocalizationStore.getLocale(); + + if (aDisplayName !== bDisplayName) { + return aDisplayName.localeCompare(bDisplayName, locale, {numeric: true}); + } + + return a.name.localeCompare(b.name, locale, {numeric: true}); + } else if (aStartsWith) { + return -1; + } + + return 1; +} + +export default class SwitchTeamProvider extends Provider { + handlePretextChanged(suggestionId, teamPrefix) { + if (teamPrefix) { + prefix = teamPrefix; + this.startNewRequest(suggestionId, teamPrefix); + + const allTeams = Selectors.getMyTeams(getState()); + + const teams = allTeams.filter((team) => { + return team.display_name.toLowerCase().indexOf(teamPrefix) !== -1 || + team.name.indexOf(teamPrefix) !== -1; + }); + + const teamNames = teams. + sort(quickSwitchSorter). + map((team) => team.name); + + setTimeout(() => { + AppDispatcher.handleServerAction({ + type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, + id: suggestionId, + matchedPretext: teamPrefix, + terms: teamNames, + items: teams, + component: SwitchTeamSuggestion + }); + }, 0); + + return true; + } + + return false; + } +} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 41f830ac8..a73068360 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1161,7 +1161,14 @@ "channel_select.placeholder": "--- Select a channel ---", "channel_switch_modal.dm": "(Direct Message)", "channel_switch_modal.failed_to_open": "Failed to open channel.", - "channel_switch_modal.help": "Type channel name. Use ↑↓ to browse, TAB to select, ↵ to confirm, ESC to dismiss", + "quick_switch_modal.help": "Use TAB to toggle between teams/channels, ↑↓ to browse, ↵ to confirm, ESC to dismiss", + "quick_switch_modal.help_no_team": "Type a channel name. Use ↑↓ to browse, ↵ to confirm, ESC to dismiss", + "quick_switch_modal.channels": "Channels", + "quick_switch_modal.teams": "Teams", + "quick_switch_modal.teamsShortcut.mac": "(CMD+ALT+K)", + "quick_switch_modal.teamsShortcut.windows": "(CTRL+ALT+K)", + "quick_switch_modal.channelsShortcut.mac": "(CMD+K)", + "quick_switch_modal.channelsShortcut.windows": "(CTRL+K)", "channel_switch_modal.not_found": "No matches found.", "channel_switch_modal.submit": "Switch", "channel_switch_modal.title": "Switch Channels", @@ -1950,6 +1957,8 @@ "sidebar.moreElips": "More...", "sidebar.otherMembers": "Outside this team", "sidebar.pg": "Private Channels", + "sidebar.switch_channels": "Switch Channels (CTRL + K)", + "sidebar.switch_channels.mac": "Switch Channels (CMD + K)", "sidebar.removeList": "Remove from list", "sidebar.tutorialScreen1": "<h4>Channels</h4><p><strong>Channels</strong> organize conversations across different topics. They’re open to everyone on your team. To send private communications use <strong>Direct Messages</strong> for a single person or <strong>Private Channel</strong> for multiple people.</p>", "sidebar.tutorialScreen2": "<h4>\"{townsquare}\" and \"{offtopic}\" channels</h4><p>Here are two public channels to start:</p><p><strong>{townsquare}</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p><p><strong>{offtopic}</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>", @@ -2022,7 +2031,10 @@ "sso_signup.length_error": "Name must be 3 or more characters up to a maximum of 15", "sso_signup.teamName": "Enter name of new team", "sso_signup.team_error": "Please enter a team name", + "suggestion.loading": "Loading...", "suggestion.mention.all": "CAUTION: This mentions everyone in channel", + "suggestion.mention.in_channel": "Channels", + "suggestion.mention.not_in_channel": "Other Channels", "suggestion.mention.channel": "Notifies everyone in the channel", "suggestion.mention.channels": "My Channels", "suggestion.mention.here": "Notifies everyone in the channel and online", diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss index f5489aa2b..6388fe8de 100644 --- a/webapp/sass/components/_modal.scss +++ b/webapp/sass/components/_modal.scss @@ -76,6 +76,18 @@ color: alpha-color($black, .9); width: 100%; + .channel-switch-modal { + .modal-header { + background: transparent; + min-height: 0; + padding: 0; + + .close { + top: 10px; + } + } + } + .modal--overflow { .modal-body { overflow: visible; diff --git a/webapp/sass/components/_suggestion-list.scss b/webapp/sass/components/_suggestion-list.scss index 91db7536c..77550e331 100644 --- a/webapp/sass/components/_suggestion-list.scss +++ b/webapp/sass/components/_suggestion-list.scss @@ -52,6 +52,10 @@ position: relative; } +.suggestion-loader { + margin: 6px 11px; +} + .suggestion-list__divider { line-height: 21px; margin: 5px 0 5px 5px; diff --git a/webapp/sass/layout/_navigation.scss b/webapp/sass/layout/_navigation.scss index 8f0977eba..59b348d9e 100644 --- a/webapp/sass/layout/_navigation.scss +++ b/webapp/sass/layout/_navigation.scss @@ -5,6 +5,24 @@ background: transparent; } +.nav-tabs { + margin-bottom: 10px; + + > li { + margin-right: 5px; + + > a { + border-bottom-color: transparent !important; + padding: 7px 15px; + + .small { + @include opacity(.8); + margin-left: 4px; + } + } + } +} + #navbar { input { margin: 0 5px 0 2px; diff --git a/webapp/sass/layout/_sidebar-left.scss b/webapp/sass/layout/_sidebar-left.scss index a7a99249b..d08a9ef45 100644 --- a/webapp/sass/layout/_sidebar-left.scss +++ b/webapp/sass/layout/_sidebar-left.scss @@ -39,6 +39,45 @@ } } + .sidebar__switcher { + border-top: 2px solid; + bottom: 0; + display: block; + height: 45px; + line-height: 45px; + position: absolute; + text-align: center; + text-decoration: none; + width: 100%; + + &:after { + @include single-transition(all, .15s, ease-in); + background: alpha-color($black, .1); + content: ''; + display: none; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + + span { + @include single-transition(all, .15s, ease-in); + @include opacity(.8); + } + + &:hover { + &:after { + display: block; + } + + span { + @include opacity(1); + } + } + } + .dropdown-menu { max-height: 80vh; max-width: 200px; @@ -62,7 +101,7 @@ .nav-pills__container { -webkit-overflow-scrolling: touch; - height: calc(100% - 80px); + height: calc(100% - 110px); overflow: auto; position: relative; } @@ -84,7 +123,7 @@ } .nav-pills__unread-indicator-bottom { - bottom: 20px; + bottom: 60px; } .nav { diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index 2372ec966..dd92d6b59 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -1059,17 +1059,13 @@ } .nav-pills__container { - height: 100%; + height: calc(100% - 50px); } > div { padding-bottom: 70px; } - .nav-pills__unread-indicator-bottom { - bottom: 10px; - } - .nav-pills__unread-indicator { width: 260px; } @@ -1321,6 +1317,7 @@ } } } + .post { .attachment { .attachment__image { @@ -1545,6 +1542,7 @@ top: 60px; width: calc(100% - 30px); } + .post { .attachment { .attachment__image { @@ -1557,6 +1555,19 @@ } @media screen and (max-width: 480px) { + .nav-tabs { + margin-top: 1em; + + > li { + margin-right: 0; + + a { + font-size: .9em; + padding: 6px 11px; + } + } + } + .sidebar--right { .post { &.post--compact { diff --git a/webapp/stores/modal_store.jsx b/webapp/stores/modal_store.jsx index 3478d6d5e..2b3cd0128 100644 --- a/webapp/stores/modal_store.jsx +++ b/webapp/stores/modal_store.jsx @@ -39,6 +39,7 @@ class ModalStoreClass extends EventEmitter { case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL: case ActionTypes.TOGGLE_GET_PUBLIC_LINK_MODAL: case ActionTypes.TOGGLE_DM_MODAL: + case ActionTypes.TOGGLE_QUICK_SWITCH_MODAL: this.emit(type, value, args); break; } diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 2beb7c019..dc0856888 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -172,6 +172,7 @@ export const ActionTypes = keyMirror({ TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null, TOGGLE_GET_PUBLIC_LINK_MODAL: null, TOGGLE_DM_MODAL: null, + TOGGLE_QUICK_SWITCH_MODAL: null, SUGGESTION_PRETEXT_CHANGED: null, SUGGESTION_RECEIVED_SUGGESTIONS: null, diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index da0f1fa67..0ef057dac 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -47,7 +47,10 @@ export function createSafeId(prop) { return str.replace(new RegExp(' ', 'g'), '_'); } -export function cmdOrCtrlPressed(e) { +export function cmdOrCtrlPressed(e, allowAlt = false) { + if (allowAlt) { + return (isMac() && e.metaKey) || (!isMac() && e.ctrlKey); + } return (isMac() && e.metaKey) || (!isMac() && e.ctrlKey && !e.altKey); } @@ -484,7 +487,7 @@ export function isHexColor(value) { export function applyTheme(theme) { if (theme.sidebarBg) { - changeCss('.sidebar--left, .sidebar--left .sidebar__divider .sidebar__divider__text, .app__body .modal .settings-modal .settings-table .settings-links, .app__body .sidebar--menu', 'background:' + theme.sidebarBg); + changeCss('.app__body .sidebar--left .sidebar__switcher, .sidebar--left, .sidebar--left .sidebar__divider .sidebar__divider__text, .app__body .modal .settings-modal .settings-table .settings-links, .app__body .sidebar--menu', 'background:' + theme.sidebarBg); changeCss('body.app__body', 'scrollbar-face-color:' + theme.sidebarBg); changeCss('@media(max-width: 768px){.app__body .modal .settings-modal:not(.settings-modal--tabless):not(.display--content) .modal-content', 'background:' + theme.sidebarBg); } @@ -495,10 +498,11 @@ export function applyTheme(theme) { changeCss('.sidebar--left .nav-pills__container li>a, .app__body .sidebar--right, .app__body .modal .settings-modal .nav-pills>li a', 'color:' + changeOpacity(theme.sidebarText, 0.6)); changeCss('@media(max-width: 768px){.app__body .modal .settings-modal .settings-table .nav>li>a, .app__body .sidebar--menu', 'color:' + changeOpacity(theme.sidebarText, 0.8)); changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6)); - changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText); + changeCss('.app__body .sidebar--left .sidebar__switcher, .sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText); changeCss('.sidebar--left .status .offline--icon', 'fill:' + theme.sidebarText); changeCss('.sidebar--left .status.status--group', 'background:' + changeOpacity(theme.sidebarText, 0.3)); changeCss('@media(max-width: 768px){.app__body .modal .settings-modal .settings-table .nav>li>a, .app__body .sidebar--menu .divider', 'border-color:' + changeOpacity(theme.sidebarText, 0.2)); + changeCss('.app__body .sidebar--left .sidebar__switcher', 'border-color:' + changeOpacity(theme.sidebarText, 0.2)); changeCss('@media(max-width: 768px){.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + changeOpacity(theme.sidebarText, 0.6)); } @@ -591,7 +595,7 @@ export function applyTheme(theme) { changeCss('.app__body .emoji-picker-react, .app__body .emoji-picker__search', 'background:' + theme.centerChannelBg); changeCss('.app__body .emoji-picker-react-rhs-comment, .app__body .emoji-picker__search', 'background:' + theme.centerChannelBg); - changeCss('.app__body .emoji-picker-bottom, .app__body .emoji-picker__search', 'background:' + theme.centerChannelBg); + changeCss('.app__body .nav-tabs, .app__body .nav-tabs > li.active > a, .app__body .emoji-picker-bottom, .app__body .emoji-picker__search', 'background:' + theme.centerChannelBg); } if (theme.centerChannelColor) { @@ -599,9 +603,9 @@ export function applyTheme(theme) { changeCss('.app__body .post-list__arrows, .app__body .post .flag-icon__container', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3)); changeCss('.app__body .modal .status .offline--icon, .app__body .channel-header__links .icon, .app__body .sidebar--right .sidebar--right__subheader .usage__icon', 'fill:' + theme.centerChannelColor); changeCss('@media(min-width: 768px){.app__body .post:hover .post__header .col__reply, .app__body .post.post--hovered .post__header .col__reply', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); - changeCss('.app__body .post .dropdown-menu a, .sidebar--left, .app__body .sidebar--right .sidebar--right__header, .app__body .suggestion-list__content .command', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); - changeCss('.app__body .post.post--system .post__body', 'color:' + changeOpacity(theme.centerChannelColor, 0.6)); - changeCss('.app__body .input-group-addon, .app__body .app__content, .app__body .post-create__container .post-create-body .btn-file, .app__body .post-create__container .post-create-footer .msg-typing, .app__body .suggestion-list__content .command, .app__body .modal .modal-content, .app__body .dropdown-menu, .app__body .popover, .app__body .mentions__name, .app__body .tip-overlay, .app__body .form-control[disabled], .app__body .form-control[readonly], .app__body fieldset[disabled] .form-control', 'color:' + theme.centerChannelColor); + changeCss('.app__body .nav-tabs > li > a:hover, .app__body .nav-tabs, .app__body .nav-tabs > li.active > a, .app__body .nav-tabs, .app__body .nav-tabs > li.active > a:focus, .app__body .nav-tabs, .app__body .nav-tabs > li.active > a:hover, .app__body .post .dropdown-menu a, .sidebar--left, .app__body .sidebar--right .sidebar--right__header, .app__body .suggestion-list__content .command', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); + changeCss('.app__body .post.post--system .post__body, .app__body .modal .channel-switch-modal .modal-header .close', 'color:' + changeOpacity(theme.centerChannelColor, 0.6)); + changeCss('.app__body .nav-tabs, .app__body .nav-tabs > li.active > a, pp__body .input-group-addon, .app__body .app__content, .app__body .post-create__container .post-create-body .btn-file, .app__body .post-create__container .post-create-footer .msg-typing, .app__body .suggestion-list__content .command, .app__body .modal .modal-content, .app__body .dropdown-menu, .app__body .popover, .app__body .mentions__name, .app__body .tip-overlay, .app__body .form-control[disabled], .app__body .form-control[readonly], .app__body fieldset[disabled] .form-control', 'color:' + theme.centerChannelColor); changeCss('.app__body .post .post__link', 'color:' + changeOpacity(theme.centerChannelColor, 0.65)); changeCss('.app__body #archive-link-home, .video-div .video-thumbnail__error', 'background:' + changeOpacity(theme.centerChannelColor, 0.15)); changeCss('.app__body #post-create', 'color:' + theme.centerChannelColor); diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 9f8d68877..703622450 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -4880,7 +4880,7 @@ math-expression-evaluator@^1.2.14: mattermost-redux@mattermost/mattermost-redux#webapp-master: version "0.0.1" - resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/6af89f1e58258a709601bf46ef7af2ab41d8d1f6" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/d1652dc7b636aae658d0d109919b6a74762a186d" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1" |