summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/channel_loader.jsx4
-rw-r--r--web/react/components/channel_notifications_modal.jsx12
-rw-r--r--web/react/components/get_link_modal.jsx2
-rw-r--r--web/react/components/posts_view.jsx14
-rw-r--r--web/react/components/register_app_modal.jsx192
-rw-r--r--web/react/components/search_autocomplete.jsx341
-rw-r--r--web/react/components/search_bar.jsx47
-rw-r--r--web/react/components/search_channel_provider.jsx69
-rw-r--r--web/react/components/search_user_provider.jsx62
-rw-r--r--web/react/components/sidebar.jsx67
-rw-r--r--web/react/components/suggestion_box.jsx170
-rw-r--r--web/react/components/suggestion_list.jsx157
-rw-r--r--web/react/components/team_general_tab.jsx1
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx12
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx83
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx49
-rw-r--r--web/react/components/view_image.jsx3
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()}