diff options
Diffstat (limited to 'web/react/components')
-rw-r--r-- | web/react/components/post_list.jsx | 19 | ||||
-rw-r--r-- | web/react/components/setting_item_max.jsx | 4 | ||||
-rw-r--r-- | web/react/components/setting_upload.jsx | 79 | ||||
-rw-r--r-- | web/react/components/settings_sidebar.jsx | 2 | ||||
-rw-r--r-- | web/react/components/team_feature_tab.jsx | 147 | ||||
-rw-r--r-- | web/react/components/team_import_tab.jsx | 68 | ||||
-rw-r--r-- | web/react/components/team_settings.jsx | 181 | ||||
-rw-r--r-- | web/react/components/team_settings_modal.jsx | 42 | ||||
-rw-r--r-- | web/react/components/user_settings.jsx | 1202 | ||||
-rw-r--r-- | web/react/components/user_settings_appearance.jsx | 118 | ||||
-rw-r--r-- | web/react/components/user_settings_general.jsx | 428 | ||||
-rw-r--r-- | web/react/components/user_settings_modal.jsx | 8 | ||||
-rw-r--r-- | web/react/components/user_settings_notifications.jsx | 484 | ||||
-rw-r--r-- | web/react/components/user_settings_security.jsx | 200 |
14 files changed, 1623 insertions, 1359 deletions
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 5fbee99f6..8b60f0251 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -392,13 +392,22 @@ module.exports = React.createClass({ } } else if (channel.type === 'P' || channel.type === 'O') { var uiName = channel.display_name; - var members = ChannelStore.getCurrentExtraInfo().members; var creatorName = ''; - for (var i = 0; i < members.length; i++) { - if (members[i].roles.indexOf('admin') > -1) { - creatorName = members[i].username; - break; + if (channel.creator_id.length > 0) { + var creator = UserStore.getProfile(channel.creator_id); + if (creator) { + creatorName = creator.username; + } + } + + if (creatorName === '') { + var members = ChannelStore.getCurrentExtraInfo().members; + for (var i = 0; i < members.length; i++) { + if (members[i].roles.indexOf('admin') > -1) { + creatorName = members[i].username; + break; + } } } diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 49eb58773..d3d386534 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -3,7 +3,7 @@ module.exports = React.createClass({ render: function() { - var client_error = this.props.client_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.client_error }</label></div> : null; + var clientError = this.props.clientError ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.clientError }</label></div> : null; var server_error = this.props.server_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.server_error }</label></div> : null; var inputs = this.props.inputs; @@ -19,7 +19,7 @@ module.exports = React.createClass({ <li className="setting-list-item"> <hr /> { server_error } - { client_error } + { clientError } { this.props.submit ? <a className="btn btn-sm btn-primary" onClick={this.props.submit}>Submit</a> : "" } <a className="btn btn-sm theme" href="#" onClick={this.props.updateSection}>Cancel</a> </li> diff --git a/web/react/components/setting_upload.jsx b/web/react/components/setting_upload.jsx new file mode 100644 index 000000000..870710850 --- /dev/null +++ b/web/react/components/setting_upload.jsx @@ -0,0 +1,79 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +module.exports = React.createClass({ + displayName: 'Setting Upload', + propTypes: { + title: React.PropTypes.string.isRequired, + submit: React.PropTypes.func.isRequired, + fileTypesAccepted: React.PropTypes.string.isRequired, + clientError: React.PropTypes.string, + serverError: React.PropTypes.string + }, + getInitialState: function() { + return { + clientError: this.props.clientError, + serverError: this.props.serverError + }; + }, + componentWillReceiveProps: function() { + this.setState({ + clientError: this.props.clientError, + serverError: this.props.serverError + }); + }, + doFileSelect: function(e) { + e.preventDefault(); + this.setState({ + clientError: '', + serverError: '' + }); + }, + doSubmit: function(e) { + e.preventDefault(); + var inputnode = this.refs.uploadinput.getDOMNode(); + if (inputnode.files && inputnode.files[0]) { + this.props.submit(inputnode.files[0]); + } else { + this.setState({clientError: 'No file selected.'}); + } + }, + doCancel: function(e) { + e.preventDefault(); + this.refs.uploadinput.getDOMNode().value = ''; + this.setState({ + clientError: '', + serverError: '' + }); + }, + render: function() { + var clientError = null; + if (this.state.clientError) { + clientError = ( + <div className='form-group has-error'><label className='control-label'>{this.state.clientError}</label></div> + ); + } + var serverError = null; + if (this.state.serverError) { + serverError = ( + <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div> + ); + } + return ( + <ul className='section-max'> + <li className='col-xs-12 section-title'>{this.props.title}</li> + <li className='col-xs-offset-3 col-xs-8'> + <ul className='setting-list'> + <li className='setting-list-item'> + {serverError} + {clientError} + <span className='btn btn-sm btn-primary btn-file sel-btn'>SelectFile<input ref='uploadinput' accept={this.props.fileTypesAccepted} type='file' onChange={this.onFileSelect}/></span> + <a className={'btn btn-sm btn-primary'} onClick={this.doSubmit}>Import</a> + <a className='btn btn-sm theme' href='#' onClick={this.doCancel}>Cancel</a> + </li> + </ul> + </li> + </ul> + ); + } +}); diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index b4d291622..d8091ec28 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -15,7 +15,7 @@ module.exports = React.createClass({ <div className=""> <ul className="nav nav-pills nav-stacked"> {this.props.tabs.map(function(tab) { - return <li key={tab.name+'_li'} className={self.props.activeTab == tab.name ? 'active' : ''}><a key={tab.name + '_a'} href="#" onClick={function(){self.updateTab(tab.name);}}><i key={tab.name+'_i'} className={tab.icon}></i>{tab.ui_name}</a></li> + return <li key={tab.name+'_li'} className={self.props.activeTab == tab.name ? 'active' : ''}><a key={tab.name + '_a'} href="#" onClick={function(){self.updateTab(tab.name);}}><i key={tab.name+'_i'} className={tab.icon}></i>{tab.uiName}</a></li> })} </ul> </div> diff --git a/web/react/components/team_feature_tab.jsx b/web/react/components/team_feature_tab.jsx new file mode 100644 index 000000000..ee0bfa874 --- /dev/null +++ b/web/react/components/team_feature_tab.jsx @@ -0,0 +1,147 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SettingItemMin = require('./setting_item_min.jsx'); +var SettingItemMax = require('./setting_item_max.jsx'); + +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); + +module.exports = React.createClass({ + displayName: 'Feature Tab', + propTypes: { + updateSection: React.PropTypes.func.isRequired, + team: React.PropTypes.object.isRequired, + activeSection: React.PropTypes.string.isRequired + }, + submitValetFeature: function() { + var data = {}; + data.allowValet = this.state.allowValet; + + client.updateValetFeature(data, + function() { + this.props.updateSection(''); + AsyncClient.getMyTeam(); + }.bind(this), + function(err) { + var state = this.getInitialState(); + state.serverError = err; + this.setState(state); + }.bind(this) + ); + }, + handleValetRadio: function(val) { + this.setState({allowValet: val}); + this.refs.wrapper.getDOMNode().focus(); + }, + componentWillReceiveProps: function(newProps) { + var team = newProps.team; + + var allowValet = 'false'; + if (team && team.allowValet) { + allowValet = 'true'; + } + + this.setState({allowValet: allowValet}); + }, + getInitialState: function() { + var team = this.props.team; + + var allowValet = 'false'; + if (team && team.allowValet) { + allowValet = 'true'; + } + + return {allowValet: allowValet}; + }, + onUpdateSection: function() { + if (this.props.activeSection === 'valet') { + self.props.updateSection('valet'); + } else { + self.props.updateSection(''); + } + }, + render: function() { + var clientError = null; + var serverError = null; + if (this.state.clientError) { + clientError = this.state.clientError; + } + if (this.state.serverError) { + serverError = this.state.serverError; + } + + var valetSection; + var self = this; + + if (this.props.activeSection === 'valet') { + var valetActive = ['', '']; + if (this.state.allowValet === 'false') { + valetActive[1] = 'active'; + } else { + valetActive[0] = 'active'; + } + + var inputs = []; + + function valetActivate() { + self.handleValetRadio('true'); + } + + function valetDeactivate() { + self.handleValetRadio('false'); + } + + inputs.push( + <div> + <div className='btn-group' data-toggle='buttons-radio'> + <button className={'btn btn-default ' + valetActive[0]} onClick={valetActivate}>On</button> + <button className={'btn btn-default ' + valetActive[1]} onClick={valetDeactivate}>Off</button> + </div> + <div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div> + </div> + ); + + valetSection = ( + <SettingItemMax + title='Valet (Preview - EXPERTS ONLY)' + inputs={inputs} + submit={this.submitValetFeature} + serverError={serverError} + clientError={clientError} + updateSection={this.onUpdateSection} + /> + ); + } else { + var describe = ''; + if (this.state.allowValet === 'false') { + describe = 'Off'; + } else { + describe = 'On'; + } + + valetSection = ( + <SettingItemMin + title='Valet (Preview - EXPERTS ONLY)' + describe={describe} + updateSection={this.onUpdateSection} + /> + ); + } + + return ( + <div> + <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'><i className='modal-back'></i>Feature Settings</h4> + </div> + <div ref='wrapper' className='user-settings'> + <h3 className='tab-header'>Feature Settings</h3> + <div className='divider-dark first'/> + {valetSection} + <div className='divider-dark'/> + </div> + </div> + ); + } +}); diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx new file mode 100644 index 000000000..131add999 --- /dev/null +++ b/web/react/components/team_import_tab.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); +var SettingUpload = require('./setting_upload.jsx'); + +module.exports = React.createClass({ + displayName: 'Import Tab', + getInitialState: function() { + return {status: 'ready', link: ''}; + }, + onImportFailure: function() { + this.setState({status: 'fail', link: ''}); + }, + onImportSuccess: function(data) { + this.setState({status: 'done', link: 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(data)}); + }, + doImportSlack: function(file) { + this.setState({status: 'in-progress', link: ''}); + utils.importSlack(file, this.onImportSuccess, this.onImportFailure); + }, + render: function() { + var uploadSection = ( + <SettingUpload + title='Import from Slack' + submit={this.doImportSlack} + fileTypesAccepted='.zip'/> + ); + + var messageSection; + switch (this.state.status) { + case 'ready': + messageSection = ''; + break; + case 'in-progress': + messageSection = ( + <p>Importing...</p> + ); + break; + case 'done': + messageSection = ( + <p>Import sucessfull: <a href={this.state.link} download='MattermostImportSummery.txt'>View Summery</a></p> + ); + break; + case 'fail': + messageSection = ( + <p>Import failure: <a href={this.state.link} download='MattermostImportSummery.txt'>View Summery</a></p> + ); + break; + } + + return ( + <div> + <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'><i className='modal-back'></i>Import</h4> + </div> + <div ref='wrapper' className='user-settings'> + <h3 className='tab-header'>Import</h3> + <div className='divider-dark first'/> + {uploadSection} + {messageSection} + <div className='divider-dark'/> + </div> + </div> + ); + } +}); diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index 3bbb5e892..94d536651 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -1,161 +1,62 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); -var SettingPicture = require('./setting_picture.jsx'); +var ImportTab = require('./team_import_tab.jsx'); +var FeatureTab = require('./team_feature_tab.jsx'); var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var Constants = require('../utils/constants.jsx'); - -var FeatureTab = React.createClass({ - submitValetFeature: function() { - data = {}; - data['allow_valet'] = this.state.allow_valet; - - client.updateValetFeature(data, - function(data) { - this.props.updateSection(""); - AsyncClient.getMyTeam(); - }.bind(this), - function(err) { - state = this.getInitialState(); - state.server_error = err; - this.setState(state); - }.bind(this) - ); - }, - handleValetRadio: function(val) { - this.setState({ allow_valet: val }); - this.refs.wrapper.getDOMNode().focus(); - }, - componentWillReceiveProps: function(newProps) { - var team = newProps.team; - - var allow_valet = "false"; - if (team && team.allow_valet) { - allow_valet = "true"; - } - - this.setState({ allow_valet: allow_valet }); - }, - getInitialState: function() { - var team = this.props.team; - - var allow_valet = "false"; - if (team && team.allow_valet) { - allow_valet = "true"; - } - - return { allow_valet: allow_valet }; - }, - render: function() { - var team = this.props.team; - - var client_error = this.state.client_error ? this.state.client_error : null; - var server_error = this.state.server_error ? this.state.server_error : null; - - var valetSection; - var self = this; - - if (this.props.activeSection === 'valet') { - var valetActive = ["",""]; - if (this.state.allow_valet === "false") { - valetActive[1] = "active"; - } else { - valetActive[0] = "active"; - } - - var inputs = []; - - inputs.push( - <div> - <div className="btn-group" data-toggle="buttons-radio"> - <button className={"btn btn-default "+valetActive[0]} onClick={function(){self.handleValetRadio("true")}}>On</button> - <button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button> - </div> - <div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div> - </div> - ); - - valetSection = ( - <SettingItemMax - title="Valet (Preview - EXPERTS ONLY)" - inputs={inputs} - submit={this.submitValetFeature} - server_error={server_error} - client_error={client_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault();}} - /> - ); - } else { - var describe = ""; - if (this.state.allow_valet === "false") { - describe = "Off"; - } else { - describe = "On"; - } - - valetSection = ( - <SettingItemMin - title="Valet (Preview - EXPERTS ONLY)" - describe={describe} - updateSection={function(){self.props.updateSection("valet");}} - /> - ); - } - - return ( - <div> - <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"><i className="modal-back"></i>Feature Settings</h4> - </div> - <div ref="wrapper" className="user-settings"> - <h3 className="tab-header">Feature Settings</h3> - <div className="divider-dark first"/> - {valetSection} - <div className="divider-dark"/> - </div> - </div> - ); - } -}); - module.exports = React.createClass({ + displayName: 'Team Settings', + propTypes: { + activeTab: React.PropTypes.string.isRequired, + activeSection: React.PropTypes.string.isRequired, + updateSection: React.PropTypes.func.isRequired + }, componentDidMount: function() { - TeamStore.addChangeListener(this._onChange); + TeamStore.addChangeListener(this.onChange); }, componentWillUnmount: function() { - TeamStore.removeChangeListener(this._onChange); + TeamStore.removeChangeListener(this.onChange); }, - _onChange: function () { + onChange: function() { var team = TeamStore.getCurrent(); if (!utils.areStatesEqual(this.state.team, team)) { - this.setState({ team: team }); + this.setState({team: team}); } }, getInitialState: function() { - return { team: TeamStore.getCurrent() }; + return {team: TeamStore.getCurrent()}; }, render: function() { - if (this.props.activeTab === 'general') { - return ( - <div> - </div> - ); - } else if (this.props.activeTab === 'feature') { - return ( - <div> - <FeatureTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> - </div> - ); - } else { - return <div/>; + var result; + switch (this.props.activeTab) { + case 'general': + result = ( + <div> + </div> + ); + break; + case 'feature': + result = ( + <div> + <FeatureTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> + </div> + ); + break; + case 'import': + result = ( + <div> + <ImportTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> + </div> + ); + break; + default: + result = ( + <div/> + ); + break; } + return result; } }); diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index b1c38fd16..c9f479a22 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -5,50 +5,52 @@ var SettingsSidebar = require('./settings_sidebar.jsx'); var TeamSettings = require('./team_settings.jsx'); module.exports = React.createClass({ + displayName: 'Team Settings Modal', componentDidMount: function() { - $('body').on('click', '.modal-back', function(){ + $('body').on('click', '.modal-back', function onClick() { $(this).closest('.modal-dialog').removeClass('display--content'); }); - $('body').on('click', '.modal-header .close', function(){ - setTimeout(function() { + $('body').on('click', '.modal-header .close', function onClick() { + setTimeout(function removeContent() { $('.modal-dialog.display--content').removeClass('display--content'); }, 500); }); }, updateTab: function(tab) { - this.setState({ active_tab: tab }); + this.setState({activeTab: tab}); }, updateSection: function(section) { - this.setState({ active_section: section }); + this.setState({activeSection: section}); }, getInitialState: function() { - return { active_tab: "feature", active_section: "" }; + return {activeTab: 'feature', activeSection: ''}; }, render: function() { var tabs = []; - tabs.push({name: "feature", ui_name: "Features", icon: "glyphicon glyphicon-wrench"}); + tabs.push({name: 'feature', uiName: 'Features', icon: 'glyphicon glyphicon-wrench'}); + tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'}); return ( - <div className="modal fade" ref="modal" id="team_settings" role="dialog" tabIndex="-1" aria-hidden="true"> - <div className="modal-dialog settings-modal"> - <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">Team Settings</h4> + <div className='modal fade' ref='modal' id='team_settings' role='dialog' tabIndex='-1' aria-hidden='true'> + <div className='modal-dialog settings-modal'> + <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'>Team Settings</h4> </div> - <div className="modal-body"> - <div className="settings-table"> - <div className="settings-links"> + <div className='modal-body'> + <div className='settings-table'> + <div className='settings-links'> <SettingsSidebar tabs={tabs} - activeTab={this.state.active_tab} + activeTab={this.state.activeTab} updateTab={this.updateTab} /> </div> - <div className="settings-content minimize-settings"> + <div className='settings-content minimize-settings'> <TeamSettings - activeTab={this.state.active_tab} - activeSection={this.state.active_section} + activeTab={this.state.activeTab} + activeSection={this.state.activeSection} updateSection={this.updateSection} /> </div> diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 8f29bbe57..9b0e906c5 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -2,1206 +2,34 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); -var SettingPicture = require('./setting_picture.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); var utils = require('../utils/utils.jsx'); -var Constants = require('../utils/constants.jsx'); -var assign = require('object-assign'); - -function getNotificationsStateFromStores() { - var user = UserStore.getCurrentUser(); - var soundNeeded = !utils.isBrowserFirefox(); - var sound = (!user.notify_props || user.notify_props.desktop_sound == undefined) ? "true" : user.notify_props.desktop_sound; - var desktop = (!user.notify_props || user.notify_props.desktop == undefined) ? "all" : user.notify_props.desktop; - var email = (!user.notify_props || user.notify_props.email == undefined) ? "true" : user.notify_props.email; - - var username_key = false; - var mention_key = false; - var custom_keys = ""; - var first_name_key = false; - var all_key = false; - var channel_key = false; - - if (user.notify_props) { - if (user.notify_props.mention_keys !== undefined) { - var keys = user.notify_props.mention_keys.split(','); - - if (keys.indexOf(user.username) !== -1) { - username_key = true; - keys.splice(keys.indexOf(user.username), 1); - } else { - username_key = false; - } - - if (keys.indexOf('@'+user.username) !== -1) { - mention_key = true; - keys.splice(keys.indexOf('@'+user.username), 1); - } else { - mention_key = false; - } - - custom_keys = keys.join(','); - } - - if (user.notify_props.first_name !== undefined) { - first_name_key = user.notify_props.first_name === "true"; - } - - if (user.notify_props.all !== undefined) { - all_key = user.notify_props.all === "true"; - } - - if (user.notify_props.channel !== undefined) { - channel_key = user.notify_props.channel === "true"; - } - } - - return { notify_level: desktop, enable_email: email, soundNeeded: soundNeeded, enable_sound: sound, username_key: username_key, mention_key: mention_key, custom_keys: custom_keys, custom_keys_checked: custom_keys.length > 0, first_name_key: first_name_key, all_key: all_key, channel_key: channel_key }; -} - - -var NotificationsTab = React.createClass({ - handleSubmit: function() { - data = {} - data["user_id"] = this.props.user.id; - data["email"] = this.state.enable_email; - data["desktop_sound"] = this.state.enable_sound; - data["desktop"] = this.state.notify_level; - - var mention_keys = []; - if (this.state.username_key) mention_keys.push(this.props.user.username); - if (this.state.mention_key) mention_keys.push('@'+this.props.user.username); - - var string_keys = mention_keys.join(','); - if (this.state.custom_keys.length > 0 && this.state.custom_keys_checked) { - string_keys += ',' + this.state.custom_keys; - } - - data["mention_keys"] = string_keys; - data["first_name"] = this.state.first_name_key ? "true" : "false"; - data["all"] = this.state.all_key ? "true" : "false"; - data["channel"] = this.state.channel_key ? "true" : "false"; - - client.updateUserNotifyProps(data, - function(data) { - this.props.updateSection(""); - AsyncClient.getMe(); - }.bind(this), - function(err) { - this.setState({ server_error: err.message }); - }.bind(this) - ); - }, - handleClose: function() { - $(this.getDOMNode()).find(".form-control").each(function() { - this.value = ""; - }); - - this.setState(assign({},getNotificationsStateFromStores(),{server_error: null})); - - this.props.updateTab('general'); - }, - componentDidMount: function() { - UserStore.addChangeListener(this._onChange); - $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentWillUnmount: function() { - UserStore.removeChangeListener(this._onChange); - $('#user_settings').off('hidden.bs.modal', this.handleClose); - this.props.updateSection(''); - }, - _onChange: function() { - var newState = getNotificationsStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { - this.setState(newState); - } - }, - getInitialState: function() { - return getNotificationsStateFromStores(); - }, - handleNotifyRadio: function(notifyLevel) { - this.setState({ notify_level: notifyLevel }); - this.refs.wrapper.getDOMNode().focus(); - }, - handleEmailRadio: function(enableEmail) { - this.setState({ enable_email: enableEmail }); - this.refs.wrapper.getDOMNode().focus(); - }, - handleSoundRadio: function(enableSound) { - this.setState({ enable_sound: enableSound }); - this.refs.wrapper.getDOMNode().focus(); - }, - updateUsernameKey: function(val) { - this.setState({ username_key: val }); - }, - updateMentionKey: function(val) { - this.setState({ mention_key: val }); - }, - updateFirstNameKey: function(val) { - this.setState({ first_name_key: val }); - }, - updateAllKey: function(val) { - this.setState({ all_key: val }); - }, - updateChannelKey: function(val) { - this.setState({ channel_key: val }); - }, - updateCustomMentionKeys: function() { - var checked = this.refs.customcheck.getDOMNode().checked; - - if (checked) { - var text = this.refs.custommentions.getDOMNode().value; - - // remove all spaces and split string into individual keys - this.setState({ custom_keys: text.replace(/ /g, ''), custom_keys_checked: true }); - } else { - this.setState({ custom_keys: "", custom_keys_checked: false }); - } - }, - onCustomChange: function() { - this.refs.customcheck.getDOMNode().checked = true; - this.updateCustomMentionKeys(); - }, - render: function() { - var server_error = this.state.server_error ? this.state.server_error : null; - - var self = this; - - var user = this.props.user; - - var desktopSection; - if (this.props.activeSection === 'desktop') { - var notifyActive = [false, false, false]; - if (this.state.notify_level === "mention") { - notifyActive[1] = true; - } else if (this.state.notify_level === "none") { - notifyActive[2] = true; - } else { - notifyActive[0] = true; - } - - var inputs = []; - - inputs.push( - <div> - <div className="radio"> - <label> - <input type="radio" checked={notifyActive[0]} onClick={function(){self.handleNotifyRadio("all")}}>For all activity</input> - </label> - <br/> - </div> - <div className="radio"> - <label> - <input type="radio" checked={notifyActive[1]} onClick={function(){self.handleNotifyRadio("mention")}}>Only for mentions and private messages</input> - </label> - <br/> - </div> - <div className="radio"> - <label> - <input type="radio" checked={notifyActive[2]} onClick={function(){self.handleNotifyRadio("none")}}>Never</input> - </label> - </div> - </div> - ); - - desktopSection = ( - <SettingItemMax - title="Send desktop notifications" - inputs={inputs} - submit={this.handleSubmit} - server_error={server_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault();}} - /> - ); - } else { - var describe = ""; - if (this.state.notify_level === "mention") { - describe = "Only for mentions and private messages"; - } else if (this.state.notify_level === "none") { - describe = "Never"; - } else { - describe = "For all activity"; - } - - desktopSection = ( - <SettingItemMin - title="Send desktop notifications" - describe={describe} - updateSection={function(){self.props.updateSection("desktop");}} - /> - ); - } - - var soundSection; - if (this.props.activeSection === 'sound' && this.state.soundNeeded) { - var soundActive = ["",""]; - if (this.state.enable_sound === "false") { - soundActive[1] = "active"; - } else { - soundActive[0] = "active"; - } - - var inputs = []; - - inputs.push( - <div> - <div className="btn-group" data-toggle="buttons-radio"> - <button className={"btn btn-default "+soundActive[0]} onClick={function(){self.handleSoundRadio("true")}}>On</button> - <button className={"btn btn-default "+soundActive[1]} onClick={function(){self.handleSoundRadio("false")}}>Off</button> - </div> - </div> - ); - - soundSection = ( - <SettingItemMax - title="Desktop notification sounds" - inputs={inputs} - submit={this.handleSubmit} - server_error={server_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault();}} - /> - ); - } else { - var describe = ""; - if (!this.state.soundNeeded) { - describe = "Please configure notification sounds in your browser settings" - } else if (this.state.enable_sound === "false") { - describe = "Off"; - } else { - describe = "On"; - } - - soundSection = ( - <SettingItemMin - title="Desktop notification sounds" - describe={describe} - updateSection={function(){self.props.updateSection("sound");}} - disableOpen = {!this.state.soundNeeded} - /> - ); - } - - var emailSection; - if (this.props.activeSection === 'email') { - var emailActive = ["",""]; - if (this.state.enable_email === "false") { - emailActive[1] = "active"; - } else { - emailActive[0] = "active"; - } - - var inputs = []; - - inputs.push( - <div> - <div className="btn-group" data-toggle="buttons-radio"> - <button className={"btn btn-default "+emailActive[0]} onClick={function(){self.handleEmailRadio("true")}}>On</button> - <button className={"btn btn-default "+emailActive[1]} onClick={function(){self.handleEmailRadio("false")}}>Off</button> - </div> - <div><br/>{"Email notifications are sent for mentions and private messages after you have been away from " + config.SiteName + " for 5 minutes."}</div> - </div> - ); - - emailSection = ( - <SettingItemMax - title="Email notifications" - inputs={inputs} - submit={this.handleSubmit} - server_error={server_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault();}} - /> - ); - } else { - var describe = ""; - if (this.state.enable_email === "false") { - describe = "Off"; - } else { - describe = "On"; - } - - emailSection = ( - <SettingItemMin - title="Email notifications" - describe={describe} - updateSection={function(){self.props.updateSection("email");}} - /> - ); - } - - var keysSection; - if (this.props.activeSection === 'keys') { - var inputs = []; - - if (user.first_name) { - inputs.push( - <div> - <div className="checkbox"> - <label> - <input type="checkbox" checked={this.state.first_name_key} onChange={function(e){self.updateFirstNameKey(e.target.checked);}}>{'Your case sensitive first name "' + user.first_name + '"'}</input> - </label> - </div> - </div> - ); - } - - inputs.push( - <div> - <div className="checkbox"> - <label> - <input type="checkbox" checked={this.state.username_key} onChange={function(e){self.updateUsernameKey(e.target.checked);}}>{'Your non-case sensitive username "' + user.username + '"'}</input> - </label> - </div> - </div> - ); - - inputs.push( - <div> - <div className="checkbox"> - <label> - <input type="checkbox" checked={this.state.mention_key} onChange={function(e){self.updateMentionKey(e.target.checked);}}>{'Your username mentioned "@' + user.username + '"'}</input> - </label> - </div> - </div> - ); - - inputs.push( - <div> - <div className="checkbox"> - <label> - <input type="checkbox" checked={this.state.all_key} onChange={function(e){self.updateAllKey(e.target.checked);}}>{'Team-wide mentions "@all"'}</input> - </label> - </div> - </div> - ); - - inputs.push( - <div> - <div className="checkbox"> - <label> - <input type="checkbox" checked={this.state.channel_key} onChange={function(e){self.updateChannelKey(e.target.checked);}}>{'Channel-wide mentions "@channel"'}</input> - </label> - </div> - </div> - ); - - inputs.push( - <div> - <div className="checkbox"> - <label> - <input ref="customcheck" type="checkbox" checked={this.state.custom_keys_checked} onChange={this.updateCustomMentionKeys}>{'Other non-case sensitive words, separated by commas:'}</input> - </label> - </div> - <input ref="custommentions" className="form-control mentions-input" type="text" defaultValue={this.state.custom_keys} onChange={this.onCustomChange} /> - </div> - ); - - keysSection = ( - <SettingItemMax - title="Words that trigger mentions" - inputs={inputs} - submit={this.handleSubmit} - server_error={server_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault();}} - /> - ); - } else { - var keys = []; - if (this.state.first_name_key) keys.push(user.first_name); - if (this.state.username_key) keys.push(user.username); - if (this.state.mention_key) keys.push('@'+user.username); - if (this.state.all_key) keys.push('@all'); - if (this.state.channel_key) keys.push('@channel'); - if (this.state.custom_keys.length > 0) keys = keys.concat(this.state.custom_keys.split(',')); - - var describe = ""; - for (var i = 0; i < keys.length; i++) { - describe += '"' + keys[i] + '", '; - } - - if (describe.length > 0) { - describe = describe.substring(0, describe.length - 2); - } else { - describe = "No words configured"; - } - - keysSection = ( - <SettingItemMin - title="Words that trigger mentions" - describe={describe} - updateSection={function(){self.props.updateSection("keys");}} - /> - ); - } - - return ( - <div> - <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"><i className="modal-back"></i>Notifications</h4> - </div> - <div ref="wrapper" className="user-settings"> - <h3 className="tab-header">Notifications</h3> - <div className="divider-dark first"/> - {desktopSection} - <div className="divider-light"/> - {soundSection} - <div className="divider-light"/> - {emailSection} - <div className="divider-light"/> - {keysSection} - <div className="divider-dark"/> - </div> - </div> - - ); - } -}); - -var SecurityTab = React.createClass({ - submitPassword: function(e) { - e.preventDefault(); - - var user = this.props.user; - var currentPassword = this.state.currentPassword; - var newPassword = this.state.newPassword; - var confirmPassword = this.state.confirmPassword; - - if (currentPassword === '') { - this.setState({passwordError: 'Please enter your current password', serverError: ''}); - return; - } - - if (newPassword.length < 5) { - this.setState({passwordError: 'New passwords must be at least 5 characters', serverError: ''}); - return; - } - - if (newPassword !== confirmPassword) { - this.setState({passwordError: 'The new passwords you entered do not match', serverError: ''}); - return; - } - - var data = {}; - data.user_id = user.id; - data.current_password = currentPassword; - data.new_password = newPassword; - - client.updatePassword(data, - function() { - this.props.updateSection(''); - AsyncClient.getMe(); - this.setState({currentPassword: '', newPassword: '', confirmPassword: ''}); - }.bind(this), - function(err) { - var state = this.getInitialState(); - if (err.message) { - state.serverError = err.message; - } else { - state.serverError = err; - } - state.passwordError = ''; - this.setState(state); - }.bind(this) - ); - }, - updateCurrentPassword: function(e) { - this.setState({currentPassword: e.target.value}); - }, - updateNewPassword: function(e) { - this.setState({newPassword: e.target.value}); - }, - updateConfirmPassword: function(e) { - this.setState({confirmPassword: e.target.value}); - }, - handleHistoryOpen: function() { - this.setState({willReturn: true}); - $("#user_settings").modal('hide'); - }, - handleDevicesOpen: function() { - this.setState({willReturn: true}); - $("#user_settings").modal('hide'); - }, - handleClose: function() { - $(this.getDOMNode()).find('.form-control').each(function() { - this.value = ''; - }); - this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); - - if (!this.state.willReturn) { - this.props.updateTab('general'); - } else { - this.setState({willReturn: false}); - } - }, - componentDidMount: function() { - $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentWillUnmount: function() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - this.props.updateSection(''); - }, - getInitialState: function() { - return {currentPassword: '', newPassword: '', confirmPassword: '', willReturn: false}; - }, - render: function() { - var serverError = this.state.serverError ? this.state.serverError : null; - var passwordError = this.state.passwordError ? this.state.passwordError : null; - - var updateSectionStatus; - var passwordSection; - var self = this; - if (this.props.activeSection === 'password') { - var inputs = []; - var submit = null; - - if (this.props.user.auth_service === '') { - inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>Current Password</label> - <div className='col-sm-7'> - <input className='form-control' type='password' onChange={this.updateCurrentPassword} value={this.state.currentPassword}/> - </div> - </div> - ); - inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>New Password</label> - <div className='col-sm-7'> - <input className='form-control' type='password' onChange={this.updateNewPassword} value={this.state.newPassword}/> - </div> - </div> - ); - inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>Retype New Password</label> - <div className='col-sm-7'> - <input className='form-control' type='password' onChange={this.updateConfirmPassword} value={this.state.confirmPassword}/> - </div> - </div> - ); - - submit = this.submitPassword; - } else { - inputs.push( - <div className='form-group'> - <label className='col-sm-12'>Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label> - </div> - ); - } - - updateSectionStatus = function(e) { - self.props.updateSection(''); - self.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); - e.preventDefault(); - }; - - passwordSection = ( - <SettingItemMax - title='Password' - inputs={inputs} - submit={submit} - server_error={serverError} - client_error={passwordError} - updateSection={updateSectionStatus} - /> - ); - } else { - var describe; - if (this.props.user.auth_service === '') { - var d = new Date(this.props.user.last_password_update); - var hour = d.getHours() % 12 ? String(d.getHours() % 12) : '12'; - var min = d.getMinutes() < 10 ? '0' + d.getMinutes() : String(d.getMinutes()); - var timeOfDay = d.getHours() >= 12 ? ' pm' : ' am'; - describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; - } else { - describe = 'Log in done through GitLab'; - } - - updateSectionStatus = function() { - self.props.updateSection('password'); - }; - - passwordSection = ( - <SettingItemMin - title='Password' - describe={describe} - updateSection={updateSectionStatus} - /> - ); - } - - return ( - <div> - <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'><i className='modal-back'></i>Security Settings</h4> - </div> - <div className='user-settings'> - <h3 className='tab-header'>Security Settings</h3> - <div className='divider-dark first'/> - {passwordSection} - <div className='divider-dark'/> - <br></br> - <a data-toggle='modal' className='security-links theme' data-target='#access-history' href='#' onClick={this.handleHistoryOpen}><i className='fa fa-clock-o'></i>View Access History</a> - <b> </b> - <a data-toggle='modal' className='security-links theme' data-target='#activity-log' href='#' onClick={this.handleDevicesOpen}><i className='fa fa-globe'></i>View and Logout of Active Sessions</a> - </div> - </div> - ); - } -}); - -var GeneralTab = React.createClass({ - submitActive: false, - submitUsername: function(e) { - e.preventDefault(); - - var user = this.props.user; - var username = this.state.username.trim(); - - var usernameError = utils.isValidUsername(username); - if (usernameError === 'Cannot use a reserved word as a username.') { - this.setState({clientError: 'This username is reserved, please choose a new one.'}); - return; - } else if (usernameError) { - this.setState({clientError: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'."}); - return; - } - - if (user.username === username) { - this.setState({clientError: 'You must submit a new username'}); - return; - } - - user.username = username; - - this.submitUser(user); - }, - submitNickname: function(e) { - e.preventDefault(); - - var user = UserStore.getCurrentUser(); - var nickname = this.state.nickname.trim(); - - if (user.nickname === nickname) { - this.setState({clientError: 'You must submit a new nickname'}); - return; - } - - user.nickname = nickname; - - this.submitUser(user); - }, - submitName: function(e) { - e.preventDefault(); - - var user = UserStore.getCurrentUser(); - var firstName = this.state.firstName.trim(); - var lastName = this.state.lastName.trim(); - - if (user.first_name === firstName && user.last_name === lastName) { - this.setState({clientError: 'You must submit a new first or last name'}); - return; - } - - user.first_name = firstName; - user.last_name = lastName; - - this.submitUser(user); - }, - submitEmail: function(e) { - e.preventDefault(); - - var user = UserStore.getCurrentUser(); - var email = this.state.email.trim().toLowerCase(); - - if (user.email === email) { - return; - } - - if (email === '' || !utils.isEmail(email)) { - this.setState({emailError: 'Please enter a valid email address'}); - return; - } - - user.email = email; - - this.submitUser(user); - }, - submitUser: function(user) { - client.updateUser(user, - function() { - this.updateSection(''); - AsyncClient.getMe(); - }.bind(this), - function(err) { - var state = this.getInitialState(); - if (err.message) { - state.serverError = err.message; - } else { - state.serverError = err; - } - this.setState(state); - }.bind(this) - ); - }, - submitPicture: function(e) { - e.preventDefault(); - - if (!this.state.picture) { - return; - } - - if (!this.submitActive) { - return; - } - - var picture = this.state.picture; - - if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') { - this.setState({clientError: 'Only JPG or PNG images may be used for profile pictures'}); - return; - } - - var formData = new FormData(); - formData.append('image', picture, picture.name); - this.setState({loadingPicture: true}); - - client.uploadProfileImage(formData, - function() { - this.submitActive = false; - AsyncClient.getMe(); - window.location.reload(); - }.bind(this), - function(err) { - var state = this.getInitialState(); - state.serverError = err; - this.setState(state); - }.bind(this) - ); - }, - updateUsername: function(e) { - this.setState({username: e.target.value}); - }, - updateFirstName: function(e) { - this.setState({firstName: e.target.value}); - }, - updateLastName: function(e) { - this.setState({lastName: e.target.value}); - }, - updateNickname: function(e) { - this.setState({nickname: e.target.value}); - }, - updateEmail: function(e) { - this.setState({email: e.target.value}); - }, - updatePicture: function(e) { - if (e.target.files && e.target.files[0]) { - this.setState({picture: e.target.files[0]}); - - this.submitActive = true; - this.setState({clientError: null}); - } else { - this.setState({picture: null}); - } - }, - updateSection: function(section) { - this.setState({clientError: ''}); - this.submitActive = false; - this.props.updateSection(section); - }, - handleClose: function() { - $(this.getDOMNode()).find('.form-control').each(function() { - this.value = ''; - }); - - this.setState(assign({}, this.getInitialState(), {clientError: null, serverError: null, emailError: null})); - this.props.updateSection(''); - }, - componentDidMount: function() { - $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentWillUnmount: function() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - }, - getInitialState: function() { - var user = this.props.user; - - return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname, - email: user.email, picture: null, loadingPicture: false}; - }, - render: function() { - var user = this.props.user; - - var clientError = null; - if (this.state.clientError) { - clientError = this.state.clientError; - } - var serverError = null; - if (this.state.serverError) { - serverError = this.state.serverError; - } - var emailError = null; - if (this.state.emailError) { - emailError = this.state.emailError; - } - - var nameSection; - var self = this; - var inputs = []; - - if (this.props.activeSection === 'name') { - inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>First Name</label> - <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateFirstName} value={this.state.firstName}/> - </div> - </div> - ); - - inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>Last Name</label> - <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateLastName} value={this.state.lastName}/> - </div> - </div> - ); - - nameSection = ( - <SettingItemMax - title='Full Name' - inputs={inputs} - submit={this.submitName} - server_error={serverError} - client_error={clientError} - updateSection={function(e) { - self.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - var fullName = ''; - - if (user.first_name && user.last_name) { - fullName = user.first_name + ' ' + user.last_name; - } else if (user.first_name) { - fullName = user.first_name; - } else if (user.last_name) { - fullName = user.last_name; - } - - nameSection = ( - <SettingItemMin - title='Full Name' - describe={fullName} - updateSection={function() { - self.updateSection('name'); - }} - /> - ); - } - - var nicknameSection; - if (this.props.activeSection === 'nickname') { - inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Nickname'}</label> - <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateNickname} value={this.state.nickname}/> - </div> - </div> - ); - - nicknameSection = ( - <SettingItemMax - title='Nickname' - inputs={inputs} - submit={this.submitNickname} - server_error={serverError} - client_error={clientError} - updateSection={function(e) { - self.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - nicknameSection = ( - <SettingItemMin - title='Nickname' - describe={UserStore.getCurrentUser().nickname} - updateSection={function() { - self.updateSection('nickname'); - }} - /> - ); - } - - var usernameSection; - if (this.props.activeSection === 'username') { - inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Username'}</label> - <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateUsername} value={this.state.username}/> - </div> - </div> - ); - - usernameSection = ( - <SettingItemMax - title='Username' - inputs={inputs} - submit={this.submitUsername} - server_error={serverError} - client_error={clientError} - updateSection={function(e) { - self.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - usernameSection = ( - <SettingItemMin - title='Username' - describe={UserStore.getCurrentUser().username} - updateSection={function() { - self.updateSection('username'); - }} - /> - ); - } - var emailSection; - if (this.props.activeSection === 'email') { - inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>Primary Email</label> - <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateEmail} value={this.state.email}/> - </div> - </div> - ); - - emailSection = ( - <SettingItemMax - title='Email' - inputs={inputs} - submit={this.submitEmail} - server_error={serverError} - client_error={emailError} - updateSection={function(e) { - self.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - emailSection = ( - <SettingItemMin - title='Email' - describe={UserStore.getCurrentUser().email} - updateSection={function() { - self.updateSection('email'); - }} - /> - ); - } - - var pictureSection; - if (this.props.activeSection === 'picture') { - pictureSection = ( - <SettingPicture - title='Profile Picture' - submit={this.submitPicture} - src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update} - server_error={serverError} - client_error={clientError} - updateSection={function(e) { - self.updateSection(''); - e.preventDefault(); - }} - picture={this.state.picture} - pictureChange={this.updatePicture} - submitActive={this.submitActive} - loadingPicture={this.state.loadingPicture} - /> - ); - } else { - var minMessage = 'Click \'Edit\' to upload an image.'; - if (user.last_picture_update) { - minMessage = 'Image last updated ' + utils.displayDate(user.last_picture_update); - } - pictureSection = ( - <SettingItemMin - title='Profile Picture' - describe={minMessage} - updateSection={function() { - self.updateSection('picture'); - }} - /> - ); - } - return ( - <div> - <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'><i className='modal-back'></i>General Settings</h4> - </div> - <div className='user-settings'> - <h3 className='tab-header'>General Settings</h3> - <div className='divider-dark first'/> - {nameSection} - <div className='divider-light'/> - {usernameSection} - <div className='divider-light'/> - {nicknameSection} - <div className='divider-light'/> - {emailSection} - <div className='divider-light'/> - {pictureSection} - <div className='divider-dark'/> - </div> - </div> - ); - } -}); - -var AppearanceTab = React.createClass({ - submitTheme: function(e) { - e.preventDefault(); - var user = UserStore.getCurrentUser(); - if (!user.props) user.props = {}; - user.props.theme = this.state.theme; - - client.updateUser(user, - function(data) { - this.props.updateSection(""); - window.location.reload(); - }.bind(this), - function(err) { - state = this.getInitialState(); - state.server_error = err; - this.setState(state); - }.bind(this) - ); - }, - updateTheme: function(e) { - var hex = utils.rgb2hex(e.target.style.backgroundColor); - this.setState({ theme: hex.toLowerCase() }); - }, - handleClose: function() { - this.setState({server_error: null}); - this.props.updateTab('general'); - }, - componentDidMount: function() { - if (this.props.activeSection === "theme") { - $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); - } - $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentDidUpdate: function() { - if (this.props.activeSection === "theme") { - $('.color-btn').removeClass('active-border'); - $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); - } - }, - componentWillUnmount: function() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - this.props.updateSection(''); - }, - getInitialState: function() { - var user = UserStore.getCurrentUser(); - var theme = config.ThemeColors != null ? config.ThemeColors[0] : "#2389d7"; - if (user.props && user.props.theme) { - theme = user.props.theme; - } - return { theme: theme.toLowerCase() }; - }, - render: function() { - var server_error = this.state.server_error ? this.state.server_error : null; - - - var themeSection; - var self = this; - - if (config.ThemeColors != null) { - if (this.props.activeSection === 'theme') { - var theme_buttons = []; - - for (var i = 0; i < config.ThemeColors.length; i++) { - theme_buttons.push(<button ref={config.ThemeColors[i]} type="button" className="btn btn-lg color-btn" style={{backgroundColor: config.ThemeColors[i]}} onClick={this.updateTheme} />); - } - - var inputs = []; - - inputs.push( - <li className="setting-list-item"> - <div className="btn-group" data-toggle="buttons-radio"> - { theme_buttons } - </div> - </li> - ); - - themeSection = ( - <SettingItemMax - title="Theme Color" - inputs={inputs} - submit={this.submitTheme} - server_error={server_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault;}} - /> - ); - } else { - themeSection = ( - <SettingItemMin - title="Theme Color" - describe={this.state.theme} - updateSection={function(){self.props.updateSection("theme");}} - /> - ); - } - } - - return ( - <div> - <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"><i className="modal-back"></i>Appearance Settings</h4> - </div> - <div className="user-settings"> - <h3 className="tab-header">Appearance Settings</h3> - <div className="divider-dark first"/> - {themeSection} - <div className="divider-dark"/> - </div> - </div> - ); - } -}); +var NotificationsTab = require('./user_settings_notifications.jsx'); +var SecurityTab = require('./user_settings_security.jsx'); +var GeneralTab = require('./user_settings_general.jsx'); +var AppearanceTab = require('./user_settings_appearance.jsx'); module.exports = React.createClass({ displayName: 'UserSettings', + propTypes: { + activeTab: React.PropTypes.string, + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func + }, componentDidMount: function() { - UserStore.addChangeListener(this._onChange); + UserStore.addChangeListener(this.onListenerChange); }, componentWillUnmount: function() { - UserStore.removeChangeListener(this._onChange); + UserStore.removeChangeListener(this.onListenerChange); }, - _onChange: function () { + onListenerChange: function () { var user = UserStore.getCurrentUser(); if (!utils.areStatesEqual(this.state.user, user)) { - this.setState({ user: user }); + this.setState({user: user}); } }, getInitialState: function() { - return { user: UserStore.getCurrentUser() }; + return {user: UserStore.getCurrentUser()}; }, render: function() { if (this.props.activeTab === 'general') { diff --git a/web/react/components/user_settings_appearance.jsx b/web/react/components/user_settings_appearance.jsx new file mode 100644 index 000000000..0a17f1687 --- /dev/null +++ b/web/react/components/user_settings_appearance.jsx @@ -0,0 +1,118 @@ +var UserStore = require('../stores/user_store.jsx'); +var SettingItemMin = require('./setting_item_min.jsx'); +var SettingItemMax = require('./setting_item_max.jsx'); +var client = require('../utils/client.jsx'); +var utils = require('../utils/utils.jsx'); + +module.exports = React.createClass({ + submitTheme: function(e) { + e.preventDefault(); + var user = UserStore.getCurrentUser(); + if (!user.props) user.props = {}; + user.props.theme = this.state.theme; + + client.updateUser(user, + function(data) { + this.props.updateSection(""); + window.location.reload(); + }.bind(this), + function(err) { + state = this.getInitialState(); + state.server_error = err; + this.setState(state); + }.bind(this) + ); + }, + updateTheme: function(e) { + var hex = utils.rgb2hex(e.target.style.backgroundColor); + this.setState({ theme: hex.toLowerCase() }); + }, + handleClose: function() { + this.setState({server_error: null}); + this.props.updateTab('general'); + }, + componentDidMount: function() { + if (this.props.activeSection === "theme") { + $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); + } + $('#user_settings').on('hidden.bs.modal', this.handleClose); + }, + componentDidUpdate: function() { + if (this.props.activeSection === "theme") { + $('.color-btn').removeClass('active-border'); + $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); + } + }, + componentWillUnmount: function() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); + }, + getInitialState: function() { + var user = UserStore.getCurrentUser(); + var theme = config.ThemeColors != null ? config.ThemeColors[0] : "#2389d7"; + if (user.props && user.props.theme) { + theme = user.props.theme; + } + return { theme: theme.toLowerCase() }; + }, + render: function() { + var server_error = this.state.server_error ? this.state.server_error : null; + + + var themeSection; + var self = this; + + if (config.ThemeColors != null) { + if (this.props.activeSection === 'theme') { + var theme_buttons = []; + + for (var i = 0; i < config.ThemeColors.length; i++) { + theme_buttons.push(<button ref={config.ThemeColors[i]} type="button" className="btn btn-lg color-btn" style={{backgroundColor: config.ThemeColors[i]}} onClick={this.updateTheme} />); + } + + var inputs = []; + + inputs.push( + <li className="setting-list-item"> + <div className="btn-group" data-toggle="buttons-radio"> + { theme_buttons } + </div> + </li> + ); + + themeSection = ( + <SettingItemMax + title="Theme Color" + inputs={inputs} + submit={this.submitTheme} + server_error={server_error} + updateSection={function(e){self.props.updateSection("");e.preventDefault;}} + /> + ); + } else { + themeSection = ( + <SettingItemMin + title="Theme Color" + describe={this.state.theme} + updateSection={function(){self.props.updateSection("theme");}} + /> + ); + } + } + + return ( + <div> + <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"><i className="modal-back"></i>Appearance Settings</h4> + </div> + <div className="user-settings"> + <h3 className="tab-header">Appearance Settings</h3> + <div className="divider-dark first"/> + {themeSection} + <div className="divider-dark"/> + </div> + </div> + ); + } +}); diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings_general.jsx new file mode 100644 index 000000000..5e7bbcb51 --- /dev/null +++ b/web/react/components/user_settings_general.jsx @@ -0,0 +1,428 @@ +var UserStore = require('../stores/user_store.jsx'); +var SettingItemMin = require('./setting_item_min.jsx'); +var SettingItemMax = require('./setting_item_max.jsx'); +var SettingPicture = require('./setting_picture.jsx'); +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); +var utils = require('../utils/utils.jsx'); +var assign = require('object-assign'); + +module.exports = React.createClass({ + displayName: 'GeneralTab', + submitActive: false, + submitUsername: function(e) { + e.preventDefault(); + + var user = this.props.user; + var username = this.state.username.trim(); + + var usernameError = utils.isValidUsername(username); + if (usernameError === 'Cannot use a reserved word as a username.') { + this.setState({clientError: 'This username is reserved, please choose a new one.'}); + return; + } else if (usernameError) { + this.setState({clientError: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'."}); + return; + } + + if (user.username === username) { + this.setState({clientError: 'You must submit a new username'}); + return; + } + + user.username = username; + + this.submitUser(user); + }, + submitNickname: function(e) { + e.preventDefault(); + + var user = UserStore.getCurrentUser(); + var nickname = this.state.nickname.trim(); + + if (user.nickname === nickname) { + this.setState({clientError: 'You must submit a new nickname'}); + return; + } + + user.nickname = nickname; + + this.submitUser(user); + }, + submitName: function(e) { + e.preventDefault(); + + var user = UserStore.getCurrentUser(); + var firstName = this.state.firstName.trim(); + var lastName = this.state.lastName.trim(); + + if (user.first_name === firstName && user.last_name === lastName) { + this.setState({clientError: 'You must submit a new first or last name'}); + return; + } + + user.first_name = firstName; + user.last_name = lastName; + + this.submitUser(user); + }, + submitEmail: function(e) { + e.preventDefault(); + + var user = UserStore.getCurrentUser(); + var email = this.state.email.trim().toLowerCase(); + + if (user.email === email) { + return; + } + + if (email === '' || !utils.isEmail(email)) { + this.setState({emailError: 'Please enter a valid email address'}); + return; + } + + user.email = email; + + this.submitUser(user); + }, + submitUser: function(user) { + client.updateUser(user, + function() { + this.updateSection(''); + AsyncClient.getMe(); + }.bind(this), + function(err) { + var state = this.getInitialState(); + if (err.message) { + state.serverError = err.message; + } else { + state.serverError = err; + } + this.setState(state); + }.bind(this) + ); + }, + submitPicture: function(e) { + e.preventDefault(); + + if (!this.state.picture) { + return; + } + + if (!this.submitActive) { + return; + } + + var picture = this.state.picture; + + if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') { + this.setState({clientError: 'Only JPG or PNG images may be used for profile pictures'}); + return; + } + + var formData = new FormData(); + formData.append('image', picture, picture.name); + this.setState({loadingPicture: true}); + + client.uploadProfileImage(formData, + function() { + this.submitActive = false; + AsyncClient.getMe(); + window.location.reload(); + }.bind(this), + function(err) { + var state = this.getInitialState(); + state.serverError = err; + this.setState(state); + }.bind(this) + ); + }, + updateUsername: function(e) { + this.setState({username: e.target.value}); + }, + updateFirstName: function(e) { + this.setState({firstName: e.target.value}); + }, + updateLastName: function(e) { + this.setState({lastName: e.target.value}); + }, + updateNickname: function(e) { + this.setState({nickname: e.target.value}); + }, + updateEmail: function(e) { + this.setState({email: e.target.value}); + }, + updatePicture: function(e) { + if (e.target.files && e.target.files[0]) { + this.setState({picture: e.target.files[0]}); + + this.submitActive = true; + this.setState({clientError: null}); + } else { + this.setState({picture: null}); + } + }, + updateSection: function(section) { + this.setState(assign({}, this.getInitialState(), {clientError: ''})); + this.submitActive = false; + this.props.updateSection(section); + }, + handleClose: function() { + $(this.getDOMNode()).find('.form-control').each(function() { + this.value = ''; + }); + + this.setState(assign({}, this.getInitialState(), {clientError: null, serverError: null, emailError: null})); + this.props.updateSection(''); + }, + componentDidMount: function() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + }, + componentWillUnmount: function() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + }, + getInitialState: function() { + var user = this.props.user; + + return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname, + email: user.email, picture: null, loadingPicture: false}; + }, + render: function() { + var user = this.props.user; + + var clientError = null; + if (this.state.clientError) { + clientError = this.state.clientError; + } + var serverError = null; + if (this.state.serverError) { + serverError = this.state.serverError; + } + var emailError = null; + if (this.state.emailError) { + emailError = this.state.emailError; + } + + var nameSection; + var self = this; + var inputs = []; + + if (this.props.activeSection === 'name') { + inputs.push( + <div className='form-group'> + <label className='col-sm-5 control-label'>First Name</label> + <div className='col-sm-7'> + <input className='form-control' type='text' onChange={this.updateFirstName} value={this.state.firstName}/> + </div> + </div> + ); + + inputs.push( + <div className='form-group'> + <label className='col-sm-5 control-label'>Last Name</label> + <div className='col-sm-7'> + <input className='form-control' type='text' onChange={this.updateLastName} value={this.state.lastName}/> + </div> + </div> + ); + + nameSection = ( + <SettingItemMax + title='Full Name' + inputs={inputs} + submit={this.submitName} + server_error={serverError} + client_error={clientError} + updateSection={function(e) { + self.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + var fullName = ''; + + if (user.first_name && user.last_name) { + fullName = user.first_name + ' ' + user.last_name; + } else if (user.first_name) { + fullName = user.first_name; + } else if (user.last_name) { + fullName = user.last_name; + } + + nameSection = ( + <SettingItemMin + title='Full Name' + describe={fullName} + updateSection={function() { + self.updateSection('name'); + }} + /> + ); + } + + var nicknameSection; + if (this.props.activeSection === 'nickname') { + inputs.push( + <div className='form-group'> + <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Nickname'}</label> + <div className='col-sm-7'> + <input className='form-control' type='text' onChange={this.updateNickname} value={this.state.nickname}/> + </div> + </div> + ); + + nicknameSection = ( + <SettingItemMax + title='Nickname' + inputs={inputs} + submit={this.submitNickname} + server_error={serverError} + client_error={clientError} + updateSection={function(e) { + self.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + nicknameSection = ( + <SettingItemMin + title='Nickname' + describe={UserStore.getCurrentUser().nickname} + updateSection={function() { + self.updateSection('nickname'); + }} + /> + ); + } + + var usernameSection; + if (this.props.activeSection === 'username') { + inputs.push( + <div className='form-group'> + <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Username'}</label> + <div className='col-sm-7'> + <input className='form-control' type='text' onChange={this.updateUsername} value={this.state.username}/> + </div> + </div> + ); + + usernameSection = ( + <SettingItemMax + title='Username' + inputs={inputs} + submit={this.submitUsername} + server_error={serverError} + client_error={clientError} + updateSection={function(e) { + self.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + usernameSection = ( + <SettingItemMin + title='Username' + describe={UserStore.getCurrentUser().username} + updateSection={function() { + self.updateSection('username'); + }} + /> + ); + } + var emailSection; + if (this.props.activeSection === 'email') { + inputs.push( + <div className='form-group'> + <label className='col-sm-5 control-label'>Primary Email</label> + <div className='col-sm-7'> + <input className='form-control' type='text' onChange={this.updateEmail} value={this.state.email}/> + </div> + </div> + ); + + emailSection = ( + <SettingItemMax + title='Email' + inputs={inputs} + submit={this.submitEmail} + server_error={serverError} + client_error={emailError} + updateSection={function(e) { + self.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + emailSection = ( + <SettingItemMin + title='Email' + describe={UserStore.getCurrentUser().email} + updateSection={function() { + self.updateSection('email'); + }} + /> + ); + } + + var pictureSection; + if (this.props.activeSection === 'picture') { + pictureSection = ( + <SettingPicture + title='Profile Picture' + submit={this.submitPicture} + src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update} + server_error={serverError} + client_error={clientError} + updateSection={function(e) { + self.updateSection(''); + e.preventDefault(); + }} + picture={this.state.picture} + pictureChange={this.updatePicture} + submitActive={this.submitActive} + loadingPicture={this.state.loadingPicture} + /> + ); + } else { + var minMessage = 'Click \'Edit\' to upload an image.'; + if (user.last_picture_update) { + minMessage = 'Image last updated ' + utils.displayDate(user.last_picture_update); + } + pictureSection = ( + <SettingItemMin + title='Profile Picture' + describe={minMessage} + updateSection={function() { + self.updateSection('picture'); + }} + /> + ); + } + return ( + <div> + <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'><i className='modal-back'></i>General Settings</h4> + </div> + <div className='user-settings'> + <h3 className='tab-header'>General Settings</h3> + <div className='divider-dark first'/> + {nameSection} + <div className='divider-light'/> + {usernameSection} + <div className='divider-light'/> + {nicknameSection} + <div className='divider-light'/> + {emailSection} + <div className='divider-light'/> + {pictureSection} + <div className='divider-dark'/> + </div> + </div> + ); + } +}); diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings_modal.jsx index 7181c4020..f5a555951 100644 --- a/web/react/components/user_settings_modal.jsx +++ b/web/react/components/user_settings_modal.jsx @@ -26,10 +26,10 @@ module.exports = React.createClass({ }, render: function() { var tabs = []; - tabs.push({name: "general", ui_name: "General", icon: "glyphicon glyphicon-cog"}); - tabs.push({name: "security", ui_name: "Security", icon: "glyphicon glyphicon-lock"}); - tabs.push({name: "notifications", ui_name: "Notifications", icon: "glyphicon glyphicon-exclamation-sign"}); - tabs.push({name: "appearance", ui_name: "Appearance", icon: "glyphicon glyphicon-wrench"}); + tabs.push({name: "general", uiName: "General", icon: "glyphicon glyphicon-cog"}); + tabs.push({name: "security", uiName: "Security", icon: "glyphicon glyphicon-lock"}); + tabs.push({name: "notifications", uiName: "Notifications", icon: "glyphicon glyphicon-exclamation-sign"}); + tabs.push({name: "appearance", uiName: "Appearance", icon: "glyphicon glyphicon-wrench"}); return ( <div className="modal fade" ref="modal" id="user_settings" role="dialog" tabIndex="-1" aria-hidden="true"> diff --git a/web/react/components/user_settings_notifications.jsx b/web/react/components/user_settings_notifications.jsx new file mode 100644 index 000000000..33ae01eaa --- /dev/null +++ b/web/react/components/user_settings_notifications.jsx @@ -0,0 +1,484 @@ +var UserStore = require('../stores/user_store.jsx'); +var SettingItemMin = require('./setting_item_min.jsx'); +var SettingItemMax = require('./setting_item_max.jsx'); +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); +var utils = require('../utils/utils.jsx'); +var assign = require('object-assign'); + +function getNotificationsStateFromStores() { + var user = UserStore.getCurrentUser(); + var soundNeeded = !utils.isBrowserFirefox(); + + var sound = 'true'; + if (user.notify_props && user.notify_props.desktop_sound) { + sound = user.notify_props.desktop_sound; + } + var desktop = 'all'; + if (user.notify_props && user.notify_props.desktop) { + desktop = user.notify_props.desktop; + } + var email = 'true'; + if (user.notify_props && user.notify_props.email) { + email = user.notify_props.email; + } + + var usernameKey = false; + var mentionKey = false; + var customKeys = ''; + var firstNameKey = false; + var allKey = false; + var channelKey = false; + + if (user.notify_props) { + if (user.notify_props.mention_keys) { + var keys = user.notify_props.mention_keys.split(','); + + if (keys.indexOf(user.username) !== -1) { + usernameKey = true; + keys.splice(keys.indexOf(user.username), 1); + } else { + usernameKey = false; + } + + if (keys.indexOf('@' + user.username) !== -1) { + mentionKey = true; + keys.splice(keys.indexOf('@' + user.username), 1); + } else { + mentionKey = false; + } + + customKeys = keys.join(','); + } + + if (user.notify_props.first_name) { + firstNameKey = user.notify_props.first_name === 'true'; + } + + if (user.notify_props.all) { + allKey = user.notify_props.all === 'true'; + } + + if (user.notify_props.channel) { + channelKey = user.notify_props.channel === 'true'; + } + } + + return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound, usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0, firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey}; +} + +module.exports = React.createClass({ + displayName: 'NotificationsTab', + propTypes: { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string, + activeTab: React.PropTypes.string + }, + handleSubmit: function() { + var data = {}; + data.user_id = this.props.user.id; + data.email = this.state.enableEmail; + data.desktop_sound = this.state.enableSound; + data.desktop = this.state.notifyLevel; + + var mentionKeys = []; + if (this.state.usernameKey) { + mentionKeys.push(this.props.user.username); + } + if (this.state.mentionKey) { + mentionKeys.push('@' + this.props.user.username); + } + + var stringKeys = mentionKeys.join(','); + if (this.state.customKeys.length > 0 && this.state.customKeysChecked) { + stringKeys += ',' + this.state.customKeys; + } + + data.mention_keys = stringKeys; + data.first_name = this.state.firstNameKey.toString(); + data.all = this.state.allKey.toString(); + data.channel = this.state.channelKey.toString(); + + client.updateUserNotifyProps(data, + function success() { + this.props.updateSection(''); + AsyncClient.getMe(); + }.bind(this), + function failure(err) { + this.setState({serverError: err.message}); + }.bind(this) + ); + }, + handleClose: function() { + $(this.getDOMNode()).find('.form-control').each(function clearField() { + this.value = ''; + }); + + this.setState(assign({}, getNotificationsStateFromStores(), {serverError: null})); + + this.props.updateTab('general'); + }, + updateSection: function(section) { + this.setState(this.getInitialState()); + this.props.updateSection(section); + }, + componentDidMount: function() { + UserStore.addChangeListener(this.onListenerChange); + $('#user_settings').on('hidden.bs.modal', this.handleClose); + }, + componentWillUnmount: function() { + UserStore.removeChangeListener(this.onListenerChange); + $('#user_settings').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); + }, + onListenerChange: function() { + var newState = getNotificationsStateFromStores(); + if (!utils.areStatesEqual(newState, this.state)) { + this.setState(newState); + } + }, + getInitialState: function() { + return getNotificationsStateFromStores(); + }, + handleNotifyRadio: function(notifyLevel) { + this.setState({notifyLevel: notifyLevel}); + this.refs.wrapper.getDOMNode().focus(); + }, + handleEmailRadio: function(enableEmail) { + this.setState({enableEmail: enableEmail}); + this.refs.wrapper.getDOMNode().focus(); + }, + handleSoundRadio: function(enableSound) { + this.setState({enableSound: enableSound}); + this.refs.wrapper.getDOMNode().focus(); + }, + updateUsernameKey: function(val) { + this.setState({usernameKey: val}); + }, + updateMentionKey: function(val) { + this.setState({mentionKey: val}); + }, + updateFirstNameKey: function(val) { + this.setState({firstNameKey: val}); + }, + updateAllKey: function(val) { + this.setState({allKey: val}); + }, + updateChannelKey: function(val) { + this.setState({channelKey: val}); + }, + updateCustomMentionKeys: function() { + var checked = this.refs.customcheck.getDOMNode().checked; + + if (checked) { + var text = this.refs.custommentions.getDOMNode().value; + + // remove all spaces and split string into individual keys + this.setState({customKeys: text.replace(/ /g, ''), customKeysChecked: true}); + } else { + this.setState({customKeys: '', customKeysChecked: false}); + } + }, + onCustomChange: function() { + this.refs.customcheck.getDOMNode().checked = true; + this.updateCustomMentionKeys(); + }, + render: function() { + var serverError = null; + if (this.state.serverError) { + serverError = this.state.serverError; + } + + var self = this; + + var user = this.props.user; + + var desktopSection; + if (this.props.activeSection === 'desktop') { + var notifyActive = [false, false, false]; + if (this.state.notifyLevel === 'mention') { + notifyActive[1] = true; + } else if (this.state.notifyLevel === 'none') { + notifyActive[2] = true; + } else { + notifyActive[0] = true; + } + + var inputs = []; + + inputs.push( + <div> + <div className='radio'> + <label> + <input type='radio' checked={notifyActive[0]} onClick={function(){self.handleNotifyRadio('all')}}>For all activity</input> + </label> + <br/> + </div> + <div className='radio'> + <label> + <input type='radio' checked={notifyActive[1]} onClick={function(){self.handleNotifyRadio('mention')}}>Only for mentions and private messages</input> + </label> + <br/> + </div> + <div className='radio'> + <label> + <input type='radio' checked={notifyActive[2]} onClick={function(){self.handleNotifyRadio('none')}}>Never</input> + </label> + </div> + </div> + ); + + desktopSection = ( + <SettingItemMax + title='Send desktop notifications' + inputs={inputs} + submit={this.handleSubmit} + server_error={serverError} + updateSection={function(e){self.updateSection('');e.preventDefault();}} + /> + ); + } else { + var describe = ''; + if (this.state.notifyLevel === 'mention') { + describe = 'Only for mentions and private messages'; + } else if (this.state.notifyLevel === 'none') { + describe = 'Never'; + } else { + describe = 'For all activity'; + } + + desktopSection = ( + <SettingItemMin + title='Send desktop notifications' + describe={describe} + updateSection={function(){self.updateSection('desktop');}} + /> + ); + } + + var soundSection; + if (this.props.activeSection === 'sound' && this.state.soundNeeded) { + var soundActive = ['', '']; + if (this.state.enableSound === 'false') { + soundActive[1] = 'active'; + } else { + soundActive[0] = 'active'; + } + + var inputs = []; + + inputs.push( + <div> + <div className='btn-group' data-toggle='buttons-radio'> + <button className={'btn btn-default '+soundActive[0]} onClick={function(){self.handleSoundRadio('true')}}>On</button> + <button className={'btn btn-default '+soundActive[1]} onClick={function(){self.handleSoundRadio('false')}}>Off</button> + </div> + </div> + ); + + soundSection = ( + <SettingItemMax + title='Desktop notification sounds' + inputs={inputs} + submit={this.handleSubmit} + server_error={serverError} + updateSection={function(e){self.updateSection('');e.preventDefault();}} + /> + ); + } else { + var describe = ''; + if (!this.state.soundNeeded) { + describe = 'Please configure notification sounds in your browser settings' + } else if (this.state.enableSound === 'false') { + describe = 'Off'; + } else { + describe = 'On'; + } + + soundSection = ( + <SettingItemMin + title='Desktop notification sounds' + describe={describe} + updateSection={function(){self.updateSection('sound');}} + disableOpen = {!this.state.soundNeeded} + /> + ); + } + + var emailSection; + if (this.props.activeSection === 'email') { + var emailActive = ['','']; + if (this.state.enableEmail === 'false') { + emailActive[1] = 'active'; + } else { + emailActive[0] = 'active'; + } + + var inputs = []; + + inputs.push( + <div> + <div className='btn-group' data-toggle='buttons-radio'> + <button className={'btn btn-default '+emailActive[0]} onClick={function(){self.handleEmailRadio('true')}}>On</button> + <button className={'btn btn-default '+emailActive[1]} onClick={function(){self.handleEmailRadio('false')}}>Off</button> + </div> + <div><br/>{'Email notifications are sent for mentions and private messages after you have been away from ' + config.SiteName + ' for 5 minutes.'}</div> + </div> + ); + + emailSection = ( + <SettingItemMax + title='Email notifications' + inputs={inputs} + submit={this.handleSubmit} + server_error={serverError} + updateSection={function(e){self.updateSection('');e.preventDefault();}} + /> + ); + } else { + var describe = ''; + if (this.state.enableEmail === 'false') { + describe = 'Off'; + } else { + describe = 'On'; + } + + emailSection = ( + <SettingItemMin + title='Email notifications' + describe={describe} + updateSection={function(){self.updateSection('email');}} + /> + ); + } + + var keysSection; + if (this.props.activeSection === 'keys') { + var inputs = []; + + if (user.first_name) { + inputs.push( + <div> + <div className='checkbox'> + <label> + <input type='checkbox' checked={this.state.firstNameKey} onChange={function(e){self.updateFirstNameKey(e.target.checked);}}>{'Your case sensitive first name "' + user.first_name + '"'}</input> + </label> + </div> + </div> + ); + } + + inputs.push( + <div> + <div className='checkbox'> + <label> + <input type='checkbox' checked={this.state.usernameKey} onChange={function(e){self.updateUsernameKey(e.target.checked);}}>{'Your non-case sensitive username "' + user.username + '"'}</input> + </label> + </div> + </div> + ); + + inputs.push( + <div> + <div className='checkbox'> + <label> + <input type='checkbox' checked={this.state.mentionKey} onChange={function(e){self.updateMentionKey(e.target.checked);}}>{'Your username mentioned "@' + user.username + '"'}</input> + </label> + </div> + </div> + ); + + inputs.push( + <div> + <div className='checkbox'> + <label> + <input type='checkbox' checked={this.state.allKey} onChange={function(e){self.updateAllKey(e.target.checked);}}>{'Team-wide mentions "@all"'}</input> + </label> + </div> + </div> + ); + + inputs.push( + <div> + <div className='checkbox'> + <label> + <input type='checkbox' checked={this.state.channelKey} onChange={function(e){self.updateChannelKey(e.target.checked);}}>{'Channel-wide mentions "@channel"'}</input> + </label> + </div> + </div> + ); + + inputs.push( + <div> + <div className='checkbox'> + <label> + <input ref='customcheck' type='checkbox' checked={this.state.customKeysChecked} onChange={this.updateCustomMentionKeys}>{'Other non-case sensitive words, separated by commas:'}</input> + </label> + </div> + <input ref='custommentions' className='form-control mentions-input' type='text' defaultValue={this.state.customKeys} onChange={this.onCustomChange} /> + </div> + ); + + keysSection = ( + <SettingItemMax + title='Words that trigger mentions' + inputs={inputs} + submit={this.handleSubmit} + server_error={serverError} + updateSection={function(e){self.updateSection('');e.preventDefault();}} + /> + ); + } else { + var keys = []; + if (this.state.firstNameKey) keys.push(user.first_name); + if (this.state.usernameKey) keys.push(user.username); + if (this.state.mentionKey) keys.push('@'+user.username); + if (this.state.allKey) keys.push('@all'); + if (this.state.channelKey) keys.push('@channel'); + if (this.state.customKeys.length > 0) keys = keys.concat(this.state.customKeys.split(',')); + + var describe = ''; + for (var i = 0; i < keys.length; i++) { + describe += '"' + keys[i] + '", '; + } + + if (describe.length > 0) { + describe = describe.substring(0, describe.length - 2); + } else { + describe = 'No words configured'; + } + + keysSection = ( + <SettingItemMin + title='Words that trigger mentions' + describe={describe} + updateSection={function(){self.updateSection('keys');}} + /> + ); + } + + return ( + <div> + <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'><i className='modal-back'></i>Notifications</h4> + </div> + <div ref='wrapper' className='user-settings'> + <h3 className='tab-header'>Notifications</h3> + <div className='divider-dark first'/> + {desktopSection} + <div className='divider-light'/> + {soundSection} + <div className='divider-light'/> + {emailSection} + <div className='divider-light'/> + {keysSection} + <div className='divider-dark'/> + </div> + </div> + + ); + } +}); diff --git a/web/react/components/user_settings_security.jsx b/web/react/components/user_settings_security.jsx new file mode 100644 index 000000000..568d3fe99 --- /dev/null +++ b/web/react/components/user_settings_security.jsx @@ -0,0 +1,200 @@ +var SettingItemMin = require('./setting_item_min.jsx'); +var SettingItemMax = require('./setting_item_max.jsx'); +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); +var Constants = require('../utils/constants.jsx'); + +module.exports = React.createClass({ + displayName: 'SecurityTab', + submitPassword: function(e) { + e.preventDefault(); + + var user = this.props.user; + var currentPassword = this.state.currentPassword; + var newPassword = this.state.newPassword; + var confirmPassword = this.state.confirmPassword; + + if (currentPassword === '') { + this.setState({passwordError: 'Please enter your current password', serverError: ''}); + return; + } + + if (newPassword.length < 5) { + this.setState({passwordError: 'New passwords must be at least 5 characters', serverError: ''}); + return; + } + + if (newPassword !== confirmPassword) { + this.setState({passwordError: 'The new passwords you entered do not match', serverError: ''}); + return; + } + + var data = {}; + data.user_id = user.id; + data.current_password = currentPassword; + data.new_password = newPassword; + + client.updatePassword(data, + function() { + this.props.updateSection(''); + AsyncClient.getMe(); + this.setState({currentPassword: '', newPassword: '', confirmPassword: ''}); + }.bind(this), + function(err) { + var state = this.getInitialState(); + if (err.message) { + state.serverError = err.message; + } else { + state.serverError = err; + } + state.passwordError = ''; + this.setState(state); + }.bind(this) + ); + }, + updateCurrentPassword: function(e) { + this.setState({currentPassword: e.target.value}); + }, + updateNewPassword: function(e) { + this.setState({newPassword: e.target.value}); + }, + updateConfirmPassword: function(e) { + this.setState({confirmPassword: e.target.value}); + }, + handleHistoryOpen: function() { + this.setState({willReturn: true}); + $("#user_settings").modal('hide'); + }, + handleDevicesOpen: function() { + this.setState({willReturn: true}); + $("#user_settings").modal('hide'); + }, + handleClose: function() { + $(this.getDOMNode()).find('.form-control').each(function() { + this.value = ''; + }); + this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + + if (!this.state.willReturn) { + this.props.updateTab('general'); + } else { + this.setState({willReturn: false}); + } + }, + componentDidMount: function() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + }, + componentWillUnmount: function() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); + }, + getInitialState: function() { + return {currentPassword: '', newPassword: '', confirmPassword: '', willReturn: false}; + }, + render: function() { + var serverError = this.state.serverError ? this.state.serverError : null; + var passwordError = this.state.passwordError ? this.state.passwordError : null; + + var updateSectionStatus; + var passwordSection; + var self = this; + if (this.props.activeSection === 'password') { + var inputs = []; + var submit = null; + + if (this.props.user.auth_service === '') { + inputs.push( + <div className='form-group'> + <label className='col-sm-5 control-label'>Current Password</label> + <div className='col-sm-7'> + <input className='form-control' type='password' onChange={this.updateCurrentPassword} value={this.state.currentPassword}/> + </div> + </div> + ); + inputs.push( + <div className='form-group'> + <label className='col-sm-5 control-label'>New Password</label> + <div className='col-sm-7'> + <input className='form-control' type='password' onChange={this.updateNewPassword} value={this.state.newPassword}/> + </div> + </div> + ); + inputs.push( + <div className='form-group'> + <label className='col-sm-5 control-label'>Retype New Password</label> + <div className='col-sm-7'> + <input className='form-control' type='password' onChange={this.updateConfirmPassword} value={this.state.confirmPassword}/> + </div> + </div> + ); + + submit = this.submitPassword; + } else { + inputs.push( + <div className='form-group'> + <label className='col-sm-12'>Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label> + </div> + ); + } + + updateSectionStatus = function(e) { + self.props.updateSection(''); + self.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + e.preventDefault(); + }; + + passwordSection = ( + <SettingItemMax + title='Password' + inputs={inputs} + submit={submit} + server_error={serverError} + client_error={passwordError} + updateSection={updateSectionStatus} + /> + ); + } else { + var describe; + if (this.props.user.auth_service === '') { + var d = new Date(this.props.user.last_password_update); + var hour = d.getHours() % 12 ? String(d.getHours() % 12) : '12'; + var min = d.getMinutes() < 10 ? '0' + d.getMinutes() : String(d.getMinutes()); + var timeOfDay = d.getHours() >= 12 ? ' pm' : ' am'; + describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; + } else { + describe = 'Log in done through GitLab'; + } + + updateSectionStatus = function() { + self.props.updateSection('password'); + }; + + passwordSection = ( + <SettingItemMin + title='Password' + describe={describe} + updateSection={updateSectionStatus} + /> + ); + } + + return ( + <div> + <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'><i className='modal-back'></i>Security Settings</h4> + </div> + <div className='user-settings'> + <h3 className='tab-header'>Security Settings</h3> + <div className='divider-dark first'/> + {passwordSection} + <div className='divider-dark'/> + <br></br> + <a data-toggle='modal' className='security-links theme' data-target='#access-history' href='#' onClick={this.handleHistoryOpen}><i className='fa fa-clock-o'></i>View Access History</a> + <b> </b> + <a data-toggle='modal' className='security-links theme' data-target='#activity-log' href='#' onClick={this.handleDevicesOpen}><i className='fa fa-globe'></i>View and Logout of Active Sessions</a> + </div> + </div> + ); + } +}); |