summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/.eslintrc2
-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
-rw-r--r--web/react/dispatcher/event_helpers.jsx11
-rw-r--r--web/react/stores/channel_store.jsx44
-rw-r--r--web/react/stores/modal_store.jsx1
-rw-r--r--web/react/stores/suggestion_store.jsx246
-rw-r--r--web/react/utils/async_client.jsx20
-rw-r--r--web/react/utils/constants.jsx25
-rw-r--r--web/react/utils/text_formatting.jsx2
-rw-r--r--web/react/utils/utils.jsx28
26 files changed, 1098 insertions, 566 deletions
diff --git a/web/react/.eslintrc b/web/react/.eslintrc
index 935bb638a..baaf7eaa5 100644
--- a/web/react/.eslintrc
+++ b/web/react/.eslintrc
@@ -47,7 +47,7 @@
"no-irregular-whitespace": 2,
"no-unexpected-multiline": 2,
"no-unreachable": 2,
- "no-magic-numbers": [1, { "enforceConst": true, "detectObjects": true } ],
+ "no-magic-numbers": [1, { "ignore": [-1, 0, 1, 2], "enforceConst": true, "detectObjects": true } ],
"valid-typeof": 2,
"block-scoped-var": 2,
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()}
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx
index d7f255aaa..57b4eaa11 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -11,8 +11,8 @@ import * as Client from '../utils/client.jsx';
export function emitChannelClickEvent(channel) {
AsyncClient.getChannels();
- AsyncClient.getChannelExtraInfo();
- AsyncClient.updateLastViewedAt();
+ AsyncClient.getChannelExtraInfo(channel.id);
+ AsyncClient.updateLastViewedAt(channel.id);
AsyncClient.getPosts(channel.id);
AppDispatcher.handleViewAction({
@@ -104,3 +104,10 @@ export function showInviteMemberModal() {
value: true
});
}
+
+export function showRegisterAppModal() {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_REGISTER_APP_MODAL,
+ value: true
+ });
+}
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index dec4926f5..5dec86951 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -7,6 +7,7 @@ import EventEmitter from 'events';
var Utils;
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
+const NotificationPrefs = Constants.NotificationPrefs;
const CHANGE_EVENT = 'change';
const LEAVE_EVENT = 'leave';
@@ -37,6 +38,10 @@ class ChannelStoreClass extends EventEmitter {
this.getByName = this.getByName.bind(this);
this.pSetPostMode = this.pSetPostMode.bind(this);
this.getPostMode = this.getPostMode.bind(this);
+ this.setUnreadCount = this.setUnreadCount.bind(this);
+ this.setUnreadCounts = this.setUnreadCounts.bind(this);
+ this.getUnreadCount = this.getUnreadCount.bind(this);
+ this.getUnreadCounts = this.getUnreadCounts.bind(this);
this.currentId = null;
this.postMode = this.POST_MODE_CHANNEL;
@@ -45,6 +50,7 @@ class ChannelStoreClass extends EventEmitter {
this.moreChannels = {};
this.moreChannels.loading = true;
this.extraInfos = {};
+ this.unreadCounts = {};
}
get POST_MODE_CHANNEL() {
return 1;
@@ -120,18 +126,18 @@ class ChannelStoreClass extends EventEmitter {
this.currentId = id;
}
resetCounts(id) {
- var cm = this.pGetChannelMembers();
+ const cm = this.channelMembers;
for (var cmid in cm) {
if (cm[cmid].channel_id === id) {
var c = this.get(id);
if (c) {
cm[cmid].msg_count = this.get(id).total_msg_count;
cm[cmid].mention_count = 0;
+ this.setUnreadCount(id);
}
break;
}
}
- this.pStoreChannelMembers(cm);
}
getCurrentId() {
return this.currentId;
@@ -250,6 +256,38 @@ class ChannelStoreClass extends EventEmitter {
getPostMode() {
return this.postMode;
}
+
+ setUnreadCount(id) {
+ const ch = this.get(id);
+ const chMember = this.getMember(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;
+ }
+
+ this.unreadCounts[id] = {msgs: chUnreadCount, mentions: chMentionCount};
+ }
+
+ setUnreadCounts() {
+ const channels = this.getAll();
+ channels.forEach((ch) => {
+ this.setUnreadCount(ch.id);
+ });
+ }
+
+ getUnreadCount(id) {
+ return this.unreadCounts[id] || {msgs: 0, mentions: 0};
+ }
+
+ getUnreadCounts() {
+ return this.unreadCounts;
+ }
}
var ChannelStore = new ChannelStoreClass();
@@ -281,6 +319,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
if (currentId) {
ChannelStore.resetCounts(currentId);
}
+ ChannelStore.setUnreadCounts();
ChannelStore.emitChange();
break;
@@ -291,6 +330,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
if (currentId) {
ChannelStore.resetCounts(currentId);
}
+ ChannelStore.setUnreadCount(action.channel.id);
ChannelStore.emitChange();
break;
diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx
index a26a97f53..9f33cf022 100644
--- a/web/react/stores/modal_store.jsx
+++ b/web/react/stores/modal_store.jsx
@@ -35,6 +35,7 @@ class ModalStoreClass extends EventEmitter {
case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL:
case ActionTypes.TOGGLE_DELETE_POST_MODAL:
case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
+ case ActionTypes.TOGGLE_REGISTER_APP_MODAL:
this.emit(type, value, args);
break;
}
diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx
new file mode 100644
index 000000000..016929501
--- /dev/null
+++ b/web/react/stores/suggestion_store.jsx
@@ -0,0 +1,246 @@
+// 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 EventEmitter from 'events';
+
+const ActionTypes = Constants.ActionTypes;
+
+const COMPLETE_WORD_EVENT = 'complete_word';
+const PRETEXT_CHANGED_EVENT = 'pretext_changed';
+const SUGGESTIONS_CHANGED_EVENT = 'suggestions_changed';
+
+class SuggestionStore extends EventEmitter {
+ constructor() {
+ super();
+
+ this.addSuggestionsChangedListener = this.addSuggestionsChangedListener.bind(this);
+ this.removeSuggestionsChangedListener = this.removeSuggestionsChangedListener.bind(this);
+ this.emitSuggestionsChanged = this.emitSuggestionsChanged.bind(this);
+
+ this.addPretextChangedListener = this.addPretextChangedListener.bind(this);
+ this.removePretextChangedListener = this.removePretextChangedListener.bind(this);
+ this.emitPretextChanged = this.emitPretextChanged.bind(this);
+
+ this.addCompleteWordListener = this.addCompleteWordListener.bind(this);
+ this.removeCompleteWordListener = this.removeCompleteWordListener.bind(this);
+ this.emitCompleteWord = this.emitCompleteWord.bind(this);
+
+ this.handleEventPayload = this.handleEventPayload.bind(this);
+ this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
+
+ // this.suggestions stores the state of all SuggestionBoxes by mapping their unique identifier to an
+ // object with the following fields:
+ // pretext: the text before the cursor
+ // matchedPretext: the text before the cursor that will be replaced if an autocomplete term is selected
+ // terms: a list of strings which the previously typed text may be replaced by
+ // items: a list of objects backing the terms which may be used in rendering
+ // components: a list of react components that can be used to render their corresponding item
+ // selection: the term currently selected by the keyboard
+ this.suggestions = new Map();
+ }
+
+ addSuggestionsChangedListener(id, callback) {
+ this.on(SUGGESTIONS_CHANGED_EVENT + id, callback);
+ }
+ removeSuggestionsChangedListener(id, callback) {
+ this.removeListener(SUGGESTIONS_CHANGED_EVENT + id, callback);
+ }
+ emitSuggestionsChanged(id) {
+ this.emit(SUGGESTIONS_CHANGED_EVENT + id);
+ }
+
+ addPretextChangedListener(id, callback) {
+ this.on(PRETEXT_CHANGED_EVENT + id, callback);
+ }
+ removePretextChangedListener(id, callback) {
+ this.removeListener(PRETEXT_CHANGED_EVENT + id, callback);
+ }
+ emitPretextChanged(id, pretext) {
+ this.emit(PRETEXT_CHANGED_EVENT + id, pretext);
+ }
+
+ addCompleteWordListener(id, callback) {
+ this.on(COMPLETE_WORD_EVENT + id, callback);
+ }
+ removeCompleteWordListener(id, callback) {
+ this.removeListener(COMPLETE_WORD_EVENT + id, callback);
+ }
+ emitCompleteWord(id, term) {
+ this.emit(COMPLETE_WORD_EVENT + id, term);
+ }
+
+ registerSuggestionBox(id) {
+ this.suggestions.set(id, {
+ pretext: '',
+ matchedPretext: '',
+ terms: [],
+ items: [],
+ components: [],
+ selection: ''
+ });
+ }
+
+ unregisterSuggestionBox(id) {
+ this.suggestions.delete(id);
+ }
+
+ clearSuggestions(id) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.matchedPretext = '';
+ suggestion.terms = [];
+ suggestion.items = [];
+ suggestion.components = [];
+ suggestion.selection = '';
+ }
+
+ hasSuggestions(id) {
+ return this.suggestions.get(id).terms.length > 0;
+ }
+
+ setPretext(id, pretext) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.pretext = pretext;
+ }
+
+ setMatchedPretext(id, matchedPretext) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.matchedPretext = matchedPretext;
+ }
+
+ addSuggestion(id, term, item, component) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.terms.push(term);
+ suggestion.items.push(item);
+ suggestion.components.push(component);
+ }
+
+ addSuggestions(id, terms, items, component) {
+ const suggestion = this.suggestions.get(id);
+
+ suggestion.terms.push(...terms);
+ suggestion.items.push(...items);
+
+ for (let i = 0; i < terms.length; i++) {
+ suggestion.components.push(component);
+ }
+ }
+
+ // make sure that if suggestions exist, then one of them is selected. return true if the selection changes.
+ ensureSelectionExists(id) {
+ const suggestion = this.suggestions.get(id);
+
+ if (suggestion.terms.length > 0) {
+ // if the current selection is no longer in the map, select the first term in the list
+ if (!suggestion.selection || suggestion.terms.indexOf(suggestion.selection) === -1) {
+ suggestion.selection = suggestion.terms[0];
+
+ return true;
+ }
+ } else if (suggestion.selection) {
+ suggestion.selection = '';
+
+ return true;
+ }
+
+ return false;
+ }
+
+ getPretext(id) {
+ return this.suggestions.get(id).pretext;
+ }
+
+ getMatchedPretext(id) {
+ return this.suggestions.get(id).matchedPretext;
+ }
+
+ getItems(id) {
+ return this.suggestions.get(id).items;
+ }
+
+ getTerms(id) {
+ return this.suggestions.get(id).terms;
+ }
+
+ getComponents(id) {
+ return this.suggestions.get(id).components;
+ }
+
+ getSelection(id) {
+ return this.suggestions.get(id).selection;
+ }
+
+ selectNext(id) {
+ this.setSelectionByDelta(id, 1);
+ }
+
+ selectPrevious(id) {
+ this.setSelectionByDelta(id, -1);
+ }
+
+ setSelectionByDelta(id, delta) {
+ const suggestion = this.suggestions.get(id);
+
+ let selectionIndex = suggestion.terms.indexOf(suggestion.selection);
+
+ if (selectionIndex === -1) {
+ // this should never happen since selection should always be in terms
+ throw new Error('selection is not in terms');
+ }
+
+ selectionIndex += delta;
+
+ if (selectionIndex < 0) {
+ selectionIndex = 0;
+ } else if (selectionIndex > suggestion.terms.length - 1) {
+ selectionIndex = suggestion.terms.length - 1;
+ }
+
+ suggestion.selection = suggestion.terms[selectionIndex];
+ }
+
+ handleEventPayload(payload) {
+ const {type, id, ...other} = payload.action; // eslint-disable-line no-redeclare
+
+ switch (type) {
+ case ActionTypes.SUGGESTION_PRETEXT_CHANGED:
+ this.clearSuggestions(id);
+
+ this.setPretext(id, other.pretext);
+ this.emitPretextChanged(id, other.pretext);
+
+ this.ensureSelectionExists(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
+ this.setMatchedPretext(id, other.matchedPretext);
+ this.addSuggestions(id, other.terms, other.items, other.componentType);
+
+ this.ensureSelectionExists(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ case ActionTypes.SUGGESTION_SELECT_NEXT:
+ this.selectNext(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ case ActionTypes.SUGGESTION_SELECT_PREVIOUS:
+ this.selectPrevious(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ case ActionTypes.SUGGESTION_COMPLETE_WORD:
+ this.emitCompleteWord(id, other.term || this.getSelection(id), this.getMatchedPretext(id));
+
+ this.setPretext(id, '');
+ this.clearSuggestions(id);
+ this.emitSuggestionsChanged(id);
+ break;
+ }
+ }
+}
+
+export default new SuggestionStore(); \ No newline at end of file
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 8cf111d55..5df43b548 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -106,10 +106,15 @@ export function getChannel(id) {
);
}
-export function updateLastViewedAt() {
- const channelId = ChannelStore.getCurrentId();
+export function updateLastViewedAt(id) {
+ let channelId;
+ if (id) {
+ channelId = id;
+ } else {
+ channelId = ChannelStore.getCurrentId();
+ }
- if (channelId === null) {
+ if (channelId == null) {
return;
}
@@ -159,8 +164,13 @@ export function getMoreChannels(force) {
}
}
-export function getChannelExtraInfo() {
- const channelId = ChannelStore.getCurrentId();
+export function getChannelExtraInfo(id) {
+ let channelId;
+ if (id) {
+ channelId = id;
+ } else {
+ channelId = ChannelStore.getCurrentId();
+ }
if (channelId != null) {
if (isCallInProgress('getChannelExtraInfo_' + channelId)) {
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index cbeab18ba..8164095b9 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -24,6 +24,7 @@ export default {
RECIEVED_POST: null,
RECIEVED_EDIT_POST: null,
RECIEVED_SEARCH: null,
+ RECIEVED_SEARCH_TERM: null,
RECIEVED_POST_SELECTED: null,
RECIEVED_MENTION_DATA: null,
RECIEVED_ADD_MENTION: null,
@@ -49,7 +50,14 @@ export default {
TOGGLE_IMPORT_THEME_MODAL: null,
TOGGLE_INVITE_MEMBER_MODAL: null,
TOGGLE_DELETE_POST_MODAL: null,
- TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null
+ TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
+ TOGGLE_REGISTER_APP_MODAL: null,
+
+ SUGGESTION_PRETEXT_CHANGED: null,
+ SUGGESTION_RECEIVED_SUGGESTIONS: null,
+ SUGGESTION_COMPLETE_WORD: null,
+ SUGGESTION_SELECT_NEXT: null,
+ SUGGESTION_SELECT_PREVIOUS: null
}),
PayloadSources: keyMirror({
@@ -345,6 +353,21 @@ export default {
}
],
DEFAULT_CODE_THEME: 'github',
+ FONTS: {
+ 'Droid Serif': 'font--droid_serif',
+ 'Roboto Slab': 'font--roboto_slab',
+ Lora: 'font--lora',
+ Slabo: 'font--slabo',
+ Arvo: 'font--arvo',
+ 'Open Sans': 'font--open_sans',
+ Roboto: 'font--roboto',
+ 'PT Sans': 'font--pt_sans',
+ Lato: 'font--lato',
+ 'Source Sans Pro': 'font--source_sans_pro',
+ 'Exo 2': 'font--exo_2',
+ Ubuntu: 'font--ubuntu'
+ },
+ DEFAULT_FONT: 'Open Sans',
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 3a912fd75..f0bd46f9d 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -188,7 +188,7 @@ function highlightCurrentMentions(text, tokens) {
const newAlias = `MM_SELFMENTION${index}`;
newTokens.set(newAlias, {
- value: `<span class='mention-highlight'>${alias}</span>` + token.extraText,
+ value: `<span class='mention-highlight'>${alias}</span>` + (token.extraText || ''),
originalText: token.originalText
});
output = output.replace(alias, newAlias);
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index f38d3e051..ab09ea919 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -539,11 +539,11 @@ export function applyTheme(theme) {
if (theme.sidebarText) {
changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
- changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
- changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
}
if (theme.sidebarUnreadText) {
@@ -552,7 +552,7 @@ export function applyTheme(theme) {
if (theme.sidebarTextHoverBg) {
changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1);
- changeCss('@media(max-width: 960px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
}
if (theme.sidebarTextActiveBorder) {
@@ -568,7 +568,7 @@ export function applyTheme(theme) {
changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
- changeCss('@media(max-width: 960px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarHeaderBg, 1);
changeCss('.attachment .attachment__container', 'border-left-color:' + theme.sidebarHeaderBg, 1);
}
@@ -579,7 +579,7 @@ export function applyTheme(theme) {
changeCss('.modal .modal-header .modal-title, .modal .modal-header .modal-title .name, .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('#navbar .navbar-default .navbar-brand .heading', 'color:' + theme.sidebarHeaderTextColor, 1);
changeCss('#navbar .navbar-default .navbar-toggle .icon-bar, ', 'background:' + theme.sidebarHeaderTextColor, 1);
- changeCss('@media(max-width: 960px){.search-bar__container', 'color:' + theme.sidebarHeaderTextColor, 2);
+ changeCss('@media(max-width: 768px){.search-bar__container', 'color:' + theme.sidebarHeaderTextColor, 2);
}
if (theme.onlineIndicator) {
@@ -607,7 +607,7 @@ export function applyTheme(theme) {
changeCss('.popover.right>.arrow:after, .tip-overlay.tip-overlay--sidebar .arrow, .tip-overlay.tip-overlay--header .arrow', 'border-right-color:' + theme.centerChannelBg, 1);
changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1);
changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1);
- changeCss('.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
+ changeCss('@media(min-width: 768px){.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
changeCss('.attachment__content', 'background:' + theme.centerChannelBg, 1);
}
@@ -638,8 +638,7 @@ export function applyTheme(theme) {
changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
- changeCss('.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
- changeCss('@media(max-width: 960px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
+ changeCss('@media(min-width: 768px){.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2);
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
@@ -694,6 +693,19 @@ export function applyTheme(theme) {
}
updateCodeTheme(theme.codeTheme);
}
+
+export function applyFont(fontName) {
+ const body = document.querySelector('body');
+ const keys = Object.getOwnPropertyNames(body.classList);
+ keys.forEach((k) => {
+ const className = body.classList[k];
+ if (className && className.lastIndexOf('font') === 0) {
+ body.classList.remove(className);
+ }
+ });
+ body.classList.add(Constants.FONTS[fontName]);
+}
+
export function changeCss(className, classValue, classRepeat) {
// we need invisible container to store additional css definitions
var cssMainContainer = $('#css-modifier-container');