diff options
author | =Corey Hulen <corey@hulen.com> | 2015-10-25 22:42:38 -0700 |
---|---|---|
committer | =Corey Hulen <corey@hulen.com> | 2015-10-25 22:42:38 -0700 |
commit | 70b5918734d996a2b5b3710c2ed519f77f4764d4 (patch) | |
tree | 12aac9f88de68cb79404a3985e6bfe774ad8f066 /web/react | |
parent | 473221dbada7ad7739d6a969d9d3d5c9c276941b (diff) | |
parent | ae2898107d275176126ab07ca1886fd7fd7ddad4 (diff) | |
download | chat-70b5918734d996a2b5b3710c2ed519f77f4764d4.tar.gz chat-70b5918734d996a2b5b3710c2ed519f77f4764d4.tar.bz2 chat-70b5918734d996a2b5b3710c2ed519f77f4764d4.zip |
Merge branch 'master' into PLT-25
Diffstat (limited to 'web/react')
20 files changed, 437 insertions, 124 deletions
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx index 6151d4bdd..43700bf36 100644 --- a/web/react/components/channel_notifications.jsx +++ b/web/react/components/channel_notifications.jsx @@ -136,16 +136,15 @@ export default class ChannelNotifications extends React.Component { var inputs = []; inputs.push( - <div> + <div key='channel-notification-level-radio'> <div className='radio'> <label> <input type='radio' checked={notifyActive[0]} onChange={this.handleUpdateNotifyLevel.bind(this, 'default')} - > + /> {`Global default (${globalNotifyLevelName})`} - </input> </label> <br/> </div> @@ -155,9 +154,8 @@ export default class ChannelNotifications extends React.Component { type='radio' checked={notifyActive[1]} onChange={this.handleUpdateNotifyLevel.bind(this, 'all')} - > + /> {'For all activity'} - </input> </label> <br/> </div> @@ -167,9 +165,8 @@ export default class ChannelNotifications extends React.Component { type='radio' checked={notifyActive[2]} onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')} - > + /> {'Only for mentions'} - </input> </label> <br/> </div> @@ -179,9 +176,8 @@ export default class ChannelNotifications extends React.Component { type='radio' checked={notifyActive[3]} onChange={this.handleUpdateNotifyLevel.bind(this, 'none')} - > + /> {'Never'} - </input> </label> </div> </div> @@ -274,16 +270,15 @@ export default class ChannelNotifications extends React.Component { if (this.state.activeSection === 'markUnreadLevel') { const inputs = [( - <div> + <div key='channel-notification-unread-radio'> <div className='radio'> <label> <input type='radio' checked={this.state.markUnreadLevel === 'all'} onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')} - > + /> {'For all unread messages'} - </input> </label> <br /> </div> @@ -293,9 +288,8 @@ export default class ChannelNotifications extends React.Component { type='radio' checked={this.state.markUnreadLevel === 'mention'} onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')} - > + /> {'Only for mentions'} - </input> </label> <br /> </div> @@ -370,7 +364,7 @@ export default class ChannelNotifications extends React.Component { data-dismiss='modal' > <span aria-hidden='true'>×</span> - <span className='sr-only'>Close</span> + <span className='sr-only'>{'Close'}</span> </button> <h4 className='modal-title'>Notification Preferences for <span className='name'>{this.state.title}</span></h4> </div> diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 4986f88b6..86a4b04cf 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -260,6 +260,12 @@ export default class InviteMemberModal extends React.Component { var content = null; var sendButton = null; + + var sendButtonLabel = 'Send Invitation'; + if (this.state.inviteIds.length > 1) { + sendButtonLabel = 'Send Invitations'; + } + if (this.state.emailEnabled) { content = ( <div> @@ -281,7 +287,7 @@ export default class InviteMemberModal extends React.Component { onClick={this.handleSubmit} type='button' className='btn btn-primary' - >Send Invitations</button> + >{sendButtonLabel}</button> ); } else { var teamInviteLink = null; diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 743e30854..41746d1d7 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -169,7 +169,7 @@ export default class MoreDirectChannels extends React.Component { } return ( - <tr> + <tr key={'direct-channel-row-user' + user.id}> <td key={user.id} className='direct-channel' diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx new file mode 100644 index 000000000..03c7b894c --- /dev/null +++ b/web/react/components/search_autocomplete.jsx @@ -0,0 +1,249 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const ChannelStore = require('../stores/channel_store.jsx'); +const KeyCodes = require('../utils/constants.jsx').KeyCodes; +const UserStore = require('../stores/user_store.jsx'); +const Utils = require('../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.updateSuggestions = this.updateSuggestions.bind(this); + + this.state = { + show: false, + mode: '', + filter: '', + selection: 0, + suggestions: new Map() + }; + } + + componentDidMount() { + $(document).on('click', this.handleDocumentClick); + } + + componentWillUnmount() { + $(document).off('click', this.handleDocumentClick); + } + + handleClick(value) { + this.completeWord(value); + } + + handleDocumentClick(e) { + const container = $(ReactDOM.findDOMNode(this.refs.container)); + + 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.completeSelectedWord(); + } + } + + completeSelectedWord() { + if (this.state.mode === 'channels') { + this.completeWord(this.state.suggestions[this.state.selection].name); + } else if (this.state.mode === 'users') { + this.completeWord(this.state.suggestions[this.state.selection].username); + } + } + + 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 + }); + } + + updateSuggestions(mode, filter) { + let suggestions = []; + + if (mode === 'channels') { + let channels = ChannelStore.getAll(); + + if (filter) { + channels = channels.filter((channel) => channel.name.startsWith(filter)); + } + + channels.sort((a, b) => a.name.localeCompare(b.name)); + + 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 + }); + } + + render() { + if (!this.state.show || this.state.suggestions.length === 0) { + return null; + } + + let suggestions = []; + + if (this.state.mode === 'channels') { + suggestions = this.state.suggestions.map((channel, index) => { + let className = 'search-autocomplete__channel'; + if (this.state.selection === index) { + className += ' selected'; + } + + return ( + <div + key={channel.name} + ref={channel.name} + onClick={this.handleClick.bind(this, channel.name)} + className={className} + > + {channel.name} + </div> + ); + }); + } else if (this.state.mode === 'users') { + suggestions = this.state.suggestions.map((user, index) => { + let className = 'search-autocomplete__user'; + if (this.state.selection === index) { + className += ' selected'; + } + + return ( + <div + key={user.username} + ref={user.username} + onClick={this.handleClick.bind(this, user.username)} + className={className} + > + <img + className='profile-img' + src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at} + /> + {user.username} + </div> + ); + }); + } + + return ( + <div + ref='container' + className='search-autocomplete' + > + {suggestions} + </div> + ); + } +} + +SearchAutocomplete.propTypes = { + completeWord: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index 7540b41a4..e1d36ad7d 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -9,6 +9,7 @@ var utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; var Popover = ReactBootstrap.Popover; +var SearchAutocomplete = require('./search_autocomplete.jsx'); export default class SearchBar extends React.Component { constructor() { @@ -16,11 +17,13 @@ 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; @@ -74,11 +77,18 @@ 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; PostStore.storeSearchTerm(term); PostStore.emitSearchTermChange(false); this.setState({searchTerm: term}); + + this.refs.autocomplete.handleInputChange(e.target, term); } handleMouseInput(e) { e.preventDefault(); @@ -97,7 +107,7 @@ export default class SearchBar extends React.Component { this.setState({isSearching: true}); client.search( terms, - function success(data) { + (data) => { this.setState({isSearching: false}); if (utils.isMobile()) { ReactDOM.findDOMNode(this.refs.search).value = ''; @@ -108,11 +118,11 @@ export default class SearchBar extends React.Component { results: data, is_mention_search: isMentionSearch }); - }.bind(this), - function error(err) { + }, + (err) => { this.setState({isSearching: false}); AsyncClient.dispatchError(err, 'search'); - }.bind(this) + } ); } } @@ -120,6 +130,24 @@ export default class SearchBar extends React.Component { e.preventDefault(); 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); + + PostStore.storeSearchTerm(text); + PostStore.emitSearchTermChange(false); + this.setState({searchTerm: text}); + } + render() { var isSearching = null; if (this.state.isSearching) { @@ -143,12 +171,13 @@ export default class SearchBar extends React.Component { className='search__clear' onClick={this.clearFocus} > - Cancel + {'Cancel'} </span> <form role='form' className='search__form relative-div' onSubmit={this.handleSubmit} + style={{overflow: 'visible'}} > <span className='glyphicon glyphicon-search sidebar__search-icon' /> <input @@ -160,10 +189,16 @@ export default class SearchBar extends React.Component { onFocus={this.handleUserFocus} onBlur={this.handleUserBlur} onChange={this.handleUserInput} + onKeyDown={this.handleKeyDown} onMouseUp={this.handleMouseInput} /> {isSearching} + <SearchAutocomplete + ref='autocomplete' + completeWord={this.completeWord} + /> <Popover + id='searchbar-help-popup' placement='bottom' className={helpClass} > diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 4f0fe3ed0..774f98a43 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -36,7 +36,7 @@ export default class SettingItemMax extends React.Component { if (this.props.width === 'full') { widthClass = 'col-sm-12'; } else { - widthClass = 'col-sm-9 col-sm-offset-3'; + widthClass = 'col-sm-10 col-sm-offset-2'; } return ( diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index 66568e1c8..4af46c35a 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -2,6 +2,10 @@ // See License.txt for license information. export default class SettingsSidebar extends React.Component { + componentDidUpdate() { + $('.settings-modal').find('.modal-body').scrollTop(0); + $('.settings-modal').find('.modal-body').perfectScrollbar('update'); + } constructor(props) { super(props); diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index b55373dba..5c5995020 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -19,6 +19,7 @@ export default class TeamSettingsModal extends React.Component { componentDidMount() { $('body').on('click', '.modal-back', function handleBackClick() { $(this).closest('.modal-dialog').removeClass('display--content'); + $(this).closest('.modal-dialog').find('.settings-table .nav li.active').removeClass('active'); }); $('body').on('click', '.modal-header .close', () => { setTimeout(() => { diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx index b42343da0..7b4db8fae 100644 --- a/web/react/components/team_signup_send_invites_page.jsx +++ b/web/react/components/team_signup_send_invites_page.jsx @@ -15,11 +15,6 @@ export default class TeamSignupSendInvitesPage extends React.Component { this.state = { emailEnabled: global.window.mm_config.SendEmailNotifications === 'true' }; - - if (!this.state.emailEnabled) { - this.props.state.wizard = 'username'; - this.props.updateParent(this.props.state); - } } submitBack(e) { e.preventDefault(); diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx index 6d333aed6..02d5cab8e 100644 --- a/web/react/components/team_signup_url_page.jsx +++ b/web/react/components/team_signup_url_page.jsx @@ -54,7 +54,11 @@ export default class TeamSignupUrlPage extends React.Component { if (data) { this.setState({nameError: 'This URL is unavailable. Please try another.'}); } else { - this.props.state.wizard = 'send_invites'; + if (global.window.mm_config.SendEmailNotifications === 'true') { + this.props.state.wizard = 'send_invites'; + } else { + this.props.state.wizard = 'username'; + } this.props.state.team.type = 'O'; this.props.state.team.name = name; diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx index ee325fcaf..9448413ce 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/team_signup_welcome_page.jsx @@ -104,21 +104,19 @@ export default class TeamSignupWelcomePage extends React.Component { return ( <div> - <p> - <img - className='signup-team-logo' - src='/static/images/logo.png' - /> - <h3 className='sub-heading'>Welcome to:</h3> - <h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1> - </p> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h3 className='sub-heading'>Welcome to:</h3> + <h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1> <p className='margin--less'>Let's set up your new team</p> - <p> + <div> Please confirm your email address:<br /> <div className='inner__content'> <div className='block--gray'>{this.props.state.team.email}</div> </div> - </p> + </div> <p className='margin--extra color--light'> Your account will administer the new team site. <br /> You can add other administrators later. diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx index 812169be4..6b8c09718 100644 --- a/web/react/components/user_settings/manage_incoming_hooks.jsx +++ b/web/react/components/user_settings/manage_incoming_hooks.jsx @@ -119,24 +119,23 @@ export default class ManageIncomingHooks extends React.Component { hooks.push( <div key={hook.id} - className='font--small' + className='webhook__item' > - <div className='padding-top x2 divider-light'></div> - <div className='padding-top x2'> - <strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span> + <div className='padding-top x2 webhook__url'> + <strong>{'URL: '}</strong> + <span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span> </div> <div className='padding-top'> <strong>{'Channel: '}</strong>{c.display_name} </div> - <div className='padding-top'> - <a - className={'text-danger'} - href='#' - onClick={this.removeHook.bind(this, hook.id)} - > - {'Remove'} - </a> - </div> + <a + className={'webhook__remove'} + href='#' + onClick={this.removeHook.bind(this, hook.id)} + > + <span aria-hidden='true'>{'×'}</span> + </a> + <div className='padding-top x2 divider-light'></div> </div> ); } @@ -148,35 +147,38 @@ export default class ManageIncomingHooks extends React.Component { } else if (hooks.length > 0) { displayHooks = hooks; } else { - displayHooks = <label>{': None'}</label>; + displayHooks = <div className='padding-top x2'>{'None'}</div>; } const existingHooks = ( - <div className='padding-top x2'> + <div className='webhooks__container'> <label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label> - {displayHooks} + <div className='padding-top divider-light'></div> + <div className='webhooks__list'> + {displayHooks} + </div> </div> ); return ( <div key='addIncomingHook'> {'Create webhook URLs for use in external integrations. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'} - <br/> - <br/> - <label className='control-label'>{'Add a new incoming webhook'}</label> - <div className='padding-top'> - <select - ref='channelName' - className='form-control' - value={this.state.channelId} - onChange={this.updateChannelId} - > - {options} - </select> - {serverError} - <div className='padding-top'> + <label className='control-label padding-top x2'>{'Add a new incoming webhook'}</label> + <div className='row padding-top'> + <div className='col-sm-10 padding-bottom'> + <select + ref='channelName' + className='form-control' + value={this.state.channelId} + onChange={this.updateChannelId} + > + {options} + </select> + {serverError} + </div> + <div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'> <a - className={'btn btn-sm btn-primary' + disableButton} + className={'btn form-control no-padding btn-sm btn-primary' + disableButton} href='#' onClick={this.addNewHook} > diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index f6d6b515b..6e9b2205d 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -6,6 +6,7 @@ var Constants = require('../../utils/constants.jsx'); var ChannelStore = require('../../stores/channel_store.jsx'); var LoadingScreen = require('../loading_screen.jsx'); + export default class ManageOutgoingHooks extends React.Component { constructor() { super(); @@ -180,9 +181,8 @@ export default class ManageOutgoingHooks extends React.Component { hooks.push( <div key={hook.id} - className='font--small' + className='webhook__item' > - <div className='padding-top x2 divider-light'></div> <div className='padding-top x2'> <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span> </div> @@ -199,15 +199,15 @@ export default class ManageOutgoingHooks extends React.Component { > {'Regen Token'} </a> - <span>{' - '}</span> <a - className='text-danger' + className='webhook__remove' href='#' onClick={this.removeHook.bind(this, hook.id)} > - {'Remove'} + <span aria-hidden='true'>{'×'}</span> </a> </div> + <div className='padding-top x2 divider-light'></div> </div> ); }); @@ -218,13 +218,16 @@ export default class ManageOutgoingHooks extends React.Component { } else if (hooks.length > 0) { displayHooks = hooks; } else { - displayHooks = <label>{': None'}</label>; + displayHooks = <div className='padding-top x2'>{'None'}</div>; } const existingHooks = ( - <div className='padding-top x2'> + <div className='webhooks__container'> <label className='control-label padding-top x2'>{'Existing outgoing webhooks'}</label> - {displayHooks} + <div className='padding-top divider-light'></div> + <div className='webhooks__list'> + {displayHooks} + </div> </div> ); @@ -234,41 +237,49 @@ export default class ManageOutgoingHooks extends React.Component { <div key='addOutgoingHook'> <label className='control-label'>{'Add a new outgoing webhook'}</label> <div className='padding-top'> - <strong>{'Channel:'}</strong> - <select - ref='channelName' - className='form-control' - value={this.state.channelId} - onChange={this.updateChannelId} - > - {options} - </select> - <span>{'Only public channels can be used'}</span> - <br/> - <br/> - <strong>{'Trigger Words:'}</strong> - <input - ref='triggerWords' - className='form-control' - value={this.state.triggerWords} - onChange={this.updateTriggerWords} - placeholder='Optional if channel selected' - /> - <span>{'Comma separated words to trigger on'}</span> - <br/> - <br/> - <strong>{'Callback URLs:'}</strong> - <textarea - ref='callbackURLs' - className='form-control no-resize' - value={this.state.callbackURLs} - resize={false} - rows={3} - onChange={this.updateCallbackURLs} - /> - <span>{'New line separated URLs that will receive the HTTP POST event'}</span> - {serverError} - <div className='padding-top'> + <div> + <label className='control-label'>{'Channel'}</label> + <div className='padding-top'> + <select + ref='channelName' + className='form-control' + value={this.state.channelId} + onChange={this.updateChannelId} + > + {options} + </select> + </div> + <div className='padding-top'>{'Only public channels can be used'}</div> + </div> + <div className='padding-top x2'> + <label className='control-label'>{'Trigger Words:'}</label> + <div className='padding-top'> + <input + ref='triggerWords' + className='form-control' + value={this.state.triggerWords} + onChange={this.updateTriggerWords} + placeholder='Optional if channel selected' + /> + </div> + <div className='padding-top'>{'Comma separated words to trigger on'}</div> + </div> + <div className='padding-top x2'> + <label className='control-label'>{'Callback URLs:'}</label> + <div className='padding-top'> + <textarea + ref='callbackURLs' + className='form-control no-resize' + value={this.state.callbackURLs} + resize={false} + rows={3} + onChange={this.updateCallbackURLs} + /> + </div> + <div className='padding-top'>{'New line separated URLs that will receive the HTTP POST event'}</div> + {serverError} + </div> + <div className='padding-top padding-bottom'> <a className={'btn btn-sm btn-primary'} href='#' diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index 83a6bf53a..4b1e5e532 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -43,7 +43,6 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMax title='Incoming Webhooks' - width = 'full' inputs={inputs} updateSection={(e) => { this.updateSection(''); @@ -55,7 +54,6 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMin title='Incoming Webhooks' - width = 'full' describe='Manage your incoming webhooks (Developer feature)' updateSection={() => { this.updateSection('incoming-hooks'); diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index adaca44ee..75fb8aa3c 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -74,9 +74,9 @@ class BrowserStoreClass { var result = null; try { if (this.isLocalStorageSupported()) { - result = JSON.parse(getPrefix() + localStorage.getItem(name)); + result = JSON.parse(localStorage.getItem(getPrefix() + name)); } else { - result = JSON.parse(getPrefix() + sessionStorage.getItem(name)); + result = JSON.parse(sessionStorage.getItem(getPrefix() + name)); } } catch (err) { result = null; diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 8c3489001..9410c1e9c 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -158,7 +158,7 @@ function handleNewPostEvent(msg) { // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { if (window.isActive) { - AsyncClient.updateLastViewedAt(); + AsyncClient.updateLastViewedAt(true); } } else { AsyncClient.getChannel(msg.channel_id); diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 575e6d51e..ce80c5ec9 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -48,6 +48,7 @@ class UserStoreClass extends EventEmitter { this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this); this.getProfiles = this.getProfiles.bind(this); this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this); + this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this); this.saveProfile = this.saveProfile.bind(this); this.setSessions = this.setSessions.bind(this); this.getSessions = this.getSessions.bind(this); @@ -215,6 +216,19 @@ class UserStoreClass extends EventEmitter { return active; } + getActiveOnlyProfileList() { + const profileMap = this.getActiveOnlyProfiles(); + const profiles = []; + + for (const id in profileMap) { + if (profileMap.hasOwnProperty(id)) { + profiles.push(profileMap[id]); + } + } + + return profiles; + } + saveProfile(profile) { var ps = this.getProfiles(); ps[profile.id] = profile; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index fb7631159..b1bc71d54 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -152,14 +152,14 @@ export function getChannel(id) { ); } -export function updateLastViewedAt() { +export function updateLastViewedAt(force) { const channelId = ChannelStore.getCurrentId(); if (channelId === null) { return; } - if (isCallInProgress(`updateLastViewed${channelId}`)) { + if (isCallInProgress(`updateLastViewed${channelId}`) && !force) { return; } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 7d2626fc1..72773bf05 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -311,6 +311,7 @@ module.exports = { RIGHT: 39, BACKSPACE: 8, ENTER: 13, - ESCAPE: 27 + ESCAPE: 27, + SPACE: 32 } }; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 3d0cee562..67a9d6983 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -431,6 +431,7 @@ export function applyTheme(theme) { if (theme.sidebarTextActiveColor) { changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2); + changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1); } if (theme.sidebarHeaderBg) { @@ -494,7 +495,7 @@ export function applyTheme(theme) { changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); - changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); changeCss('.popover.left>.arrow', 'border-left-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); @@ -509,7 +510,7 @@ export function applyTheme(theme) { changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1); 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('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); + changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2); changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); @@ -521,7 +522,7 @@ export function applyTheme(theme) { changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); + changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1); changeCss('.post.current--user:hover .post-body ', 'background: none;', 1); changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2); |