diff options
Diffstat (limited to 'web/react/components')
-rw-r--r-- | web/react/components/channel_loader.jsx | 4 | ||||
-rw-r--r-- | web/react/components/channel_notifications_modal.jsx | 12 | ||||
-rw-r--r-- | web/react/components/get_link_modal.jsx | 2 | ||||
-rw-r--r-- | web/react/components/posts_view.jsx | 14 | ||||
-rw-r--r-- | web/react/components/register_app_modal.jsx | 192 | ||||
-rw-r--r-- | web/react/components/search_autocomplete.jsx | 341 | ||||
-rw-r--r-- | web/react/components/search_bar.jsx | 47 | ||||
-rw-r--r-- | web/react/components/search_channel_provider.jsx | 69 | ||||
-rw-r--r-- | web/react/components/search_user_provider.jsx | 62 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 67 | ||||
-rw-r--r-- | web/react/components/suggestion_box.jsx | 170 | ||||
-rw-r--r-- | web/react/components/suggestion_list.jsx | 157 | ||||
-rw-r--r-- | web/react/components/team_general_tab.jsx | 1 | ||||
-rw-r--r-- | web/react/components/user_settings/user_settings_developer.jsx | 12 | ||||
-rw-r--r-- | web/react/components/user_settings/user_settings_display.jsx | 83 | ||||
-rw-r--r-- | web/react/components/user_settings/user_settings_notifications.jsx | 49 | ||||
-rw-r--r-- | web/react/components/view_image.jsx | 3 |
17 files changed, 739 insertions, 546 deletions
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index c8f1196a8..13045d732 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -10,6 +10,7 @@ import SocketStore from '../stores/socket_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import PostStore from '../stores/post_store.jsx'; import UserStore from '../stores/user_store.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; import * as Utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; @@ -69,6 +70,9 @@ export default class ChannelLoader extends React.Component { Utils.applyTheme(Constants.THEMES.default); } + const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT}).value; + Utils.applyFont(selectedFont); + $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { if (ev.type === 'mouseenter') { $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after'); diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx index 79b769c8a..887589468 100644 --- a/web/react/components/channel_notifications_modal.jsx +++ b/web/react/components/channel_notifications_modal.jsx @@ -32,11 +32,13 @@ export default class ChannelNotificationsModal extends React.Component { activeSection: '' }; } - componentDidMount() { - ChannelStore.addChangeListener(this.onListenerChange); - } - componentWillUnmount() { - ChannelStore.removeChangeListener(this.onListenerChange); + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + this.onListenerChange(); + ChannelStore.addChangeListener(this.onListenerChange); + } else { + ChannelStore.removeChangeListener(this.onListenerChange); + } } onListenerChange() { const curChannelId = ChannelStore.getCurrentId(); diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index df5d6b8e1..fd20834f4 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -75,7 +75,7 @@ export default class GetLinkModal extends React.Component { onHide={this.onHide} > <Modal.Header closeButton={true}> - {this.props.title} + <h4 className='modal-title'>{this.props.title}</h4> </Modal.Header> <Modal.Body> {helpText} diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index 242b26b91..d0eee5a23 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -280,18 +280,22 @@ export default class PostsView extends React.Component { this.updateScrolling(); } window.addEventListener('resize', this.handleResize); - $(this.refs.postlist).perfectScrollbar(); - PreferenceStore.addChangeListener(this.updateState); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); - PreferenceStore.removeChangeListener(this.updateState); } componentDidUpdate() { if (this.props.postList != null) { this.updateScrolling(); } - $(this.refs.postlist).perfectScrollbar('update'); + } + componentWillReceiveProps(nextProps) { + if (!this.props.isActive && nextProps.isActive) { + this.updateState(); + PreferenceStore.addChangeListener(this.updateState); + } else if (this.props.isActive && !nextProps.isActive) { + PreferenceStore.removeChangeListener(this.updateState); + } } shouldComponentUpdate(nextProps, nextState) { if (this.props.isActive !== nextProps.isActive) { @@ -373,7 +377,7 @@ export default class PostsView extends React.Component { return ( <div ref='postlist' - className={'ps-container post-list-holder-by-time ' + activeClass} + className={'post-list-holder-by-time ' + activeClass} onScroll={this.handleScroll} > <div className='post-list__table'> diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx index 100600c4b..f49b33f73 100644 --- a/web/react/components/register_app_modal.jsx +++ b/web/react/components/register_app_modal.jsx @@ -2,21 +2,57 @@ // See License.txt for license information. import * as Client from '../utils/client.jsx'; +import ModalStore from '../stores/modal_store.jsx'; + +const Modal = ReactBootstrap.Modal; + +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; export default class RegisterAppModal extends React.Component { constructor() { super(); - this.register = this.register.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); this.onHide = this.onHide.bind(this); this.save = this.save.bind(this); + this.updateShow = this.updateShow.bind(this); - this.state = {clientId: '', clientSecret: '', saved: false}; + this.state = { + clientId: '', + clientSecret: '', + saved: false, + show: false + }; } componentDidMount() { - $(ReactDOM.findDOMNode(this)).on('hide.bs.modal', this.onHide); + ModalStore.addModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow); + } + componentWillUnmount() { + ModalStore.removeModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow); + } + updateShow(show) { + if (!show) { + if (this.state.clientId !== '' && !this.state.saved) { + return; + } + + this.setState({ + clientId: '', + clientSecret: '', + saved: false, + homepageError: null, + callbackError: null, + serverError: null, + nameError: null + }); + } + + this.setState({show}); } - register() { + handleSubmit(e) { + e.preventDefault(); + var state = this.state; state.serverError = null; @@ -94,6 +130,7 @@ export default class RegisterAppModal extends React.Component { } var body = ''; + var footer = ''; if (this.state.clientId === '') { body = ( <div className='settings-modal'> @@ -148,24 +185,29 @@ export default class RegisterAppModal extends React.Component { </div> </div> {serverError} - <hr /> - <a - className='btn btn-sm theme pull-right' - href='#' - data-dismiss='modal' - aria-label='Close' - > - {'Cancel'} - </a> - <a - className='btn btn-sm btn-primary pull-right' - onClick={this.register} - > - {'Register'} - </a> </div> </div> ); + + footer = ( + <div> + <button + type='button' + className='btn btn-default' + onClick={() => this.updateShow(false)} + > + {'Cancel'} + </button> + <button + onClick={this.handleSubmit} + type='submit' + className='btn btn-primary' + tabIndex='3' + > + {'Register'} + </button> + </div> + ); } else { var btnClass = ' disabled'; if (this.state.saved) { @@ -173,17 +215,35 @@ export default class RegisterAppModal extends React.Component { } body = ( - <div className='form-group user-settings'> - <h3>{'Your Application Credentials'}</h3> - <br/> - <br/> - <label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label> - <label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label> + <div className='form-horizontal user-settings'> + <h4 className='padding-bottom x3'>{'Your Application Credentials'}</h4> <br/> + <div className='row'> + <label className='col-sm-4 control-label'>{'Client ID'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + value={this.state.clientId} + readOnly='true' + /> + </div> + </div> <br/> + <div className='row padding-top x2'> + <label className='col-sm-4 control-label'>{'Client Secret'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + value={this.state.clientSecret} + readOnly='true' + /> + </div> + </div> <br/> <br/> - <strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong> + <strong>{'Save these somewhere SAFE and SECURE. Treat your Client ID as your app\'s username and your Client Secret as the app\'s password.'}</strong> <br/> <br/> <div className='checkbox'> @@ -192,56 +252,50 @@ export default class RegisterAppModal extends React.Component { ref='save' type='checkbox' checked={this.state.saved} - onClick={this.save} - > - {'I have saved both my Client Id and Client Secret somewhere safe'} - </input> + onChange={this.save} + /> + {'I have saved both my Client Id and Client Secret somewhere safe'} </label> </div> - <a - className={'btn btn-sm btn-primary pull-right' + btnClass} - href='#' - data-dismiss='modal' - aria-label='Close' - > - {'Close'} - </a> </div> ); + + footer = ( + <a + className={'btn btn-sm btn-primary pull-right' + btnClass} + href='#' + onClick={(e) => { + e.preventDefault(); + this.updateShow(false); + }} + > + {'Close'} + </a> + ); } return ( - <div - className='modal fade' - ref='modal' - id='register_app' - role='dialog' - aria-hidden='true' - > - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>{'×'}</span> - </button> - <h4 - className='modal-title' - ref='title' - > - {'Developer Applications'} - </h4> - </div> - <div className='modal-body'> - {body} - </div> - </div> - </div> - </div> + <span> + <Modal + show={this.state.show} + onHide={() => this.updateShow(false)} + > + <Modal.Header closeButton={true}> + <Modal.Title>{'Developer Applications'}</Modal.Title> + </Modal.Header> + <form + role='form' + className='form-horizontal' + > + <Modal.Body> + {body} + </Modal.Body> + <Modal.Footer> + {footer} + </Modal.Footer> + </form> + </Modal> + </span> ); } } diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx deleted file mode 100644 index 4c0aa0166..000000000 --- a/web/react/components/search_autocomplete.jsx +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ChannelStore from '../stores/channel_store.jsx'; -import Constants from '../utils/constants.jsx'; -const KeyCodes = Constants.KeyCodes; -const Popover = ReactBootstrap.Popover; -import UserStore from '../stores/user_store.jsx'; -import * as Utils from '../utils/utils.jsx'; - -const patterns = new Map([ - ['channels', /\b(?:in|channel):\s*(\S*)$/i], - ['users', /\bfrom:\s*(\S*)$/i] -]); - -export default class SearchAutocomplete extends React.Component { - constructor(props) { - super(props); - - this.handleClick = this.handleClick.bind(this); - this.handleDocumentClick = this.handleDocumentClick.bind(this); - this.handleInputChange = this.handleInputChange.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - - this.completeWord = this.completeWord.bind(this); - this.getSelection = this.getSelection.bind(this); - this.scrollToItem = this.scrollToItem.bind(this); - this.updateSuggestions = this.updateSuggestions.bind(this); - - this.renderChannelSuggestion = this.renderChannelSuggestion.bind(this); - this.renderUserSuggestion = this.renderUserSuggestion.bind(this); - - this.state = { - show: false, - mode: '', - filter: '', - selection: 0, - suggestions: new Map() - }; - } - - componentDidMount() { - $(document).on('click', this.handleDocumentClick); - } - - componentDidUpdate(prevProps, prevState) { - const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content'); - - if (this.state.show && this.state.suggestions.length > 0) { - if (!prevState.show) { - content.perfectScrollbar(); - content.css('max-height', $(window).height() - 200); - } - - // keep the keyboard selection visible when scrolling - this.scrollToItem(this.getSelection()); - } - } - - componentWillUnmount() { - $(document).off('click', this.handleDocumentClick); - } - - handleClick(value) { - this.completeWord(value); - } - - handleDocumentClick(e) { - const container = $(ReactDOM.findDOMNode(this.refs.searchPopover)); - - if (!(container.is(e.target) || container.has(e.target).length > 0)) { - this.setState({ - show: false - }); - } - } - - handleInputChange(textbox, text) { - const caret = Utils.getCaretPosition(textbox); - const preText = text.substring(0, caret); - - let mode = ''; - let filter = ''; - for (const [modeForPattern, pattern] of patterns) { - const result = pattern.exec(preText); - - if (result) { - mode = modeForPattern; - filter = result[1]; - break; - } - } - - if (mode !== this.state.mode || filter !== this.state.filter) { - this.updateSuggestions(mode, filter); - } - - this.setState({ - mode, - filter, - show: mode || filter - }); - } - - handleKeyDown(e) { - if (!this.state.show || this.state.suggestions.length === 0) { - return; - } - - if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) { - e.preventDefault(); - - let selection = this.state.selection; - - if (e.which === KeyCodes.UP) { - selection -= 1; - } else { - selection += 1; - } - - if (selection >= 0 && selection < this.state.suggestions.length) { - this.setState({ - selection - }); - } - } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) { - e.preventDefault(); - - this.completeWord(this.getSelection()); - } - } - - completeWord(value) { - // add a space so that anything else typed doesn't interfere with the search flag - this.props.completeWord(this.state.filter, value + ' '); - - this.setState({ - show: false, - mode: '', - filter: '', - selection: 0 - }); - } - - getSelection() { - if (this.state.suggestions.length > 0) { - if (this.state.mode === 'channels') { - return this.state.suggestions[this.state.selection].name; - } else if (this.state.mode === 'users') { - return this.state.suggestions[this.state.selection].username; - } - } - - return ''; - } - - scrollToItem(itemName) { - const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content'); - const visibleContentHeight = content[0].clientHeight; - const actualContentHeight = content[0].scrollHeight; - - if (this.state.suggestions.length > 0 && visibleContentHeight < actualContentHeight) { - const contentTop = content.scrollTop(); - const contentTopPadding = parseInt(content.css('padding-top'), 10); - const contentBottomPadding = parseInt(content.css('padding-top'), 10); - - const item = $(this.refs[itemName]); - const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10); - const itemBottom = item[0].offsetTop + item.height() + parseInt(item.css('margin-bottom'), 10); - - if (itemTop - contentTopPadding < contentTop) { - // the item is off the top of the visible space - content.scrollTop(itemTop - contentTopPadding); - } else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) { - // the item has gone off the bottom of the visible space - content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding); - } - } - } - - updateSuggestions(mode, filter) { - let suggestions = []; - - if (mode === 'channels') { - let channels = ChannelStore.getAll(); - - if (filter) { - channels = channels.filter((channel) => channel.name.startsWith(filter) && channel.type !== 'D'); - } else { - // don't show direct channels - channels = channels.filter((channel) => channel.type !== 'D'); - } - - channels.sort((a, b) => { - // put public channels first and then sort alphabebetically - if (a.type === b.type) { - return a.name.localeCompare(b.name); - } else if (a.type === Constants.OPEN_CHANNEL) { - return -1; - } - - return 1; - }); - - suggestions = channels; - } else if (mode === 'users') { - let users = UserStore.getActiveOnlyProfileList(); - - if (filter) { - users = users.filter((user) => user.username.startsWith(filter)); - } - - users.sort((a, b) => a.username.localeCompare(b.username)); - - suggestions = users; - } - - let selection = this.state.selection; - - // keep the same user/channel selected if it's still visible as a suggestion - if (selection > 0 && this.state.suggestions.length > 0) { - // we can't just use indexOf to find if the selection is still in the list since they are different javascript objects - const currentSelectionId = this.state.suggestions[selection].id; - let found = false; - - for (let i = 0; i < suggestions.length; i++) { - if (suggestions[i].id === currentSelectionId) { - selection = i; - found = true; - - break; - } - } - - if (!found) { - selection = 0; - } - } else { - selection = 0; - } - - this.setState({ - suggestions, - selection - }); - } - - renderChannelSuggestion(channel) { - let className = 'search-autocomplete__item'; - if (channel.name === this.getSelection()) { - className += ' selected'; - } - - return ( - <div - key={channel.name} - ref={channel.name} - onClick={this.handleClick.bind(this, channel.name)} - className={className} - > - {channel.name} - </div> - ); - } - - renderUserSuggestion(user) { - let className = 'search-autocomplete__item'; - if (user.username === this.getSelection()) { - className += ' selected'; - } - - return ( - <div - key={user.username} - ref={user.username} - onClick={this.handleClick.bind(this, user.username)} - className={className} - > - <img - className='profile-img rounded' - src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at} - /> - {user.username} - </div> - ); - } - - render() { - if (!this.state.show || this.state.suggestions.length === 0) { - return null; - } - - let suggestions = []; - - if (this.state.mode === 'channels') { - const publicChannels = this.state.suggestions.filter((channel) => channel.type === Constants.OPEN_CHANNEL); - if (publicChannels.length > 0) { - suggestions.push( - <div - key='public-channel-divider' - className='search-autocomplete__divider' - > - <span>{'Public ' + Utils.getChannelTerm(Constants.OPEN_CHANNEL) + 's'}</span> - </div> - ); - suggestions = suggestions.concat(publicChannels.map(this.renderChannelSuggestion)); - } - - const privateChannels = this.state.suggestions.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL); - if (privateChannels.length > 0) { - suggestions.push( - <div - key='private-channel-divider' - className='search-autocomplete__divider' - > - <span>{'Private ' + Utils.getChannelTerm(Constants.PRIVATE_CHANNEL) + 's'}</span> - </div> - ); - suggestions = suggestions.concat(privateChannels.map(this.renderChannelSuggestion)); - } - } else if (this.state.mode === 'users') { - suggestions = this.state.suggestions.map(this.renderUserSuggestion); - } - - return ( - <Popover - ref='searchPopover' - onShow={this.componentDidMount} - id='search-autocomplete__popover' - className='search-help-popover autocomplete visible' - placement='bottom' - > - {suggestions} - </Popover> - ); - } -} - -SearchAutocomplete.propTypes = { - completeWord: React.PropTypes.func.isRequired -}; diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index 32f0f93bf..0ea5c451a 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -5,11 +5,13 @@ import * as client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; import SearchStore from '../stores/search_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import SuggestionBox from '../components/suggestion_box.jsx'; +import SearchChannelProvider from '../components/search_channel_provider.jsx'; +import SearchUserProvider from '../components/search_user_provider.jsx'; import * as utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; var ActionTypes = Constants.ActionTypes; var Popover = ReactBootstrap.Popover; -import SearchAutocomplete from './search_autocomplete.jsx'; export default class SearchBar extends React.Component { constructor() { @@ -17,17 +19,17 @@ export default class SearchBar extends React.Component { this.mounted = false; this.onListenerChange = this.onListenerChange.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); this.handleUserInput = this.handleUserInput.bind(this); this.handleUserFocus = this.handleUserFocus.bind(this); this.handleUserBlur = this.handleUserBlur.bind(this); this.performSearch = this.performSearch.bind(this); this.handleSubmit = this.handleSubmit.bind(this); - this.completeWord = this.completeWord.bind(this); const state = this.getSearchTermStateFromStores(); state.focused = false; this.state = state; + + this.suggestionProviders = [new SearchChannelProvider(), new SearchUserProvider()]; } getSearchTermStateFromStores() { var term = SearchStore.getSearchTerm() || ''; @@ -77,18 +79,11 @@ export default class SearchBar extends React.Component { results: null }); } - handleKeyDown(e) { - if (this.refs.autocomplete) { - this.refs.autocomplete.handleKeyDown(e); - } - } - handleUserInput(e) { - var term = e.target.value; + handleUserInput(text) { + var term = text; SearchStore.storeSearchTerm(term); SearchStore.emitSearchTermChange(false); this.setState({searchTerm: term}); - - this.refs.autocomplete.handleInputChange(e.target, term); } handleUserBlur() { this.setState({focused: false}); @@ -128,23 +123,6 @@ export default class SearchBar extends React.Component { this.performSearch(this.state.searchTerm.trim()); } - completeWord(partialWord, word) { - const textbox = ReactDOM.findDOMNode(this.refs.search); - let text = textbox.value; - - const caret = utils.getCaretPosition(textbox); - const preText = text.substring(0, caret - partialWord.length); - const postText = text.substring(caret); - text = preText + word + postText; - - textbox.value = text; - utils.setCaretPosition(textbox, preText.length + word.length); - - SearchStore.storeSearchTerm(text); - SearchStore.emitSearchTermChange(false); - this.setState({searchTerm: text}); - } - render() { var isSearching = null; if (this.state.isSearching) { @@ -178,22 +156,17 @@ export default class SearchBar extends React.Component { autoComplete='off' > <span className='glyphicon glyphicon-search sidebar__search-icon' /> - <input - type='text' + <SuggestionBox ref='search' className='form-control search-bar' placeholder='Search' value={this.state.searchTerm} onFocus={this.handleUserFocus} onBlur={this.handleUserBlur} - onChange={this.handleUserInput} - onKeyDown={this.handleKeyDown} + onUserInput={this.handleUserInput} + providers={this.suggestionProviders} /> {isSearching} - <SearchAutocomplete - ref='autocomplete' - completeWord={this.completeWord} - /> <Popover id='searchbar-help-popup' placement='bottom' diff --git a/web/react/components/search_channel_provider.jsx b/web/react/components/search_channel_provider.jsx new file mode 100644 index 000000000..6b2fa2d62 --- /dev/null +++ b/web/react/components/search_channel_provider.jsx @@ -0,0 +1,69 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ChannelStore from '../stores/channel_store.jsx'; +import Constants from '../utils/constants.jsx'; +import SuggestionStore from '../stores/suggestion_store.jsx'; + +class SearchChannelSuggestion extends React.Component { + render() { + const {item, isSelection, onClick} = this.props; + + let className = 'search-autocomplete__item'; + if (isSelection) { + className += ' selected'; + } + + return ( + <div + onClick={onClick} + className={className} + > + {item.name} + </div> + ); + } +} + +SearchChannelSuggestion.propTypes = { + item: React.PropTypes.object.isRequired, + isSelection: React.PropTypes.bool, + onClick: React.PropTypes.func +}; + +export default class SearchChannelProvider { + handlePretextChanged(suggestionId, pretext) { + const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext); + if (captured) { + const channelPrefix = captured[1]; + + const channels = ChannelStore.getAll(); + const publicChannels = []; + const privateChannels = []; + + for (const id of Object.keys(channels)) { + const channel = channels[id]; + + // don't show direct channels + if (channel.type !== Constants.DM_CHANNEL && channel.name.startsWith(channelPrefix)) { + if (channel.type === Constants.OPEN_CHANNEL) { + publicChannels.push(channel); + } else { + privateChannels.push(channel); + } + } + } + + publicChannels.sort((a, b) => a.name.localeCompare(b.name)); + const publicChannelNames = publicChannels.map((channel) => channel.name); + + privateChannels.sort((a, b) => a.name.localeCompare(b.name)); + const privateChannelNames = privateChannels.map((channel) => channel.name); + + SuggestionStore.setMatchedPretext(suggestionId, channelPrefix); + + SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion); + SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion); + } + } +} diff --git a/web/react/components/search_user_provider.jsx b/web/react/components/search_user_provider.jsx new file mode 100644 index 000000000..7c1711d36 --- /dev/null +++ b/web/react/components/search_user_provider.jsx @@ -0,0 +1,62 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import SuggestionStore from '../stores/suggestion_store.jsx'; +import UserStore from '../stores/user_store.jsx'; + +class SearchUserSuggestion extends React.Component { + render() { + const {item, isSelection, onClick} = this.props; + + let className = 'search-autocomplete__item'; + if (isSelection) { + className += ' selected'; + } + + return ( + <div + className={className} + onClick={onClick} + > + <img + className='profile-img rounded' + src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at} + /> + {item.username} + </div> + ); + } +} + +SearchUserSuggestion.propTypes = { + item: React.PropTypes.object.isRequired, + isSelection: React.PropTypes.bool, + onClick: React.PropTypes.func +}; + +export default class SearchUserProvider { + handlePretextChanged(suggestionId, pretext) { + const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext); + if (captured) { + const usernamePrefix = captured[1]; + + const users = UserStore.getProfiles(); + let filtered = []; + + for (const id of Object.keys(users)) { + const user = users[id]; + + if (user.username.startsWith(usernamePrefix)) { + filtered.push(user); + } + } + + filtered = filtered.sort((a, b) => a.username.localeCompare(b.username)); + + const usernames = filtered.map((user) => user.username); + + SuggestionStore.setMatchedPretext(suggestionId, usernamePrefix); + SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion); + } + } +} diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index b4c037183..3d7f449d1 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -3,7 +3,6 @@ import NewChannelFlow from './new_channel_flow.jsx'; import MoreDirectChannels from './more_direct_channels.jsx'; -import SearchBox from './search_bar.jsx'; import SidebarHeader from './sidebar_header.jsx'; import UnreadChannelIndicator from './unread_channel_indicator.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; @@ -20,7 +19,6 @@ import * as Utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; const Preferences = Constants.Preferences; const TutorialSteps = Constants.TutorialSteps; -const NotificationPrefs = Constants.NotificationPrefs; const Tooltip = ReactBootstrap.Tooltip; const OverlayTrigger = ReactBootstrap.OverlayTrigger; @@ -39,7 +37,6 @@ export default class Sidebar extends React.Component { this.onScroll = this.onScroll.bind(this); this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this); this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this); - this.updateScrollbar = this.updateScrollbar.bind(this); this.handleResize = this.handleResize.bind(this); this.showNewChannelModal = this.showNewChannelModal.bind(this); @@ -49,8 +46,6 @@ export default class Sidebar extends React.Component { this.createChannelElement = this.createChannelElement.bind(this); this.updateTitle = this.updateTitle.bind(this); - this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this); - this.getUnreadCount = this.getUnreadCount.bind(this); this.isLeaving = new Map(); @@ -60,43 +55,15 @@ export default class Sidebar extends React.Component { state.loadingDMChannel = -1; state.windowWidth = Utils.windowWidth(); this.state = state; - - this.unreadCountPerChannel = {}; - this.setUnreadCountPerChannel(); - } - setUnreadCountPerChannel() { - const channels = ChannelStore.getAll(); - const members = ChannelStore.getAllMembers(); - const channelUnreadCounts = {}; - - channels.forEach((ch) => { - const chMember = members[ch.id]; - let chMentionCount = chMember.mention_count; - let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount; - - if (ch.type === 'D') { - chMentionCount = chUnreadCount; - chUnreadCount = 0; - } else if (chMember.notify_props && chMember.notify_props.mark_unread === NotificationPrefs.MENTION) { - chUnreadCount = 0; - } - - channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount}; - }); - - this.unreadCountPerChannel = channelUnreadCounts; } - getUnreadCount(channelId) { - let mentions = 0; + getTotalUnreadCount() { let msgs = 0; + let mentions = 0; + const unreadCounts = this.state.unreadCounts; - if (channelId) { - return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions}; - } - - Object.keys(this.unreadCountPerChannel).forEach((chId) => { - msgs += this.unreadCountPerChannel[chId].msgs; - mentions += this.unreadCountPerChannel[chId].mentions; + Object.keys(unreadCounts).forEach((chId) => { + msgs += unreadCounts[chId].msgs; + mentions += unreadCounts[chId].mentions; }); return {msgs, mentions}; @@ -157,6 +124,7 @@ export default class Sidebar extends React.Component { privateChannels, visibleDirectChannels, hiddenDirectChannelCount, + unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER }; } @@ -170,7 +138,6 @@ export default class Sidebar extends React.Component { this.updateTitle(); this.updateUnreadIndicators(); - this.updateScrollbar(); window.addEventListener('resize', this.handleResize); @@ -187,7 +154,6 @@ export default class Sidebar extends React.Component { componentDidUpdate() { this.updateTitle(); this.updateUnreadIndicators(); - this.updateScrollbar(); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); @@ -204,8 +170,6 @@ export default class Sidebar extends React.Component { windowHeight: Utils.windowHeight() }); } - updateScrollbar() { - } onChange() { this.setState(this.getStateFromStores()); } @@ -222,7 +186,7 @@ export default class Sidebar extends React.Component { currentChannelName = Utils.getDirectTeammate(channel.id).username; } - const unread = this.getUnreadCount(); + const unread = this.getTotalUnreadCount(); const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : ''; const unreadTitle = unread.msgs > 0 ? '* ' : ''; document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName; @@ -348,13 +312,13 @@ export default class Sidebar extends React.Component { } createChannelElement(channel, index, arr, handleClose) { - var members = this.state.members; - var activeId = this.state.activeId; - var channelMember = members[channel.id]; - var unreadCount = this.getUnreadCount(channel.id); - var msgCount; + const members = this.state.members; + const activeId = this.state.activeId; + const channelMember = members[channel.id]; + const unreadCount = this.state.unreadCounts[channel.id] || {msgs: 0, mentions: 0}; + let msgCount; - var linkClass = ''; + let linkClass = ''; if (channel.id === activeId) { linkClass = 'active'; } @@ -511,8 +475,6 @@ export default class Sidebar extends React.Component { render() { this.badgesActive = false; - this.setUnreadCountPerChannel(); - // keep track of the first and last unread channels so we can use them to set the unread indicators this.firstUnreadChannel = null; this.lastUnreadChannel = null; @@ -586,7 +548,6 @@ export default class Sidebar extends React.Component { teamName={TeamStore.getCurrent().name} teamType={TeamStore.getCurrent().type} /> - <SearchBox /> <UnreadChannelIndicator show={this.state.showTopUnread} diff --git a/web/react/components/suggestion_box.jsx b/web/react/components/suggestion_box.jsx new file mode 100644 index 000000000..a72e17430 --- /dev/null +++ b/web/react/components/suggestion_box.jsx @@ -0,0 +1,170 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from '../utils/constants.jsx'; +import SuggestionList from './suggestion_list.jsx'; +import SuggestionStore from '../stores/suggestion_store.jsx'; +import * as Utils from '../utils/utils.jsx'; + +const ActionTypes = Constants.ActionTypes; +const KeyCodes = Constants.KeyCodes; + +export default class SuggestionBox extends React.Component { + constructor(props) { + super(props); + + this.handleDocumentClick = this.handleDocumentClick.bind(this); + this.handleFocus = this.handleFocus.bind(this); + + this.handleChange = this.handleChange.bind(this); + this.handleCompleteWord = this.handleCompleteWord.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handlePretextChanged = this.handlePretextChanged.bind(this); + + this.suggestionId = Utils.generateId(); + + this.state = { + focused: false + }; + } + + componentDidMount() { + SuggestionStore.registerSuggestionBox(this.suggestionId); + $(document).on('click', this.handleDocumentClick); + + SuggestionStore.addCompleteWordListener(this.suggestionId, this.handleCompleteWord); + SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged); + } + + componentWillUnmount() { + SuggestionStore.removeCompleteWordListener(this.suggestionId, this.handleCompleteWord); + SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged); + + SuggestionStore.unregisterSuggestionBox(this.suggestionId); + $(document).off('click', this.handleDocumentClick); + } + + handleDocumentClick(e) { + if (!this.state.focused) { + return; + } + + const container = $(ReactDOM.findDOMNode(this)); + if (!(container.is(e.target) || container.has(e.target).length > 0)) { + // we can't just use blur for this because it fires and hides the children before + // their click handlers can be called + this.setState({ + focused: false + }); + } + } + + handleFocus() { + this.setState({ + focused: true + }); + + if (this.props.onFocus) { + this.props.onFocus(); + } + } + + handleChange(e) { + const textbox = ReactDOM.findDOMNode(this.refs.textbox); + const caret = Utils.getCaretPosition(textbox); + const pretext = textbox.value.substring(0, caret); + + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, + id: this.suggestionId, + pretext + }); + + if (this.props.onUserInput) { + this.props.onUserInput(textbox.value); + } + + if (this.props.onChange) { + this.props.onChange(e); + } + } + + handleCompleteWord(term) { + const textbox = ReactDOM.findDOMNode(this.refs.textbox); + const caret = Utils.getCaretPosition(textbox); + + const text = this.props.value; + const prefix = text.substring(0, caret - SuggestionStore.getMatchedPretext(this.suggestionId).length); + const suffix = text.substring(caret); + + if (this.props.onUserInput) { + this.props.onUserInput(prefix + term + ' ' + suffix); + } + + // set the caret position after the next rendering + window.requestAnimationFrame(() => { + Utils.setCaretPosition(textbox, prefix.length + term.length + 1); + }); + } + + handleKeyDown(e) { + if (e.which === KeyCodes.UP) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_PREVIOUS, + id: this.suggestionId + }); + e.preventDefault(); + } else if (e.which === KeyCodes.DOWN) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_NEXT, + id: this.suggestionId + }); + e.preventDefault(); + } else if ((e.which === KeyCodes.SPACE || e.which === KeyCodes.ENTER) && SuggestionStore.hasSuggestions(this.suggestionId)) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_COMPLETE_WORD, + id: this.suggestionId + }); + e.preventDefault(); + } else if (this.props.onKeyDown) { + this.props.onKeyDown(e); + } + } + + handlePretextChanged(pretext) { + for (const provider of this.props.providers) { + provider.handlePretextChanged(this.suggestionId, pretext); + } + } + + render() { + const newProps = Object.assign({}, this.props, { + onFocus: this.handleFocus, + onChange: this.handleChange, + onKeyDown: this.handleKeyDown + }); + + return ( + <div> + <input + ref='textbox' + type='text' + {...newProps} + /> + <SuggestionList suggestionId={this.suggestionId} /> + </div> + ); + } +} + +SuggestionBox.propTypes = { + value: React.PropTypes.string.isRequired, + onUserInput: React.PropTypes.func, + providers: React.PropTypes.arrayOf(React.PropTypes.object), + + // explicitly name any input event handlers we override and need to manually call + onChange: React.PropTypes.func, + onKeyDown: React.PropTypes.func, + onFocus: React.PropTypes.func +}; diff --git a/web/react/components/suggestion_list.jsx b/web/react/components/suggestion_list.jsx new file mode 100644 index 000000000..04d8f3e60 --- /dev/null +++ b/web/react/components/suggestion_list.jsx @@ -0,0 +1,157 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from '../utils/constants.jsx'; +import SuggestionStore from '../stores/suggestion_store.jsx'; +import * as Utils from '../utils/utils.jsx'; + +export default class SuggestionList extends React.Component { + constructor(props) { + super(props); + + this.handleItemClick = this.handleItemClick.bind(this); + this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this); + + this.scrollToItem = this.scrollToItem.bind(this); + + this.state = { + items: [], + terms: [], + components: [], + selection: '' + }; + } + + componentDidMount() { + SuggestionStore.addSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged); + } + + componentDidUpdate(prevProps, prevState) { + if (this.state.items.length > 0 && prevState.items.length === 0) { + const content = $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content'); + content.perfectScrollbar(); + } + } + + componentWillUnmount() { + SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged); + } + + handleItemClick(term, e) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD, + id: this.props.suggestionId, + term + }); + + e.preventDefault(); + } + + handleSuggestionsChanged() { + const selection = SuggestionStore.getSelection(this.props.suggestionId); + + this.setState({ + items: SuggestionStore.getItems(this.props.suggestionId), + terms: SuggestionStore.getTerms(this.props.suggestionId), + components: SuggestionStore.getComponents(this.props.suggestionId), + selection + }); + + if (selection) { + window.requestAnimationFrame(() => this.scrollToItem(this.state.selection)); + } + } + + scrollToItem(term) { + const content = $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content'); + const visibleContentHeight = content[0].clientHeight; + const actualContentHeight = content[0].scrollHeight; + + if (visibleContentHeight < actualContentHeight) { + const contentTop = content.scrollTop(); + const contentTopPadding = parseInt(content.css('padding-top'), 10); + const contentBottomPadding = parseInt(content.css('padding-top'), 10); + + const item = $(ReactDOM.findDOMNode(this.refs[term])); + const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10); + const itemBottom = item[0].offsetTop + item.height() + parseInt(item.css('margin-bottom'), 10); + + if (itemTop - contentTopPadding < contentTop) { + // the item is off the top of the visible space + content.scrollTop(itemTop - contentTopPadding); + } else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) { + // the item has gone off the bottom of the visible space + content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding); + } + } + } + + renderChannelDivider(type) { + let text; + if (type === Constants.OPEN_CHANNEL) { + text = 'Public ' + Utils.getChannelTerm(type) + 's'; + } else { + text = 'Private ' + Utils.getChannelTerm(type) + 's'; + } + + return ( + <div + key={type + '-divider'} + className='search-autocomplete__divider' + > + <span>{text}</span> + </div> + ); + } + + render() { + if (this.state.items.length === 0) { + return null; + } + + const items = []; + for (let i = 0; i < this.state.items.length; i++) { + const item = this.state.items[i]; + const term = this.state.terms[i]; + const isSelection = term === this.state.selection; + + // ReactComponent names need to be upper case when used in JSX + const Component = this.state.components[i]; + + // temporary hack to add dividers between public and private channels in the search suggestion list + if (i === 0 || item.type !== this.state.items[i - 1].type) { + if (item.type === Constants.OPEN_CHANNEL) { + items.push(this.renderChannelDivider(Constants.OPEN_CHANNEL)); + } else if (item.type === Constants.PRIVATE_CHANNEL) { + items.push(this.renderChannelDivider(Constants.PRIVATE_CHANNEL)); + } + } + + items.push( + <Component + key={term} + ref={term} + item={item} + isSelection={isSelection} + onClick={this.handleItemClick.bind(this, term)} + /> + ); + } + + return ( + <ReactBootstrap.Popover + ref='popover' + id='search-autocomplete__popover' + className='search-help-popover autocomplete visible' + placement='bottom' + > + {items} + </ReactBootstrap.Popover> + ); + } +} + +SuggestionList.propTypes = { + suggestionId: React.PropTypes.string.isRequired +};
\ No newline at end of file diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index 795fad671..03715d585 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -437,6 +437,7 @@ export default class GeneralTab extends React.Component { <input className='form-control' type='text' + maxLength='22' onChange={this.updateName} value={this.state.name} /> diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 2d02c255a..01e13be57 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -3,16 +3,19 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; +import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; export default class DeveloperTab extends React.Component { constructor(props) { super(props); + this.register = this.register.bind(this); + this.state = {}; } register() { - $('#user_settings1').modal('hide'); - $('#register_app').modal('show'); + this.props.closeModal(); + EventHelpers.showRegisterAppModal(); } render() { var appSection; @@ -21,7 +24,10 @@ export default class DeveloperTab extends React.Component { var inputs = []; inputs.push( - <div className='form-group'> + <div + key='registerbtn' + className='form-group' + > <div className='col-sm-7'> <a className='btn btn-sm btn-primary' diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx index 43c8d33d1..dc3865c68 100644 --- a/web/react/components/user_settings/user_settings_display.jsx +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -6,14 +6,17 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import Constants from '../../utils/constants.jsx'; import PreferenceStore from '../../stores/preference_store.jsx'; +import * as Utils from '../../utils/utils.jsx'; function getDisplayStateFromStores() { const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'}); const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'}); + const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT}); return { militaryTime: militaryTime.value, - nameFormat: nameFormat.value + nameFormat: nameFormat.value, + selectedFont: selectedFont.value }; } @@ -24,15 +27,20 @@ export default class UserSettingsDisplay extends React.Component { this.handleSubmit = this.handleSubmit.bind(this); this.handleClockRadio = this.handleClockRadio.bind(this); this.handleNameRadio = this.handleNameRadio.bind(this); + this.handleFont = this.handleFont.bind(this); this.updateSection = this.updateSection.bind(this); this.state = getDisplayStateFromStores(); + this.selectedFont = this.state.selectedFont; } handleSubmit() { const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime); const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat); + const fontPreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', this.state.selectedFont); - savePreferences([timePreference, namePreference], + this.selectedFont = this.state.selectedFont; + + savePreferences([timePreference, namePreference, fontPreference], () => { PreferenceStore.emitChange(); this.updateSection(''); @@ -48,6 +56,10 @@ export default class UserSettingsDisplay extends React.Component { handleNameRadio(nameFormat) { this.setState({nameFormat}); } + handleFont(selectedFont) { + Utils.applyFont(selectedFont); + this.setState({selectedFont}); + } updateSection(section) { this.setState(getDisplayStateFromStores()); this.props.updateSection(section); @@ -56,6 +68,8 @@ export default class UserSettingsDisplay extends React.Component { const serverError = this.state.serverError || null; let clockSection; let nameFormatSection; + let fontSection; + if (this.props.activeSection === 'clock') { const clockFormat = [false, false]; if (this.state.militaryTime === 'true') { @@ -209,6 +223,69 @@ export default class UserSettingsDisplay extends React.Component { ); } + if (this.props.activeSection === 'font') { + const options = []; + Object.keys(Constants.FONTS).forEach((fontName, idx) => { + const className = Constants.FONTS[fontName]; + options.push( + <option + key={'font_' + idx} + value={fontName} + className={className} + > + {fontName} + </option> + ); + }); + + const inputs = [ + <div key='userDisplayNameOptions'> + <div + className='input-group theme-group dropdown' + > + <select + className='form-control' + type='text' + value={this.state.selectedFont} + onChange={(e) => this.handleFont(e.target.value)} + > + {options} + </select> + <span className={'input-group-addon ' + Constants.FONTS[this.state.selectedFont]}> + {this.state.selectedFont} + </span> + </div> + <div><br/>{'Select the font displayed in the Mattermost user interface.'}</div> + </div> + ]; + + fontSection = ( + <SettingItemMax + title='Display Font' + inputs={inputs} + submit={this.handleSubmit} + server_error={serverError} + updateSection={(e) => { + if (this.selectedFont !== this.state.selectedFont) { + this.handleFont(this.selectedFont); + } + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + fontSection = ( + <SettingItemMin + title='Display Font' + describe={this.state.selectedFont} + updateSection={() => { + this.props.updateSection('font'); + }} + /> + ); + } + return ( <div> <div className='modal-header'> @@ -239,6 +316,8 @@ export default class UserSettingsDisplay extends React.Component { <div className='divider-dark'/> {nameFormatSection} <div className='divider-dark'/> + {fontSection} + <div className='divider-dark'/> </div> </div> ); diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index e025bf670..f762405af 100644 --- a/web/react/components/user_settings/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -78,7 +78,9 @@ export default class NotificationsTab extends React.Component { super(props); this.handleSubmit = this.handleSubmit.bind(this); + this.handleCancel = this.handleCancel.bind(this); this.updateSection = this.updateSection.bind(this); + this.updateState = this.updateState.bind(this); this.onListenerChange = this.onListenerChange.bind(this); this.handleNotifyRadio = this.handleNotifyRadio.bind(this); this.handleEmailRadio = this.handleEmailRadio.bind(this); @@ -128,10 +130,21 @@ export default class NotificationsTab extends React.Component { }.bind(this) ); } + handleCancel(e) { + this.updateState(); + this.props.updateSection(''); + e.preventDefault(); + } updateSection(section) { - this.setState(getNotificationsStateFromStores()); + this.updateState(); this.props.updateSection(section); } + updateState() { + const newState = getNotificationsStateFromStores(); + if (!Utils.areObjectsEqual(newState, this.state)) { + this.setState(newState); + } + } componentDidMount() { UserStore.addChangeListener(this.onListenerChange); } @@ -139,10 +152,7 @@ export default class NotificationsTab extends React.Component { UserStore.removeChangeListener(this.onListenerChange); } onListenerChange() { - var newState = getNotificationsStateFromStores(); - if (!Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } + this.updateState(); } handleNotifyRadio(notifyLevel) { this.setState({notifyLevel: notifyLevel}); @@ -245,11 +255,6 @@ export default class NotificationsTab extends React.Component { </div> ); - handleUpdateDesktopSection = function updateDesktopSection(e) { - this.props.updateSection(''); - e.preventDefault(); - }.bind(this); - const extraInfo = <span>{'Desktop notifications are available on Firefox, Safari, and Chrome.'}</span>; desktopSection = ( @@ -259,7 +264,7 @@ export default class NotificationsTab extends React.Component { inputs={inputs} submit={this.handleSubmit} server_error={serverError} - updateSection={handleUpdateDesktopSection} + updateSection={this.handleCancel} /> ); } else { @@ -324,11 +329,6 @@ export default class NotificationsTab extends React.Component { </div> ); - handleUpdateSoundSection = function updateSoundSection(e) { - this.props.updateSection(''); - e.preventDefault(); - }.bind(this); - const extraInfo = <span>{'Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'}</span>; soundSection = ( @@ -338,7 +338,7 @@ export default class NotificationsTab extends React.Component { inputs={inputs} submit={this.handleSubmit} server_error={serverError} - updateSection={handleUpdateSoundSection} + updateSection={this.handleCancel} /> ); } else { @@ -405,18 +405,13 @@ export default class NotificationsTab extends React.Component { </div> ); - handleUpdateEmailSection = function updateEmailSection(e) { - this.props.updateSection(''); - e.preventDefault(); - }.bind(this); - emailSection = ( <SettingItemMax title='Email notifications' inputs={inputs} submit={this.handleSubmit} server_error={serverError} - updateSection={handleUpdateEmailSection} + updateSection={this.handleCancel} /> ); } else { @@ -566,17 +561,13 @@ export default class NotificationsTab extends React.Component { </div> ); - handleUpdateKeysSection = function updateKeysSection(e) { - this.props.updateSection(''); - e.preventDefault(); - }.bind(this); keysSection = ( <SettingItemMax title='Words that trigger mentions' inputs={inputs} submit={this.handleSubmit} server_error={serverError} - updateSection={handleUpdateKeysSection} + updateSection={this.handleCancel} /> ); } else { @@ -653,7 +644,7 @@ export default class NotificationsTab extends React.Component { ref='wrapper' className='user-settings' > - <h3 className='tab-header'>Notifications</h3> + <h3 className='tab-header'>{'Notifications'}</h3> <div className='divider-dark first'/> {desktopSection} <div className='divider-light'/> diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 2b505607e..820f8fd8e 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -423,10 +423,11 @@ export default class ViewImageModal extends React.Component { onClick={this.props.onModalDismissed} > <div - className={'image-wrapper ' + bgClass} + className={'image-wrapper'} onClick={this.props.onModalDismissed} > <div + className={bgClass} onMouseEnter={this.onMouseEnterImage} onMouseLeave={this.onMouseLeaveImage} onClick={(e) => e.stopPropagation()} |