summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
author=Corey Hulen <corey@hulen.com>2015-09-23 12:55:58 -0700
committer=Corey Hulen <corey@hulen.com>2015-09-23 12:55:58 -0700
commit2b5b8f95ed9de37a15dc68c38c46a1da2bb1e160 (patch)
treed5bf8318dc82d96cd34b83481e1cc5c8289d170a /web/react/components
parent9e04909c0a3672d27c148c931d82b225cc86dfe5 (diff)
parent0170cfe604e6cfb430be0b6181243ca85a9ab27b (diff)
downloadchat-2b5b8f95ed9de37a15dc68c38c46a1da2bb1e160.tar.gz
chat-2b5b8f95ed9de37a15dc68c38c46a1da2bb1e160.tar.bz2
chat-2b5b8f95ed9de37a15dc68c38c46a1da2bb1e160.zip
Merge branch 'master' into PLT-11-email
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/channel_header.jsx2
-rw-r--r--web/react/components/channel_loader.jsx35
-rw-r--r--web/react/components/create_post.jsx50
-rw-r--r--web/react/components/email_verify.jsx14
-rw-r--r--web/react/components/more_direct_channels.jsx2
-rw-r--r--web/react/components/new_channel_modal.jsx3
-rw-r--r--web/react/components/password_reset_send_link.jsx7
-rw-r--r--web/react/components/popover_list_members.jsx2
-rw-r--r--web/react/components/post.jsx51
-rw-r--r--web/react/components/post_body.jsx1
-rw-r--r--web/react/components/post_list.jsx6
-rw-r--r--web/react/components/post_list_container.jsx1
-rw-r--r--web/react/components/register_app_modal.jsx2
-rw-r--r--web/react/components/rhs_comment.jsx10
-rw-r--r--web/react/components/rhs_header_post.jsx1
-rw-r--r--web/react/components/rhs_root_post.jsx3
-rw-r--r--web/react/components/search_results_header.jsx1
-rw-r--r--web/react/components/search_results_item.jsx2
-rw-r--r--web/react/components/sidebar.jsx2
-rw-r--r--web/react/components/sidebar_right_menu.jsx4
-rw-r--r--web/react/components/signup_user_complete.jsx23
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx108
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx179
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx55
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx261
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx6
28 files changed, 627 insertions, 208 deletions
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'>&times;</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_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'>&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
};
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>
);