diff options
author | =Corey Hulen <corey@hulen.com> | 2015-09-23 14:38:34 -0700 |
---|---|---|
committer | =Corey Hulen <corey@hulen.com> | 2015-09-23 14:38:34 -0700 |
commit | 7f3bfdbe0c2442714bb5a14adfdeac1576123601 (patch) | |
tree | 44962a914b86cf66a1da49300f12986db49f010e /web/react/components | |
parent | 1626a6de6f16ba0878160b0a7eae9f49b8d34d4f (diff) | |
parent | c7c644874e98db0ae83c5a44ec50ff811d9b3f46 (diff) | |
download | chat-7f3bfdbe0c2442714bb5a14adfdeac1576123601.tar.gz chat-7f3bfdbe0c2442714bb5a14adfdeac1576123601.tar.bz2 chat-7f3bfdbe0c2442714bb5a14adfdeac1576123601.zip |
Fixing merge
Diffstat (limited to 'web/react/components')
32 files changed, 669 insertions, 250 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 25476251f..92f0bbdce 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -10,7 +10,7 @@ var LoadingScreen = require('../loading_screen.jsx'); var EmailSettingsTab = require('./email_settings.jsx'); var LogSettingsTab = require('./log_settings.jsx'); var LogsTab = require('./logs.jsx'); -var ImageSettingsTab = require('./image_settings.jsx'); +var FileSettingsTab = require('./image_settings.jsx'); var PrivacySettingsTab = require('./privacy_settings.jsx'); var RateSettingsTab = require('./rate_settings.jsx'); var GitLabSettingsTab = require('./gitlab_settings.jsx'); @@ -128,7 +128,7 @@ export default class AdminController extends React.Component { } else if (this.state.selected === 'logs') { tab = <LogsTab />; } else if (this.state.selected === 'image_settings') { - tab = <ImageSettingsTab config={this.state.config} />; + tab = <FileSettingsTab config={this.state.config} />; } else if (this.state.selected === 'privacy_settings') { tab = <PrivacySettingsTab config={this.state.config} />; } else if (this.state.selected === 'rate_settings') { diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index b8413d6c7..375a6a8e9 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -180,7 +180,7 @@ export default class AdminSidebar extends React.Component { className={this.isSelected('image_settings')} onClick={this.handleClick.bind(this, 'image_settings', null)} > - {'Image Settings'} + {'File Settings'} </a> </li> <li> diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx index f84f1c735..25d5ad857 100644 --- a/web/react/components/admin_console/image_settings.jsx +++ b/web/react/components/admin_console/image_settings.jsx @@ -5,7 +5,7 @@ var Client = require('../../utils/client.jsx'); var AsyncClient = require('../../utils/async_client.jsx'); var crypto = require('crypto'); -export default class ImageSettings extends React.Component { +export default class FileSettings extends React.Component { constructor(props) { super(props); @@ -16,7 +16,7 @@ export default class ImageSettings extends React.Component { this.state = { saveNeeded: false, serverError: null, - DriverName: this.props.config.ImageSettings.DriverName + DriverName: this.props.config.FileSettings.DriverName }; } @@ -42,61 +42,61 @@ export default class ImageSettings extends React.Component { $('#save-button').button('loading'); var config = this.props.config; - config.ImageSettings.DriverName = React.findDOMNode(this.refs.DriverName).value; - config.ImageSettings.Directory = React.findDOMNode(this.refs.Directory).value; - config.ImageSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value; - config.ImageSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value; - config.ImageSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value; - config.ImageSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value; - config.ImageSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked; - - config.ImageSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim(); - - if (config.ImageSettings.PublicLinkSalt === '') { - config.ImageSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); - React.findDOMNode(this.refs.PublicLinkSalt).value = config.ImageSettings.PublicLinkSalt; + config.FileSettings.DriverName = React.findDOMNode(this.refs.DriverName).value; + config.FileSettings.Directory = React.findDOMNode(this.refs.Directory).value; + config.FileSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value; + config.FileSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value; + config.FileSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value; + config.FileSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value; + config.FileSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked; + + config.FileSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim(); + + if (config.FileSettings.PublicLinkSalt === '') { + config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); + React.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt; } var thumbnailWidth = 120; if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) { thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10); } - config.ImageSettings.ThumbnailWidth = thumbnailWidth; + config.FileSettings.ThumbnailWidth = thumbnailWidth; React.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth; var thumbnailHeight = 100; if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10))) { thumbnailHeight = parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10); } - config.ImageSettings.ThumbnailHeight = thumbnailHeight; + config.FileSettings.ThumbnailHeight = thumbnailHeight; React.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight; var previewWidth = 1024; if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10))) { previewWidth = parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10); } - config.ImageSettings.PreviewWidth = previewWidth; + config.FileSettings.PreviewWidth = previewWidth; React.findDOMNode(this.refs.PreviewWidth).value = previewWidth; var previewHeight = 0; if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10))) { previewHeight = parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10); } - config.ImageSettings.PreviewHeight = previewHeight; + config.FileSettings.PreviewHeight = previewHeight; React.findDOMNode(this.refs.PreviewHeight).value = previewHeight; var profileWidth = 128; if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10))) { profileWidth = parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10); } - config.ImageSettings.ProfileWidth = profileWidth; + config.FileSettings.ProfileWidth = profileWidth; React.findDOMNode(this.refs.ProfileWidth).value = profileWidth; var profileHeight = 128; if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10))) { profileHeight = parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10); } - config.ImageSettings.ProfileHeight = profileHeight; + config.FileSettings.ProfileHeight = profileHeight; React.findDOMNode(this.refs.ProfileHeight).value = profileHeight; Client.saveConfig( @@ -143,7 +143,7 @@ export default class ImageSettings extends React.Component { return ( <div className='wrapper--fixed'> - <h3>{'Image Settings'}</h3> + <h3>{'File Settings'}</h3> <form className='form-horizontal' role='form' @@ -161,7 +161,7 @@ export default class ImageSettings extends React.Component { className='form-control' id='DriverName' ref='DriverName' - defaultValue={this.props.config.ImageSettings.DriverName} + defaultValue={this.props.config.FileSettings.DriverName} onChange={this.handleChange.bind(this, 'DriverName')} > <option value=''>{'Disable File Storage'}</option> @@ -185,7 +185,7 @@ export default class ImageSettings extends React.Component { id='Directory' ref='Directory' placeholder='Ex "./data/"' - defaultValue={this.props.config.ImageSettings.Directory} + defaultValue={this.props.config.FileSettings.Directory} onChange={this.handleChange} disabled={!enableFile} /> @@ -207,7 +207,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3AccessKeyId' ref='AmazonS3AccessKeyId' placeholder='Ex "AKIADTOVBGERKLCBV"' - defaultValue={this.props.config.ImageSettings.AmazonS3AccessKeyId} + defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId} onChange={this.handleChange} disabled={!enableS3} /> @@ -229,7 +229,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3SecretAccessKey' ref='AmazonS3SecretAccessKey' placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"' - defaultValue={this.props.config.ImageSettings.AmazonS3SecretAccessKey} + defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey} onChange={this.handleChange} disabled={!enableS3} /> @@ -251,7 +251,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3Bucket' ref='AmazonS3Bucket' placeholder='Ex "mattermost-media"' - defaultValue={this.props.config.ImageSettings.AmazonS3Bucket} + defaultValue={this.props.config.FileSettings.AmazonS3Bucket} onChange={this.handleChange} disabled={!enableS3} /> @@ -273,7 +273,7 @@ export default class ImageSettings extends React.Component { id='AmazonS3Region' ref='AmazonS3Region' placeholder='Ex "us-east-1"' - defaultValue={this.props.config.ImageSettings.AmazonS3Region} + defaultValue={this.props.config.FileSettings.AmazonS3Region} onChange={this.handleChange} disabled={!enableS3} /> @@ -295,7 +295,7 @@ export default class ImageSettings extends React.Component { id='ThumbnailWidth' ref='ThumbnailWidth' placeholder='Ex "120"' - defaultValue={this.props.config.ImageSettings.ThumbnailWidth} + defaultValue={this.props.config.FileSettings.ThumbnailWidth} onChange={this.handleChange} /> <p className='help-text'>{'Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p> @@ -316,7 +316,7 @@ export default class ImageSettings extends React.Component { id='ThumbnailHeight' ref='ThumbnailHeight' placeholder='Ex "100"' - defaultValue={this.props.config.ImageSettings.ThumbnailHeight} + defaultValue={this.props.config.FileSettings.ThumbnailHeight} onChange={this.handleChange} /> <p className='help-text'>{'Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p> @@ -337,7 +337,7 @@ export default class ImageSettings extends React.Component { id='PreviewWidth' ref='PreviewWidth' placeholder='Ex "1024"' - defaultValue={this.props.config.ImageSettings.PreviewWidth} + defaultValue={this.props.config.FileSettings.PreviewWidth} onChange={this.handleChange} /> <p className='help-text'>{'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}</p> @@ -358,7 +358,7 @@ export default class ImageSettings extends React.Component { id='PreviewHeight' ref='PreviewHeight' placeholder='Ex "0"' - defaultValue={this.props.config.ImageSettings.PreviewHeight} + defaultValue={this.props.config.FileSettings.PreviewHeight} onChange={this.handleChange} /> <p className='help-text'>{'Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'}</p> @@ -379,7 +379,7 @@ export default class ImageSettings extends React.Component { id='ProfileWidth' ref='ProfileWidth' placeholder='Ex "1024"' - defaultValue={this.props.config.ImageSettings.ProfileWidth} + defaultValue={this.props.config.FileSettings.ProfileWidth} onChange={this.handleChange} /> <p className='help-text'>{'Width of profile picture.'}</p> @@ -400,7 +400,7 @@ export default class ImageSettings extends React.Component { id='ProfileHeight' ref='ProfileHeight' placeholder='Ex "0"' - defaultValue={this.props.config.ImageSettings.ProfileHeight} + defaultValue={this.props.config.FileSettings.ProfileHeight} onChange={this.handleChange} /> <p className='help-text'>{'Height of profile picture.'}</p> @@ -421,7 +421,7 @@ export default class ImageSettings extends React.Component { name='EnablePublicLink' value='true' ref='EnablePublicLink' - defaultChecked={this.props.config.ImageSettings.EnablePublicLink} + defaultChecked={this.props.config.FileSettings.EnablePublicLink} onChange={this.handleChange} /> {'true'} @@ -431,7 +431,7 @@ export default class ImageSettings extends React.Component { type='radio' name='EnablePublicLink' value='false' - defaultChecked={!this.props.config.ImageSettings.EnablePublicLink} + defaultChecked={!this.props.config.FileSettings.EnablePublicLink} onChange={this.handleChange} /> {'false'} @@ -454,7 +454,7 @@ export default class ImageSettings extends React.Component { id='PublicLinkSalt' ref='PublicLinkSalt' placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"' - defaultValue={this.props.config.ImageSettings.PublicLinkSalt} + defaultValue={this.props.config.FileSettings.PublicLinkSalt} onChange={this.handleChange} /> <p className='help-text'>{'32-character salt added to signing of public image links.'}</p> @@ -491,6 +491,6 @@ export default class ImageSettings extends React.Component { } } -ImageSettings.propTypes = { +FileSettings.propTypes = { config: React.PropTypes.object }; diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 8d23ec646..b81936b57 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -55,7 +55,7 @@ export default class ChannelHeader extends React.Component { if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } - $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover click', html: true, delay: {show: 500, hide: 500}}); + $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); } onSocketChange(msg) { if (msg.action === 'new_user') { diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index ce6f60f87..962ba26ee 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -12,6 +12,7 @@ var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var Utils = require('../utils/utils.jsx'); +var Constants = require('../utils/constants.jsx'); export default class ChannelLoader extends React.Component { constructor(props) { @@ -68,33 +69,19 @@ export default class ChannelLoader extends React.Component { /* Update CSS classes to match user theme */ var user = UserStore.getCurrentUser(); - if (user.props && user.props.theme) { - Utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';'); - Utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';'); - Utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';'); - Utils.changeCss('.mention', 'background: ' + user.props.theme + ';'); - Utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';'); - Utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}'); - Utils.changeCss('.search-item-container:hover', 'background: ' + Utils.changeOpacity(user.props.theme, 0.05) + ';'); - } - - if (user.props.theme !== '#000000' && user.props.theme !== '#585858') { - Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, -10) + ';'); - Utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;'); - } else if (user.props.theme === '#000000') { - Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +50) + ';'); - $('.team__header').addClass('theme--black'); - } else if (user.props.theme === '#585858') { - Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +10) + ';'); - $('.team__header').addClass('theme--gray'); + if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { + Utils.applyTheme(user.theme_props); + } else { + Utils.applyTheme(Constants.THEMES.default); } /* Setup global mouse events */ - $('body').on('click.userpopover', function popOver(e) { - if ($(e.target).attr('data-toggle') !== 'popover' && - $(e.target).parents('.popover.in').length === 0) { - $('.user-popover').popover('hide'); - } + $('body').on('click', function hidePopover(e) { + $('[data-toggle="popover"]').each(function eachPopover() { + if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) { + $(this).popover('hide'); + } + }); }); $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index d9e67836d..abad60154 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -23,6 +23,7 @@ export default class CreatePost extends React.Component { this.lastTime = 0; + this.getCurrentDraft = this.getCurrentDraft.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.postMsgKeyPress = this.postMsgKeyPress.bind(this); this.handleUserInput = this.handleUserInput.bind(this); @@ -36,23 +37,15 @@ export default class CreatePost extends React.Component { PostStore.clearDraftUploads(); - const draft = PostStore.getCurrentDraft(); - let previews = []; - let messageText = ''; - let uploadsInProgress = []; - if (draft && draft.previews && draft.message) { - previews = draft.previews; - messageText = draft.message; - uploadsInProgress = draft.uploadsInProgress; - } + const draft = this.getCurrentDraft(); this.state = { channelId: ChannelStore.getCurrentId(), - messageText: messageText, - uploadsInProgress: uploadsInProgress, - previews: previews, + messageText: draft.messageText, + uploadsInProgress: draft.uploadsInProgress, + previews: draft.previews, submitting: false, - initialText: messageText + initialText: draft.messageText }; } componentDidUpdate(prevProps, prevState) { @@ -60,6 +53,24 @@ export default class CreatePost extends React.Component { this.resizePostHolder(); } } + getCurrentDraft() { + const draft = PostStore.getCurrentDraft(); + const safeDraft = {previews: [], messageText: '', uploadsInProgress: []}; + + if (draft) { + if (draft.message) { + safeDraft.messageText = draft.message; + } + if (draft.previews) { + safeDraft.previews = draft.previews; + } + if (draft.uploadsInProgress) { + safeDraft.uploadsInProgress = draft.uploadsInProgress; + } + } + + return safeDraft; + } handleSubmit(e) { e.preventDefault(); @@ -253,18 +264,9 @@ export default class CreatePost extends React.Component { onChange() { const channelId = ChannelStore.getCurrentId(); if (this.state.channelId !== channelId) { - let draft = PostStore.getCurrentDraft(); - - let previews = []; - let messageText = ''; - let uploadsInProgress = []; - if (draft && draft.previews && draft.message) { - previews = draft.previews; - messageText = draft.message; - uploadsInProgress = draft.uploadsInProgress; - } + const draft = this.getCurrentDraft(); - this.setState({channelId: channelId, messageText: messageText, initialText: messageText, submitting: false, serverError: null, postError: null, previews: previews, uploadsInProgress: uploadsInProgress}); + this.setState({channelId: channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress}); } } getFileCount(channelId) { diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx index 92123956f..8d3f15525 100644 --- a/web/react/components/email_verify.jsx +++ b/web/react/components/email_verify.jsx @@ -10,12 +10,14 @@ export default class EmailVerify extends React.Component { this.state = {}; } handleResend() { - window.location.href = window.location.href + '&resend=true'; + const newAddress = window.location.href.replace('&resend_success=true', ''); + window.location.href = newAddress + '&resend=true'; } render() { var title = ''; var body = ''; var resend = ''; + var resendConfirm = ''; if (this.props.isVerified === 'true') { title = global.window.config.SiteName + ' Email Verified'; body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>; @@ -30,6 +32,9 @@ export default class EmailVerify extends React.Component { Resend Email </button> ); + if (this.props.resendSuccess) { + resendConfirm = <div><br /><p className='alert alert-success'><i className='fa fa-check'></i>{' Verification email sent.'}</p></div>; + } } return ( @@ -41,6 +46,7 @@ export default class EmailVerify extends React.Component { <div className='panel-body'> {body} {resend} + {resendConfirm} </div> </div> </div> @@ -51,10 +57,12 @@ export default class EmailVerify extends React.Component { EmailVerify.defaultProps = { isVerified: 'false', teamURL: '', - userEmail: '' + userEmail: '', + resendSuccess: 'false' }; EmailVerify.propTypes = { isVerified: React.PropTypes.string, teamURL: React.PropTypes.string, - userEmail: React.PropTypes.string + userEmail: React.PropTypes.string, + resendSuccess: React.PropTypes.string }; diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index f27b09ecc..b7bce9b34 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -114,7 +114,7 @@ export default class MoreDirectChannels extends React.Component { <span aria-hidden='true'>×</span> <span className='sr-only'>Close</span> </button> - <h4 className='modal-title'>More Private Messages</h4> + <h4 className='modal-title'>More Direct Messages</h4> </div> <div className='modal-body'> <ul className='nav nav-pills nav-stacked'> diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx index c43137744..c8ef59b4a 100644 --- a/web/react/components/new_channel_modal.jsx +++ b/web/react/components/new_channel_modal.jsx @@ -93,6 +93,7 @@ export default class NewChannelModal extends React.Component { <span> <Modal show={this.props.show} + bsSize='large' onHide={this.props.onModalDismissed} > <Modal.Header closeButton={true}> @@ -122,7 +123,7 @@ export default class NewChannelModal extends React.Component { /> {displayNameError} <p className='input__help dark'> - {'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('} + {'URL: ' + prettyTeamURL + this.props.channelData.name + ' ('} <a href='#' onClick={this.props.onChangeURLPressed} diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx index 1e6cc3607..37d4a58cb 100644 --- a/web/react/components/password_reset_send_link.jsx +++ b/web/react/components/password_reset_send_link.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. +const Utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); export default class PasswordResetSendLink extends React.Component { @@ -15,8 +16,8 @@ export default class PasswordResetSendLink extends React.Component { e.preventDefault(); var state = {}; - var email = React.findDOMNode(this.refs.email).value.trim(); - if (!email) { + var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase(); + if (!email || !Utils.isEmail(email)) { state.error = 'Please enter a valid email address.'; this.setState(state); return; @@ -67,7 +68,7 @@ export default class PasswordResetSendLink extends React.Component { <p>{'To reset your password, enter the email address you used to sign up for ' + this.props.teamDisplayName + '.'}</p> <div className={formClass}> <input - type='text' + type='email' className='form-control' name='email' ref='email' diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index ec873dd00..a2ca8b00f 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -65,7 +65,7 @@ export default class PopoverListMembers extends React.Component { > {count} <span - className='glyphicon glyphicon-user' + className='fa fa-user' aria-hidden='true' /> </div> diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index d3c6befd0..9127f00de 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -51,7 +51,7 @@ export default class Post extends React.Component { var post = this.props.post; client.createPost(post, post.channel_id, - function success(data) { + (data) => { AsyncClient.getPosts(); var channel = ChannelStore.get(post.channel_id); @@ -65,11 +65,11 @@ export default class Post extends React.Component { post: data }); }, - function error() { + () => { post.state = Constants.POST_FAILED; PostStore.updatePendingPost(post); this.forceUpdate(); - }.bind(this) + } ); post.state = Constants.POST_LOADING; @@ -81,31 +81,52 @@ export default class Post extends React.Component { return true; } - return false; - } - render() { - var post = this.props.post; - var parentPost = this.props.parentPost; - var posts = this.props.posts; + if (nextProps.sameRoot !== this.props.sameRoot) { + return true; + } - var type = 'Post'; - if (post.root_id && post.root_id.length > 0) { - type = 'Comment'; + if (nextProps.sameUser !== this.props.sameUser) { + return true; + } + + if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) { + return true; } - var commentCount = 0; - var commentRootId; + return false; + } + getCommentCount(props) { + const post = props.post; + const parentPost = props.parentPost; + const posts = props.posts; + + let commentCount = 0; + let commentRootId; if (parentPost) { commentRootId = post.root_id; } else { commentRootId = post.id; } - for (var postId in posts) { + for (let postId in posts) { if (posts[postId].root_id === commentRootId) { commentCount += 1; } } + return commentCount; + } + render() { + var post = this.props.post; + var parentPost = this.props.parentPost; + var posts = this.props.posts; + + var type = 'Post'; + if (post.root_id && post.root_id.length > 0) { + type = 'Comment'; + } + + const commentCount = this.getCommentCount(this.props); + var rootUser; if (this.props.sameRoot) { rootUser = 'same--root'; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index e0682e997..dbbcdc409 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -35,7 +35,6 @@ export default class PostBody extends React.Component { parseEmojis() { twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); - global.window.emojify.run(React.findDOMNode(this.refs.message_span)); } componentDidMount() { diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 703e548fb..218922b67 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -326,8 +326,8 @@ export default class PostList extends React.Component { <strong><UserProfile userId={teammate.id} /></strong> </div> <p className='channel-intro-text'> - {'This is the start of your private message history with ' + teammateName + '.'}<br/> - {'Private messages and files shared here are not shown to people outside this area.'} + {'This is the start of your direct message history with ' + teammateName + '.'}<br/> + {'Direct messages and files shared here are not shown to people outside this area.'} </p> <a className='intro-links' @@ -346,7 +346,7 @@ export default class PostList extends React.Component { return ( <div className='channel-intro'> - <p className='channel-intro-text'>{'This is the start of your private message history with this teammate. Private messages and files shared here are not shown to people outside this area.'}</p> + <p className='channel-intro-text'>{'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}</p> </div> ); } diff --git a/web/react/components/post_list_container.jsx b/web/react/components/post_list_container.jsx index 0815ac883..e59d85d41 100644 --- a/web/react/components/post_list_container.jsx +++ b/web/react/components/post_list_container.jsx @@ -49,6 +49,7 @@ export default class PostListContainer extends React.Component { for (let i = 0; i <= this.state.postLists.length - 1; i++) { postListCtls.push( <PostList + key={'postlistkey' + i} channelId={postLists[i]} isActive={postLists[i] === channelId} /> diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx index 3dd5c094e..473ff3f91 100644 --- a/web/react/components/register_app_modal.jsx +++ b/web/react/components/register_app_modal.jsx @@ -228,7 +228,7 @@ export default class RegisterAppModal 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/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index fe31ac381..4d1892a69 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -56,7 +56,6 @@ export default class RhsComment extends React.Component { } parseEmojis() { twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); - global.window.emojify.run(React.findDOMNode(this.refs.message_holder)); } componentDidMount() { this.parseEmojis(); @@ -114,14 +113,7 @@ export default class RhsComment extends React.Component { var ownerOptions; if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { ownerOptions = ( - <div - className='dropdown' - onClick={ - function scroll() { - $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50); - } - } - > + <div className='dropdown'> <a href='#' className='dropdown-toggle theme' diff --git a/web/react/components/rhs_header_post.jsx b/web/react/components/rhs_header_post.jsx index 5156ec4d7..f55c4095e 100644 --- a/web/react/components/rhs_header_post.jsx +++ b/web/react/components/rhs_header_post.jsx @@ -65,6 +65,7 @@ export default class RhsHeaderPost extends React.Component { aria-label='Close' onClick={this.handleClose} > + <i className='fa fa-sign-out'/> </button> </div> ); diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 2ea697c5b..e661bdce1 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -20,7 +20,6 @@ export default class RhsRootPost extends React.Component { } parseEmojis() { twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); - global.window.emojify.run(React.findDOMNode(this.refs.message_holder)); } componentDidMount() { this.parseEmojis(); @@ -54,7 +53,7 @@ export default class RhsRootPost extends React.Component { var channelName; if (channel) { if (channel.type === 'D') { - channelName = 'Private Message'; + channelName = 'Direct Message'; } else { channelName = channel.display_name; } diff --git a/web/react/components/search_results_header.jsx b/web/react/components/search_results_header.jsx index 694f0c55d..4e8a3ef10 100644 --- a/web/react/components/search_results_header.jsx +++ b/web/react/components/search_results_header.jsx @@ -50,6 +50,7 @@ export default class SearchResultsHeader extends React.Component { title='Close' onClick={this.handleClose} > + <i className='fa fa-sign-out'/> </button> </div> ); diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 0e951f5c6..32b521560 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -64,7 +64,7 @@ export default class SearchResultsItem extends React.Component { if (channel) { channelName = channel.display_name; if (channel.type === 'D') { - channelName = 'Private Message'; + channelName = 'Direct Message'; } } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 87007edcc..14664ed4d 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -566,7 +566,7 @@ export default class Sidebar extends React.Component { {privateChannelItems} </ul> <ul className='nav nav-pills nav-stacked'> - <li><h4>Private Messages</h4></li> + <li><h4>Direct Messages</h4></li> {directMessageItems} {directMessageMore} </ul> diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 2671d560b..f1341d9d7 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -6,6 +6,10 @@ var client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); export default class SidebarRightMenu extends React.Component { + componentDidMount() { + $('.sidebar--left .dropdown-menu').perfectScrollbar(); + } + constructor(props) { super(props); diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 7f320e0b2..4112138fa 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -4,7 +4,7 @@ const ChoosePage = require('./team_signup_choose_auth.jsx'); const EmailSignUpPage = require('./team_signup_with_email.jsx'); const SSOSignupPage = require('./team_signup_with_sso.jsx'); -var Constants = require('../utils/constants.jsx'); +const Constants = require('../utils/constants.jsx'); export default class TeamSignUp extends React.Component { constructor(props) { diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 4dad1ef4f..8311747ee 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); @@ -30,13 +30,26 @@ export default class SignupUserComplete extends React.Component { handleSubmit(e) { e.preventDefault(); + const providedEmail = React.findDOMNode(this.refs.email).value.trim(); + if (!providedEmail) { + this.setState({nameError: '', emailError: 'This field is required', passwordError: ''}); + return; + } + + if (!Utils.isEmail(providedEmail)) { + this.setState({nameError: '', emailError: 'Please enter a valid email address', passwordError: ''}); + return; + } + + this.state.user.email = providedEmail; + this.state.user.username = React.findDOMNode(this.refs.name).value.trim().toLowerCase(); if (!this.state.user.username) { this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''}); return; } - var usernameError = utils.isValidUsername(this.state.user.username); + var usernameError = Utils.isValidUsername(this.state.user.username); if (usernameError === 'Cannot use a reserved word as a username.') { this.setState({nameError: 'This username is reserved, please choose a new one.', emailError: '', passwordError: '', serverError: ''}); return; @@ -50,12 +63,6 @@ export default class SignupUserComplete extends React.Component { return; } - this.state.user.email = React.findDOMNode(this.refs.email).value.trim(); - if (!this.state.user.email) { - this.setState({nameError: '', emailError: 'This field is required', passwordError: ''}); - return; - } - this.state.user.password = React.findDOMNode(this.refs.password).value.trim(); if (!this.state.user.password || this.state.user.password .length < 5) { this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''}); 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..e6aa2f5b9 --- /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.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 +}; 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 eb677f50b..5113d2429 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' diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index fde4970ce..8d364cde7 100644 --- a/web/react/components/user_settings/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -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> ); |