diff options
Diffstat (limited to 'web/react/components/user_settings')
6 files changed, 501 insertions, 106 deletions
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx new file mode 100644 index 000000000..44630a318 --- /dev/null +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -0,0 +1,108 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Constants = require('../../utils/constants.jsx'); + +export default class CustomThemeChooser extends React.Component { + constructor(props) { + super(props); + + this.onPickerChange = this.onPickerChange.bind(this); + this.onInputChange = this.onInputChange.bind(this); + this.pasteBoxChange = this.pasteBoxChange.bind(this); + + this.state = {}; + } + componentDidMount() { + $('.color-picker').colorpicker().on('changeColor', this.onPickerChange); + } + onPickerChange(e) { + const theme = this.props.theme; + theme[e.target.id] = e.color.toHex(); + theme.type = 'custom'; + this.props.updateTheme(theme); + } + onInputChange(e) { + const theme = this.props.theme; + theme[e.target.parentNode.id] = e.target.value; + theme.type = 'custom'; + this.props.updateTheme(theme); + } + pasteBoxChange(e) { + const text = e.target.value; + + if (text.length === 0) { + return; + } + + const colors = text.split(','); + + const theme = {type: 'custom'}; + let index = 0; + Constants.THEME_ELEMENTS.forEach((element) => { + if (index < colors.length) { + theme[element.id] = colors[index]; + } + index++; + }); + + this.props.updateTheme(theme); + } + render() { + const theme = this.props.theme; + + const elements = []; + let colors = ''; + Constants.THEME_ELEMENTS.forEach((element) => { + elements.push( + <div className='col-sm-4 form-group'> + <label className='custom-label'>{element.uiName}</label> + <div + className='input-group color-picker' + id={element.id} + > + <input + className='form-control' + type='text' + defaultValue={theme[element.id]} + onChange={this.onInputChange} + /> + <span className='input-group-addon'><i></i></span> + </div> + </div> + ); + + colors += theme[element.id] + ','; + }); + + const pasteBox = ( + <div className='col-sm-12'> + <label className='custom-label'> + {'Copy and paste to share theme colors:'} + </label> + <input + type='text' + className='form-control' + value={colors} + onChange={this.pasteBoxChange} + /> + </div> + ); + + return ( + <div> + <div className='row form-group'> + {elements} + </div> + <div className='row'> + {pasteBox} + </div> + </div> + ); + } +} + +CustomThemeChooser.propTypes = { + theme: React.PropTypes.object.isRequired, + updateTheme: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx new file mode 100644 index 000000000..48be83afe --- /dev/null +++ b/web/react/components/user_settings/import_theme_modal.jsx @@ -0,0 +1,179 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const UserStore = require('../../stores/user_store.jsx'); +const Utils = require('../../utils/utils.jsx'); +const Client = require('../../utils/client.jsx'); +const Modal = ReactBootstrap.Modal; + +const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); +const Constants = require('../../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; + +export default class ImportThemeModal extends React.Component { + constructor(props) { + super(props); + + this.updateShow = this.updateShow.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + + this.state = { + inputError: '', + show: false + }; + } + componentDidMount() { + UserStore.addImportModalListener(this.updateShow); + } + componentWillUnmount() { + UserStore.removeImportModalListener(this.updateShow); + } + updateShow(show) { + this.setState({show}); + } + handleSubmit(e) { + e.preventDefault(); + + const text = React.findDOMNode(this.refs.input).value; + + if (!this.isInputValid(text)) { + this.setState({inputError: 'Invalid format, please try copying and pasting in again.'}); + return; + } + + const colors = text.split(','); + const theme = {type: 'custom'}; + + theme.sidebarBg = colors[0]; + theme.sidebarText = colors[5]; + theme.sidebarUnreadText = colors[5]; + theme.sidebarTextHoverBg = colors[4]; + theme.sidebarTextHoverColor = colors[5]; + theme.sidebarTextActiveBg = colors[2]; + theme.sidebarTextActiveColor = colors[3]; + theme.sidebarHeaderBg = colors[1]; + theme.sidebarHeaderTextColor = colors[5]; + theme.onlineIndicator = colors[6]; + theme.mentionBj = colors[7]; + theme.mentionColor = '#ffffff'; + theme.centerChannelBg = '#ffffff'; + theme.centerChannelColor = '#333333'; + theme.linkColor = '#2389d7'; + theme.buttonBg = '#26a970'; + theme.buttonColor = '#ffffff'; + + let user = UserStore.getCurrentUser(); + user.theme_props = theme; + + Client.updateUser(user, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_ME, + me: data + }); + + this.setState({show: false}); + Utils.applyTheme(theme); + $('#user_settings').modal('show'); + }, + (err) => { + var state = this.getStateFromStores(); + state.serverError = err; + this.setState(state); + } + ); + } + isInputValid(text) { + if (text.length === 0) { + return false; + } + + if (text.indexOf(' ') !== -1) { + return false; + } + + if (text.length > 0 && text.indexOf(',') === -1) { + return false; + } + + if (text.length > 0) { + const colors = text.split(','); + + if (colors.length !== 8) { + return false; + } + + for (let i = 0; i < colors.length; i++) { + if (colors[i].length !== 7 && colors[i].length !== 4) { + return false; + } + + if (colors[i].charAt(0) !== '#') { + return false; + } + } + } + + return true; + } + handleChange(e) { + if (this.isInputValid(e.target.value)) { + this.setState({inputError: null}); + } else { + this.setState({inputError: 'Invalid format, please try copying and pasting in again.'}); + } + } + render() { + return ( + <span> + <Modal + show={this.state.show} + onHide={() => this.setState({show: false})} + > + <Modal.Header closeButton={true}> + <Modal.Title>{'Import Slack Theme'}</Modal.Title> + </Modal.Header> + <form + role='form' + className='form-horizontal' + > + <Modal.Body> + <p> + {'To import a theme, go to a Slack team and look for “”Preferences” -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'} + </p> + <div className='form-group less'> + <div className='col-sm-9'> + <input + ref='input' + type='text' + className='form-control' + onChange={this.handleChange} + /> + {this.state.inputError} + </div> + </div> + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={() => this.setState({show: false})} + > + {'Cancel'} + </button> + <button + onClick={this.handleSubmit} + type='submit' + className='btn btn-primary' + tabIndex='3' + > + {'Submit'} + </button> + </Modal.Footer> + </form> + </Modal> + </span> + ); + } +} diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx new file mode 100644 index 000000000..e36503053 --- /dev/null +++ b/web/react/components/user_settings/premade_theme_chooser.jsx @@ -0,0 +1,55 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Utils = require('../../utils/utils.jsx'); +var Constants = require('../../utils/constants.jsx'); + +export default class PremadeThemeChooser extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { + const theme = this.props.theme; + + const premadeThemes = []; + for (const k in Constants.THEMES) { + if (Constants.THEMES.hasOwnProperty(k)) { + const premadeTheme = $.extend(true, {}, Constants.THEMES[k]); + + let activeClass = ''; + if (premadeTheme.type === theme.type) { + activeClass = 'active'; + } + + premadeThemes.push( + <div className='col-sm-3 premade-themes'> + <div + className={activeClass} + onClick={() => this.props.updateTheme(premadeTheme)} + > + <label> + <img + className='img-responsive' + src={'/static/images/themes/' + premadeTheme.type + '.png'} + /> + <div className='theme-label'>{Utils.toTitleCase(premadeTheme.type)}</div> + </label> + </div> + </div> + ); + } + } + + return ( + <div className='row'> + {premadeThemes} + </div> + ); + } +} + +PremadeThemeChooser.propTypes = { + theme: React.PropTypes.object.isRequired, + updateTheme: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index aec3b319d..7617f04d1 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -2,78 +2,119 @@ // 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 Client = require('../../utils/client.jsx'); var Utils = require('../../utils/utils.jsx'); -var ThemeColors = ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000']; +const CustomThemeChooser = require('./custom_theme_chooser.jsx'); +const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); +const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); +const Constants = require('../../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; export default class UserSettingsAppearance extends React.Component { constructor(props) { super(props); + this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); this.handleClose = this.handleClose.bind(this); + this.handleImportModal = this.handleImportModal.bind(this); this.state = this.getStateFromStores(); + + this.originalTheme = this.state.theme; + } + componentDidMount() { + UserStore.addChangeListener(this.onChange); + + if (this.props.activeSection === 'theme') { + $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); + } + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentDidUpdate() { + if (this.props.activeSection === 'theme') { + $('.color-btn').removeClass('active-border'); + $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); + } + } + componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); + $('#user_settings').off('hidden.bs.modal', this.handleClose); } getStateFromStores() { - var user = UserStore.getCurrentUser(); - var theme = '#2389d7'; - if (ThemeColors != null) { - theme = ThemeColors[0]; + const user = UserStore.getCurrentUser(); + let theme = null; + + if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { + theme = user.theme_props; + } else { + theme = $.extend(true, {}, Constants.THEMES.default); } - if (user.props && user.props.theme) { - theme = user.props.theme; + + let type = 'premade'; + if (theme.type === 'custom') { + type = 'custom'; } - return {theme: theme.toLowerCase()}; + return {theme, type}; + } + onChange() { + const newState = this.getStateFromStores(); + + if (!Utils.areStatesEqual(this.state, newState)) { + this.setState(newState); + } } submitTheme(e) { e.preventDefault(); var user = UserStore.getCurrentUser(); - if (!user.props) { - user.props = {}; - } - user.props.theme = this.state.theme; + user.theme_props = this.state.theme; Client.updateUser(user, - function success() { - this.props.updateSection(''); - window.location.reload(); - }.bind(this), - function fail(err) { + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_ME, + me: data + }); + + $('#user_settings').off('hidden.bs.modal', this.handleClose); + this.props.updateTab('general'); + $('#user_settings').modal('hide'); + }, + (err) => { var state = this.getStateFromStores(); state.serverError = err; this.setState(state); - }.bind(this) + } ); } - updateTheme(e) { - var hex = Utils.rgb2hex(e.target.style.backgroundColor); - this.setState({theme: hex.toLowerCase()}); + updateTheme(theme) { + this.setState({theme}); + Utils.applyTheme(theme); } - handleClose() { - this.setState({serverError: null}); - this.props.updateTab('general'); + updateType(type) { + this.setState({type}); } - componentDidMount() { - if (this.props.activeSection === 'theme') { - $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); - } - $('#user_settings').on('hidden.bs.modal', this.handleClose); - } - componentDidUpdate() { - if (this.props.activeSection === 'theme') { - $('.color-btn').removeClass('active-border'); - $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); - } + handleClose() { + const state = this.getStateFromStores(); + state.serverError = null; + + Utils.applyTheme(state.theme); + + this.setState(state); + + $('.ps-container.modal-body').scrollTop(0); + $('.ps-container.modal-body').perfectScrollbar('update'); + $('#user_settings').modal('hide'); } - componentWillUnmount() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - this.props.updateSection(''); + handleImportModal() { + $('#user_settings').modal('hide'); + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL, + value: true + }); } render() { var serverError; @@ -81,67 +122,73 @@ export default class UserSettingsAppearance extends React.Component { serverError = this.state.serverError; } - var themeSection; - var self = this; - - if (ThemeColors != null) { - if (this.props.activeSection === 'theme') { - var themeButtons = []; - - for (var i = 0; i < ThemeColors.length; i++) { - themeButtons.push( - <button - key={ThemeColors[i] + 'key' + i} - ref={ThemeColors[i]} - type='button' - className='btn btn-lg color-btn' - style={{backgroundColor: ThemeColors[i]}} - onClick={this.updateTheme} - /> - ); - } - - var inputs = []; - - inputs.push( - <li - key='themeColorSetting' - className='setting-list-item' - > - <div - className='btn-group' - data-toggle='buttons-radio' - > - {themeButtons} - </div> - </li> - ); - - themeSection = ( - <SettingItemMax - title='Theme Color' - inputs={inputs} - submit={this.submitTheme} - serverError={serverError} - updateSection={function updateSection(e) { - self.props.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - themeSection = ( - <SettingItemMin - title='Theme Color' - describe={this.state.theme} - updateSection={function updateSection() { - self.props.updateSection('theme'); - }} - /> - ); - } + const displayCustom = this.state.type === 'custom'; + + let custom; + let premade; + if (displayCustom) { + custom = ( + <CustomThemeChooser + theme={this.state.theme} + updateTheme={this.updateTheme} + /> + ); + } else { + premade = ( + <PremadeThemeChooser + theme={this.state.theme} + updateTheme={this.updateTheme} + /> + ); } + const themeUI = ( + <div className='section-max appearance-section'> + <div className='col-sm-12'> + <div className='radio'> + <label> + <input type='radio' + checked={!displayCustom} + onChange={this.updateType.bind(this, 'premade')} + > + {'Theme Colors'} + </input> + </label> + <br/> + </div> + {premade} + <div className='radio'> + <label> + <input type='radio' + checked={displayCustom} + onChange={this.updateType.bind(this, 'custom')} + > + {'Custom Theme'} + </input> + </label> + <br/> + </div> + {custom} + <hr /> + {serverError} + <a + className='btn btn-sm btn-primary' + href='#' + onClick={this.submitTheme} + > + {'Submit'} + </a> + <a + className='btn btn-sm theme' + href='#' + onClick={this.handleClose} + > + {'Cancel'} + </a> + </div> + </div> + ); + return ( <div> <div className='modal-header'> @@ -151,21 +198,28 @@ export default class UserSettingsAppearance extends React.Component { data-dismiss='modal' aria-label='Close' > - <span aria-hidden='true'>×</span> + <span aria-hidden='true'>{'×'}</span> </button> <h4 className='modal-title' ref='title' > - <i className='modal-back'></i>Appearance Settings + <i className='modal-back'></i>{'Appearance Settings'} </h4> </div> <div className='user-settings'> - <h3 className='tab-header'>Appearance Settings</h3> + <h3 className='tab-header'>{'Appearance Settings'}</h3> <div className='divider-dark first'/> - {themeSection} + {themeUI} <div className='divider-dark'/> </div> + <br/> + <a + className='theme' + onClick={this.handleImportModal} + > + {'Import from Slack'} + </a> </div> ); } @@ -176,6 +230,5 @@ UserSettingsAppearance.defaultProps = { }; UserSettingsAppearance.propTypes = { activeSection: React.PropTypes.string, - updateSection: React.PropTypes.func, updateTab: React.PropTypes.func }; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 1694aaa79..d9fb43902 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -64,7 +64,7 @@ export default class DeveloperTab extends React.Component { data-dismiss='modal' aria-label='Close' > - <span aria-hidden='true'>{'x'}</span> + <span aria-hidden='true'>{'×'}</span> </button> <h4 className='modal-title' diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 1b22e6045..430a7ec7c 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -60,7 +60,7 @@ export default class UserSettingsModal extends React.Component { data-dismiss='modal' aria-label='Close' > - <span aria-hidden='true'>{'x'}</span> + <span aria-hidden='true'>{'×'}</span> </button> <h4 className='modal-title' |