Merge branch 'master' into PLT-11-email

Этот коммит содержится в:
=Corey Hulen
2015-09-23 12:55:58 -07:00
родитель 9e04909c0a 0170cfe604
Коммит 2b5b8f95ed
68 изменённых файлов: 1509 добавлений и 358 удалений

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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.toLowerCase() + '.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
};

Просмотреть файл

@@ -2,64 +2,32 @@
// 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();
}
getStateFromStores() {
var user = UserStore.getCurrentUser();
var theme = '#2389d7';
if (ThemeColors != null) {
theme = ThemeColors[0];
}
if (user.props && user.props.theme) {
theme = user.props.theme;
}
return {theme: theme.toLowerCase()};
}
submitTheme(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
if (!user.props) {
user.props = {};
}
user.props.theme = this.state.theme;
Client.updateUser(user,
function success() {
this.props.updateSection('');
window.location.reload();
}.bind(this),
function fail(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()});
}
handleClose() {
this.setState({serverError: null});
this.props.updateTab('general');
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');
}
@@ -72,8 +40,81 @@ export default class UserSettingsAppearance extends React.Component {
}
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
$('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateSection('');
}
getStateFromStores() {
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);
}
let type = 'premade';
if (theme.type === 'custom') {
type = 'custom';
}
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();
user.theme_props = this.state.theme;
Client.updateUser(user,
(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);
}
);
}
updateTheme(theme) {
this.setState({theme});
Utils.applyTheme(theme);
}
updateType(type) {
this.setState({type});
}
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');
}
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;
const displayCustom = this.state.type === 'custom';
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');
}}
/>
);
}
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'>&times;</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
};

Просмотреть файл

@@ -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'

Просмотреть файл

@@ -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'

Просмотреть файл

@@ -241,7 +241,7 @@ export default class NotificationsTab extends React.Component {
checked={notifyActive[1]}
onChange={this.handleNotifyRadio.bind(this, 'mention')}
>
Only for mentions and private messages
Only for mentions and direct messages
</input>
</label>
<br/>
@@ -277,7 +277,7 @@ export default class NotificationsTab extends React.Component {
} else {
let describe = '';
if (this.state.notifyLevel === 'mention') {
describe = 'Only for mentions and private messages';
describe = 'Only for mentions and direct messages';
} else if (this.state.notifyLevel === 'none') {
describe = 'Never';
} else {
@@ -414,7 +414,7 @@ export default class NotificationsTab extends React.Component {
</label>
<br/>
</div>
<div><br/>{'Email notifications are sent for mentions and private messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
<div><br/>{'Email notifications are sent for mentions and direct messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
</div>
);