summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--api/post.go2
-rw-r--r--api/user.go3
-rw-r--r--doc/developer/code-contribution.md19
-rw-r--r--model/user.go5
-rw-r--r--store/sql_user_store.go2
-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
-rw-r--r--web/react/package.json18
-rw-r--r--web/react/pages/channel.jsx6
-rw-r--r--web/react/pages/verify.jsx1
-rw-r--r--web/react/stores/user_store.jsx16
-rw-r--r--web/react/utils/constants.jsx139
-rw-r--r--web/react/utils/emoticons.jsx159
-rw-r--r--web/react/utils/markdown.jsx26
-rw-r--r--web/react/utils/text_formatting.jsx81
-rw-r--r--web/react/utils/utils.jsx191
-rw-r--r--web/sass-files/sass/partials/_base.scss7
-rw-r--r--web/sass-files/sass/partials/_colorpicker.scss251
-rw-r--r--web/sass-files/sass/partials/_forms.scss1
-rw-r--r--web/sass-files/sass/partials/_headers.scss4
-rw-r--r--web/sass-files/sass/partials/_modal.scss10
-rw-r--r--web/sass-files/sass/partials/_post.scss2
-rw-r--r--web/sass-files/sass/partials/_search.scss1
-rw-r--r--web/sass-files/sass/partials/_settings.scss30
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss6
-rw-r--r--web/sass-files/sass/partials/_sidebar--right.scss11
-rw-r--r--web/sass-files/sass/styles.scss1
-rwxr-xr-xweb/static/css/bootstrap-colorpicker.min.css9
-rwxr-xr-xweb/static/images/bootstrap-colorpicker/alpha-horizontal.pngbin0 -> 3635 bytes
-rwxr-xr-xweb/static/images/bootstrap-colorpicker/alpha.pngbin0 -> 3271 bytes
-rwxr-xr-xweb/static/images/bootstrap-colorpicker/hue-horizontal.pngbin0 -> 2837 bytes
-rwxr-xr-xweb/static/images/bootstrap-colorpicker/hue.pngbin0 -> 2972 bytes
-rwxr-xr-xweb/static/images/bootstrap-colorpicker/saturation.pngbin0 -> 8817 bytes
-rw-r--r--web/static/images/themes/dark.pngbin0 -> 75371 bytes
-rw-r--r--web/static/images/themes/mattermost.pngbin0 -> 66828 bytes
-rw-r--r--web/static/images/themes/slack.pngbin0 -> 68603 bytes
-rwxr-xr-xweb/static/js/bootstrap-colorpicker.min.js1
-rwxr-xr-xweb/static/js/emojify.min.js4
-rw-r--r--web/templates/channel.html1
-rw-r--r--web/templates/head.html7
-rw-r--r--web/web.go6
68 files changed, 1504 insertions, 353 deletions
diff --git a/README.md b/README.md
index d6b721dd1..58f4887f6 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ Please see the [features pages of the Mattermost website](http://www.mattermost.
- [Mattermost Forum](http://forum.mattermost.org/) - For technical questions and answers
- [Issue Tracker](http://www.mattermost.org/filing-issues/) - For reporting bugs
- [Feature Ideas Forum](http://www.mattermost.org/feature-requests/) - For sharing ideas for future versions
-- [Contributuion Guidelines](http://www.mattermost.org/contribute-to-mattermost/) - For contributing code or feedback to the project
+- [Contribution Guidelines](http://www.mattermost.org/contribute-to-mattermost/) - For contributing code or feedback to the project
Follow us on Twitter at [@MattermostHQ](https://twitter.com/mattermosthq).
diff --git a/api/post.go b/api/post.go
index 4294ae03c..0379f6af5 100644
--- a/api/post.go
+++ b/api/post.go
@@ -160,7 +160,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
channel = result.Data.(*model.Channel)
if channel.Type == model.CHANNEL_DIRECT {
bodyText = "You have one new message."
- subjectText = "New Private Message"
+ subjectText = "New Direct Message"
} else {
bodyText = "You have one new mention."
subjectText = "New Mention"
diff --git a/api/user.go b/api/user.go
index d61afb027..5bb65e2ed 100644
--- a/api/user.go
+++ b/api/user.go
@@ -171,9 +171,6 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
}
user.MakeNonNil()
- if len(user.Props["theme"]) == 0 {
- user.AddProp("theme", utils.Cfg.TeamSettings.DefaultThemeColor)
- }
if result := <-Srv.Store.User().Save(user); result.Err != nil {
c.Err = result.Err
diff --git a/doc/developer/code-contribution.md b/doc/developer/code-contribution.md
index 708b324a0..51521be1b 100644
--- a/doc/developer/code-contribution.md
+++ b/doc/developer/code-contribution.md
@@ -33,25 +33,6 @@ git checkout -b <branch name>
1. Please add yourself to the Mattermost [approved contributor list](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true) prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/).
- For pull requests made by contributors not yet added to the approved contributor list, a reviewer may respond:
-
- ```
-Thanks @[GITHUB_USERNAME] for the pull request!
-
-Before we can review, we need to add you to the list of approved contributors for the Mattermost project.
-
-**Please help complete the Mattermost [contribution license agreement](http://www.mattermost.org/mattermost-contributor-agreement/)?**
-
-This is a standard procedure for many open source projects. You can view a list of past contributors who have completed the form [here](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true).
-
-After completing the form, it should be processed within 24 hours and reviewers for your pull request will be able to proceed.
-
-Please let us know if you have any questions.
-
-We are very happy to have you join our growing community!
-```
-
-
2. When you submit your pull request please include the Ticket ID at the beginning of your pull request comment, followed by a colon.
For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples.
diff --git a/model/user.go b/model/user.go
index fdc519b99..3a2c9d56c 100644
--- a/model/user.go
+++ b/model/user.go
@@ -47,6 +47,7 @@ type User struct {
AllowMarketing bool `json:"allow_marketing"`
Props StringMap `json:"props"`
NotifyProps StringMap `json:"notify_props"`
+ ThemeProps StringMap `json:"theme_props"`
LastPasswordUpdate int64 `json:"last_password_update"`
LastPictureUpdate int64 `json:"last_picture_update"`
FailedAttempts int `json:"failed_attempts"`
@@ -108,6 +109,10 @@ func (u *User) IsValid() *AppError {
return NewAppError("User.IsValid", "Invalid user, password and auth data cannot both be set", "user_id="+u.Id)
}
+ if len(u.ThemeProps) > 2000 {
+ return NewAppError("User.IsValid", "Invalid theme", "user_id="+u.Id)
+ }
+
return nil
}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 778df367e..3fd1c82b5 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -32,6 +32,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
table.ColMap("Roles").SetMaxSize(64)
table.ColMap("Props").SetMaxSize(4000)
table.ColMap("NotifyProps").SetMaxSize(2000)
+ table.ColMap("ThemeProps").SetMaxSize(2000)
table.SetUniqueTogether("Email", "TeamId")
table.SetUniqueTogether("Username", "TeamId")
}
@@ -40,6 +41,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
}
func (us SqlUserStore) UpgradeSchemaIfNeeded() {
+ us.CreateColumnIfNotExists("Users", "ThemeProps", "varchar(2000)", "character varying(2000)", "{}")
}
func (us SqlUserStore) CreateIndexesIfNotExists() {
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>
);
diff --git a/web/react/package.json b/web/react/package.json
index dd7d45f8a..a9eba6c6c 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -4,13 +4,14 @@
"private": true,
"dependencies": {
"autolinker": "0.18.1",
+ "babel-runtime": "5.8.24",
+ "bootstrap-colorpicker": "2.2.0",
"flux": "2.1.1",
"keymirror": "0.1.1",
+ "marked": "0.3.5",
"object-assign": "3.0.0",
"react-zeroclipboard-mixin": "0.1.0",
- "twemoji": "1.4.1",
- "babel-runtime": "5.8.24",
- "marked": "0.3.5"
+ "twemoji": "1.4.1"
},
"devDependencies": {
"browserify": "11.0.1",
@@ -28,8 +29,15 @@
},
"browserify": {
"transform": [
- ["babelify", { "optional": ["runtime"] }],
- "envify"
+ [
+ "babelify",
+ {
+ "optional": [
+ "runtime"
+ ]
+ }
+ ],
+ "envify"
]
},
"jest": {
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index d24fe0b98..07207c556 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -34,6 +34,7 @@ var ActivityLogModal = require('../components/activity_log_modal.jsx');
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx');
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
+var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -85,6 +86,11 @@ function setupChannelPage(props) {
);
React.render(
+ <ImportThemeModal />,
+ document.getElementById('import_theme_modal')
+ );
+
+ React.render(
<TeamSettingsModal teamDisplayName={props.TeamDisplayName} />,
document.getElementById('team_settings_modal')
);
diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx
index e48471bbd..16a9846e5 100644
--- a/web/react/pages/verify.jsx
+++ b/web/react/pages/verify.jsx
@@ -9,6 +9,7 @@ global.window.setupVerifyPage = function setupVerifyPage(props) {
isVerified={props.IsVerified}
teamURL={props.TeamURL}
userEmail={props.UserEmail}
+ resendSuccess={props.ResendSuccess}
/>,
document.getElementById('verify')
);
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index f75c1d4c3..8842263fa 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -14,6 +14,7 @@ var CHANGE_EVENT_SESSIONS = 'change_sessions';
var CHANGE_EVENT_AUDITS = 'change_audits';
var CHANGE_EVENT_TEAMS = 'change_teams';
var CHANGE_EVENT_STATUSES = 'change_statuses';
+var TOGGLE_IMPORT_MODAL_EVENT = 'toggle_import_modal';
class UserStoreClass extends EventEmitter {
constructor() {
@@ -34,6 +35,9 @@ class UserStoreClass extends EventEmitter {
this.emitStatusesChange = this.emitStatusesChange.bind(this);
this.addStatusesChangeListener = this.addStatusesChangeListener.bind(this);
this.removeStatusesChangeListener = this.removeStatusesChangeListener.bind(this);
+ this.emitToggleImportModal = this.emitToggleImportModal.bind(this);
+ this.addImportModalListener = this.addImportModalListener.bind(this);
+ this.removeImportModalListener = this.removeImportModalListener.bind(this);
this.setCurrentId = this.setCurrentId.bind(this);
this.getCurrentId = this.getCurrentId.bind(this);
this.getCurrentUser = this.getCurrentUser.bind(this);
@@ -114,6 +118,15 @@ class UserStoreClass extends EventEmitter {
removeStatusesChangeListener(callback) {
this.removeListener(CHANGE_EVENT_STATUSES, callback);
}
+ emitToggleImportModal(value) {
+ this.emit(TOGGLE_IMPORT_MODAL_EVENT, value);
+ }
+ addImportModalListener(callback) {
+ this.on(TOGGLE_IMPORT_MODAL_EVENT, callback);
+ }
+ removeImportModalListener(callback) {
+ this.removeListener(TOGGLE_IMPORT_MODAL_EVENT, callback);
+ }
setCurrentId(id) {
this.gCurrentId = id;
if (id == null) {
@@ -321,6 +334,9 @@ UserStore.dispatchToken = AppDispatcher.register(function registry(payload) {
UserStore.pSetStatuses(action.statuses);
UserStore.emitStatusesChange();
break;
+ case ActionTypes.TOGGLE_IMPORT_THEME_MODAL:
+ UserStore.emitToggleImportModal(action.value);
+ break;
default:
}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 03e4635b5..41e9e9ca6 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -36,7 +36,9 @@ module.exports = {
RECIEVED_CONFIG: null,
- RECIEVED_LOGS: null
+ RECIEVED_LOGS: null,
+
+ TOGGLE_IMPORT_THEME_MODAL: null
}),
PayloadSources: keyMirror({
@@ -110,5 +112,138 @@ module.exports = {
ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>",
MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>",
- COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>"
+ COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>",
+ THEMES: {
+ default: {
+ type: 'Mattermost',
+ sidebarBg: '#fafafa',
+ sidebarText: '#999999',
+ sidebarUnreadText: '#333333',
+ sidebarTextHoverBg: '#e6f2fa',
+ sidebarTextHoverColor: '#999999',
+ sidebarTextActiveBg: '#e1e1e1',
+ sidebarTextActiveColor: '#111111',
+ sidebarHeaderBg: '#2389d7',
+ sidebarHeaderTextColor: '#ffffff',
+ onlineIndicator: '#7DBE00',
+ mentionBj: '#2389d7',
+ mentionColor: '#ffffff',
+ centerChannelBg: '#ffffff',
+ centerChannelColor: '#333333',
+ linkColor: '#2389d7',
+ buttonBg: '#2389d7',
+ buttonColor: '#FFFFFF'
+ },
+ slack: {
+ type: 'Slack',
+ sidebarBg: '#4D394B',
+ sidebarText: '#ab9ba9',
+ sidebarUnreadText: '#FFFFFF',
+ sidebarTextHoverBg: '#3e313c',
+ sidebarTextHoverColor: '#ab9ba9',
+ sidebarTextActiveBg: '#4c9689',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#4D394B',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#4c9689',
+ mentionBj: '#eb4d5c',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#FFFFFF',
+ centerChannelColor: '#333333',
+ linkColor: '#2389d7',
+ buttonBg: '#26a970',
+ buttonColor: '#FFFFFF'
+ },
+ dark: {
+ type: 'Dark',
+ sidebarBg: '#1B2C3E',
+ sidebarText: '#bbbbbb',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#4A5664',
+ sidebarTextHoverColor: '#bbbbbb',
+ sidebarTextActiveBg: '#39769C',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#1B2C3E',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#4c9689',
+ mentionBj: '#B74A4A',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#2F3E4E',
+ centerChannelColor: '#DDDDDD',
+ linkColor: '#A4FFEB',
+ buttonBg: '#2B9C99',
+ buttonColor: '#FFFFFF'
+ }
+ },
+ THEME_ELEMENTS: [
+ {
+ id: 'sidebarBg',
+ uiName: 'Sidebar BG'
+ },
+ {
+ id: 'sidebarText',
+ uiName: 'Sidebar text color'
+ },
+ {
+ id: 'sidebarHeaderBg',
+ uiName: 'Sidebar Header BG'
+ },
+ {
+ id: 'sidebarHeaderTextColor',
+ uiName: 'Sidebar Header text color'
+ },
+ {
+ id: 'sidebarUnreadText',
+ uiName: 'Sidebar unread text color'
+ },
+ {
+ id: 'sidebarTextHoverBg',
+ uiName: 'Sidebar text hover BG'
+ },
+ {
+ id: 'sidebarTextHoverColor',
+ uiName: 'Sidebar text hover color'
+ },
+ {
+ id: 'sidebarTextActiveBg',
+ uiName: 'Sidebar text active BG'
+ },
+ {
+ id: 'sidebarTextActiveColor',
+ uiName: 'Sidebar text active color'
+ },
+ {
+ id: 'onlineIndicator',
+ uiName: 'Online indicator'
+ },
+ {
+ id: 'mentionBj',
+ uiName: 'Mention jewel BG'
+ },
+ {
+ id: 'mentionColor',
+ uiName: 'Mention jewel text color'
+ },
+ {
+ id: 'centerChannelBg',
+ uiName: 'Center channel BG'
+ },
+ {
+ id: 'centerChannelColor',
+ uiName: 'Center channel text color'
+ },
+ {
+ id: 'linkColor',
+ uiName: 'Link color'
+ },
+ {
+ id: 'buttonBg',
+ uiName: 'Button BG'
+ },
+
+ {
+ id: 'buttonColor',
+ uiName: 'Button Color'
+ }
+ ]
};
diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx
new file mode 100644
index 000000000..7210201ff
--- /dev/null
+++ b/web/react/utils/emoticons.jsx
@@ -0,0 +1,159 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const emoticonPatterns = {
+ smile: /:-?\)/g, // :)
+ open_mouth: /:o/gi, // :o
+ scream: /:-o/gi, // :-o
+ smirk: /[:;]-?]/g, // :]
+ grinning: /[:;]-?d/gi, // :D
+ stuck_out_tongue_closed_eyes: /x-d/gi, // x-d
+ stuck_out_tongue_winking_eye: /[:;]-?p/gi, // ;p
+ rage: /:-?[\[@]/g, // :@
+ frowning: /:-?\(/g, // :(
+ sob: /:['’]-?\(|:&#x27;\(/g, // :`(
+ kissing_heart: /:-?\*/g, // :*
+ wink: /;-?\)/g, // ;)
+ pensive: /:-?\//g, // :/
+ confounded: /:-?s/gi, // :s
+ flushed: /:-?\|/g, // :|
+ relaxed: /:-?\$/g, // :$
+ mask: /:-x/gi, // :-x
+ heart: /<3|&lt;3/g, // <3
+ broken_heart: /<\/3|&lt;&#x2F;3/g, // </3
+ thumbsup: /:\+1:/g, // :+1:
+ thumbsdown: /:\-1:/g // :-1:
+};
+
+function initializeEmoticonMap() {
+ const emoticonNames =
+ ('+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,' +
+ 'anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,' +
+ 'arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,' +
+ 'arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,' +
+ 'arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,' +
+ 'baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,' +
+ 'bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,' +
+ 'bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,' +
+ 'black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,blue_car,blue_heart,' +
+ 'blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,boy,bread,' +
+ 'bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,busstop,' +
+ 'bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,' +
+ 'capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,' +
+ 'checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,' +
+ 'cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,' +
+ 'clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,' +
+ 'clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,' +
+ 'clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,' +
+ 'construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,' +
+ 'couplekiss,cow,cow2,credit_card,crescent_moon,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,' +
+ 'cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,' +
+ 'deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,' +
+ 'dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,' +
+ 'dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,' +
+ 'eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,' +
+ 'european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,' +
+ 'factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,' +
+ 'fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,' +
+ 'five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,' +
+ 'four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,' +
+ 'gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,' +
+ 'green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,' +
+ 'hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,' +
+ 'heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,' +
+ 'heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,' +
+ 'herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,' +
+ 'hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,' +
+ 'ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,' +
+ 'interrobang,iphone,it,izakaya_lantern,jack_o_lantern,japan,japanese_castle,japanese_goblin,japanese_ogre,jeans,' +
+ 'joy,joy_cat,jp,key,keycap_ten,kimono,kiss,kissing,kissing_cat,kissing_closed_eyes,kissing_face,kissing_heart,' +
+ 'kissing_smiling_eyes,koala,koko,kr,large_blue_circle,large_blue_diamond,large_orange_diamond,last_quarter_moon,' +
+ 'last_quarter_moon_with_face,laughing,leaves,ledger,left_luggage,left_right_arrow,leftwards_arrow_with_hook,' +
+ 'lemon,leo,leopard,libra,light_rail,link,lips,lipstick,lock,lock_with_ink_pen,lollipop,loop,loudspeaker,' +
+ 'love_hotel,love_letter,low_brightness,m,mag,mag_right,mahjong,mailbox,mailbox_closed,mailbox_with_mail,' +
+ 'mailbox_with_no_mail,man,man_with_gua_pi_mao,man_with_turban,mans_shoe,maple_leaf,mask,massage,meat_on_bone,' +
+ 'mega,melon,memo,mens,metal,metro,microphone,microscope,milky_way,minibus,minidisc,mobile_phone_off,' +
+ 'money_with_wings,moneybag,monkey,monkey_face,monorail,mortar_board,mount_fuji,mountain_bicyclist,' +
+ 'mountain_cableway,mountain_railway,mouse,mouse2,movie_camera,moyai,muscle,mushroom,musical_keyboard,' +
+ 'musical_note,musical_score,mute,nail_care,name_badge,neckbeard,necktie,negative_squared_cross_mark,' +
+ 'neutral_face,new,new_moon,new_moon_with_face,newspaper,ng,nine,no_bell,no_bicycles,no_entry,no_entry_sign,' +
+ 'no_good,no_mobile_phones,no_mouth,no_pedestrians,no_smoking,non-potable_water,nose,notebook,' +
+ 'notebook_with_decorative_cover,notes,nut_and_bolt,o,o2,ocean,octocat,octopus,oden,office,ok,ok_hand,' +
+ 'ok_woman,older_man,older_woman,on,oncoming_automobile,oncoming_bus,oncoming_police_car,oncoming_taxi,one,' +
+ 'open_file_folder,open_hands,open_mouth,ophiuchus,orange_book,outbox_tray,ox,package,page_facing_up,' +
+ 'page_with_curl,pager,palm_tree,panda_face,paperclip,parking,part_alternation_mark,partly_sunny,' +
+ 'passport_control,paw_prints,peach,pear,pencil,pencil2,penguin,pensive,performing_arts,persevere,' +
+ 'person_frowning,person_with_blond_hair,person_with_pouting_face,phone,pig,pig2,pig_nose,pill,pineapple,pisces,' +
+ 'pizza,plus1,point_down,point_left,point_right,point_up,point_up_2,police_car,poodle,poop,post_office,' +
+ 'postal_horn,postbox,potable_water,pouch,poultry_leg,pound,pouting_cat,pray,princess,punch,purple_heart,purse,' +
+ 'pushpin,put_litter_in_its_place,question,rabbit,rabbit2,racehorse,radio,radio_button,rage,rage1,rage2,rage3,' +
+ 'rage4,railway_car,rainbow,raised_hand,raised_hands,raising_hand,ram,ramen,rat,recycle,red_car,red_circle,' +
+ 'registered,relaxed,relieved,repeat,repeat_one,restroom,revolving_hearts,rewind,ribbon,rice,rice_ball,' +
+ 'rice_cracker,rice_scene,ring,rocket,roller_coaster,rooster,rose,rotating_light,round_pushpin,rowboat,ru,' +
+ 'rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,' +
+ 'satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,' +
+ 'seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,' +
+ 'ski,skull,sleeping,sleepy,slot_machine,small_blue_diamond,small_orange_diamond,small_red_triangle,' +
+ 'small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,smirk,smirk_cat,smoking,snail,snake,' +
+ 'snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,spaghetti,sparkle,sparkler,' +
+ 'sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,star2,stars,station,' +
+ 'statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,stuck_out_tongue_closed_eyes,' +
+ 'stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,sunrise_over_mountains,surfer,' +
+ 'sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,symbols,syringe,tada,' +
+ 'tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,thought_balloon,' +
+ 'three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,tophat,' +
+ 'tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,' +
+ 'trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,' +
+ 'two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,' +
+ 'u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,' +
+ 'video_camera,video_game,violin,virgo,volcano,vs,walking,waning_crescent_moon,waning_gibbous_moon,warning,watch,' +
+ 'water_buffalo,watermelon,wave,wavy_dash,waxing_crescent_moon,waxing_gibbous_moon,wc,weary,wedding,whale,whale2,' +
+ 'wheelchair,white_check_mark,white_circle,white_flower,white_large_square,white_medium_small_square,' +
+ 'white_medium_square,white_small_square,white_square_button,wind_chime,wine_glass,wink,wolf,woman,' +
+ 'womans_clothes,womans_hat,womens,worried,wrench,x,yellow_heart,yen,yum,zap,zero,zzz').split(',');
+
+ // use a map to help make lookups faster instead of having to use indexOf on an array
+ const out = new Map();
+
+ for (let i = 0; i < emoticonNames.length; i++) {
+ out[emoticonNames[i]] = true;
+ }
+
+ return out;
+}
+
+const emoticonMap = initializeEmoticonMap();
+
+export function handleEmoticons(text, tokens) {
+ let output = text;
+
+ function replaceEmoticonWithToken(match, name) {
+ if (emoticonMap[name]) {
+ const index = tokens.size;
+ const alias = `MM_EMOTICON${index}`;
+
+ tokens.set(alias, {
+ value: `<img align="absmiddle" alt=${match} class="emoji" src=${getImagePathForEmoticon(name)} title=${match} />`,
+ originalText: match
+ });
+
+ return alias;
+ }
+
+ return match;
+ }
+
+ output = output.replace(/:([a-zA-Z0-9_-]+):/g, replaceEmoticonWithToken);
+
+ $.each(emoticonPatterns, (name, pattern) => {
+ // this might look a bit funny, but since the name isn't contained in the actual match
+ // like with the named emoticons, we need to add it in manually
+ output = output.replace(pattern, (match) => replaceEmoticonWithToken(match, name));
+ });
+
+ return output;
+}
+
+function getImagePathForEmoticon(name) {
+ return `/static/images/emoji/${name}.png`;
+}
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 96da54217..347024e1a 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -1,9 +1,25 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+const TextFormatting = require('./text_formatting.jsx');
+
const marked = require('marked');
export class MattermostMarkdownRenderer extends marked.Renderer {
+ constructor(options, formattingOptions = {}) {
+ super(options);
+
+ this.heading = this.heading.bind(this);
+ this.text = this.text.bind(this);
+
+ this.formattingOptions = formattingOptions;
+ }
+
+ heading(text, level, raw) {
+ const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`;
+ return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
+ }
+
link(href, title, text) {
let outHref = href;
@@ -11,7 +27,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
outHref = `http://${outHref}`;
}
- let output = '<a class="theme" href="' + outHref + '"';
+ let output = '<a class="theme markdown__link" href="' + outHref + '"';
if (title) {
output += ' title="' + title + '"';
}
@@ -19,4 +35,12 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
return output;
}
+
+ table(header, body) {
+ return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
+ }
+
+ text(text) {
+ return TextFormatting.doFormatText(text, this.formattingOptions);
+ }
}
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 4e390f708..56bf49c3f 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -3,41 +3,58 @@
const Autolinker = require('autolinker');
const Constants = require('./constants.jsx');
+const Emoticons = require('./emoticons.jsx');
const Markdown = require('./markdown.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('./utils.jsx');
const marked = require('marked');
-const markdownRenderer = new Markdown.MattermostMarkdownRenderer();
-
// Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and
// @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options
// as part of the second parameter:
// - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing.
// - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true.
// - singleline - Specifies whether or not to remove newlines. Defaults to false.
+// - emoticons - Enables emoticon parsing. Defaults to true.
// - markdown - Enables markdown parsing. Defaults to true.
export function formatText(text, options = {}) {
- if (!('markdown' in options)) {
- options.markdown = true;
- }
-
- // wait until marked can sanitize the html so that we don't break markdown block quotes
let output;
- if (!options.markdown) {
- output = sanitizeHtml(text);
+
+ if (!('markdown' in options) || options.markdown) {
+ // the markdown renderer will call doFormatText as necessary so just call marked
+ output = marked(text, {
+ renderer: new Markdown.MattermostMarkdownRenderer(null, options),
+ sanitize: true
+ });
} else {
- output = text;
+ output = sanitizeHtml(text);
+ output = doFormatText(output, options);
+ }
+
+ // replace newlines with spaces if necessary
+ if (options.singleline) {
+ output = replaceNewlines(output);
}
+ return output;
+}
+
+// Performs most of the actual formatting work for formatText. Not intended to be called normally.
+export function doFormatText(text, options) {
+ let output = text;
+
const tokens = new Map();
// replace important words and phrases with tokens
- output = autolinkUrls(output, tokens, !!options.markdown);
+ output = autolinkUrls(output, tokens);
output = autolinkAtMentions(output, tokens);
output = autolinkHashtags(output, tokens);
+ if (!('emoticons' in options) || options.emoticon) {
+ output = Emoticons.handleEmoticons(output, tokens);
+ }
+
if (options.searchTerm) {
output = highlightSearchTerm(output, tokens, options.searchTerm);
}
@@ -46,22 +63,9 @@ export function formatText(text, options = {}) {
output = highlightCurrentMentions(output, tokens);
}
- // perform markdown parsing while we have an html-free input string
- if (options.markdown) {
- output = marked(output, {
- renderer: markdownRenderer,
- sanitize: true
- });
- }
-
// reinsert tokens with formatted versions of the important words and phrases
output = replaceTokens(output, tokens);
- // replace newlines with html line breaks
- if (options.singleline) {
- output = replaceNewlines(output);
- }
-
return output;
}
@@ -78,7 +82,7 @@ export function sanitizeHtml(text) {
return output;
}
-function autolinkUrls(text, tokens, markdown) {
+function autolinkUrls(text, tokens) {
function replaceUrlWithToken(autolinker, match) {
const linkText = match.getMatchedText();
let url = linkText;
@@ -108,30 +112,7 @@ function autolinkUrls(text, tokens, markdown) {
replaceFn: replaceUrlWithToken
});
- let output = text;
-
- // temporarily replace markdown links if markdown is enabled so that we don't accidentally parse them twice
- const markdownLinkTokens = new Map();
- if (markdown) {
- function replaceMarkdownLinkWithToken(markdownLink) {
- const index = markdownLinkTokens.size;
- const alias = `MM_MARKDOWNLINK${index}`;
-
- markdownLinkTokens.set(alias, {value: markdownLink});
-
- return alias;
- }
-
- output = output.replace(/\]\([^\)]*\)/g, replaceMarkdownLinkWithToken);
- }
-
- output = autolinker.link(output);
-
- if (markdown) {
- output = replaceTokens(output, markdownLinkTokens);
- }
-
- return output;
+ return autolinker.link(text);
}
function autolinkAtMentions(text, tokens) {
@@ -241,7 +222,7 @@ function autolinkHashtags(text, tokens) {
return prefix + alias;
}
- return output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, replaceHashtagWithToken);
+ return output.replace(/(^|\W)(#[a-zA-Z][a-zA-Z0-9.\-_]*)\b/g, replaceHashtagWithToken);
}
function highlightSearchTerm(text, tokens, searchTerm) {
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 074591489..82bb82d6b 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -542,7 +542,119 @@ export function toTitleCase(str) {
return str.replace(/\w\S*/g, doTitleCase);
}
-export function changeCss(className, classValue) {
+export function applyTheme(theme) {
+ if (theme.sidebarBg) {
+ changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarBg, 1);
+ }
+
+ if (theme.sidebarText) {
+ changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
+ changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1);
+ changeCss('.sidebar--right .sidebar-right__body', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
+ changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
+ }
+
+ if (theme.sidebarUnreadText) {
+ changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1);
+ }
+
+ if (theme.sidebarTextHoverBg) {
+ changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1);
+ }
+
+ if (theme.sidebarTextHoverColor) {
+ changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2);
+ }
+
+ if (theme.sidebarTextActiveBg) {
+ changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1);
+ }
+
+ if (theme.sidebarTextActiveColor) {
+ changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2);
+ }
+
+ if (theme.sidebarHeaderBg) {
+ changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
+ }
+
+ if (theme.sidebarHeaderTextColor) {
+ changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('.sidebar--left .team__header .user__name, .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8), 1);
+ changeCss('.sidebar--left .team__header:hover .user__name, .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('.modal .modal-header .modal-title, .modal .modal-header .modal-title .name, .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('#navbar .navbar-default .navbar-brand .heading, ', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('#navbar .navbar-default .navbar-toggle .icon-bar, ', 'background:' + theme.sidebarHeaderTextColor, 1);
+ }
+
+ if (theme.onlineIndicator) {
+ changeCss('.sidebar--left .status .online--icon', 'fill:' + theme.onlineIndicator, 1);
+ }
+
+ if (theme.mentionBj) {
+ changeCss('.sidebar--left .nav-pills__unread-indicator', 'background:' + theme.mentionBj, 1);
+ changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj, 1);
+ }
+
+ if (theme.mentionColor) {
+ changeCss('.sidebar--left .nav-pills__unread-indicator', 'color:' + theme.mentionColor, 2);
+ changeCss('.sidebar--left .badge', 'color:' + theme.mentionColor, 2);
+ }
+
+ if (theme.centerChannelBg) {
+ changeCss('.app__content', 'background:' + theme.centerChannelBg, 1);
+ changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
+ changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.search-bar__container .search__form .search-bar', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1);
+ }
+
+ if (theme.centerChannelColor) {
+ changeCss('.app__content', 'color:' + theme.centerChannelColor, 2);
+ changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
+ changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
+ changeCss('.custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '!important; color: ' + theme.centerChannelColor, 1);
+ changeCss('.search-bar__container .search__form .search-bar', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: ' + theme.centerChannelColor, 2);
+ changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
+ changeCss('.date-separator .separator__text, .new-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.date-separator .separator__hr, .new-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
+ changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
+ }
+
+ if (theme.linkColor) {
+ changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1);
+ changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1);
+ }
+
+ if (theme.buttonBg) {
+ changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
+ changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
+ }
+
+ if (theme.buttonColor) {
+ changeCss('.btn.btn-primary', 'color:' + theme.buttonColor, 2);
+ }
+}
+
+export function changeCss(className, classValue, classRepeat) {
// we need invisible container to store additional css definitions
var cssMainContainer = $('#css-modifier-container');
if (cssMainContainer.length === 0) {
@@ -552,9 +664,9 @@ export function changeCss(className, classValue) {
}
// and we need one div for each class
- var classContainer = cssMainContainer.find('div[data-class="' + className + '"]');
+ var classContainer = cssMainContainer.find('div[data-class="' + className + classRepeat + '"]');
if (classContainer.length === 0) {
- classContainer = $('<div data-class="' + className + '"></div>');
+ classContainer = $('<div data-class="' + className + classRepeat + '"></div>');
classContainer.appendTo(cssMainContainer);
}
@@ -760,57 +872,47 @@ Image.prototype.load = function imageLoad(url, progressCallback) {
Image.prototype.completedPercentage = 0;
export function changeColor(colourIn, amt) {
- var usePound = false;
- var col = colourIn;
+ var hex = colourIn;
+ var lum = amt;
- if (col[0] === '#') {
- col = col.slice(1);
- usePound = true;
+ // validate hex string
+ hex = String(hex).replace(/[^0-9a-f]/gi, '');
+ if (hex.length < 6) {
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
+ lum = lum || 0;
- var num = parseInt(col, 16);
-
- var r = (num >> 16) + amt;
-
- if (r > 255) {
- r = 255;
- } else if (r < 0) {
- r = 0;
- }
-
- var b = ((num >> 8) & 0x00FF) + amt;
-
- if (b > 255) {
- b = 255;
- } else if (b < 0) {
- b = 0;
+ // convert to decimal and change luminosity
+ var rgb = '#';
+ var c;
+ var i;
+ for (i = 0; i < 3; i++) {
+ c = parseInt(hex.substr(i * 2, 2), 16);
+ c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
+ rgb += ('00' + c).substr(c.length);
}
- var g = (num & 0x0000FF) + amt;
-
- if (g > 255) {
- g = 255;
- } else if (g < 0) {
- g = 0;
- }
+ return rgb;
+}
- var pound = '#';
- if (!usePound) {
- pound = '';
+export function changeOpacity(oldColor, opacity) {
+ var color = oldColor;
+ if (color[0] === '#') {
+ color = color.slice(1);
}
- return pound + String('000000' + (g | (b << 8) | (r << 16)).toString(16)).slice(-6);
-}
+ if (color.length === 3) {
+ const tempColor = color;
+ color = '';
-export function changeOpacity(oldColor, opacity) {
- var col = oldColor;
- if (col[0] === '#') {
- col = col.slice(1);
+ color += tempColor[0] + tempColor[0];
+ color += tempColor[1] + tempColor[1];
+ color += tempColor[2] + tempColor[2];
}
- var r = parseInt(col.substring(0, 2), 16);
- var g = parseInt(col.substring(2, 4), 16);
- var b = parseInt(col.substring(4, 6), 16);
+ var r = parseInt(color.substring(0, 2), 16);
+ var g = parseInt(color.substring(2, 4), 16);
+ var b = parseInt(color.substring(4, 6), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
}
@@ -955,7 +1057,8 @@ export function getTeamURLFromAddressBar() {
export function getShortenedTeamURL() {
const teamURL = getTeamURLFromAddressBar();
- if (teamURL.length > 24) {
+ if (teamURL.length > 35) {
return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length) + '/';
}
+ return teamURL + '/';
}
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index cdc2152e9..592d5e62e 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -40,14 +40,11 @@ b, strong {
a {
word-break: break-word;
-}
-
-a.theme {
color: $primary-color;
}
-div.theme {
- background-color: $primary-color;
+a:focus, a:hover {
+ color: $primary-color;
}
.tooltip {
diff --git a/web/sass-files/sass/partials/_colorpicker.scss b/web/sass-files/sass/partials/_colorpicker.scss
new file mode 100644
index 000000000..431f9d8d0
--- /dev/null
+++ b/web/sass-files/sass/partials/_colorpicker.scss
@@ -0,0 +1,251 @@
+/*!
+ * Bootstrap Colorpicker
+ * http://mjolnic.github.io/bootstrap-colorpicker/
+ *
+ * Originally written by (c) 2012 Stefan Petre
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ */
+
+.colorpicker-saturation {
+ float: left;
+ width: 100px;
+ height: 100px;
+ cursor: crosshair;
+ background-image: url("../images/bootstrap-colorpicker/saturation.png");
+}
+
+.colorpicker-saturation i {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: block;
+ width: 5px;
+ height: 5px;
+ margin: -4px 0 0 -4px;
+ border: 1px solid #000;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+.colorpicker-saturation i b {
+ display: block;
+ width: 5px;
+ height: 5px;
+ border: 1px solid #fff;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+.colorpicker-hue,
+.colorpicker-alpha {
+ float: left;
+ width: 15px;
+ height: 100px;
+ margin-bottom: 4px;
+ margin-left: 4px;
+ cursor: row-resize;
+}
+
+.colorpicker-hue i,
+.colorpicker-alpha i {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: block;
+ width: 100%;
+ height: 1px;
+ margin-top: -1px;
+ background: #000;
+ border-top: 1px solid #fff;
+}
+
+.colorpicker-hue {
+ background-image: url("../images/bootstrap-colorpicker/hue.png");
+}
+
+.colorpicker-alpha {
+ display: none;
+ background-image: url("../images/bootstrap-colorpicker/alpha.png");
+}
+
+.colorpicker-saturation,
+.colorpicker-hue,
+.colorpicker-alpha {
+ background-size: contain;
+}
+
+.colorpicker {
+ top: 0;
+ left: 0;
+ z-index: 2500;
+ min-width: 130px;
+ padding: 4px;
+ margin-top: 1px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ *zoom: 1;
+}
+
+.colorpicker:before,
+.colorpicker:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.colorpicker:after {
+ clear: both;
+}
+
+.colorpicker:before {
+ position: absolute;
+ top: -7px;
+ left: 6px;
+ display: inline-block;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-left: 7px solid transparent;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ content: '';
+}
+
+.colorpicker:after {
+ position: absolute;
+ top: -6px;
+ left: 7px;
+ display: inline-block;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #ffffff;
+ border-left: 6px solid transparent;
+ content: '';
+}
+
+.colorpicker div {
+ position: relative;
+}
+
+.colorpicker.colorpicker-with-alpha {
+ min-width: 140px;
+}
+
+.colorpicker.colorpicker-with-alpha .colorpicker-alpha {
+ display: block;
+}
+
+.colorpicker-color {
+ height: 10px;
+ margin-top: 5px;
+ clear: both;
+ background-image: url("../images/bootstrap-colorpicker/alpha.png");
+ background-position: 0 100%;
+}
+
+.colorpicker-color div {
+ height: 10px;
+}
+
+.colorpicker-selectors {
+ display: none;
+ height: 10px;
+ margin-top: 5px;
+ clear: both;
+}
+
+.colorpicker-selectors i {
+ float: left;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+}
+
+.colorpicker-selectors i + i {
+ margin-left: 3px;
+}
+
+.colorpicker-element .input-group-addon i,
+.colorpicker-element .add-on i {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ vertical-align: text-top;
+ cursor: pointer;
+}
+
+.colorpicker.colorpicker-inline {
+ position: relative;
+ z-index: auto;
+ display: inline-block;
+ float: none;
+}
+
+.colorpicker.colorpicker-horizontal {
+ width: 110px;
+ height: auto;
+ min-width: 110px;
+}
+
+.colorpicker.colorpicker-horizontal .colorpicker-saturation {
+ margin-bottom: 4px;
+}
+
+.colorpicker.colorpicker-horizontal .colorpicker-color {
+ width: 100px;
+}
+
+.colorpicker.colorpicker-horizontal .colorpicker-hue,
+.colorpicker.colorpicker-horizontal .colorpicker-alpha {
+ float: left;
+ width: 100px;
+ height: 15px;
+ margin-bottom: 4px;
+ margin-left: 0;
+ cursor: col-resize;
+}
+
+.colorpicker.colorpicker-horizontal .colorpicker-hue i,
+.colorpicker.colorpicker-horizontal .colorpicker-alpha i {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: block;
+ width: 1px;
+ height: 15px;
+ margin-top: 0;
+ background: #ffffff;
+ border: none;
+}
+
+.colorpicker.colorpicker-horizontal .colorpicker-hue {
+ background-image: url("../images/bootstrap-colorpicker/hue-horizontal.png");
+}
+
+.colorpicker.colorpicker-horizontal .colorpicker-alpha {
+ background-image: url("../images/bootstrap-colorpicker/alpha-horizontal.png");
+}
+
+.colorpicker.colorpicker-hidden {
+ display: none;
+}
+
+.colorpicker.colorpicker-visible {
+ display: block;
+}
+
+.colorpicker-inline.colorpicker-visible {
+ display: inline-block;
+}
+
+.colorpicker-right:before {
+ right: 6px;
+ left: auto;
+}
+
+.colorpicker-right:after {
+ right: 7px;
+ left: auto;
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_forms.scss b/web/sass-files/sass/partials/_forms.scss
index c8b08f44d..65ea161d4 100644
--- a/web/sass-files/sass/partials/_forms.scss
+++ b/web/sass-files/sass/partials/_forms.scss
@@ -18,6 +18,7 @@
.input__help {
color: #777;
margin: 10px 0 0 10px;
+ word-break: break-word;
&.dark {
color: #222;
}
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 702f0fd60..a20d1f48b 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -231,6 +231,10 @@
width: 45px;
color: #999;
cursor: pointer;
+ .fa {
+ margin-left: 3px;
+ font-size: 16px;
+ }
}
&.alt {
margin: 0;
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index a046cd904..38e9b4174 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -8,6 +8,16 @@
@include opacity(0.7);
}
}
+ a, a:focus, a:hover {
+ color: #2389D7;
+ }
+ .btn.btn-primary {
+ background: #4285f4;
+ &:hover, &:focus, &:active {
+ background: $primary-color--hover;
+ color: #fff;
+ }
+ }
.info__label {
font-weight: 600;
text-align: right;
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 6940cf2fb..d4f02cf4b 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -275,7 +275,7 @@ body.ios {
&.current--user {
.post-body {
@include border-radius(4px);
- background: #f5f5f5;
+ background: rgba(#000, 0.05);
}
}
&.post--comment {
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index 9ae41ebb0..9abdd40da 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -107,4 +107,5 @@
.search-highlight.theme, .search-highlight {
background-color: #FFF2BB;
+ color: #333;
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 2b59a943b..8dcd8f35c 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -79,6 +79,36 @@
}
}
+ .appearance-section {
+ .premade-themes {
+ .theme-label {
+ font-weight: 400;
+ margin-top: 5px;
+ }
+ img {
+ border: 3px solid transparent;
+ }
+ .active {
+ img {
+ border-color: $primary-color;
+ }
+ }
+ }
+ .custom-label {
+ font-weight: normal;
+ font-size: 13px;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 0;
+ }
+ .radio {
+ label {
+ font-weight: 600;
+ }
+ }
+ }
+
.section-title {
margin-bottom: 5px;
font-weight: 600;
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 94583b153..6a418e270 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -16,6 +16,12 @@
overflow-y: auto;
max-width: 200px;
width: 200px;
+ a {
+ color: #262626 !important;
+ &:hover, &:focus {
+ background: #f5f5f5 !important;
+ }
+ }
}
.search__form {
margin: 0;
diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss
index d02a92448..b37dbf421 100644
--- a/web/sass-files/sass/partials/_sidebar--right.scss
+++ b/web/sass-files/sass/partials/_sidebar--right.scss
@@ -51,9 +51,10 @@
margin: 11px 0 0 0;
width: 22px;
height: 22px;
- background: url("../images/closeSidebar.png");
- @include background-size(100% 100%);
opacity: 0.5;
+ font-size: 22px;
+ line-height: 0;
+ background: none;
float: right;
outline: none;
border: none;
@@ -61,11 +62,15 @@
&:hover, &:active {
opacity: 0.8;
}
+ i {
+ position: relative;
+ top: -2px;
+ }
}
.sidebar--right__header {
font-size: 1em;
text-transform: uppercase;
- color: #444;
+ color: inherit;
height: 44px;
padding: 0 1em;
line-height: 44px;
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index de1db57e8..923a6e99b 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -10,6 +10,7 @@
@import "partials/perfect-scrollbar";
@import "partials/font-awesome";
@import "partials/base";
+@import "partials/colorpicker";
// Channel Css
@import "partials/headers";
diff --git a/web/static/css/bootstrap-colorpicker.min.css b/web/static/css/bootstrap-colorpicker.min.css
new file mode 100755
index 000000000..5656e768f
--- /dev/null
+++ b/web/static/css/bootstrap-colorpicker.min.css
@@ -0,0 +1,9 @@
+/*!
+ * Bootstrap Colorpicker
+ * http://mjolnic.github.io/bootstrap-colorpicker/
+ *
+ * Originally written by (c) 2012 Stefan Petre
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ */.colorpicker-saturation{float:left;width:100px;height:100px;cursor:crosshair;background-image:url("../images/bootstrap-colorpicker/saturation.png")}.colorpicker-saturation i{position:absolute;top:0;left:0;display:block;width:5px;height:5px;margin:-4px 0 0 -4px;border:1px solid #000;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-saturation i b{display:block;width:5px;height:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-hue,.colorpicker-alpha{float:left;width:15px;height:100px;margin-bottom:4px;margin-left:4px;cursor:row-resize}.colorpicker-hue i,.colorpicker-alpha i{position:absolute;top:0;left:0;display:block;width:100%;height:1px;margin-top:-1px;background:#000;border-top:1px solid #fff}.colorpicker-hue{background-image:url("../images/bootstrap-colorpicker/hue.png")}.colorpicker-alpha{display:none;background-image:url("../images/bootstrap-colorpicker/alpha.png")}.colorpicker-saturation,.colorpicker-hue,.colorpicker-alpha{background-size:contain}.colorpicker{top:0;left:0;z-index:2500;min-width:130px;padding:4px;margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1}.colorpicker:before,.colorpicker:after{display:table;line-height:0;content:""}.colorpicker:after{clear:both}.colorpicker:before{position:absolute;top:-7px;left:6px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.colorpicker:after{position:absolute;top:-6px;left:7px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.colorpicker div{position:relative}.colorpicker.colorpicker-with-alpha{min-width:140px}.colorpicker.colorpicker-with-alpha .colorpicker-alpha{display:block}.colorpicker-color{height:10px;margin-top:5px;clear:both;background-image:url("../images/bootstrap-colorpicker/alpha.png");background-position:0 100%}.colorpicker-color div{height:10px}.colorpicker-selectors{display:none;height:10px;margin-top:5px;clear:both}.colorpicker-selectors i{float:left;width:10px;height:10px;cursor:pointer}.colorpicker-selectors i+i{margin-left:3px}.colorpicker-element .input-group-addon i,.colorpicker-element .add-on i{display:inline-block;width:16px;height:16px;vertical-align:text-top;cursor:pointer}.colorpicker.colorpicker-inline{position:relative;z-index:auto;display:inline-block;float:none}.colorpicker.colorpicker-horizontal{width:110px;height:auto;min-width:110px}.colorpicker.colorpicker-horizontal .colorpicker-saturation{margin-bottom:4px}.colorpicker.colorpicker-horizontal .colorpicker-color{width:100px}.colorpicker.colorpicker-horizontal .colorpicker-hue,.colorpicker.colorpicker-horizontal .colorpicker-alpha{float:left;width:100px;height:15px;margin-bottom:4px;margin-left:0;cursor:col-resize}.colorpicker.colorpicker-horizontal .colorpicker-hue i,.colorpicker.colorpicker-horizontal .colorpicker-alpha i{position:absolute;top:0;left:0;display:block;width:1px;height:15px;margin-top:0;background:#fff;border:0}.colorpicker.colorpicker-horizontal .colorpicker-hue{background-image:url("../images/bootstrap-colorpicker/hue-horizontal.png")}.colorpicker.colorpicker-horizontal .colorpicker-alpha{background-image:url("../images/bootstrap-colorpicker/alpha-horizontal.png")}.colorpicker.colorpicker-hidden{display:none}.colorpicker.colorpicker-visible{display:block}.colorpicker-inline.colorpicker-visible{display:inline-block}.colorpicker-right:before{right:6px;left:auto}.colorpicker-right:after{right:7px;left:auto}
diff --git a/web/static/images/bootstrap-colorpicker/alpha-horizontal.png b/web/static/images/bootstrap-colorpicker/alpha-horizontal.png
new file mode 100755
index 000000000..d0a65c08b
--- /dev/null
+++ b/web/static/images/bootstrap-colorpicker/alpha-horizontal.png
Binary files differ
diff --git a/web/static/images/bootstrap-colorpicker/alpha.png b/web/static/images/bootstrap-colorpicker/alpha.png
new file mode 100755
index 000000000..38043f1c8
--- /dev/null
+++ b/web/static/images/bootstrap-colorpicker/alpha.png
Binary files differ
diff --git a/web/static/images/bootstrap-colorpicker/hue-horizontal.png b/web/static/images/bootstrap-colorpicker/hue-horizontal.png
new file mode 100755
index 000000000..a0d9add8e
--- /dev/null
+++ b/web/static/images/bootstrap-colorpicker/hue-horizontal.png
Binary files differ
diff --git a/web/static/images/bootstrap-colorpicker/hue.png b/web/static/images/bootstrap-colorpicker/hue.png
new file mode 100755
index 000000000..d89560e99
--- /dev/null
+++ b/web/static/images/bootstrap-colorpicker/hue.png
Binary files differ
diff --git a/web/static/images/bootstrap-colorpicker/saturation.png b/web/static/images/bootstrap-colorpicker/saturation.png
new file mode 100755
index 000000000..594ae50ed
--- /dev/null
+++ b/web/static/images/bootstrap-colorpicker/saturation.png
Binary files differ
diff --git a/web/static/images/themes/dark.png b/web/static/images/themes/dark.png
new file mode 100644
index 000000000..832f64d2e
--- /dev/null
+++ b/web/static/images/themes/dark.png
Binary files differ
diff --git a/web/static/images/themes/mattermost.png b/web/static/images/themes/mattermost.png
new file mode 100644
index 000000000..4a321adcb
--- /dev/null
+++ b/web/static/images/themes/mattermost.png
Binary files differ
diff --git a/web/static/images/themes/slack.png b/web/static/images/themes/slack.png
new file mode 100644
index 000000000..dc70c7dc2
--- /dev/null
+++ b/web/static/images/themes/slack.png
Binary files differ
diff --git a/web/static/js/bootstrap-colorpicker.min.js b/web/static/js/bootstrap-colorpicker.min.js
new file mode 100755
index 000000000..05c0e0744
--- /dev/null
+++ b/web/static/js/bootstrap-colorpicker.min.js
@@ -0,0 +1 @@
+!function(a){"use strict";"object"==typeof exports?module.exports=a(window.jQuery):"function"==typeof define&&define.amd?define(["jquery"],a):window.jQuery&&!window.jQuery.fn.colorpicker&&a(window.jQuery)}(function(a){"use strict";var b=function(b,c){this.value={h:0,s:0,b:0,a:1},this.origFormat=null,c&&a.extend(this.colors,c),b&&(void 0!==b.toLowerCase?(b+="",this.setColor(b)):void 0!==b.h&&(this.value=b))};b.prototype={constructor:b,colors:{aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32",transparent:"transparent"},_sanitizeNumber:function(a){return"number"==typeof a?a:isNaN(a)||null===a||""===a||void 0===a?1:void 0!==a.toLowerCase?parseFloat(a):1},isTransparent:function(a){return a?(a=a.toLowerCase().trim(),"transparent"===a||a.match(/#?00000000/)||a.match(/(rgba|hsla)\(0,0,0,0?\.?0\)/)):!1},rgbaIsTransparent:function(a){return 0===a.r&&0===a.g&&0===a.b&&0===a.a},setColor:function(a){a=a.toLowerCase().trim(),a&&(this.isTransparent(a)?this.value={h:0,s:0,b:0,a:0}:this.value=this.stringToHSB(a)||{h:0,s:0,b:0,a:1})},stringToHSB:function(b){b=b.toLowerCase();var c;"undefined"!=typeof this.colors[b]&&(b=this.colors[b],c="alias");var d=this,e=!1;return a.each(this.stringParsers,function(a,f){var g=f.re.exec(b),h=g&&f.parse.apply(d,[g]),i=c||f.format||"rgba";return h?(e=i.match(/hsla?/)?d.RGBtoHSB.apply(d,d.HSLtoRGB.apply(d,h)):d.RGBtoHSB.apply(d,h),d.origFormat=i,!1):!0}),e},setHue:function(a){this.value.h=1-a},setSaturation:function(a){this.value.s=a},setBrightness:function(a){this.value.b=1-a},setAlpha:function(a){this.value.a=parseInt(100*(1-a),10)/100},toRGB:function(a,b,c,d){a||(a=this.value.h,b=this.value.s,c=this.value.b),a*=360;var e,f,g,h,i;return a=a%360/60,i=c*b,h=i*(1-Math.abs(a%2-1)),e=f=g=c-i,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a],{r:Math.round(255*e),g:Math.round(255*f),b:Math.round(255*g),a:d||this.value.a}},toHex:function(a,b,c,d){var e=this.toRGB(a,b,c,d);return this.rgbaIsTransparent(e)?"transparent":"#"+(1<<24|parseInt(e.r)<<16|parseInt(e.g)<<8|parseInt(e.b)).toString(16).substr(1)},toHSL:function(a,b,c,d){a=a||this.value.h,b=b||this.value.s,c=c||this.value.b,d=d||this.value.a;var e=a,f=(2-b)*c,g=b*c;return g/=f>0&&1>=f?f:2-f,f/=2,g>1&&(g=1),{h:isNaN(e)?0:e,s:isNaN(g)?0:g,l:isNaN(f)?0:f,a:isNaN(d)?0:d}},toAlias:function(a,b,c,d){var e=this.toHex(a,b,c,d);for(var f in this.colors)if(this.colors[f]===e)return f;return!1},RGBtoHSB:function(a,b,c,d){a/=255,b/=255,c/=255;var e,f,g,h;return g=Math.max(a,b,c),h=g-Math.min(a,b,c),e=0===h?null:g===a?(b-c)/h:g===b?(c-a)/h+2:(a-b)/h+4,e=(e+360)%6*60/360,f=0===h?0:h/g,{h:this._sanitizeNumber(e),s:f,b:g,a:this._sanitizeNumber(d)}},HueToRGB:function(a,b,c){return 0>c?c+=1:c>1&&(c-=1),1>6*c?a+(b-a)*c*6:1>2*c?b:2>3*c?a+(b-a)*(2/3-c)*6:a},HSLtoRGB:function(a,b,c,d){0>b&&(b=0);var e;e=.5>=c?c*(1+b):c+b-c*b;var f=2*c-e,g=a+1/3,h=a,i=a-1/3,j=Math.round(255*this.HueToRGB(f,e,g)),k=Math.round(255*this.HueToRGB(f,e,h)),l=Math.round(255*this.HueToRGB(f,e,i));return[j,k,l,this._sanitizeNumber(d)]},toString:function(a){a=a||"rgba";var b=!1;switch(a){case"rgb":return b=this.toRGB(),this.rgbaIsTransparent(b)?"transparent":"rgb("+b.r+","+b.g+","+b.b+")";case"rgba":return b=this.toRGB(),"rgba("+b.r+","+b.g+","+b.b+","+b.a+")";case"hsl":return b=this.toHSL(),"hsl("+Math.round(360*b.h)+","+Math.round(100*b.s)+"%,"+Math.round(100*b.l)+"%)";case"hsla":return b=this.toHSL(),"hsla("+Math.round(360*b.h)+","+Math.round(100*b.s)+"%,"+Math.round(100*b.l)+"%,"+b.a+")";case"hex":return this.toHex();case"alias":return this.toAlias()||this.toHex();default:return b}},stringParsers:[{re:/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*?\)/,format:"rgb",parse:function(a){return[a[1],a[2],a[3],1]}},{re:/rgb\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*?\)/,format:"rgb",parse:function(a){return[2.55*a[1],2.55*a[2],2.55*a[3],1]}},{re:/rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"rgba",parse:function(a){return[a[1],a[2],a[3],a[4]]}},{re:/rgba\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"rgba",parse:function(a){return[2.55*a[1],2.55*a[2],2.55*a[3],a[4]]}},{re:/hsl\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*?\)/,format:"hsl",parse:function(a){return[a[1]/360,a[2]/100,a[3]/100,a[4]]}},{re:/hsla\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,format:"hsla",parse:function(a){return[a[1]/360,a[2]/100,a[3]/100,a[4]]}},{re:/#?([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,format:"hex",parse:function(a){return[parseInt(a[1],16),parseInt(a[2],16),parseInt(a[3],16),1]}},{re:/#?([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,format:"hex",parse:function(a){return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16),1]}}],colorNameToHex:function(a){return"undefined"!=typeof this.colors[a.toLowerCase()]?this.colors[a.toLowerCase()]:!1}};var c={horizontal:!1,inline:!1,color:!1,format:!1,input:"input",container:!1,component:".add-on, .input-group-addon",sliders:{saturation:{maxLeft:100,maxTop:100,callLeft:"setSaturation",callTop:"setBrightness"},hue:{maxLeft:0,maxTop:100,callLeft:!1,callTop:"setHue"},alpha:{maxLeft:0,maxTop:100,callLeft:!1,callTop:"setAlpha"}},slidersHorz:{saturation:{maxLeft:100,maxTop:100,callLeft:"setSaturation",callTop:"setBrightness"},hue:{maxLeft:100,maxTop:0,callLeft:"setHue",callTop:!1},alpha:{maxLeft:100,maxTop:0,callLeft:"setAlpha",callTop:!1}},template:'<div class="colorpicker dropdown-menu"><div class="colorpicker-saturation"><i><b></b></i></div><div class="colorpicker-hue"><i></i></div><div class="colorpicker-alpha"><i></i></div><div class="colorpicker-color"><div /></div><div class="colorpicker-selectors"></div></div>',align:"right",customClass:null,colorSelectors:null},d=function(d,e){if(this.element=a(d).addClass("colorpicker-element"),this.options=a.extend(!0,{},c,this.element.data(),e),this.component=this.options.component,this.component=this.component!==!1?this.element.find(this.component):!1,this.component&&0===this.component.length&&(this.component=!1),this.container=this.options.container===!0?this.element:this.options.container,this.container=this.container!==!1?a(this.container):!1,this.input=this.element.is("input")?this.element:this.options.input?this.element.find(this.options.input):!1,this.input&&0===this.input.length&&(this.input=!1),this.color=new b(this.options.color!==!1?this.options.color:this.getValue(),this.options.colorSelectors),this.format=this.options.format!==!1?this.options.format:this.color.origFormat,this.picker=a(this.options.template),this.options.customClass&&this.picker.addClass(this.options.customClass),this.options.inline?this.picker.addClass("colorpicker-inline colorpicker-visible"):this.picker.addClass("colorpicker-hidden"),this.options.horizontal&&this.picker.addClass("colorpicker-horizontal"),("rgba"===this.format||"hsla"===this.format||this.options.format===!1)&&this.picker.addClass("colorpicker-with-alpha"),"right"===this.options.align&&this.picker.addClass("colorpicker-right"),this.options.colorSelectors){var f=this;a.each(this.options.colorSelectors,function(b,c){var d=a("<i />").css("background-color",c).data("class",b);d.click(function(){f.setValue(a(this).css("background-color"))}),f.picker.find(".colorpicker-selectors").append(d)}),this.picker.find(".colorpicker-selectors").show()}this.picker.on("mousedown.colorpicker touchstart.colorpicker",a.proxy(this.mousedown,this)),this.picker.appendTo(this.container?this.container:a("body")),this.input!==!1&&(this.input.on({"keyup.colorpicker":a.proxy(this.keyup,this)}),this.input.on({"change.colorpicker":a.proxy(this.change,this)}),this.component===!1&&this.element.on({"focus.colorpicker":a.proxy(this.show,this)}),this.options.inline===!1&&this.element.on({"focusout.colorpicker":a.proxy(this.hide,this)})),this.component!==!1&&this.component.on({"click.colorpicker":a.proxy(this.show,this)}),this.input===!1&&this.component===!1&&this.element.on({"click.colorpicker":a.proxy(this.show,this)}),this.input!==!1&&this.component!==!1&&"color"===this.input.attr("type")&&this.input.on({"click.colorpicker":a.proxy(this.show,this),"focus.colorpicker":a.proxy(this.show,this)}),this.update(),a(a.proxy(function(){this.element.trigger("create")},this))};d.Color=b,d.prototype={constructor:d,destroy:function(){this.picker.remove(),this.element.removeData("colorpicker").off(".colorpicker"),this.input!==!1&&this.input.off(".colorpicker"),this.component!==!1&&this.component.off(".colorpicker"),this.element.removeClass("colorpicker-element"),this.element.trigger({type:"destroy"})},reposition:function(){if(this.options.inline!==!1||this.options.container)return!1;var a=this.container&&this.container[0]!==document.body?"position":"offset",b=this.component||this.element,c=b[a]();"right"===this.options.align&&(c.left-=this.picker.outerWidth()-b.outerWidth()),this.picker.css({top:c.top+b.outerHeight(),left:c.left})},show:function(b){return this.isDisabled()?!1:(this.picker.addClass("colorpicker-visible").removeClass("colorpicker-hidden"),this.reposition(),a(window).on("resize.colorpicker",a.proxy(this.reposition,this)),!b||this.hasInput()&&"color"!==this.input.attr("type")||b.stopPropagation&&b.preventDefault&&(b.stopPropagation(),b.preventDefault()),this.options.inline===!1&&a(window.document).on({"mousedown.colorpicker":a.proxy(this.hide,this)}),void this.element.trigger({type:"showPicker",color:this.color}))},hide:function(){this.picker.addClass("colorpicker-hidden").removeClass("colorpicker-visible"),a(window).off("resize.colorpicker",this.reposition),a(document).off({"mousedown.colorpicker":this.hide}),this.update(),this.element.trigger({type:"hidePicker",color:this.color})},updateData:function(a){return a=a||this.color.toString(this.format),this.element.data("color",a),a},updateInput:function(a){if(a=a||this.color.toString(this.format),this.input!==!1){if(this.options.colorSelectors){var c=new b(a,this.options.colorSelectors),d=c.toAlias();"undefined"!=typeof this.options.colorSelectors[d]&&(a=d)}this.input.prop("value",a)}return a},updatePicker:function(a){void 0!==a&&(this.color=new b(a,this.options.colorSelectors));var c=this.options.horizontal===!1?this.options.sliders:this.options.slidersHorz,d=this.picker.find("i");return 0!==d.length?(this.options.horizontal===!1?(c=this.options.sliders,d.eq(1).css("top",c.hue.maxTop*(1-this.color.value.h)).end().eq(2).css("top",c.alpha.maxTop*(1-this.color.value.a))):(c=this.options.slidersHorz,d.eq(1).css("left",c.hue.maxLeft*(1-this.color.value.h)).end().eq(2).css("left",c.alpha.maxLeft*(1-this.color.value.a))),d.eq(0).css({top:c.saturation.maxTop-this.color.value.b*c.saturation.maxTop,left:this.color.value.s*c.saturation.maxLeft}),this.picker.find(".colorpicker-saturation").css("backgroundColor",this.color.toHex(this.color.value.h,1,1,1)),this.picker.find(".colorpicker-alpha").css("backgroundColor",this.color.toHex()),this.picker.find(".colorpicker-color, .colorpicker-color div").css("backgroundColor",this.color.toString(this.format)),a):void 0},updateComponent:function(a){if(a=a||this.color.toString(this.format),this.component!==!1){var b=this.component.find("i").eq(0);b.length>0?b.css({backgroundColor:a}):this.component.css({backgroundColor:a})}return a},update:function(a){var b;return(this.getValue(!1)!==!1||a===!0)&&(b=this.updateComponent(),this.updateInput(b),this.updateData(b),this.updatePicker()),b},setValue:function(a){this.color=new b(a,this.options.colorSelectors),this.update(!0),this.element.trigger({type:"changeColor",color:this.color,value:a})},getValue:function(a){a=void 0===a?"#000000":a;var b;return b=this.hasInput()?this.input.val():this.element.data("color"),(void 0===b||""===b||null===b)&&(b=a),b},hasInput:function(){return this.input!==!1},isDisabled:function(){return this.hasInput()?this.input.prop("disabled")===!0:!1},disable:function(){return this.hasInput()?(this.input.prop("disabled",!0),this.element.trigger({type:"disable",color:this.color,value:this.getValue()}),!0):!1},enable:function(){return this.hasInput()?(this.input.prop("disabled",!1),this.element.trigger({type:"enable",color:this.color,value:this.getValue()}),!0):!1},currentSlider:null,mousePointer:{left:0,top:0},mousedown:function(b){b.pageX||b.pageY||!b.originalEvent||(b.pageX=b.originalEvent.touches[0].pageX,b.pageY=b.originalEvent.touches[0].pageY),b.stopPropagation(),b.preventDefault();var c=a(b.target),d=c.closest("div"),e=this.options.horizontal?this.options.slidersHorz:this.options.sliders;if(!d.is(".colorpicker")){if(d.is(".colorpicker-saturation"))this.currentSlider=a.extend({},e.saturation);else if(d.is(".colorpicker-hue"))this.currentSlider=a.extend({},e.hue);else{if(!d.is(".colorpicker-alpha"))return!1;this.currentSlider=a.extend({},e.alpha)}var f=d.offset();this.currentSlider.guide=d.find("i")[0].style,this.currentSlider.left=b.pageX-f.left,this.currentSlider.top=b.pageY-f.top,this.mousePointer={left:b.pageX,top:b.pageY},a(document).on({"mousemove.colorpicker":a.proxy(this.mousemove,this),"touchmove.colorpicker":a.proxy(this.mousemove,this),"mouseup.colorpicker":a.proxy(this.mouseup,this),"touchend.colorpicker":a.proxy(this.mouseup,this)}).trigger("mousemove")}return!1},mousemove:function(a){a.pageX||a.pageY||!a.originalEvent||(a.pageX=a.originalEvent.touches[0].pageX,a.pageY=a.originalEvent.touches[0].pageY),a.stopPropagation(),a.preventDefault();var b=Math.max(0,Math.min(this.currentSlider.maxLeft,this.currentSlider.left+((a.pageX||this.mousePointer.left)-this.mousePointer.left))),c=Math.max(0,Math.min(this.currentSlider.maxTop,this.currentSlider.top+((a.pageY||this.mousePointer.top)-this.mousePointer.top)));return this.currentSlider.guide.left=b+"px",this.currentSlider.guide.top=c+"px",this.currentSlider.callLeft&&this.color[this.currentSlider.callLeft].call(this.color,b/this.currentSlider.maxLeft),this.currentSlider.callTop&&this.color[this.currentSlider.callTop].call(this.color,c/this.currentSlider.maxTop),"setAlpha"===this.currentSlider.callTop&&this.options.format===!1&&(1!==this.color.value.a?(this.format="rgba",this.color.origFormat="rgba"):(this.format="hex",this.color.origFormat="hex")),this.update(!0),this.element.trigger({type:"changeColor",color:this.color}),!1},mouseup:function(b){return b.stopPropagation(),b.preventDefault(),a(document).off({"mousemove.colorpicker":this.mousemove,"touchmove.colorpicker":this.mousemove,"mouseup.colorpicker":this.mouseup,"touchend.colorpicker":this.mouseup}),!1},change:function(a){this.keyup(a)},keyup:function(a){38===a.keyCode?(this.color.value.a<1&&(this.color.value.a=Math.round(100*(this.color.value.a+.01))/100),this.update(!0)):40===a.keyCode?(this.color.value.a>0&&(this.color.value.a=Math.round(100*(this.color.value.a-.01))/100),this.update(!0)):(this.color=new b(this.input.val(),this.options.colorSelectors),this.color.origFormat&&this.options.format===!1&&(this.format=this.color.origFormat),this.getValue(!1)!==!1&&(this.updateData(),this.updateComponent(),this.updatePicker())),this.element.trigger({type:"changeColor",color:this.color,value:this.input.val()})}},a.colorpicker=d,a.fn.colorpicker=function(b){var c,e=arguments,f=this.each(function(){var f=a(this),g=f.data("colorpicker"),h="object"==typeof b?b:{};g||"string"==typeof b?"string"==typeof b&&(c=g[b].apply(g,Array.prototype.slice.call(e,1))):f.data("colorpicker",new d(this,h))});return"getValue"===b?c:f},a.fn.colorpicker.constructor=d}); \ No newline at end of file
diff --git a/web/static/js/emojify.min.js b/web/static/js/emojify.min.js
deleted file mode 100755
index 4fedf3205..000000000
--- a/web/static/js/emojify.min.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/*! emojify.js - v1.0.5 -
- * Copyright (c) Hassan Khan 2015
- */
-!function(e,a){"use strict";"function"==typeof define&&define.amd?define([],a):"object"==typeof exports?module.exports=a():e.emojify=a()}(this,function(){"use strict";var e=function(){function e(){var e={named:/:([a-z0-9A-Z_-]+):/,smile:/:-?\)/g,open_mouth:/:o/gi,scream:/:-o/gi,smirk:/[:;]-?]/g,grinning:/[:;]-?d/gi,stuck_out_tongue_closed_eyes:/x-d/gi,stuck_out_tongue_winking_eye:/[:;]-?p/gi,rage:/:-?[\[@]/g,frowning:/:-?\(/g,sob:/:['’]-?\(|:&#x27;\(/g,kissing_heart:/:-?\*/g,wink:/;-?\)/g,pensive:/:-?\//g,confounded:/:-?s/gi,flushed:/:-?\|/g,relaxed:/:-?\$/g,mask:/:-x/gi,heart:/<3|&lt;3/g,broken_heart:/<\/3|&lt;&#x2F;3/g,thumbsup:/:\+1:/g,thumbsdown:/:\-1:/g};return d.ignore_emoticons&&(e={named:/:([a-z0-9A-Z_-]+):/,thumbsup:/:\+1:/g,thumbsdown:/:\-1:/g}),Object.keys(e).map(function(a){return[e[a],a]})}function a(){var e=_.map(function(e){var a=e[0],o=a.source||a;return o=o.replace(/(^|[^\[])\^/g,"$1"),"("+o+")"}).join("|");return new RegExp(e,"gi")}function o(e){return" "===e||" "===e||"\r"===e||"\n"===e||""===e||e===String.fromCharCode(160)}function r(e){var a=null;if(e.replacer)a=e.replacer.apply({config:d},[":"+e.emojiName+":",e.emojiName]);else{var o=d.tag_type||h[d.mode];a=e.win.document.createElement(o),"img"!==o?a.setAttribute("class","emoji emoji-"+e.emojiName):(a.setAttribute("align","absmiddle"),a.setAttribute("alt",":"+e.emojiName+":"),a.setAttribute("class","emoji"),a.setAttribute("src",d.img_dir+"/"+e.emojiName+".png")),a.setAttribute("title",":"+e.emojiName+":")}e.node.splitText(e.match.index),e.node.nextSibling.nodeValue=e.node.nextSibling.nodeValue.substr(e.match[0].length,e.node.nextSibling.nodeValue.length),a.appendChild(e.node.splitText(e.match.index)),e.node.parentNode.insertBefore(a,e.node.nextSibling)}function t(e){if(e[1]&&e[2]){var a=e[2];if(m[a])return a}else for(var o=3;o<e.length-1;o++)if(e[o])return _[o-2][1]}function i(e,a){var o=this.config.tag_type||h[this.config.mode];return"img"!==o?"<"+o+" class='emoji emoji-"+a+"' title=':"+a+":'></"+o+">":"<img align='absmiddle' alt=':"+a+":' class='emoji' src='"+this.config.img_dir+"/"+a+".png' title=':"+a+":' />"}function n(){this.lastEmojiTerminatedAt=-1}function s(o,r){if(!o)return o;r||(r=i),_=e(),c=a();var t=new n;return o.replace(c,function(){var e=Array.prototype.slice.call(arguments,0,-2),a=arguments[arguments.length-2],o=arguments[arguments.length-1],i=t.validate(e,a,o);return i?r.apply({config:d},[arguments[0],i]):arguments[0]})}function l(o,i){"undefined"==typeof o&&(o=d.only_crawl_id?document.getElementById(d.only_crawl_id):document.body);var s=o.ownerDocument,l=s.defaultView||s.parentWindow,u=function(e,a){var o;if(e.hasChildNodes())for(o=e.firstChild;o;)a(o)&&u(o,a),o=o.nextSibling},g=function(e){for(var a,o=[],s=new n;null!==(a=c.exec(e.data));)s.validate(a,a.index,a.input)&&o.push(a);for(var _=o.length;_-->0;){var u=t(o[_]);r({node:e,match:o[_],emojiName:u,replacer:i,win:l})}};_=e(),c=a();var m=[],h=new RegExp(d.blacklist.elements.join("|"),"i"),p=new RegExp(d.blacklist.classes.join("|"),"i");if("undefined"!=typeof l.document.createTreeWalker)for(var b,f=l.document.createTreeWalker(o,l.NodeFilter.SHOW_TEXT|l.NodeFilter.SHOW_ELEMENT,function(e){return 1!==e.nodeType?l.NodeFilter.FILTER_ACCEPT:e.tagName.match(h)||"svg"===e.tagName||e.className.match(p)?l.NodeFilter.FILTER_REJECT:l.NodeFilter.FILTER_SKIP},!1);null!==(b=f.nextNode());)m.push(b);else u(o,function(e){return"undefined"!=typeof e.tagName&&e.tagName.match(h)||"undefined"!=typeof e.className&&e.className.match(p)?!1:1===e.nodeType?!0:(m.push(e),!0)});m.forEach(g)}var _,c,u="+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,blue_car,blue_heart,blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,boy,bread,bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,busstop,bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,couplekiss,cow,cow2,credit_card,crescent_moon,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,interrobang,iphone,it,izakaya_lantern,jack_o_lantern,japan,japanese_castle,japanese_goblin,japanese_ogre,jeans,joy,joy_cat,jp,key,keycap_ten,kimono,kiss,kissing,kissing_cat,kissing_closed_eyes,kissing_face,kissing_heart,kissing_smiling_eyes,koala,koko,kr,large_blue_circle,large_blue_diamond,large_orange_diamond,last_quarter_moon,last_quarter_moon_with_face,laughing,leaves,ledger,left_luggage,left_right_arrow,leftwards_arrow_with_hook,lemon,leo,leopard,libra,light_rail,link,lips,lipstick,lock,lock_with_ink_pen,lollipop,loop,loudspeaker,love_hotel,love_letter,low_brightness,m,mag,mag_right,mahjong,mailbox,mailbox_closed,mailbox_with_mail,mailbox_with_no_mail,man,man_with_gua_pi_mao,man_with_turban,mans_shoe,maple_leaf,mask,massage,meat_on_bone,mega,melon,memo,mens,metal,metro,microphone,microscope,milky_way,minibus,minidisc,mobile_phone_off,money_with_wings,moneybag,monkey,monkey_face,monorail,mortar_board,mount_fuji,mountain_bicyclist,mountain_cableway,mountain_railway,mouse,mouse2,movie_camera,moyai,muscle,mushroom,musical_keyboard,musical_note,musical_score,mute,nail_care,name_badge,neckbeard,necktie,negative_squared_cross_mark,neutral_face,new,new_moon,new_moon_with_face,newspaper,ng,nine,no_bell,no_bicycles,no_entry,no_entry_sign,no_good,no_mobile_phones,no_mouth,no_pedestrians,no_smoking,non-potable_water,nose,notebook,notebook_with_decorative_cover,notes,nut_and_bolt,o,o2,ocean,octocat,octopus,oden,office,ok,ok_hand,ok_woman,older_man,older_woman,on,oncoming_automobile,oncoming_bus,oncoming_police_car,oncoming_taxi,one,open_file_folder,open_hands,open_mouth,ophiuchus,orange_book,outbox_tray,ox,package,page_facing_up,page_with_curl,pager,palm_tree,panda_face,paperclip,parking,part_alternation_mark,partly_sunny,passport_control,paw_prints,peach,pear,pencil,pencil2,penguin,pensive,performing_arts,persevere,person_frowning,person_with_blond_hair,person_with_pouting_face,phone,pig,pig2,pig_nose,pill,pineapple,pisces,pizza,plus1,point_down,point_left,point_right,point_up,point_up_2,police_car,poodle,poop,post_office,postal_horn,postbox,potable_water,pouch,poultry_leg,pound,pouting_cat,pray,princess,punch,purple_heart,purse,pushpin,put_litter_in_its_place,question,rabbit,rabbit2,racehorse,radio,radio_button,rage,rage1,rage2,rage3,rage4,railway_car,rainbow,raised_hand,raised_hands,raising_hand,ram,ramen,rat,recycle,red_car,red_circle,registered,relaxed,relieved,repeat,repeat_one,restroom,revolving_hearts,rewind,ribbon,rice,rice_ball,rice_cracker,rice_scene,ring,rocket,roller_coaster,rooster,rose,rotating_light,round_pushpin,rowboat,ru,rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,ski,skull,sleeping,sleepy,slot_machine,small_blue_diamond,small_orange_diamond,small_red_triangle,small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,smirk,smirk_cat,smoking,snail,snake,snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,spaghetti,sparkle,sparkler,sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,star2,stars,station,statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,stuck_out_tongue_closed_eyes,stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,sunrise_over_mountains,surfer,sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,symbols,syringe,tada,tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,thought_balloon,three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,tophat,tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,video_camera,video_game,violin,virgo,volcano,vs,walking,waning_crescent_moon,waning_gibbous_moon,warning,watch,water_buffalo,watermelon,wave,wavy_dash,waxing_crescent_moon,waxing_gibbous_moon,wc,weary,wedding,whale,whale2,wheelchair,white_check_mark,white_circle,white_flower,white_large_square,white_medium_small_square,white_medium_square,white_small_square,white_square_button,wind_chime,wine_glass,wink,wolf,woman,womans_clothes,womans_hat,womens,worried,wrench,x,yellow_heart,yen,yum,zap,zero,zzz",g=u.split(/,/),m=g.reduce(function(e,a){return e[a]=!0,e},{}),d={blacklist:{ids:[],classes:["no-emojify"],elements:["script","textarea","a","pre","code"]},tag_type:null,only_crawl_id:null,img_dir:"images/emoji",ignore_emoticons:!1,mode:"img"},h={img:"img",sprite:"span","data-uri":"span"};return n.prototype={validate:function(e,a,r){function i(){return n.lastEmojiTerminatedAt=_+a,s}var n=this,s=t(e);if(s){var l=e[0],_=l.length;if(0===a)return i();if(r.length===l.length+a)return i();var c=this.lastEmojiTerminatedAt===a;if(c)return i();if(o(r.charAt(a-1)))return i();var u=o(r.charAt(l.length+a));return u&&c?i():void 0}}},{defaultConfig:d,emojiNames:g,setConfig:function(e){Object.keys(d).forEach(function(a){a in e&&(d[a]=e[a])})},replace:s,run:l}}();return e}); \ No newline at end of file
diff --git a/web/templates/channel.html b/web/templates/channel.html
index 92aaaf02f..2af94e415 100644
--- a/web/templates/channel.html
+++ b/web/templates/channel.html
@@ -29,6 +29,7 @@
<div id="edit_mention_tab"></div>
<div id="get_link_modal"></div>
<div id="user_settings_modal"></div>
+ <div id="import_theme_modal"></div>
<div id="team_settings_modal"></div>
<div id="invite_member_modal"></div>
<div id="edit_channel_modal"></div>
diff --git a/web/templates/head.html b/web/templates/head.html
index 39d5a262c..e1f2fe43f 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -25,10 +25,12 @@
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
+ <link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css" rel="stylesheet">
<script src="/static/js/react-with-addons-0.13.3.js"></script>
<script src="/static/js/jquery-1.11.1.js"></script>
<script src="/static/js/bootstrap-3.3.5.js"></script>
+ <script src="/static/js/bootstrap-colorpicker.min.js"></script>
<script src="/static/js/react-bootstrap-0.25.1.js"></script>
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
@@ -40,11 +42,6 @@
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
- <script src="/static/js/emojify.min.js"></script>
- <script>
- emojify.setConfig({img_dir: '/static/images/emoji'});
- </script>
-
<style id="antiClickjack">body{display:none !important;}</style>
<script src="/static/js/bundle.js"></script>
<script type="text/javascript">
diff --git a/web/web.go b/web/web.go
index afd310af6..3b36f3d56 100644
--- a/web/web.go
+++ b/web/web.go
@@ -355,6 +355,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
resend := r.URL.Query().Get("resend")
+ resendSuccess := r.URL.Query().Get("resend_success")
name := r.URL.Query().Get("teamname")
email := r.URL.Query().Get("email")
hashedId := r.URL.Query().Get("hid")
@@ -375,7 +376,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
} else {
user := result.Data.(*model.User)
api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
- http.Redirect(w, r, "/", http.StatusFound)
+
+ newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1)
+ http.Redirect(w, r, newAddress, http.StatusFound)
return
}
}
@@ -400,6 +403,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
page.Props["IsVerified"] = isVerified
page.Props["TeamURL"] = c.GetTeamURLFromTeam(team)
page.Props["UserEmail"] = email
+ page.Props["ResendSuccess"] = resendSuccess
page.Render(c, w)
}