diff options
Diffstat (limited to 'web/react')
72 files changed, 1393 insertions, 572 deletions
diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx index e8a46086a..6962876d4 100644 --- a/web/react/components/about_build_modal.jsx +++ b/web/react/components/about_build_modal.jsx @@ -14,7 +14,7 @@ export default class AboutBuildModal extends React.Component { } render() { - const config = global.window.config; + const config = global.window.mm_config; return ( <Modal diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx index c80811bcd..fd6d92c4a 100644 --- a/web/react/components/admin_console/admin_sidebar_header.jsx +++ b/web/react/components/admin_console/admin_sidebar_header.jsx @@ -3,6 +3,7 @@ var AdminNavbarDropdown = require('./admin_navbar_dropdown.jsx'); var UserStore = require('../../stores/user_store.jsx'); +var Utils = require('../../utils/utils.jsx'); export default class SidebarHeader extends React.Component { constructor(props) { @@ -36,7 +37,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( <img className='user__picture' - src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} + src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()} /> ); } diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index 395e22e6c..f7e92672d 100644 --- a/web/react/components/admin_console/user_item.jsx +++ b/web/react/components/admin_console/user_item.jsx @@ -215,7 +215,7 @@ export default class UserItem extends React.Component { <div className='row member-div'> <img className='post-profile-img pull-left' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} + src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} height='36' width='36' /> diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 270631db2..55b4a55c0 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -26,7 +26,6 @@ export default class ChannelLoader extends React.Component { } componentDidMount() { /* Initial aysnc loads */ - AsyncClient.getMe(); AsyncClient.getPosts(ChannelStore.getCurrentId()); AsyncClient.getChannels(true, true); AsyncClient.getChannelExtraInfo(true); diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx index 6151d4bdd..43700bf36 100644 --- a/web/react/components/channel_notifications.jsx +++ b/web/react/components/channel_notifications.jsx @@ -136,16 +136,15 @@ export default class ChannelNotifications extends React.Component { var inputs = []; inputs.push( - <div> + <div key='channel-notification-level-radio'> <div className='radio'> <label> <input type='radio' checked={notifyActive[0]} onChange={this.handleUpdateNotifyLevel.bind(this, 'default')} - > + /> {`Global default (${globalNotifyLevelName})`} - </input> </label> <br/> </div> @@ -155,9 +154,8 @@ export default class ChannelNotifications extends React.Component { type='radio' checked={notifyActive[1]} onChange={this.handleUpdateNotifyLevel.bind(this, 'all')} - > + /> {'For all activity'} - </input> </label> <br/> </div> @@ -167,9 +165,8 @@ export default class ChannelNotifications extends React.Component { type='radio' checked={notifyActive[2]} onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')} - > + /> {'Only for mentions'} - </input> </label> <br/> </div> @@ -179,9 +176,8 @@ export default class ChannelNotifications extends React.Component { type='radio' checked={notifyActive[3]} onChange={this.handleUpdateNotifyLevel.bind(this, 'none')} - > + /> {'Never'} - </input> </label> </div> </div> @@ -274,16 +270,15 @@ export default class ChannelNotifications extends React.Component { if (this.state.activeSection === 'markUnreadLevel') { const inputs = [( - <div> + <div key='channel-notification-unread-radio'> <div className='radio'> <label> <input type='radio' checked={this.state.markUnreadLevel === 'all'} onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')} - > + /> {'For all unread messages'} - </input> </label> <br /> </div> @@ -293,9 +288,8 @@ export default class ChannelNotifications extends React.Component { type='radio' checked={this.state.markUnreadLevel === 'mention'} onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')} - > + /> {'Only for mentions'} - </input> </label> <br /> </div> @@ -370,7 +364,7 @@ export default class ChannelNotifications extends React.Component { data-dismiss='modal' > <span aria-hidden='true'>×</span> - <span className='sr-only'>Close</span> + <span className='sr-only'>{'Close'}</span> </button> <h4 className='modal-title'>Notification Preferences for <span className='name'>{this.state.title}</span></h4> </div> diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 435c7d542..18936e808 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -147,7 +147,7 @@ export default class CreateComment extends React.Component { } const t = Date.now(); - if ((t - this.lastTime) > 5000) { + if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}}); this.lastTime = t; } diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 8b5fc4162..32ee31efe 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -38,6 +38,7 @@ export default class CreatePost extends React.Component { this.getFileCount = this.getFileCount.bind(this); this.handleArrowUp = this.handleArrowUp.bind(this); this.handleResize = this.handleResize.bind(this); + this.sendMessage = this.sendMessage.bind(this); PostStore.clearDraftUploads(); @@ -122,6 +123,11 @@ export default class CreatePost extends React.Component { post.message, false, (data) => { + if (data.response === 'not implemented') { + this.sendMessage(post); + return; + } + PostStore.storeDraft(data.channel_id, null); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); @@ -130,63 +136,70 @@ export default class CreatePost extends React.Component { } }, (err) => { - const state = {}; - state.serverError = err.message; - state.submitting = false; - this.setState(state); - } - ); - } else { - post.channel_id = this.state.channelId; - post.filenames = this.state.previews; - - const time = Utils.getTimestamp(); - const userId = UserStore.getCurrentId(); - post.pending_post_id = `${userId}:${time}`; - post.user_id = userId; - post.create_at = time; - post.root_id = this.state.rootId; - post.parent_id = this.state.parentId; - - const channel = ChannelStore.get(this.state.channelId); - - PostStore.storePendingPost(post); - PostStore.storeDraft(channel.id, null); - this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); - - Client.createPost(post, channel, - (data) => { - AsyncClient.getPosts(); - - const member = ChannelStore.getMember(channel.id); - member.msg_count = channel.total_msg_count; - member.last_viewed_at = Date.now(); - ChannelStore.setChannelMember(member); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_POST, - post: data - }); - }, - (err) => { - const state = {}; - - if (err.message === 'Invalid RootId parameter') { - if ($('#post_deleted').length > 0) { - $('#post_deleted').modal('show'); - } - PostStore.removePendingPost(post.pending_post_id); + if (err.sendMessage) { + this.sendMessage(post); } else { - post.state = Constants.POST_FAILED; - PostStore.updatePendingPost(post); + const state = {}; + state.serverError = err.message; + state.submitting = false; + this.setState(state); } - - state.submitting = false; - this.setState(state); } ); + } else { + this.sendMessage(post); } } + sendMessage(post) { + post.channel_id = this.state.channelId; + post.filenames = this.state.previews; + + const time = Utils.getTimestamp(); + const userId = UserStore.getCurrentId(); + post.pending_post_id = `${userId}:${time}`; + post.user_id = userId; + post.create_at = time; + post.root_id = this.state.rootId; + post.parent_id = this.state.parentId; + + const channel = ChannelStore.get(this.state.channelId); + + PostStore.storePendingPost(post); + PostStore.storeDraft(channel.id, null); + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); + + Client.createPost(post, channel, + (data) => { + AsyncClient.getPosts(); + + const member = ChannelStore.getMember(channel.id); + member.msg_count = channel.total_msg_count; + member.last_viewed_at = Date.now(); + ChannelStore.setChannelMember(member); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST, + post: data + }); + }, + (err) => { + const state = {}; + + if (err.message === 'Invalid RootId parameter') { + if ($('#post_deleted').length > 0) { + $('#post_deleted').modal('show'); + } + PostStore.removePendingPost(post.pending_post_id); + } else { + post.state = Constants.POST_FAILED; + PostStore.updatePendingPost(post); + } + + state.submitting = false; + this.setState(state); + } + ); + } postMsgKeyPress(e) { if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { e.preventDefault(); @@ -195,7 +208,7 @@ export default class CreatePost extends React.Component { } const t = Date.now(); - if ((t - this.lastTime) > 5000) { + if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}}); this.lastTime = t; } @@ -240,8 +253,14 @@ export default class CreatePost extends React.Component { this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); } handleUploadError(err, clientId) { + let message = err; + if (message && typeof message !== 'string') { + // err is an AppError from the server + message = err.message; + } + if (clientId === -1) { - this.setState({serverError: err}); + this.setState({serverError: message}); } else { const draft = PostStore.getDraft(this.state.channelId); @@ -252,7 +271,7 @@ export default class CreatePost extends React.Component { PostStore.storeDraft(this.state.channelId, draft); - this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err}); + this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message}); } } handleTextDrop(text) { diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx index 940b01f8d..9c07853b7 100644 --- a/web/react/components/email_verify.jsx +++ b/web/react/components/email_verify.jsx @@ -19,10 +19,10 @@ export default class EmailVerify extends React.Component { var resend = ''; var resendConfirm = ''; if (this.props.isVerified === 'true') { - title = global.window.config.SiteName + ' Email Verified'; + title = global.window.mm_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>; } else { - title = global.window.config.SiteName + ': You are almost done'; + title = global.window.mm_config.SiteName + ': You are almost done'; body = <p>Please verify your email address. Check your inbox for an email.</p>; resend = ( <button diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx index 6311d9460..f098384aa 100644 --- a/web/react/components/error_bar.jsx +++ b/web/react/components/error_bar.jsx @@ -9,12 +9,8 @@ export default class ErrorBar extends React.Component { this.onErrorChange = this.onErrorChange.bind(this); this.handleClose = this.handleClose.bind(this); - this.prevTimer = null; this.state = ErrorStore.getLastError(); - if (this.isValidError(this.state)) { - this.prevTimer = setTimeout(this.handleClose, 10000); - } } isValidError(s) { @@ -56,16 +52,8 @@ export default class ErrorBar extends React.Component { onErrorChange() { var newState = ErrorStore.getLastError(); - if (this.prevTimer != null) { - clearInterval(this.prevTimer); - this.prevTimer = null; - } - if (newState) { this.setState(newState); - if (!this.isConnectionError(newState)) { - this.prevTimer = setTimeout(this.handleClose, 10000); - } } else { this.setState({message: null}); } diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 57cccc4e0..4d4e8390c 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -36,7 +36,7 @@ export default class FileAttachment extends React.Component { if (type === 'image') { var self = this; // Need this reference since we use the given "this" - $('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) { + $('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) { return function loader() { $(this).remove(); if (name in self.refs) { @@ -147,7 +147,7 @@ export default class FileAttachment extends React.Component { var re3 = new RegExp('\\)', 'g'); var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); + $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')'); } } removeBackgroundImage(name) { diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx index a40ed1dcf..df5deb8bc 100644 --- a/web/react/components/file_preview.jsx +++ b/web/react/components/file_preview.jsx @@ -34,7 +34,7 @@ export default class FilePreview extends React.Component { if (filename.indexOf('/api/v1/files/get') !== -1) { filename = filename.split('/api/v1/files/get')[1]; } - filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename; + filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + Utils.getSessionIndex(); if (type === 'image') { previews.push( diff --git a/web/react/components/file_upload_overlay.jsx b/web/react/components/file_upload_overlay.jsx index d991dd625..dbba00022 100644 --- a/web/react/components/file_upload_overlay.jsx +++ b/web/react/components/file_upload_overlay.jsx @@ -12,19 +12,21 @@ export default class FileUploadOverlay extends React.Component { return ( <div className={overlayClass}> - <div className='overlay__circle'> - <img - className='overlay__files' - src='/static/images/filesOverlay.png' - alt='Files' - /> - <span><i className='fa fa-upload'></i>{'Drop a file to upload it.'}</span> - <img - className='overlay__logo' - src='/static/images/logoWhite.png' - width='100' - alt='Logo' - /> + <div className='overlay__indent'> + <div className='overlay__circle'> + <img + className='overlay__files' + src='/static/images/filesOverlay.png' + alt='Files' + /> + <span><i className='fa fa-upload'></i>{'Drop a file to upload it.'}</span> + <img + className='overlay__logo' + src='/static/images/logoWhite.png' + width='100' + alt='Logo' + /> + </div> </div> </div> ); diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 90290099d..86a4b04cf 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -21,7 +21,7 @@ export default class InviteMemberModal extends React.Component { emailErrors: {}, firstNameErrors: {}, lastNameErrors: {}, - emailEnabled: global.window.config.SendEmailNotifications === 'true' + emailEnabled: global.window.mm_config.SendEmailNotifications === 'true' }; } @@ -260,6 +260,12 @@ export default class InviteMemberModal extends React.Component { var content = null; var sendButton = null; + + var sendButtonLabel = 'Send Invitation'; + if (this.state.inviteIds.length > 1) { + sendButtonLabel = 'Send Invitations'; + } + if (this.state.emailEnabled) { content = ( <div> @@ -281,7 +287,7 @@ export default class InviteMemberModal extends React.Component { onClick={this.handleSubmit} type='button' className='btn btn-primary' - >Send Invitations</button> + >{sendButtonLabel}</button> ); } else { var teamInviteLink = null; diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index c982d57ca..108735caf 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -16,7 +16,7 @@ export default class Login extends React.Component { } handleSubmit(e) { e.preventDefault(); - let state = {}; + var state = {}; const name = this.props.teamName; if (!name) { @@ -49,8 +49,7 @@ export default class Login extends React.Component { this.setState(state); Client.loginByEmail(name, email, password, - function loggedIn(data) { - UserStore.setCurrentUser(data); + () => { UserStore.setLastEmail(email); const redirect = Utils.getUrlParameter('redirect'); @@ -60,7 +59,7 @@ export default class Login extends React.Component { window.location.href = '/' + name + '/channels/town-square'; } }, - function loginFailed(err) { + (err) => { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email); return; @@ -68,7 +67,7 @@ export default class Login extends React.Component { state.serverError = err.message; this.valid = false; this.setState(state); - }.bind(this) + } ); } render() { @@ -95,7 +94,7 @@ export default class Login extends React.Component { } let loginMessage = []; - if (global.window.config.EnableSignUpWithGitLab === 'true') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { loginMessage.push( <a className='btn btn-custom-login gitlab' @@ -124,7 +123,7 @@ export default class Login extends React.Component { } let emailSignup; - if (global.window.config.EnableSignUpWithEmail === 'true') { + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { emailSignup = ( <div> <div className={'form-group' + errorClass}> @@ -186,7 +185,7 @@ export default class Login extends React.Component { <div className='signup-team__container'> <h5 className='margin--less'>Sign in to:</h5> <h2 className='signup-team__name'>{teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2> + <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2> <form onSubmit={this.handleSubmit}> {verifiedBox} <div className={'form-group' + errorClass}> diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx index 5c3695ad4..8ed94680e 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -105,7 +105,7 @@ export default class MemberListItem extends React.Component { <div className='row member-div'> <img className='post-profile-img pull-left' - src={'/api/v1/users/' + member.id + '/image?time=' + timestamp} + src={'/api/v1/users/' + member.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()} height='36' width='36' /> diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx index 3af1d3800..14db05cdb 100644 --- a/web/react/components/member_list_team_item.jsx +++ b/web/react/components/member_list_team_item.jsx @@ -169,7 +169,7 @@ export default class MemberListTeamItem extends React.Component { <div className='row member-div'> <img className='post-profile-img pull-left' - src={`/api/v1/users/${user.id}/image?time=${timestamp}`} + src={`/api/v1/users/${user.id}/image?time=${timestamp}&${Utils.getSessionIndex()}`} height='36' width='36' /> diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx index aeed724a8..050887c6f 100644 --- a/web/react/components/mention.jsx +++ b/web/react/components/mention.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); export default class Mention extends React.Component { constructor(props) { @@ -25,7 +26,7 @@ export default class Mention extends React.Component { <span> <img className='mention-img' - src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp} + src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()} /> </span> ); diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index d5b44d86b..41746d1d7 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -31,7 +31,7 @@ export default class MoreDirectChannels extends React.Component { getUsersFromStore() { const currentId = UserStore.getCurrentId(); - const profiles = UserStore.getProfiles(); + const profiles = UserStore.getActiveOnlyProfiles(); const users = []; for (const id in profiles) { @@ -169,7 +169,7 @@ export default class MoreDirectChannels extends React.Component { } return ( - <tr> + <tr key={'direct-channel-row-user' + user.id}> <td key={user.id} className='direct-channel' @@ -178,7 +178,7 @@ export default class MoreDirectChannels extends React.Component { className='profile-img pull-left' width='38' height='38' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} + src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} /> <div className='more-name'> {user.username} @@ -209,12 +209,14 @@ export default class MoreDirectChannels extends React.Component { } let users = this.state.users; - if (this.state.filter !== '') { + if (this.state.filter) { + const filter = this.state.filter.toLowerCase(); + users = users.filter((user) => { - return user.username.indexOf(this.state.filter) !== -1 || - user.first_name.indexOf(this.state.filter) !== -1 || - user.last_name.indexOf(this.state.filter) !== -1 || - user.nickname.indexOf(this.state.filter) !== -1; + return user.username.toLowerCase().indexOf(filter) !== -1 || + user.first_name.toLowerCase().indexOf(filter) !== -1 || + user.last_name.toLowerCase().indexOf(filter) !== -1 || + user.nickname.toLowerCase().indexOf(filter) !== -1; }); } diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx index 1bd23c55c..ccf8a2445 100644 --- a/web/react/components/msg_typing.jsx +++ b/web/react/components/msg_typing.jsx @@ -11,11 +11,11 @@ export default class MsgTyping extends React.Component { constructor(props) { super(props); - this.timer = null; - this.lastTime = 0; - this.onChange = this.onChange.bind(this); + this.updateTypingText = this.updateTypingText.bind(this); + this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this); + this.typingUsers = {}; this.state = { text: '' }; @@ -27,7 +27,7 @@ export default class MsgTyping extends React.Component { componentWillReceiveProps(newProps) { if (this.props.channelId !== newProps.channelId) { - this.setState({text: ''}); + this.updateTypingText(); } } @@ -36,28 +36,51 @@ export default class MsgTyping extends React.Component { } onChange(msg) { + let username = 'Someone'; if (msg.action === SocketEvents.TYPING && this.props.channelId === msg.channel_id && this.props.parentId === msg.props.parent_id) { - this.lastTime = new Date().getTime(); - - var username = 'Someone'; if (UserStore.hasProfile(msg.user_id)) { username = UserStore.getProfile(msg.user_id).username; } - this.setState({text: username + ' is typing...'}); - - if (!this.timer) { - this.timer = setInterval(function myTimer() { - if ((new Date().getTime() - this.lastTime) > 8000) { - this.setState({text: ''}); - } - }.bind(this), 3000); + if (this.typingUsers[username]) { + clearTimeout(this.typingUsers[username]); } + + this.typingUsers[username] = setTimeout(function myTimer(user) { + delete this.typingUsers[user]; + this.updateTypingText(); + }.bind(this, username), Constants.UPDATE_TYPING_MS); + + this.updateTypingText(); } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) { - this.setState({text: ''}); + if (UserStore.hasProfile(msg.user_id)) { + username = UserStore.getProfile(msg.user_id).username; + } + clearTimeout(this.typingUsers[username]); + delete this.typingUsers[username]; + this.updateTypingText(); + } + } + + updateTypingText() { + const users = Object.keys(this.typingUsers); + let text = ''; + switch (users.length) { + case 0: + text = ''; + break; + case 1: + text = users[0] + ' is typing...'; + break; + default: + const last = users.pop(); + text = users.join(', ') + ' and ' + last + ' are typing...'; + break; } + + this.setState({text}); } render() { diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 1cb13bbe5..2b68645e5 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -152,7 +152,7 @@ export default class NavbarDropdown extends React.Component { sysAdminLink = ( <li> <a - href='/admin_console' + href={'/admin_console?' + Utils.getSessionIndex()} > {'System Console'} </a> @@ -178,7 +178,7 @@ export default class NavbarDropdown extends React.Component { }); } - if (global.window.config.EnableTeamCreation === 'true') { + if (global.window.mm_config.EnableTeamCreation === 'true') { teams.push( <li key='newTeam_li'> <a diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx index 217f1b393..b452c40b7 100644 --- a/web/react/components/password_reset_form.jsx +++ b/web/react/components/password_reset_form.jsx @@ -61,7 +61,7 @@ export default class PasswordResetForm extends React.Component { <div className='signup-team__container'> <h3>Password Reset</h3> <form onSubmit={this.handlePasswordReset}> - <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.config.SiteName + ' account.'}</p> + <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}</p> <div className={formClass}> <input type='password' diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 64d6776b4..dedac8951 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -120,6 +120,10 @@ export default class Post extends React.Component { var parentPost = this.props.parentPost; var posts = this.props.posts; + if (!post.props) { + post.props = {}; + } + var type = 'Post'; if (post.root_id && post.root_id.length > 0) { type = 'Comment'; @@ -140,7 +144,7 @@ export default class Post extends React.Component { } var currentUserCss = ''; - if (UserStore.getCurrentId() === post.user_id) { + if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) { currentUserCss = 'current--user'; } @@ -158,8 +162,8 @@ export default class Post extends React.Component { var profilePic = null; if (!this.props.hideProfilePic) { - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; - if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') { + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); + if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; } @@ -200,6 +204,7 @@ export default class Post extends React.Component { posts={posts} handleCommentClick={this.handleCommentClick} retryPost={this.retryPost} + resize={this.props.resize} /> <PostInfo ref='info' @@ -223,5 +228,6 @@ Post.propTypes = { sameUser: React.PropTypes.bool, sameRoot: React.PropTypes.bool, hideProfilePic: React.PropTypes.bool, - isLastComment: React.PropTypes.bool + isLastComment: React.PropTypes.bool, + resize: React.PropTypes.func }; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index fb838b736..45eae8c6a 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -13,8 +13,12 @@ export default class PostBody extends React.Component { super(props); this.receivedYoutubeData = false; + this.isGifLoading = false; this.parseEmojis = this.parseEmojis.bind(this); + this.createEmbed = this.createEmbed.bind(this); + this.createGifEmbed = this.createGifEmbed.bind(this); + this.loadGif = this.loadGif.bind(this); this.createYoutubeEmbed = this.createYoutubeEmbed.bind(this); const linkData = Utils.extractLinks(this.props.post.message); @@ -46,6 +50,7 @@ export default class PostBody extends React.Component { componentDidUpdate() { this.parseEmojis(); + this.props.resize(); } componentWillReceiveProps(nextProps) { @@ -53,6 +58,52 @@ export default class PostBody extends React.Component { this.setState({links: linkData.links, message: linkData.text}); } + createEmbed(link) { + let embed = this.createYoutubeEmbed(link); + + if (embed != null) { + return embed; + } + + embed = this.createGifEmbed(link); + + return embed; + } + + loadGif(src) { + if (this.isGifLoading) { + return; + } + + this.isGifLoading = true; + + const gif = new Image(); + gif.src = src; + gif.onload = ( + () => { + this.setState({gifLoaded: true}); + } + ); + } + + createGifEmbed(link) { + if (link.substring(link.length - 4) !== '.gif') { + return null; + } + + if (!this.state.gifLoaded) { + this.loadGif(link); + return null; + } + + return ( + <img + className='gif-div' + src={link} + /> + ); + } + handleYoutubeTime(link) { const timeRegex = /[\\?&]t=([0-9hms]+)/; @@ -119,12 +170,12 @@ export default class PostBody extends React.Component { this.setState({youtubeTitle: metadata.title}); } - if (global.window.config.GoogleDeveloperKey && !this.receivedYoutubeData) { + if (global.window.mm_config.GoogleDeveloperKey && !this.receivedYoutubeData) { $.ajax({ async: true, url: 'https://www.googleapis.com/youtube/v3/videos', type: 'GET', - data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey}, + data: {part: 'snippet', id: youtubeId, key: global.window.mm_config.GoogleDeveloperKey}, success: success.bind(this) }); } @@ -247,7 +298,7 @@ export default class PostBody extends React.Component { let embed; if (filenames.length === 0 && this.state.links) { - embed = this.createYoutubeEmbed(this.state.links[0]); + embed = this.createEmbed(this.state.links[0]); } let fileAttachmentHolder = ''; @@ -287,5 +338,6 @@ PostBody.propTypes = { post: React.PropTypes.object.isRequired, parentPost: React.PropTypes.object, retryPost: React.PropTypes.func.isRequired, - handleCommentClick: React.PropTypes.func.isRequired + handleCommentClick: React.PropTypes.func.isRequired, + resize: React.PropTypes.func.isRequired }; diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 0ba5ce6b5..45e60c767 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -16,7 +16,7 @@ export default class PostHeader extends React.Component { let botIndicator; if (post.props && post.props.from_webhook) { - if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') { + if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( <UserProfile userId={post.user_id} diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index b7df483e9..3ceef478c 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -42,6 +42,7 @@ export default class PostList extends React.Component { this.deactivate = this.deactivate.bind(this); this.handleResize = this.handleResize.bind(this); this.resizePostList = this.resizePostList.bind(this); + this.updateScroll = this.updateScroll.bind(this); const state = this.getStateFromStores(props.channelId); state.numToDisplay = Constants.POST_CHUNK_SIZE; @@ -205,9 +206,10 @@ export default class PostList extends React.Component { this.scrollToBottom(); // there's a new post and - // it's by the user and not a comment + // it's by the user (and not from their webhook) and not a comment } else if (isNewPost && userId === firstPost.user_id && + !firstPost.props.from_webhook && !Utils.isComment(firstPost)) { this.scrollToBottom(true); @@ -237,6 +239,11 @@ export default class PostList extends React.Component { this.deactivate(); } } + updateScroll() { + if (!this.scrolled) { + this.scrollToBottom(); + } + } handleResize() { this.setState({ windowHeight: Utils.windowHeight() @@ -334,7 +341,7 @@ export default class PostList extends React.Component { <div className='post-profile-img__container channel-intro-img'> <img className='post-profile-img' - src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at} + src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()} height='50' width='50' /> @@ -550,6 +557,7 @@ export default class PostList extends React.Component { posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} + resize={this.updateScroll} /> ); diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index d3a4cfaeb..cfff04fa2 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -199,7 +199,7 @@ export default class RhsComment extends React.Component { <div className='post-profile-img__container'> <img className='post-profile-img' - src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} + src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()} height='36' width='36' /> diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index a9f1fcd30..deef389e2 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -121,7 +121,7 @@ export default class RhsRootPost extends React.Component { let botIndicator; if (post.props && post.props.from_webhook) { - if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') { + if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( <UserProfile userId={post.user_id} @@ -134,8 +134,8 @@ export default class RhsRootPost extends React.Component { botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>; } - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; - if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') { + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); + if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; } diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx new file mode 100644 index 000000000..03c7b894c --- /dev/null +++ b/web/react/components/search_autocomplete.jsx @@ -0,0 +1,249 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const ChannelStore = require('../stores/channel_store.jsx'); +const KeyCodes = require('../utils/constants.jsx').KeyCodes; +const UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); + +const patterns = new Map([ + ['channels', /\b(?:in|channel):\s*(\S*)$/i], + ['users', /\bfrom:\s*(\S*)$/i] +]); + +export default class SearchAutocomplete extends React.Component { + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + this.handleDocumentClick = this.handleDocumentClick.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + + this.completeWord = this.completeWord.bind(this); + this.updateSuggestions = this.updateSuggestions.bind(this); + + this.state = { + show: false, + mode: '', + filter: '', + selection: 0, + suggestions: new Map() + }; + } + + componentDidMount() { + $(document).on('click', this.handleDocumentClick); + } + + componentWillUnmount() { + $(document).off('click', this.handleDocumentClick); + } + + handleClick(value) { + this.completeWord(value); + } + + handleDocumentClick(e) { + const container = $(ReactDOM.findDOMNode(this.refs.container)); + + if (!(container.is(e.target) || container.has(e.target).length > 0)) { + this.setState({ + show: false + }); + } + } + + handleInputChange(textbox, text) { + const caret = Utils.getCaretPosition(textbox); + const preText = text.substring(0, caret); + + let mode = ''; + let filter = ''; + for (const [modeForPattern, pattern] of patterns) { + const result = pattern.exec(preText); + + if (result) { + mode = modeForPattern; + filter = result[1]; + break; + } + } + + if (mode !== this.state.mode || filter !== this.state.filter) { + this.updateSuggestions(mode, filter); + } + + this.setState({ + mode, + filter, + show: mode || filter + }); + } + + handleKeyDown(e) { + if (!this.state.show || this.state.suggestions.length === 0) { + return; + } + + if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) { + e.preventDefault(); + + let selection = this.state.selection; + + if (e.which === KeyCodes.UP) { + selection -= 1; + } else { + selection += 1; + } + + if (selection >= 0 && selection < this.state.suggestions.length) { + this.setState({ + selection + }); + } + } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) { + e.preventDefault(); + + this.completeSelectedWord(); + } + } + + completeSelectedWord() { + if (this.state.mode === 'channels') { + this.completeWord(this.state.suggestions[this.state.selection].name); + } else if (this.state.mode === 'users') { + this.completeWord(this.state.suggestions[this.state.selection].username); + } + } + + completeWord(value) { + // add a space so that anything else typed doesn't interfere with the search flag + this.props.completeWord(this.state.filter, value + ' '); + + this.setState({ + show: false, + mode: '', + filter: '', + selection: 0 + }); + } + + updateSuggestions(mode, filter) { + let suggestions = []; + + if (mode === 'channels') { + let channels = ChannelStore.getAll(); + + if (filter) { + channels = channels.filter((channel) => channel.name.startsWith(filter)); + } + + channels.sort((a, b) => a.name.localeCompare(b.name)); + + suggestions = channels; + } else if (mode === 'users') { + let users = UserStore.getActiveOnlyProfileList(); + + if (filter) { + users = users.filter((user) => user.username.startsWith(filter)); + } + + users.sort((a, b) => a.username.localeCompare(b.username)); + + suggestions = users; + } + + let selection = this.state.selection; + + // keep the same user/channel selected if it's still visible as a suggestion + if (selection > 0 && this.state.suggestions.length > 0) { + // we can't just use indexOf to find if the selection is still in the list since they are different javascript objects + const currentSelectionId = this.state.suggestions[selection].id; + let found = false; + + for (let i = 0; i < suggestions.length; i++) { + if (suggestions[i].id === currentSelectionId) { + selection = i; + found = true; + + break; + } + } + + if (!found) { + selection = 0; + } + } else { + selection = 0; + } + + this.setState({ + suggestions, + selection + }); + } + + render() { + if (!this.state.show || this.state.suggestions.length === 0) { + return null; + } + + let suggestions = []; + + if (this.state.mode === 'channels') { + suggestions = this.state.suggestions.map((channel, index) => { + let className = 'search-autocomplete__channel'; + if (this.state.selection === index) { + className += ' selected'; + } + + return ( + <div + key={channel.name} + ref={channel.name} + onClick={this.handleClick.bind(this, channel.name)} + className={className} + > + {channel.name} + </div> + ); + }); + } else if (this.state.mode === 'users') { + suggestions = this.state.suggestions.map((user, index) => { + let className = 'search-autocomplete__user'; + if (this.state.selection === index) { + className += ' selected'; + } + + return ( + <div + key={user.username} + ref={user.username} + onClick={this.handleClick.bind(this, user.username)} + className={className} + > + <img + className='profile-img' + src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at} + /> + {user.username} + </div> + ); + }); + } + + return ( + <div + ref='container' + className='search-autocomplete' + > + {suggestions} + </div> + ); + } +} + +SearchAutocomplete.propTypes = { + completeWord: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index 2e9764bd9..0da43e8cd 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -8,6 +8,8 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; +var Popover = ReactBootstrap.Popover; +var SearchAutocomplete = require('./search_autocomplete.jsx'); export default class SearchBar extends React.Component { constructor() { @@ -15,11 +17,17 @@ export default class SearchBar extends React.Component { this.mounted = false; this.onListenerChange = this.onListenerChange.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); this.handleUserInput = this.handleUserInput.bind(this); + this.handleUserFocus = this.handleUserFocus.bind(this); + this.handleUserBlur = this.handleUserBlur.bind(this); this.performSearch = this.performSearch.bind(this); this.handleSubmit = this.handleSubmit.bind(this); + this.completeWord = this.completeWord.bind(this); - this.state = this.getSearchTermStateFromStores(); + const state = this.getSearchTermStateFromStores(); + state.focused = false; + this.state = state; } getSearchTermStateFromStores() { var term = PostStore.getSearchTerm() || ''; @@ -69,25 +77,44 @@ export default class SearchBar extends React.Component { results: null }); } + handleKeyDown(e) { + if (this.refs.autocomplete) { + this.refs.autocomplete.handleKeyDown(e); + } + } handleUserInput(e) { var term = e.target.value; PostStore.storeSearchTerm(term); PostStore.emitSearchTermChange(false); this.setState({searchTerm: term}); + + this.refs.autocomplete.handleInputChange(e.target, term); } handleMouseInput(e) { e.preventDefault(); } + handleUserBlur() { + this.setState({focused: false}); + } handleUserFocus(e) { e.target.select(); $('.search-bar__container').addClass('focused'); + + this.setState({focused: true}); } performSearch(terms, isMentionSearch) { if (terms.length) { this.setState({isSearching: true}); + + // append * if not present + let searchTerms = terms; + if (searchTerms.search(/\*\s*$/) === -1) { + searchTerms = searchTerms + '*'; + } + client.search( - terms, - function success(data) { + searchTerms, + (data) => { this.setState({isSearching: false}); if (utils.isMobile()) { ReactDOM.findDOMNode(this.refs.search).value = ''; @@ -98,11 +125,11 @@ export default class SearchBar extends React.Component { results: data, is_mention_search: isMentionSearch }); - }.bind(this), - function error(err) { + }, + (err) => { this.setState({isSearching: false}); AsyncClient.dispatchError(err, 'search'); - }.bind(this) + } ); } } @@ -110,11 +137,35 @@ export default class SearchBar extends React.Component { e.preventDefault(); this.performSearch(this.state.searchTerm.trim()); } + + completeWord(partialWord, word) { + const textbox = ReactDOM.findDOMNode(this.refs.search); + let text = textbox.value; + + const caret = utils.getCaretPosition(textbox); + const preText = text.substring(0, caret - partialWord.length); + const postText = text.substring(caret); + text = preText + word + postText; + + textbox.value = text; + utils.setCaretPosition(textbox, preText.length + word.length); + + PostStore.storeSearchTerm(text); + PostStore.emitSearchTermChange(false); + this.setState({searchTerm: text}); + } + render() { var isSearching = null; if (this.state.isSearching) { isSearching = <span className={'glyphicon glyphicon-refresh glyphicon-refresh-animate'}></span>; } + + let helpClass = 'search-help-popover'; + if (!this.state.searchTerm && this.state.focused) { + helpClass += ' visible'; + } + return ( <div> <div @@ -127,12 +178,13 @@ export default class SearchBar extends React.Component { className='search__clear' onClick={this.clearFocus} > - Cancel + {'Cancel'} </span> <form role='form' className='search__form relative-div' onSubmit={this.handleSubmit} + style={{overflow: 'visible'}} > <span className='glyphicon glyphicon-search sidebar__search-icon' /> <input @@ -142,10 +194,31 @@ export default class SearchBar extends React.Component { placeholder='Search' value={this.state.searchTerm} onFocus={this.handleUserFocus} + onBlur={this.handleUserBlur} onChange={this.handleUserInput} + onKeyDown={this.handleKeyDown} onMouseUp={this.handleMouseInput} /> {isSearching} + <SearchAutocomplete + ref='autocomplete' + completeWord={this.completeWord} + /> + <Popover + id='searchbar-help-popup' + placement='bottom' + className={helpClass} + > + <h4>{'Search Options'}</h4> + <ul> + <li> + <span>{'Use '}</span><b>{'"quotation marks"'}</b><span>{' to search for phrases'}</span> + </li> + <li> + <span>{'Use '}</span><b>{'from:'}</b><span>{' to find posts from specific users and '}</span><b>{'in:'}</b><span>{' to find posts in specific channels'}</span> + </li> + </ul> + </Popover> </form> </div> ); diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 75d2e7a45..d212e47a3 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -77,7 +77,7 @@ export default class SearchResultsItem extends React.Component { <div className='post-profile-img__container'> <img className='post-profile-img' - src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp} + src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()} height='36' width='36' /> diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 4f0fe3ed0..774f98a43 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -36,7 +36,7 @@ export default class SettingItemMax extends React.Component { if (this.props.width === 'full') { widthClass = 'col-sm-12'; } else { - widthClass = 'col-sm-9 col-sm-offset-3'; + widthClass = 'col-sm-10 col-sm-offset-2'; } return ( diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx index 2f577fe39..b6bcb13a6 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -79,7 +79,7 @@ export default class SettingPicture extends React.Component { >Save</a> ); } - var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + global.window.config.ProfileWidth + 'px in width and ' + global.window.config.ProfileHeight + 'px height.'; + var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + global.window.mm_config.ProfileWidth + 'px in width and ' + global.window.mm_config.ProfileHeight + 'px height.'; var self = this; return ( diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index 66568e1c8..4af46c35a 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -2,6 +2,10 @@ // See License.txt for license information. export default class SettingsSidebar extends React.Component { + componentDidUpdate() { + $('.settings-modal').find('.modal-body').scrollTop(0); + $('.settings-modal').find('.modal-body').perfectScrollbar('update'); + } constructor(props) { super(props); diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index d1fe37300..5cb6d168b 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -40,6 +40,9 @@ export default class Sidebar extends React.Component { this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this); this.createChannelElement = this.createChannelElement.bind(this); + this.updateTitle = this.updateTitle.bind(this); + this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this); + this.getUnreadCount = this.getUnreadCount.bind(this); this.isLeaving = new Map(); @@ -48,8 +51,45 @@ export default class Sidebar extends React.Component { state.showDirectChannelsModal = false; state.loadingDMChannel = -1; state.windowWidth = Utils.windowWidth(); - this.state = state; + + this.unreadCountPerChannel = {}; + this.setUnreadCountPerChannel(); + } + setUnreadCountPerChannel() { + const channels = ChannelStore.getAll(); + const members = ChannelStore.getAllMembers(); + const channelUnreadCounts = {}; + + channels.forEach((ch) => { + const chMember = members[ch.id]; + let chMentionCount = chMember.mention_count; + let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount; + + if (ch.type === 'D') { + chMentionCount = chUnreadCount; + chUnreadCount = 0; + } + + channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount}; + }); + + this.unreadCountPerChannel = channelUnreadCounts; + } + getUnreadCount(channelId) { + let mentions = 0; + let msgs = 0; + + if (channelId) { + return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions}; + } + + Object.keys(this.unreadCountPerChannel).forEach((chId) => { + msgs += this.unreadCountPerChannel[chId].msgs; + mentions += this.unreadCountPerChannel[chId].mentions; + }); + + return {msgs, mentions}; } getStateFromStores() { const members = ChannelStore.getAllMembers(); @@ -183,8 +223,8 @@ export default class Sidebar extends React.Component { const channel = ChannelStore.getCurrent(); if (channel) { let currentSiteName = ''; - if (global.window.config.SiteName != null) { - currentSiteName = global.window.config.SiteName; + if (global.window.mm_config.SiteName != null) { + currentSiteName = global.window.mm_config.SiteName; } let currentChannelName = channel.display_name; @@ -192,7 +232,10 @@ export default class Sidebar extends React.Component { currentChannelName = Utils.getDirectTeammate(channel.id).username; } - document.title = currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName; + const unread = this.getUnreadCount(); + const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : ''; + const unreadTitle = unread.msgs > 0 ? '* ' : ''; + document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName; } } onScroll() { @@ -273,6 +316,7 @@ export default class Sidebar extends React.Component { var members = this.state.members; var activeId = this.state.activeId; var channelMember = members[channel.id]; + var unreadCount = this.getUnreadCount(channel.id); var msgCount; var linkClass = ''; @@ -284,7 +328,7 @@ export default class Sidebar extends React.Component { var unread = false; if (channelMember) { - msgCount = channel.total_msg_count - channelMember.msg_count; + msgCount = unreadCount.msgs + unreadCount.mentions; unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0; } @@ -301,16 +345,8 @@ export default class Sidebar extends React.Component { var badge = null; if (channelMember) { - if (channel.type === 'D') { - // direct message channels show badges for any number of unread posts - msgCount = channel.total_msg_count - channelMember.msg_count; - if (msgCount > 0) { - badge = <span className='badge pull-right small'>{msgCount}</span>; - this.badgesActive = true; - } - } else if (channelMember.mention_count > 0) { - // public and private channels only show badges for mentions - badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>; + if (unreadCount.mentions) { + badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>; this.badgesActive = true; } } else if (this.state.loadingDMChannel === index && channel.type === 'D') { @@ -434,6 +470,8 @@ export default class Sidebar extends React.Component { render() { this.badgesActive = false; + this.setUnreadCountPerChannel(); + // keep track of the first and last unread channels so we can use them to set the unread indicators this.firstUnreadChannel = null; this.lastUnreadChannel = null; diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index c3709bc0a..de28a8374 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -3,6 +3,7 @@ var NavbarDropdown = require('./navbar_dropdown.jsx'); var UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); export default class SidebarHeader extends React.Component { constructor(props) { @@ -32,7 +33,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( <img className='user__picture' - src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} + src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()} /> ); } @@ -61,7 +62,7 @@ export default class SidebarHeader extends React.Component { } SidebarHeader.defaultProps = { - teamDisplayName: global.window.config.SiteName, + teamDisplayName: global.window.mm_config.SiteName, teamType: '' }; SidebarHeader.propTypes = { diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index ac101d631..fddc98c9d 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -84,7 +84,7 @@ export default class SidebarRightMenu extends React.Component { consoleLink = ( <li> <a - href='/admin_console' + href={'/admin_console?' + utils.getSessionIndex()} > <i className='glyphicon glyphicon-wrench'></i>System Console</a> </li> @@ -92,8 +92,8 @@ export default class SidebarRightMenu extends React.Component { } var siteName = ''; - if (global.window.config.SiteName != null) { - siteName = global.window.config.SiteName; + if (global.window.mm_config.SiteName != null) { + siteName = global.window.mm_config.SiteName; } var teamDisplayName = siteName; if (this.props.teamDisplayName) { diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 48cf2c73c..1858703ef 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -14,19 +14,19 @@ export default class TeamSignUp extends React.Component { var count = 0; - if (global.window.config.EnableSignUpWithEmail === 'true') { + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { count = count + 1; } - if (global.window.config.EnableSignUpWithGitLab === 'true') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { count = count + 1; } if (count > 1) { this.state = {page: 'choose'}; - } else if (global.window.config.EnableSignUpWithEmail === 'true') { + } else if (global.window.mm_config.EnableSignUpWithEmail === 'true') { this.state = {page: 'email'}; - } else if (global.window.config.EnableSignUpWithGitLab === 'true') { + } else if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { this.state = {page: 'gitlab'}; } } diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index f74c29d27..d70ea5065 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -82,30 +82,29 @@ export default class SignupUserComplete extends React.Component { }); client.createUser(user, this.props.data, this.props.hash, - function createUserSuccess() { + () => { client.track('signup', 'signup_user_02_complete'); client.loginByEmail(this.props.teamName, user.email, user.password, - function emailLoginSuccess(data) { + () => { UserStore.setLastEmail(user.email); - UserStore.setCurrentUser(data); if (this.props.hash > 0) { BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); } window.location.href = '/' + this.props.teamName + '/channels/town-square'; - }.bind(this), - function emailLoginFailure(err) { + }, + (err) => { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.props.teamName); } else { this.setState({serverError: err.message}); } - }.bind(this) + } ); - }.bind(this), - function createUserFailure(err) { + }, + (err) => { this.setState({serverError: err.message}); - }.bind(this) + } ); } render() { @@ -149,7 +148,7 @@ export default class SignupUserComplete extends React.Component { // set up the email entry and hide it if an email was provided var yourEmailIs = ''; if (this.state.user.email) { - yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.config.SiteName}.</span>; + yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.mm_config.SiteName}.</span>; } var emailContainerStyle = 'margin--extra'; @@ -177,7 +176,7 @@ export default class SignupUserComplete extends React.Component { ); var signupMessage = []; - if (global.window.config.EnableSignUpWithGitLab === 'true') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { signupMessage.push( <a className='btn btn-custom-login gitlab' @@ -190,7 +189,7 @@ export default class SignupUserComplete extends React.Component { } var emailSignup; - if (global.window.config.EnableSignUpWithEmail === 'true') { + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { emailSignup = ( <div> <div className='inner__content'> @@ -259,7 +258,7 @@ export default class SignupUserComplete extends React.Component { /> <h5 className='margin--less'>Welcome to:</h5> <h2 className='signup-team__name'>{this.props.teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2> + <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2> <h4 className='color--light'>Let's create your account</h4> {signupMessage} {emailSignup} diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index b55373dba..5c5995020 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -19,6 +19,7 @@ export default class TeamSettingsModal extends React.Component { componentDidMount() { $('body').on('click', '.modal-back', function handleBackClick() { $(this).closest('.modal-dialog').removeClass('display--content'); + $(this).closest('.modal-dialog').find('.settings-table .nav li.active').removeClass('active'); }); $('body').on('click', '.modal-header .close', () => { setTimeout(() => { diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx index fa898f63c..0254c8b4e 100644 --- a/web/react/components/team_signup_choose_auth.jsx +++ b/web/react/components/team_signup_choose_auth.jsx @@ -8,7 +8,7 @@ export default class ChooseAuthPage extends React.Component { } render() { var buttons = []; - if (global.window.config.EnableSignUpWithGitLab === 'true') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { buttons.push( <a className='btn btn-custom-login gitlab btn-full' @@ -26,7 +26,7 @@ export default class ChooseAuthPage extends React.Component { ); } - if (global.window.config.EnableSignUpWithEmail === 'true') { + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { buttons.push( <a className='btn btn-custom-login email btn-full' diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx index daa898b53..67fd686bc 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/team_signup_password_page.jsx @@ -36,15 +36,14 @@ export default class TeamSignupPasswordPage extends React.Component { delete teamSignup.wizard; Client.createTeamFromSignup(teamSignup, - function success() { + () => { Client.track('signup', 'signup_team_08_complete'); var props = this.props; Client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, - function loginSuccess(data) { + () => { UserStore.setLastEmail(teamSignup.team.email); - UserStore.setCurrentUser(data); if (this.props.hash > 0) { BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); } @@ -54,21 +53,21 @@ export default class TeamSignupPasswordPage extends React.Component { props.updateParent(props.state, true); window.location.href = '/' + teamSignup.team.name + '/channels/town-square'; - }.bind(this), - function loginFail(err) { + }, + (err) => { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name); } else { this.setState({serverError: err.message}); $('#finish-button').button('reset'); } - }.bind(this) + } ); - }.bind(this), - function error(err) { + }, + (err) => { this.setState({serverError: err.message}); $('#finish-button').button('reset'); - }.bind(this) + } ); } render() { @@ -129,7 +128,7 @@ export default class TeamSignupPasswordPage extends React.Component { Finish </button> </div> - <p>By proceeding to create your account and use {global.window.config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.config.SiteName}.</p> + <p>By proceeding to create your account and use {global.window.mm_config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.mm_config.SiteName}.</p> <div className='margin--extra'> <a href='#' diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx index e7bc0272d..7b4db8fae 100644 --- a/web/react/components/team_signup_send_invites_page.jsx +++ b/web/react/components/team_signup_send_invites_page.jsx @@ -13,13 +13,8 @@ export default class TeamSignupSendInvitesPage extends React.Component { this.submitSkip = this.submitSkip.bind(this); this.keySubmit = this.keySubmit.bind(this); this.state = { - emailEnabled: global.window.config.SendEmailNotifications === 'true' + emailEnabled: global.window.mm_config.SendEmailNotifications === 'true' }; - - if (!this.state.emailEnabled) { - this.props.state.wizard = 'username'; - this.props.updateParent(this.props.state); - } } submitBack(e) { e.preventDefault(); diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx index 75ec2dfd9..02d5cab8e 100644 --- a/web/react/components/team_signup_url_page.jsx +++ b/web/react/components/team_signup_url_page.jsx @@ -40,7 +40,7 @@ export default class TeamSignupUrlPage extends React.Component { return; } - if (global.window.config.RestrictTeamNames === 'true') { + if (global.window.mm_config.RestrictTeamNames === 'true') { for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) { if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) { this.setState({nameError: 'URL is taken or contains a reserved word'}); @@ -54,7 +54,11 @@ export default class TeamSignupUrlPage extends React.Component { if (data) { this.setState({nameError: 'This URL is unavailable. Please try another.'}); } else { - this.props.state.wizard = 'send_invites'; + if (global.window.mm_config.SendEmailNotifications === 'true') { + this.props.state.wizard = 'send_invites'; + } else { + this.props.state.wizard = 'username'; + } this.props.state.team.type = 'O'; this.props.state.team.name = name; diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx index 21e76e2b8..d8d0dbf2c 100644 --- a/web/react/components/team_signup_username_page.jsx +++ b/web/react/components/team_signup_username_page.jsx @@ -15,7 +15,7 @@ export default class TeamSignupUsernamePage extends React.Component { } submitBack(e) { e.preventDefault(); - if (global.window.config.SendEmailNotifications === 'true') { + if (global.window.mm_config.SendEmailNotifications === 'true') { this.props.state.wizard = 'send_invites'; } else { this.props.state.wizard = 'team_url'; diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx index 1e9d8df0a..9448413ce 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/team_signup_welcome_page.jsx @@ -104,21 +104,19 @@ export default class TeamSignupWelcomePage extends React.Component { return ( <div> - <p> - <img - className='signup-team-logo' - src='/static/images/logo.png' - /> - <h3 className='sub-heading'>Welcome to:</h3> - <h1 className='margin--top-none'>{global.window.config.SiteName}</h1> - </p> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h3 className='sub-heading'>Welcome to:</h3> + <h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1> <p className='margin--less'>Let's set up your new team</p> - <p> + <div> Please confirm your email address:<br /> <div className='inner__content'> <div className='block--gray'>{this.props.state.team.email}</div> </div> - </p> + </div> <p className='margin--extra color--light'> Your account will administer the new team site. <br /> You can add other administrators later. diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 540331663..c4402ae23 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -67,13 +67,14 @@ export default class UserProfile extends React.Component { dataContent.push( <img className='user-popover__image' - src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at} + src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex()} height='128' width='128' key='user-popover-image' /> ); - if (!global.window.config.ShowEmailAddress === 'true') { + + if (!global.window.mm_config.ShowEmailAddress === 'true') { dataContent.push( <div className='text-nowrap' diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx new file mode 100644 index 000000000..eef4b24ba --- /dev/null +++ b/web/react/components/user_settings/code_theme_chooser.jsx @@ -0,0 +1,55 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +var Constants = require('../../utils/constants.jsx'); + +export default class CodeThemeChooser extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { + const theme = this.props.theme; + + const premadeThemes = []; + for (const k in Constants.CODE_THEMES) { + if (Constants.CODE_THEMES.hasOwnProperty(k)) { + let activeClass = ''; + if (k === theme.codeTheme) { + activeClass = 'active'; + } + + premadeThemes.push( + <div + className='col-xs-6 col-sm-3 premade-themes' + key={'premade-theme-key' + k} + > + <div + className={activeClass} + onClick={() => this.props.updateTheme(k)} + > + <label> + <img + className='img-responsive' + src={'/static/images/themes/code_themes/' + k + '.png'} + /> + <div className='theme-label'>{Constants.CODE_THEMES[k]}</div> + </label> + </div> + </div> + ); + } + } + + return ( + <div className='row'> + {premadeThemes} + </div> + ); + } +} + +CodeThemeChooser.propTypes = { + theme: React.PropTypes.object.isRequired, + updateTheme: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx index f5a2774a0..6b8c09718 100644 --- a/web/react/components/user_settings/manage_incoming_hooks.jsx +++ b/web/react/components/user_settings/manage_incoming_hooks.jsx @@ -96,7 +96,14 @@ export default class ManageIncomingHooks extends React.Component { const options = []; channels.forEach((channel) => { if (channel.type !== Constants.DM_CHANNEL) { - options.push(<option value={channel.id}>{channel.name}</option>); + options.push( + <option + key={'incoming-hook' + channel.id} + value={channel.id} + > + {channel.display_name} + </option> + ); } }); @@ -108,26 +115,30 @@ export default class ManageIncomingHooks extends React.Component { const hooks = []; this.state.hooks.forEach((hook) => { const c = ChannelStore.get(hook.channel_id); - hooks.push( - <div className='font--small'> - <div className='padding-top x2 divider-light'></div> - <div className='padding-top x2'> - <strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span> - </div> - <div className='padding-top'> - <strong>{'Channel: '}</strong>{c.name} - </div> - <div className='padding-top'> + if (c) { + hooks.push( + <div + key={hook.id} + className='webhook__item' + > + <div className='padding-top x2 webhook__url'> + <strong>{'URL: '}</strong> + <span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span> + </div> + <div className='padding-top'> + <strong>{'Channel: '}</strong>{c.display_name} + </div> <a - className={'text-danger'} + className={'webhook__remove'} href='#' onClick={this.removeHook.bind(this, hook.id)} > - {'Remove'} + <span aria-hidden='true'>{'×'}</span> </a> + <div className='padding-top x2 divider-light'></div> </div> - </div> - ); + ); + } }); let displayHooks; @@ -136,35 +147,38 @@ export default class ManageIncomingHooks extends React.Component { } else if (hooks.length > 0) { displayHooks = hooks; } else { - displayHooks = <label>{': None'}</label>; + displayHooks = <div className='padding-top x2'>{'None'}</div>; } const existingHooks = ( - <div className='padding-top x2'> + <div className='webhooks__container'> <label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label> - {displayHooks} + <div className='padding-top divider-light'></div> + <div className='webhooks__list'> + {displayHooks} + </div> </div> ); return ( <div key='addIncomingHook'> {'Create webhook URLs for use in external integrations. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'} - <br/> - <br/> - <label className='control-label'>{'Add a new incoming webhook'}</label> - <div className='padding-top'> - <select - ref='channelName' - className='form-control' - value={this.state.channelId} - onChange={this.updateChannelId} - > - {options} - </select> - {serverError} - <div className='padding-top'> + <label className='control-label padding-top x2'>{'Add a new incoming webhook'}</label> + <div className='row padding-top'> + <div className='col-sm-10 padding-bottom'> + <select + ref='channelName' + className='form-control' + value={this.state.channelId} + onChange={this.updateChannelId} + > + {options} + </select> + {serverError} + </div> + <div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'> <a - className={'btn btn-sm btn-primary' + disableButton} + className={'btn form-control no-padding btn-sm btn-primary' + disableButton} href='#' onClick={this.addNewHook} > diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index e83ae3bd6..6e9b2205d 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -6,6 +6,7 @@ var Constants = require('../../utils/constants.jsx'); var ChannelStore = require('../../stores/channel_store.jsx'); var LoadingScreen = require('../loading_screen.jsx'); + export default class ManageOutgoingHooks extends React.Component { constructor() { super(); @@ -128,21 +129,42 @@ export default class ManageOutgoingHooks extends React.Component { } const channels = ChannelStore.getAll(); - const options = [<option value=''>{'--- Select a channel ---'}</option>]; + const options = []; + options.push( + <option + key='select-channel' + value='' + > + {'--- Select a channel ---'} + </option> + ); + channels.forEach((channel) => { if (channel.type === Constants.OPEN_CHANNEL) { - options.push(<option value={channel.id}>{channel.name}</option>); + options.push( + <option + key={'outgoing-hook' + channel.id} + value={channel.id} + > + {channel.display_name} + </option> + ); } }); const hooks = []; this.state.hooks.forEach((hook) => { const c = ChannelStore.get(hook.channel_id); + + if (!c && hook.channel_id && hook.channel_id.length !== 0) { + return; + } + let channelDiv; if (c) { channelDiv = ( <div className='padding-top'> - <strong>{'Channel: '}</strong>{c.name} + <strong>{'Channel: '}</strong>{c.display_name} </div> ); } @@ -157,8 +179,10 @@ export default class ManageOutgoingHooks extends React.Component { } hooks.push( - <div className='font--small'> - <div className='padding-top x2 divider-light'></div> + <div + key={hook.id} + className='webhook__item' + > <div className='padding-top x2'> <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span> </div> @@ -175,15 +199,15 @@ export default class ManageOutgoingHooks extends React.Component { > {'Regen Token'} </a> - <span>{' - '}</span> <a - className='text-danger' + className='webhook__remove' href='#' onClick={this.removeHook.bind(this, hook.id)} > - {'Remove'} + <span aria-hidden='true'>{'×'}</span> </a> </div> + <div className='padding-top x2 divider-light'></div> </div> ); }); @@ -194,13 +218,16 @@ export default class ManageOutgoingHooks extends React.Component { } else if (hooks.length > 0) { displayHooks = hooks; } else { - displayHooks = <label>{': None'}</label>; + displayHooks = <div className='padding-top x2'>{'None'}</div>; } const existingHooks = ( - <div className='padding-top x2'> + <div className='webhooks__container'> <label className='control-label padding-top x2'>{'Existing outgoing webhooks'}</label> - {displayHooks} + <div className='padding-top divider-light'></div> + <div className='webhooks__list'> + {displayHooks} + </div> </div> ); @@ -210,41 +237,49 @@ export default class ManageOutgoingHooks extends React.Component { <div key='addOutgoingHook'> <label className='control-label'>{'Add a new outgoing webhook'}</label> <div className='padding-top'> - <strong>{'Channel:'}</strong> - <select - ref='channelName' - className='form-control' - value={this.state.channelId} - onChange={this.updateChannelId} - > - {options} - </select> - <span>{'Only public channels can be used'}</span> - <br/> - <br/> - <strong>{'Trigger Words:'}</strong> - <input - ref='triggerWords' - className='form-control' - value={this.state.triggerWords} - onChange={this.updateTriggerWords} - placeholder='Optional if channel selected' - /> - <span>{'Comma separated words to trigger on'}</span> - <br/> - <br/> - <strong>{'Callback URLs:'}</strong> - <textarea - ref='callbackURLs' - className='form-control no-resize' - value={this.state.callbackURLs} - resize={false} - rows={3} - onChange={this.updateCallbackURLs} - /> - <span>{'New line separated URLs that will receive the HTTP POST event'}</span> - {serverError} - <div className='padding-top'> + <div> + <label className='control-label'>{'Channel'}</label> + <div className='padding-top'> + <select + ref='channelName' + className='form-control' + value={this.state.channelId} + onChange={this.updateChannelId} + > + {options} + </select> + </div> + <div className='padding-top'>{'Only public channels can be used'}</div> + </div> + <div className='padding-top x2'> + <label className='control-label'>{'Trigger Words:'}</label> + <div className='padding-top'> + <input + ref='triggerWords' + className='form-control' + value={this.state.triggerWords} + onChange={this.updateTriggerWords} + placeholder='Optional if channel selected' + /> + </div> + <div className='padding-top'>{'Comma separated words to trigger on'}</div> + </div> + <div className='padding-top x2'> + <label className='control-label'>{'Callback URLs:'}</label> + <div className='padding-top'> + <textarea + ref='callbackURLs' + className='form-control no-resize' + value={this.state.callbackURLs} + resize={false} + rows={3} + onChange={this.updateCallbackURLs} + /> + </div> + <div className='padding-top'>{'New line separated URLs that will receive the HTTP POST event'}</div> + {serverError} + </div> + <div className='padding-top padding-bottom'> <a className={'btn btn-sm btn-primary'} href='#' diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index 8c62a189d..e94894a1d 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -7,6 +7,7 @@ var Utils = require('../../utils/utils.jsx'); const CustomThemeChooser = require('./custom_theme_chooser.jsx'); const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); +const CodeThemeChooser = require('./code_theme_chooser.jsx'); const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); const Constants = require('../../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -18,12 +19,14 @@ export default class UserSettingsAppearance extends React.Component { this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); + this.updateCodeTheme = this.updateCodeTheme.bind(this); this.handleClose = this.handleClose.bind(this); this.handleImportModal = this.handleImportModal.bind(this); this.state = this.getStateFromStores(); this.originalTheme = this.state.theme; + this.originalCodeTheme = this.state.theme.codeTheme; } componentDidMount() { UserStore.addChangeListener(this.onChange); @@ -58,6 +61,10 @@ export default class UserSettingsAppearance extends React.Component { type = 'custom'; } + if (!theme.codeTheme) { + theme.codeTheme = Constants.DEFAULT_CODE_THEME; + } + return {theme, type}; } onChange() { @@ -93,6 +100,13 @@ export default class UserSettingsAppearance extends React.Component { ); } updateTheme(theme) { + theme.codeTheme = this.state.theme.codeTheme; + this.setState({theme}); + Utils.applyTheme(theme); + } + updateCodeTheme(codeTheme) { + var theme = this.state.theme; + theme.codeTheme = codeTheme; this.setState({theme}); Utils.applyTheme(theme); } @@ -102,6 +116,7 @@ export default class UserSettingsAppearance extends React.Component { handleClose() { const state = this.getStateFromStores(); state.serverError = null; + state.theme.codeTheme = this.originalCodeTheme; Utils.applyTheme(state.theme); @@ -170,7 +185,13 @@ export default class UserSettingsAppearance extends React.Component { </div> {custom} <hr /> - {serverError} + <strong className='radio'>{'Code Theme'}</strong> + <CodeThemeChooser + theme={this.state.theme} + updateTheme={this.updateCodeTheme} + /> + <hr /> + {serverError} <a className='btn btn-sm btn-primary' href='#' diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 9c03f77a6..1c8ce3c79 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -122,7 +122,7 @@ export default class UserSettingsGeneralTab extends React.Component { () => { this.updateSection(''); AsyncClient.getMe(); - const verificationEnabled = global.window.config.SendEmailNotifications === 'true' && global.window.config.RequireEmailVerification === 'true' && emailUpdated; + const verificationEnabled = global.window.mm_config.SendEmailNotifications === 'true' && global.window.mm_config.RequireEmailVerification === 'true' && emailUpdated; if (verificationEnabled) { ErrorStore.storeLastError({message: 'Check your email at ' + user.email + ' to verify the address.'}); @@ -171,7 +171,7 @@ export default class UserSettingsGeneralTab extends React.Component { }.bind(this), function imageUploadFailure(err) { var state = this.setupInitialState(this.props); - state.serverError = err; + state.serverError = err.message; this.setState(state); }.bind(this) ); @@ -451,8 +451,8 @@ export default class UserSettingsGeneralTab extends React.Component { } var emailSection; if (this.props.activeSection === 'email') { - const emailEnabled = global.window.config.SendEmailNotifications === 'true'; - const emailVerificationEnabled = global.window.config.RequireEmailVerification === 'true'; + const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true'; + const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true'; let helpText = 'Email is used for notifications, and requires verification if changed.'; if (!emailEnabled) { @@ -542,7 +542,7 @@ export default class UserSettingsGeneralTab extends React.Component { <SettingPicture title='Profile Picture' submit={this.submitPicture} - src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update} + src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update + '&' + utils.getSessionIndex()} server_error={serverError} client_error={clientError} updateSection={function clearSection(e) { diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index 231580cc3..4b1e5e532 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -34,16 +34,15 @@ export default class UserSettingsIntegrationsTab extends React.Component { let outgoingHooksSection; var inputs = []; - if (global.window.config.EnableIncomingWebhooks === 'true') { + if (global.window.mm_config.EnableIncomingWebhooks === 'true') { if (this.props.activeSection === 'incoming-hooks') { inputs.push( - <ManageIncomingHooks /> + <ManageIncomingHooks key='incoming-hook-ui' /> ); incomingHooksSection = ( <SettingItemMax title='Incoming Webhooks' - width = 'full' inputs={inputs} updateSection={(e) => { this.updateSection(''); @@ -55,7 +54,6 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMin title='Incoming Webhooks' - width = 'full' describe='Manage your incoming webhooks (Developer feature)' updateSection={() => { this.updateSection('incoming-hooks'); @@ -65,10 +63,10 @@ export default class UserSettingsIntegrationsTab extends React.Component { } } - if (global.window.config.EnableOutgoingWebhooks === 'true') { + if (global.window.mm_config.EnableOutgoingWebhooks === 'true') { if (this.props.activeSection === 'outgoing-hooks') { inputs.push( - <ManageOutgoingHooks /> + <ManageOutgoingHooks key='outgoing-hook-ui' /> ); outgoingHooksSection = ( diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 44cd423b5..5449ae91e 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -35,10 +35,11 @@ export default class UserSettingsModal extends React.Component { tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'}); tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'}); tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'}); - if (global.window.config.EnableOAuthServiceProvider === 'true') { + if (global.window.mm_config.EnableOAuthServiceProvider === 'true') { tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'}); } - if (global.window.config.EnableIncomingWebhooks === 'true' || global.window.config.EnableOutgoingWebhooks === 'true') { + + if (global.window.mm_config.EnableIncomingWebhooks === 'true' || global.window.mm_config.EnableOutgoingWebhooks === 'true') { tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); } tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'}); diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index 8693af494..61d49acb2 100644 --- a/web/react/components/user_settings/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -413,7 +413,7 @@ export default class NotificationsTab extends React.Component { </label> <br/> </div> - <div><br/>{'Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from ' + global.window.config.SiteName + ' for more than 5 minutes.'}</div> + <div><br/>{'Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from ' + global.window.mm_config.SiteName + ' for more than 5 minutes.'}</div> </div> ); diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index bea6ce7a5..92d7cd835 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -197,7 +197,7 @@ export default class ViewImageModal extends React.Component { } fileInfo.path = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path; - return fileInfo.path + '_preview.jpg'; + return fileInfo.path + '_preview.jpg' + '?' + Utils.getSessionIndex(); } // only images have proper previews, so just use a placeholder icon for non-images @@ -306,7 +306,7 @@ export default class ViewImageModal extends React.Component { width={width} height={height} > - <source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename} /> + <source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + Utils.getSessionIndex()} /> </video> ); } else { diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx index 5b3ee540c..1287f4fba 100644 --- a/web/react/components/view_image_popover_bar.jsx +++ b/web/react/components/view_image_popover_bar.jsx @@ -7,7 +7,7 @@ export default class ViewImagePopoverBar extends React.Component { } render() { var publicLink = ''; - if (global.window.config.EnablePublicLink === 'true') { + if (global.window.mm_config.EnablePublicLink === 'true') { publicLink = ( <div> <a diff --git a/web/react/package.json b/web/react/package.json index e6a662375..9af6f5880 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -6,6 +6,7 @@ "autolinker": "0.18.1", "babel-runtime": "5.8.24", "flux": "2.1.1", + "highlight.js": "^8.9.1", "keymirror": "0.1.1", "marked": "0.3.5", "object-assign": "3.0.0", diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 20ed1bf0a..03e049db0 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -35,26 +35,18 @@ var RemovedFromChannelModal = require('../components/removed_from_channel_modal. 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 TeamStore = require('../stores/team_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; function setupChannelPage(props) { - TeamStore.setCurrentId(props.TeamId); - AppDispatcher.handleViewAction({ type: ActionTypes.CLICK_CHANNEL, name: props.ChannelName, id: props.ChannelId }); - AppDispatcher.handleViewAction({ - type: ActionTypes.CLICK_TEAM, - id: props.TeamId - }); - AsyncClient.getAllPreferences(); // ChannelLoader must be rendered first @@ -237,7 +229,7 @@ function setupChannelPage(props) { document.getElementById('register_app_modal') ); - if (global.window.config.SendEmailNotifications === 'false') { + if (global.window.mm_config.SendEmailNotifications === 'false') { ErrorStore.storeLastError({message: 'Preview Mode: Email notifications have not been configured'}); ErrorStore.emitChange(); } diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx index 5f0fa9d96..a59f2afd0 100644 --- a/web/react/pages/home.jsx +++ b/web/react/pages/home.jsx @@ -2,14 +2,15 @@ // See License.txt for license information. var ChannelStore = require('../stores/channel_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); -function setupHomePage(props) { +function setupHomePage() { var last = ChannelStore.getLastVisitedName(); if (last == null || last.length === 0) { - window.location = props.TeamURL + '/channels/' + Constants.DEFAULT_CHANNEL; + window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL; } else { - window.location = props.TeamURL + '/channels/' + last; + window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last; } } diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index c2e7df58e..75fb8aa3c 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -1,12 +1,12 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore; function getPrefix() { - if (!UserStore) { - UserStore = require('./user_store.jsx'); //eslint-disable-line global-require + if (global.window.mm_user) { + return global.window.mm_user.id + '_'; } - return UserStore.getCurrentId() + '_'; + + return 'unknown_'; } class BrowserStoreClass { @@ -17,35 +17,55 @@ class BrowserStoreClass { this.setGlobalItem = this.setGlobalItem.bind(this); this.getGlobalItem = this.getGlobalItem.bind(this); this.removeGlobalItem = this.removeGlobalItem.bind(this); - this.clear = this.clear.bind(this); this.actionOnItemsWithPrefix = this.actionOnItemsWithPrefix.bind(this); + this.actionOnGlobalItemsWithPrefix = this.actionOnGlobalItemsWithPrefix.bind(this); this.isLocalStorageSupported = this.isLocalStorageSupported.bind(this); + this.getLastServerVersion = this.getLastServerVersion.bind(this); + this.setLastServerVersion = this.setLastServerVersion.bind(this); + this.clear = this.clear.bind(this); + this.clearAll = this.clearAll.bind(this); - var currentVersion = localStorage.getItem('local_storage_version'); - if (currentVersion !== global.window.config.Version) { - this.clear(); - localStorage.setItem('local_storage_version', global.window.config.Version); + var currentVersion = sessionStorage.getItem('storage_version'); + if (currentVersion !== global.window.mm_config.Version) { + sessionStorage.clear(); + sessionStorage.setItem('storage_version', global.window.mm_config.Version); } } getItem(name, defaultValue) { - return this.getGlobalItem(getPrefix() + name, defaultValue); + var result = null; + try { + result = JSON.parse(sessionStorage.getItem(getPrefix() + name)); + } catch (err) { + result = null; + } + + if (result === null && typeof defaultValue !== 'undefined') { + result = defaultValue; + } + + return result; } setItem(name, value) { - this.setGlobalItem(getPrefix() + name, value); + sessionStorage.setItem(getPrefix() + name, JSON.stringify(value)); } removeItem(name) { - localStorage.removeItem(getPrefix() + name); + sessionStorage.removeItem(getPrefix() + name); } setGlobalItem(name, value) { try { - localStorage.setItem(name, JSON.stringify(value)); + if (this.isLocalStorageSupported()) { + localStorage.setItem(getPrefix() + name, JSON.stringify(value)); + } else { + sessionStorage.setItem(getPrefix() + name, JSON.stringify(value)); + } } catch (err) { console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console localStorage.clear(); + sessionStorage.clear(); window.location.href = window.location.href; } } @@ -53,7 +73,11 @@ class BrowserStoreClass { getGlobalItem(name, defaultValue) { var result = null; try { - result = JSON.parse(localStorage.getItem(name)); + if (this.isLocalStorageSupported()) { + result = JSON.parse(localStorage.getItem(getPrefix() + name)); + } else { + result = JSON.parse(sessionStorage.getItem(getPrefix() + name)); + } } catch (err) { result = null; } @@ -66,22 +90,46 @@ class BrowserStoreClass { } removeGlobalItem(name) { - localStorage.removeItem(name); + if (this.isLocalStorageSupported()) { + localStorage.removeItem(getPrefix() + name); + } else { + sessionStorage.removeItem(getPrefix() + name); + } } - clear() { - localStorage.clear(); - sessionStorage.clear(); + getLastServerVersion() { + return sessionStorage.getItem('last_server_version'); + } + + setLastServerVersion(version) { + sessionStorage.setItem('last_server_version', version); } /** * Preforms the given action on each item that has the given prefix * Signature for action is action(key, value) */ + actionOnGlobalItemsWithPrefix(prefix, action) { + var globalPrefix = getPrefix(); + var globalPrefixiLen = globalPrefix.length; + + var storage = sessionStorage; + if (this.isLocalStorageSupported()) { + storage = localStorage; + } + + for (var key in storage) { + if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) { + var userkey = key.substring(globalPrefixiLen); + action(userkey, this.getGlobalItem(key)); + } + } + } + actionOnItemsWithPrefix(prefix, action) { var globalPrefix = getPrefix(); var globalPrefixiLen = globalPrefix.length; - for (var key in localStorage) { + for (var key in sessionStorage) { if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) { var userkey = key.substring(globalPrefixiLen); action(userkey, this.getGlobalItem(key)); @@ -89,6 +137,15 @@ class BrowserStoreClass { } } + clear() { + sessionStorage.clear(); + } + + clearAll() { + sessionStorage.clear(); + localStorage.clear(); + } + isLocalStorageSupported() { try { sessionStorage.setItem('testSession', '1'); diff --git a/web/react/stores/error_store.jsx b/web/react/stores/error_store.jsx index a4c42dcb7..775b8e006 100644 --- a/web/react/stores/error_store.jsx +++ b/web/react/stores/error_store.jsx @@ -34,9 +34,11 @@ class ErrorStoreClass extends EventEmitter { removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + handledError() { BrowserStore.removeItem('last_error'); } + getLastError() { return BrowserStore.getItem('last_error'); } diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 0ace956d2..4a9314b31 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -324,10 +324,10 @@ class PostStoreClass extends EventEmitter { return 0; }); - BrowserStore.setItem('pending_posts_' + channelId, postList); + BrowserStore.setGlobalItem('pending_posts_' + channelId, postList); } getPendingPosts(channelId) { - return BrowserStore.getItem('pending_posts_' + channelId); + return BrowserStore.getGlobalItem('pending_posts_' + channelId); } storeUnseenDeletedPost(post) { var posts = this.getUnseenDeletedPosts(post.channel_id); @@ -371,7 +371,7 @@ class PostStoreClass extends EventEmitter { this.pStorePendingPosts(channelId, postList); } clearPendingPosts() { - BrowserStore.actionOnItemsWithPrefix('pending_posts_', function clearPending(key) { + BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', function clearPending(key) { BrowserStore.removeItem(key); }); } @@ -414,26 +414,26 @@ class PostStoreClass extends EventEmitter { } storeCurrentDraft(draft) { var channelId = ChannelStore.getCurrentId(); - BrowserStore.setItem('draft_' + channelId, draft); + BrowserStore.setGlobalItem('draft_' + channelId, draft); } getCurrentDraft() { var channelId = ChannelStore.getCurrentId(); return this.getDraft(channelId); } storeDraft(channelId, draft) { - BrowserStore.setItem('draft_' + channelId, draft); + BrowserStore.setGlobalItem('draft_' + channelId, draft); } getDraft(channelId) { - return BrowserStore.getItem('draft_' + channelId, this.getEmptyDraft()); + return BrowserStore.getGlobalItem('draft_' + channelId, this.getEmptyDraft()); } storeCommentDraft(parentPostId, draft) { - BrowserStore.setItem('comment_draft_' + parentPostId, draft); + BrowserStore.setGlobalItem('comment_draft_' + parentPostId, draft); } getCommentDraft(parentPostId) { - return BrowserStore.getItem('comment_draft_' + parentPostId, this.getEmptyDraft()); + return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft()); } clearDraftUploads() { - BrowserStore.actionOnItemsWithPrefix('draft_', function clearUploads(key, value) { + BrowserStore.actionOnGlobalItemsWithPrefix('draft_', function clearUploads(key, value) { if (value) { value.uploadsInProgress = []; BrowserStore.setItem(key, value); @@ -441,7 +441,7 @@ class PostStoreClass extends EventEmitter { }); } clearCommentDraftUploads() { - BrowserStore.actionOnItemsWithPrefix('comment_draft_', function clearUploads(key, value) { + BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', function clearUploads(key, value) { if (value) { value.uploadsInProgress = []; BrowserStore.setItem(key, value); diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 77951f214..d4b0e62db 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -38,6 +38,10 @@ class SocketStoreClass extends EventEmitter { return; } + if (!global.window.hasOwnProperty('mm_session_token_index')) { + return; + } + this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -45,7 +49,9 @@ class SocketStoreClass extends EventEmitter { if (window.location.protocol === 'https:') { protocol = 'wss://'; } - var connUrl = protocol + location.host + '/api/v1/websocket'; + + var connUrl = protocol + location.host + '/api/v1/websocket?' + Utils.getSessionIndex(); + if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console } @@ -152,9 +158,9 @@ function handleNewPostEvent(msg) { // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { if (window.isActive) { - AsyncClient.updateLastViewedAt(); + AsyncClient.updateLastViewedAt(true); } - } else { + } else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) { AsyncClient.getChannel(msg.channel_id); } diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 7001acdb1..22114ae85 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -28,29 +28,31 @@ class TeamStoreClass extends EventEmitter { this.get = this.get.bind(this); this.getByName = this.getByName.bind(this); this.getAll = this.getAll.bind(this); - this.setCurrentId = this.setCurrentId.bind(this); this.getCurrentId = this.getCurrentId.bind(this); this.getCurrent = this.getCurrent.bind(this); this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this); - this.storeTeam = this.storeTeam.bind(this); - this.pStoreTeams = this.pStoreTeams.bind(this); - this.pGetTeams = this.pGetTeams.bind(this); + this.saveTeam = this.saveTeam.bind(this); } + emitChange() { this.emit(CHANGE_EVENT); } + addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } + removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + get(id) { - var c = this.pGetTeams(); + var c = this.getAll(); return c[id]; } + getByName(name) { - var t = this.pGetTeams(); + var t = this.getAll(); for (var id in t) { if (t[id].name === name) { @@ -60,59 +62,51 @@ class TeamStoreClass extends EventEmitter { return null; } + getAll() { - return this.pGetTeams(); - } - setCurrentId(id) { - if (id === null) { - BrowserStore.removeItem('current_team_id'); - } else { - BrowserStore.setItem('current_team_id', id); - } + return BrowserStore.getItem('user_teams', {}); } + getCurrentId() { - return BrowserStore.getItem('current_team_id'); - } - getCurrent() { - var currentId = this.getCurrentId(); + var team = global.window.mm_team; - if (currentId !== null) { - return this.get(currentId); + if (team) { + return team.id; } + return null; } + + getCurrent() { + if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) { + this.saveTeam(global.window.mm_team); + } + + return global.window.mm_team; + } + getCurrentTeamUrl() { if (this.getCurrent()) { return getWindowLocationOrigin() + '/' + this.getCurrent().name; } return null; } - storeTeam(team) { - var teams = this.pGetTeams(); + + saveTeam(team) { + var teams = this.getAll(); teams[team.id] = team; - this.pStoreTeams(teams); - } - pStoreTeams(teams) { BrowserStore.setItem('user_teams', teams); } - pGetTeams() { - return BrowserStore.getItem('user_teams', {}); - } } var TeamStore = new TeamStoreClass(); -TeamStore.dispatchToken = AppDispatcher.register(function registry(payload) { +TeamStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.CLICK_TEAM: - TeamStore.setCurrentId(action.id); - TeamStore.emitChange(); - break; - case ActionTypes.RECIEVED_TEAM: - TeamStore.storeTeam(action.team); + TeamStore.saveTeam(action.team); TeamStore.emitChange(); break; diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index fa74f812d..ce80c5ec9 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -3,7 +3,6 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var EventEmitter = require('events').EventEmitter; -var client = require('../utils/client.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; @@ -38,23 +37,19 @@ class UserStoreClass extends EventEmitter { 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); this.setCurrentUser = this.setCurrentUser.bind(this); this.getLastEmail = this.getLastEmail.bind(this); this.setLastEmail = this.setLastEmail.bind(this); - this.removeCurrentUser = this.removeCurrentUser.bind(this); this.hasProfile = this.hasProfile.bind(this); this.getProfile = this.getProfile.bind(this); this.getProfileByUsername = this.getProfileByUsername.bind(this); this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this); this.getProfiles = this.getProfiles.bind(this); this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this); + this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this); this.saveProfile = this.saveProfile.bind(this); - this.pStoreProfiles = this.pStoreProfiles.bind(this); - this.pGetProfiles = this.pGetProfiles.bind(this); - this.pGetProfilesUsernameMap = this.pGetProfilesUsernameMap.bind(this); this.setSessions = this.setSessions.bind(this); this.getSessions = this.getSessions.bind(this); this.setAudits = this.setAudits.bind(this); @@ -62,138 +57,155 @@ class UserStoreClass extends EventEmitter { this.setTeams = this.setTeams.bind(this); this.getTeams = this.getTeams.bind(this); this.getCurrentMentionKeys = this.getCurrentMentionKeys.bind(this); - this.getLastVersion = this.getLastVersion.bind(this); - this.setLastVersion = this.setLastVersion.bind(this); this.setStatuses = this.setStatuses.bind(this); this.pSetStatuses = this.pSetStatuses.bind(this); this.setStatus = this.setStatus.bind(this); this.getStatuses = this.getStatuses.bind(this); this.getStatus = this.getStatus.bind(this); - - this.gCurrentId = null; } emitChange(userId) { this.emit(CHANGE_EVENT, userId); } + addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } + removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + emitSessionsChange() { this.emit(CHANGE_EVENT_SESSIONS); } + addSessionsChangeListener(callback) { this.on(CHANGE_EVENT_SESSIONS, callback); } + removeSessionsChangeListener(callback) { this.removeListener(CHANGE_EVENT_SESSIONS, callback); } + emitAuditsChange() { this.emit(CHANGE_EVENT_AUDITS); } + addAuditsChangeListener(callback) { this.on(CHANGE_EVENT_AUDITS, callback); } + removeAuditsChangeListener(callback) { this.removeListener(CHANGE_EVENT_AUDITS, callback); } + emitTeamsChange() { this.emit(CHANGE_EVENT_TEAMS); } + addTeamsChangeListener(callback) { this.on(CHANGE_EVENT_TEAMS, callback); } + removeTeamsChangeListener(callback) { this.removeListener(CHANGE_EVENT_TEAMS, callback); } + emitStatusesChange() { this.emit(CHANGE_EVENT_STATUSES); } + addStatusesChangeListener(callback) { this.on(CHANGE_EVENT_STATUSES, callback); } + 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) { - BrowserStore.removeGlobalItem('current_user_id'); - } else { - BrowserStore.setGlobalItem('current_user_id', id); + + getCurrentUser() { + if (this.getProfiles()[global.window.mm_user.id] == null) { + this.saveProfile(global.window.mm_user); } + + return global.window.mm_user; } - getCurrentId(skipFetch) { - var currentId = this.gCurrentId; - if (currentId == null) { - currentId = BrowserStore.getGlobalItem('current_user_id'); - this.gCurrentId = currentId; - } + setCurrentUser(user) { + var oldUser = global.window.mm_user; - // this is a special case to force fetch the - // current user if it's missing - // it's synchronous to block rendering - if (currentId == null && !skipFetch) { - var me = client.getMeSynchronous(); - if (me != null) { - this.setCurrentUser(me); - currentId = me.id; - } + if (oldUser.id === user.id) { + global.window.mm_user = user; + this.saveProfile(user); + } else { + throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id); } - - return currentId; } - getCurrentUser() { - if (this.getCurrentId() == null) { - return null; + + getCurrentId() { + var user = global.window.mm_user; + + if (user) { + return user.id; } - return this.pGetProfiles()[this.getCurrentId()]; - } - setCurrentUser(user) { - this.setCurrentId(user.id); - this.saveProfile(user); + return null; } + getLastEmail() { - return BrowserStore.getItem('last_email', ''); + return BrowserStore.getGlobalItem('last_email', ''); } + setLastEmail(email) { - BrowserStore.setItem('last_email', email); - } - removeCurrentUser() { - this.setCurrentId(null); + BrowserStore.setGlobalItem('last_email', email); } + hasProfile(userId) { - return this.pGetProfiles()[userId] != null; + return this.getProfiles()[userId] != null; } + getProfile(userId) { - return this.pGetProfiles()[userId]; + return this.getProfiles()[userId]; } + getProfileByUsername(username) { - return this.pGetProfilesUsernameMap()[username]; + return this.getProfilesUsernameMap()[username]; } + getProfilesUsernameMap() { - return this.pGetProfilesUsernameMap(); + var profileUsernameMap = {}; + + var profiles = this.getProfiles(); + for (var key in profiles) { + if (profiles.hasOwnProperty(key)) { + var profile = profiles[key]; + profileUsernameMap[profile.username] = profile; + } + } + + return profileUsernameMap; } + getProfiles() { - return this.pGetProfiles(); + return BrowserStore.getItem('profiles', {}); } + getActiveOnlyProfiles() { var active = {}; - var current = this.pGetProfiles(); + var current = this.getProfiles(); for (var key in current) { if (current[key].delete_at === 0) { @@ -203,45 +215,50 @@ class UserStoreClass extends EventEmitter { return active; } - saveProfile(profile) { - var ps = this.pGetProfiles(); - ps[profile.id] = profile; - this.pStoreProfiles(ps); - } - pStoreProfiles(profiles) { - BrowserStore.setItem('profiles', profiles); - var profileUsernameMap = {}; - for (var id in profiles) { - if (profiles.hasOwnProperty(id)) { - profileUsernameMap[profiles[id].username] = profiles[id]; + + getActiveOnlyProfileList() { + const profileMap = this.getActiveOnlyProfiles(); + const profiles = []; + + for (const id in profileMap) { + if (profileMap.hasOwnProperty(id)) { + profiles.push(profileMap[id]); } } - BrowserStore.setItem('profileUsernameMap', profileUsernameMap); - } - pGetProfiles() { - return BrowserStore.getItem('profiles', {}); + + return profiles; } - pGetProfilesUsernameMap() { - return BrowserStore.getItem('profileUsernameMap', {}); + + saveProfile(profile) { + var ps = this.getProfiles(); + ps[profile.id] = profile; + BrowserStore.setItem('profiles', ps); } + setSessions(sessions) { BrowserStore.setItem('sessions', sessions); } + getSessions() { return BrowserStore.getItem('sessions', {loading: true}); } + setAudits(audits) { BrowserStore.setItem('audits', audits); } + getAudits() { return BrowserStore.getItem('audits', {loading: true}); } + setTeams(teams) { BrowserStore.setItem('teams', teams); } + getTeams() { return BrowserStore.getItem('teams', []); } + getCurrentMentionKeys() { var user = this.getCurrentUser(); @@ -269,28 +286,27 @@ class UserStoreClass extends EventEmitter { return keys; } - getLastVersion() { - return BrowserStore.getItem('last_version', ''); - } - setLastVersion(version) { - BrowserStore.setItem('last_version', version); - } + setStatuses(statuses) { this.pSetStatuses(statuses); this.emitStatusesChange(); } + pSetStatuses(statuses) { BrowserStore.setItem('statuses', statuses); } + setStatus(userId, status) { var statuses = this.getStatuses(); statuses[userId] = status; this.pSetStatuses(statuses); this.emitStatusesChange(); } + getStatuses() { return BrowserStore.getItem('statuses', {}); } + getStatus(id) { return this.getStatuses()[id]; } @@ -299,7 +315,7 @@ class UserStoreClass extends EventEmitter { var UserStore = new UserStoreClass(); UserStore.setMaxListeners(0); -UserStore.dispatchToken = AppDispatcher.register(function registry(payload) { +UserStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index b22d7237e..75dd35e3f 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -3,6 +3,7 @@ var client = require('./client.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +var BrowserStore = require('../stores/browser_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); @@ -50,18 +51,18 @@ export function getChannels(force, updateLastViewed, checkVersion) { callTracker.getChannels = utils.getTimestamp(); client.getChannels( - function getChannelsSuccess(data, textStatus, xhr) { + (data, textStatus, xhr) => { callTracker.getChannels = 0; if (checkVersion) { var serverVersion = xhr.getResponseHeader('X-Version-ID'); - if (!UserStore.getLastVersion()) { - UserStore.setLastVersion(serverVersion); + if (!BrowserStore.getLastServerVersion()) { + BrowserStore.setLastServerVersion(serverVersion); } - if (serverVersion !== UserStore.getLastVersion()) { - UserStore.setLastVersion(serverVersion); + if (serverVersion !== BrowserStore.getLastServerVersion()) { + BrowserStore.setLastServerVersion(serverVersion); window.location.href = window.location.href; console.log('Detected version update refreshing the page'); //eslint-disable-line no-console } @@ -77,7 +78,7 @@ export function getChannels(force, updateLastViewed, checkVersion) { members: data.members }); }, - function getChannelsFailure(err) { + (err) => { callTracker.getChannels = 0; dispatchError(err, 'getChannels'); } @@ -131,7 +132,7 @@ export function getChannel(id) { callTracker['getChannel' + id] = utils.getTimestamp(); client.getChannel(id, - function getChannelSuccess(data, textStatus, xhr) { + (data, textStatus, xhr) => { callTracker['getChannel' + id] = 0; if (xhr.status === 304 || !data) { @@ -144,21 +145,21 @@ export function getChannel(id) { member: data.member }); }, - function getChannelFailure(err) { + (err) => { callTracker['getChannel' + id] = 0; dispatchError(err, 'getChannel'); } ); } -export function updateLastViewedAt() { +export function updateLastViewedAt(force) { const channelId = ChannelStore.getCurrentId(); if (channelId === null) { return; } - if (isCallInProgress(`updateLastViewed${channelId}`)) { + if (isCallInProgress(`updateLastViewed${channelId}`) && !force) { return; } @@ -566,8 +567,8 @@ export function getMe() { } callTracker.getMe = utils.getTimestamp(); - client.getMeSynchronous( - function getMeSyncSuccess(data, textStatus, xhr) { + client.getMe( + (data, textStatus, xhr) => { callTracker.getMe = 0; if (xhr.status === 304 || !data) { @@ -579,7 +580,7 @@ export function getMe() { me: data }); }, - function getMeSyncFailure(err) { + (err) => { callTracker.getMe = 0; dispatchError(err, 'getMe'); } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index f92633439..bc73f3c64 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -4,8 +4,8 @@ var BrowserStore = require('../stores/browser_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var ErrorStore = require('../stores/error_store.jsx'); -export function track(category, action, label, prop, val) { - global.window.analytics.track(action, {category: category, label: label, property: prop, value: val}); +export function track(category, action, label, property, value) { + global.window.analytics.track(action, {category, label, property, value}); } export function trackPage() { @@ -232,6 +232,7 @@ export function logout() { track('api', 'api_users_logout'); var currentTeamUrl = TeamStore.getCurrentTeamUrl(); BrowserStore.clear(); + ErrorStore.storeLastError(null); window.location.href = currentTeamUrl + '/logout'; } @@ -385,10 +386,9 @@ export function getAllTeams(success, error) { }); } -export function getMeSynchronous(success, error) { +export function getMe(success, error) { var currentUser = null; $.ajax({ - async: false, cache: false, url: '/api/v1/users/me', dataType: 'json', @@ -402,7 +402,7 @@ export function getMeSynchronous(success, error) { }, error: function onError(xhr, status, err) { if (error) { - var e = handleError('getMeSynchronous', xhr, status, err); + var e = handleError('getMe', xhr, status, err); error(e); } } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 1d856e067..0e89b9470 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -33,7 +33,6 @@ module.exports = { RECIEVED_MSG: null, - CLICK_TEAM: null, RECIEVED_TEAM: null, RECIEVED_CONFIG: null, @@ -99,6 +98,7 @@ module.exports = { POST_LOADING: 'loading', POST_FAILED: 'failed', POST_DELETED: 'deleted', + POST_TYPE_JOIN_LEAVE: 'join_leave', RESERVED_TEAM_NAMES: [ 'www', 'web', @@ -133,6 +133,7 @@ module.exports = { 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>", + UPDATE_TYPING_MS: 5000, THEMES: { default: { type: 'Mattermost', @@ -140,7 +141,7 @@ module.exports = { sidebarText: '#333333', sidebarUnreadText: '#333333', sidebarTextHoverBg: '#e6f2fa', - sidebarTextActiveBg: '#e1e1e1', + sidebarTextActiveBorder: '#378FD2', sidebarTextActiveColor: '#111111', sidebarHeaderBg: '#2389d7', sidebarHeaderTextColor: '#ffffff', @@ -162,7 +163,7 @@ module.exports = { sidebarText: '#fff', sidebarUnreadText: '#fff', sidebarTextHoverBg: '#136197', - sidebarTextActiveBg: '#136197', + sidebarTextActiveBorder: '#7AB0D6', sidebarTextActiveColor: '#FFFFFF', sidebarHeaderBg: '#2f81b7', sidebarHeaderTextColor: '#FFFFFF', @@ -184,7 +185,7 @@ module.exports = { sidebarText: '#fff', sidebarUnreadText: '#fff', sidebarTextHoverBg: '#4A5664', - sidebarTextActiveBg: '#39769C', + sidebarTextActiveBorder: '#39769C', sidebarTextActiveColor: '#FFFFFF', sidebarHeaderBg: '#1B2C3E', sidebarHeaderTextColor: '#FFFFFF', @@ -206,7 +207,7 @@ module.exports = { sidebarText: '#fff', sidebarUnreadText: '#fff', sidebarTextHoverBg: '#302e30', - sidebarTextActiveBg: '#484748', + sidebarTextActiveBorder: '#196CAF', sidebarTextActiveColor: '#FFFFFF', sidebarHeaderBg: '#1f1f1f', sidebarHeaderTextColor: '#FFFFFF', @@ -249,8 +250,8 @@ module.exports = { uiName: 'Sidebar Text Hover BG' }, { - id: 'sidebarTextActiveBg', - uiName: 'Sidebar Text Active BG' + id: 'sidebarTextActiveBorder', + uiName: 'Sidebar Text Active Border' }, { id: 'sidebarTextActiveColor', @@ -301,6 +302,13 @@ module.exports = { uiName: 'Mention Highlight Link' } ], + CODE_THEMES: { + github: 'GitHub', + solarized_light: 'Solarized light', + monokai: 'Monokai', + solarized_dark: 'Solarized Dark' + }, + DEFAULT_CODE_THEME: 'github', Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', CATEGORY_DISPLAY_SETTINGS: 'display_settings' @@ -312,6 +320,32 @@ module.exports = { RIGHT: 39, BACKSPACE: 8, ENTER: 13, - ESCAPE: 27 + ESCAPE: 27, + SPACE: 32 + }, + HighlightedLanguages: { + diff: 'Diff', + apache: 'Apache', + makefile: 'Makefile', + http: 'HTTP', + json: 'JSON', + markdown: 'Markdown', + javascript: 'JavaScript', + css: 'CSS', + nginx: 'nginx', + objectivec: 'Objective-C', + python: 'Python', + xml: 'XML', + perl: 'Perl', + bash: 'Bash', + php: 'PHP', + coffeescript: 'CoffeeScript', + cs: 'C#', + cpp: 'C++', + sql: 'SQL', + go: 'Go', + ruby: 'Ruby', + java: 'Java', + ini: 'ini' } }; diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx index aabddcffd..bb948b6dc 100644 --- a/web/react/utils/emoticons.jsx +++ b/web/react/utils/emoticons.jsx @@ -2,27 +2,27 @@ // See License.txt for license information. const emoticonPatterns = { - smile: /(^|\s)(:-?\))($|\s)/g, // :) - wink: /(^|\s)(;-?\))($|\s)/g, // ;) - open_mouth: /(^|\s)(:o)($|\s)/gi, // :o - scream: /(^|\s)(:-o)($|\s)/gi, // :-o - smirk: /(^|\s)(:-?])($|\s)/g, // :] - grinning: /(^|\s)(:-?d)($|\s)/gi, // :D - stuck_out_tongue_closed_eyes: /(^|\s)(x-d)($|\s)/gi, // x-d - stuck_out_tongue: /(^|\s)(:-?p)($|\s)/gi, // :p - rage: /(^|\s)(:-?[\[@])($|\s)/g, // :@ - frowning: /(^|\s)(:-?\()($|\s)/g, // :( - sob: /(^|\s)(:['’]-?\(|:'\(|:'\()($|\s)/g, // :`( - kissing_heart: /(^|\s)(:-?\*)($|\s)/g, // :* - pensive: /(^|\s)(:-?\/)($|\s)/g, // :/ - confounded: /(^|\s)(:-?s)($|\s)/gi, // :s - flushed: /(^|\s)(:-?\|)($|\s)/g, // :| - relaxed: /(^|\s)(:-?\$)($|\s)/g, // :$ - mask: /(^|\s)(:-x)($|\s)/gi, // :-x - heart: /(^|\s)(<3|<3)($|\s)/g, // <3 - broken_heart: /(^|\s)(<\/3|</3)($|\s)/g, // </3 - thumbsup: /(^|\s)(:\+1:)($|\s)/g, // :+1: - thumbsdown: /(^|\s)(:\-1:)($|\s)/g // :-1: + smile: /(^|\s)(:-?\))(?=$|\s)/g, // :) + wink: /(^|\s)(;-?\))(?=$|\s)/g, // ;) + open_mouth: /(^|\s)(:o)(?=$|\s)/gi, // :o + scream: /(^|\s)(:-o)(?=$|\s)/gi, // :-o + smirk: /(^|\s)(:-?])(?=$|\s)/g, // :] + grinning: /(^|\s)(:-?d)(?=$|\s)/gi, // :D + stuck_out_tongue_closed_eyes: /(^|\s)(x-d)(?=$|\s)/gi, // x-d + stuck_out_tongue: /(^|\s)(:-?p)(?=$|\s)/gi, // :p + rage: /(^|\s)(:-?[\[@])(?=$|\s)/g, // :@ + frowning: /(^|\s)(:-?\()(?=$|\s)/g, // :( + sob: /(^|\s)(:['’]-?\(|:'\(|:'\()(?=$|\s)/g, // :`( + kissing_heart: /(^|\s)(:-?\*)(?=$|\s)/g, // :* + pensive: /(^|\s)(:-?\/)(?=$|\s)/g, // :/ + confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s + flushed: /(^|\s)(:-?\|)(?=$|\s)/g, // :| + relaxed: /(^|\s)(:-?\$)(?=$|\s)/g, // :$ + mask: /(^|\s)(:-x)(?=$|\s)/gi, // :-x + heart: /(^|\s)(<3|<3)(?=$|\s)/g, // <3 + broken_heart: /(^|\s)(<\/3|</3)(?=$|\s)/g, // </3 + thumbsup: /(^|\s)(:\+1:)(?=$|\s)/g, // :+1: + thumbsdown: /(^|\s)(:\-1:)(?=$|\s)/g // :-1: }; function initializeEmoticonMap() { @@ -127,28 +127,28 @@ const emoticonMap = initializeEmoticonMap(); export function handleEmoticons(text, tokens) { let output = text; - function replaceEmoticonWithToken(match, prefix, name, suffix) { + function replaceEmoticonWithToken(fullMatch, prefix, matchText, 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 + value: `<img align="absmiddle" alt="${matchText}" class="emoji" src="${getImagePathForEmoticon(name)}" title="${matchText}" />`, + originalText: fullMatch }); - return prefix + alias + suffix; + return prefix + alias; } - return match; + return fullMatch; } - output = output.replace(/(^|\s):([a-zA-Z0-9_-]+):($|\s)/g, replaceEmoticonWithToken); + output = output.replace(/(^|\s)(:([a-zA-Z0-9_-]+):)(?=$|\s)/g, (fullMatch, prefix, matchText, name) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name)); $.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, prefix, emoticon, suffix) => replaceEmoticonWithToken(match, prefix, name, suffix)); + output = output.replace(pattern, (fullMatch, prefix, matchText) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name)); }); return output; diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 7a4e70054..01cc309b8 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -6,6 +6,34 @@ const Utils = require('./utils.jsx'); const marked = require('marked'); +const highlightJs = require('highlight.js/lib/highlight.js'); +const highlightJsDiff = require('highlight.js/lib/languages/diff.js'); +const highlightJsApache = require('highlight.js/lib/languages/apache.js'); +const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js'); +const highlightJsHttp = require('highlight.js/lib/languages/http.js'); +const highlightJsJson = require('highlight.js/lib/languages/json.js'); +const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js'); +const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js'); +const highlightJsCss = require('highlight.js/lib/languages/css.js'); +const highlightJsNginx = require('highlight.js/lib/languages/nginx.js'); +const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js'); +const highlightJsPython = require('highlight.js/lib/languages/python.js'); +const highlightJsXml = require('highlight.js/lib/languages/xml.js'); +const highlightJsPerl = require('highlight.js/lib/languages/perl.js'); +const highlightJsBash = require('highlight.js/lib/languages/bash.js'); +const highlightJsPhp = require('highlight.js/lib/languages/php.js'); +const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js'); +const highlightJsCs = require('highlight.js/lib/languages/cs.js'); +const highlightJsCpp = require('highlight.js/lib/languages/cpp.js'); +const highlightJsSql = require('highlight.js/lib/languages/sql.js'); +const highlightJsGo = require('highlight.js/lib/languages/go.js'); +const highlightJsRuby = require('highlight.js/lib/languages/ruby.js'); +const highlightJsJava = require('highlight.js/lib/languages/java.js'); +const highlightJsIni = require('highlight.js/lib/languages/ini.js'); + +const Constants = require('../utils/constants.jsx'); +const HighlightedLanguages = Constants.HighlightedLanguages; + export class MattermostMarkdownRenderer extends marked.Renderer { constructor(options, formattingOptions = {}) { super(options); @@ -15,6 +43,43 @@ export class MattermostMarkdownRenderer extends marked.Renderer { this.text = this.text.bind(this); this.formattingOptions = formattingOptions; + + highlightJs.registerLanguage('diff', highlightJsDiff); + highlightJs.registerLanguage('apache', highlightJsApache); + highlightJs.registerLanguage('makefile', highlightJsMakefile); + highlightJs.registerLanguage('http', highlightJsHttp); + highlightJs.registerLanguage('json', highlightJsJson); + highlightJs.registerLanguage('markdown', highlightJsMarkdown); + highlightJs.registerLanguage('javascript', highlightJsJavascript); + highlightJs.registerLanguage('css', highlightJsCss); + highlightJs.registerLanguage('nginx', highlightJsNginx); + highlightJs.registerLanguage('objectivec', highlightJsObjectivec); + highlightJs.registerLanguage('python', highlightJsPython); + highlightJs.registerLanguage('xml', highlightJsXml); + highlightJs.registerLanguage('perl', highlightJsPerl); + highlightJs.registerLanguage('bash', highlightJsBash); + highlightJs.registerLanguage('php', highlightJsPhp); + highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript); + highlightJs.registerLanguage('cs', highlightJsCs); + highlightJs.registerLanguage('cpp', highlightJsCpp); + highlightJs.registerLanguage('sql', highlightJsSql); + highlightJs.registerLanguage('go', highlightJsGo); + highlightJs.registerLanguage('ruby', highlightJsRuby); + highlightJs.registerLanguage('java', highlightJsJava); + highlightJs.registerLanguage('ini', highlightJsIni); + } + + code(code, language) { + if (!language || highlightJs.listLanguages().indexOf(language) < 0) { + let parsed = super.code(code, language); + return '<code class="hljs">' + $(parsed).text() + '</code>'; + } + + let parsed = highlightJs.highlight(language, code); + return '<div class="post-body--code">' + + '<span class="post-body--code__language">' + HighlightedLanguages[language] + '</span>' + + '<code style="white-space: pre;" class="hljs">' + parsed.value + '</code>' + + '</div>'; } br() { diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 5c2e68f1e..4b6d87254 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -91,6 +91,7 @@ export function sanitizeHtml(text) { return output; } +// Convert URLs into tokens function autolinkUrls(text, tokens) { function replaceUrlWithToken(autolinker, match) { const linkText = match.getMatchedText(); @@ -132,27 +133,61 @@ function autolinkUrls(text, tokens) { } function autolinkAtMentions(text, tokens) { - let output = text; + // Return true if provided character is punctuation + function isPunctuation(character) { + const re = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g; + return re.test(character); + } + + // Test if provided text needs to be highlighted, special mention or current user + function mentionExists(u) { + return (Constants.SPECIAL_MENTIONS.indexOf(u) !== -1 || UserStore.getProfileByUsername(u)); + } + + function addToken(username, mention, extraText) { + const index = tokens.size; + const alias = `MM_ATMENTION${index}`; + + tokens.set(alias, { + value: `<a class='mention-link' href='#' data-mention='${username}'>${mention}</a>`, + originalText: mention, + extraText + }); + return alias; + } function replaceAtMentionWithToken(fullMatch, prefix, mention, username) { - const usernameLower = username.toLowerCase(); - if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) { - const index = tokens.size; - const alias = `MM_ATMENTION${index}`; - - tokens.set(alias, { - value: `<a class='mention-link' href='#' data-mention='${usernameLower}'>${mention}</a>`, - originalText: mention - }); + let usernameLower = username.toLowerCase(); + if (mentionExists(usernameLower)) { + // Exact match + const alias = addToken(usernameLower, mention, ''); return prefix + alias; } + // Not an exact match, attempt to truncate any punctuation to see if we can find a user + const originalUsername = usernameLower; + + for (let c = usernameLower.length; c > 0; c--) { + if (isPunctuation(usernameLower[c - 1])) { + usernameLower = usernameLower.substring(0, c - 1); + + if (mentionExists(usernameLower)) { + const extraText = originalUsername.substr(c - 1); + const alias = addToken(usernameLower, '@' + usernameLower, extraText); + return prefix + alias; + } + } else { + // If the last character is not punctuation, no point in going any further + break; + } + } + return fullMatch; } - output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, replaceAtMentionWithToken); - + let output = text; + output = output.replace(/(^|\s)(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken); return output; } @@ -169,10 +204,9 @@ function highlightCurrentMentions(text, tokens) { const newAlias = `MM_SELFMENTION${index}`; newTokens.set(newAlias, { - value: `<span class='mention-highlight'>${alias}</span>`, + value: `<span class='mention-highlight'>${alias}</span>` + token.extraText, originalText: token.originalText }); - output = output.replace(alias, newAlias); } } @@ -246,7 +280,7 @@ function highlightSearchTerm(text, tokens, searchTerm) { var newTokens = new Map(); for (const [alias, token] of tokens) { - if (token.originalText === searchTerm) { + if (token.originalText.indexOf(searchTerm.replace(/\*$/, '')) > -1) { const index = tokens.size + newTokens.size; const newAlias = `MM_SEARCHTERM${index}`; @@ -276,7 +310,7 @@ function highlightSearchTerm(text, tokens, searchTerm) { return prefix + alias; } - return output.replace(new RegExp(`(^|\\W)(${searchTerm})\\b`, 'gi'), replaceSearchTermWithToken); + return output.replace(new RegExp(`()(${searchTerm})`, 'gi'), replaceSearchTermWithToken); } function replaceTokens(text, tokens) { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index b9084b26e..7a876d518 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -20,6 +20,7 @@ export function isEmail(email) { export function cleanUpUrlable(input) { var cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-'); + cleaned = cleaned.replace(/-{2,}/, '-'); cleaned = cleaned.replace(/^\-+/, ''); cleaned = cleaned.replace(/\-+$/, ''); return cleaned; @@ -118,7 +119,7 @@ export function notifyMe(title, body, channel) { } if (permission === 'granted') { - var notification = new Notification(title, {body: body, tag: body, icon: '/static/images/icon50x50.gif'}); + var notification = new Notification(title, {body: body, tag: body, icon: '/static/images/icon50x50.png'}); notification.onclick = function onClick() { window.focus(); if (channel) { @@ -402,6 +403,11 @@ export function toTitleCase(str) { } export function applyTheme(theme) { + if (!theme.codeTheme) { + theme.codeTheme = Constants.DEFAULT_CODE_THEME; + } + updateCodeTheme(theme.codeTheme); + if (theme.sidebarBg) { changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1); } @@ -425,16 +431,13 @@ export function applyTheme(theme) { changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1); } - if (theme.sidebarTextActiveBg) { - changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'background:' + theme.sidebarTextActiveBg, 1); + if (theme.sidebarTextActiveBorder) { + changeCss('.sidebar--left .nav li.active a:before, .settings-modal .nav-pills>li.active a:before', 'background:' + theme.sidebarTextActiveBorder, 1); } if (theme.sidebarTextActiveColor) { changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2); - } - - if (theme.sidebarTextActiveBg === theme.onlineIndicator) { - changeCss('.sidebar--left .nav-pills__container li.active a .status .online--icon', 'fill:' + theme.sidebarTextActiveColor, 1); + changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1); } if (theme.sidebarHeaderBg) { @@ -498,7 +501,7 @@ export function applyTheme(theme) { changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 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, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); changeCss('.popover.left>.arrow', 'border-left-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); @@ -513,7 +516,7 @@ export function applyTheme(theme) { changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1); changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); - changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); + changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2); changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); @@ -525,7 +528,7 @@ export function applyTheme(theme) { 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, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); + changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1); changeCss('.post.current--user:hover .post-body ', 'background: none;', 1); changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2); @@ -591,6 +594,27 @@ export function rgb2hex(rgbIn) { return '#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); } +export function updateCodeTheme(theme) { + const path = '/static/css/highlight/' + theme + '.css'; + const $link = $('link.code_theme'); + if (path !== $link.attr('href')) { + changeCss('code.hljs', 'visibility: hidden'); + var xmlHTTP = new XMLHttpRequest(); + xmlHTTP.open('GET', path, true); + xmlHTTP.onload = function onLoad() { + $link.attr('href', path); + if (isBrowserFirefox()) { + $link.one('load', () => { + changeCss('code.hljs', 'visibility: visible'); + }); + } else { + changeCss('code.hljs', 'visibility: visible'); + } + }; + xmlHTTP.send(); + } +} + export function placeCaretAtEnd(el) { el.focus(); if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') { @@ -873,7 +897,7 @@ export function getFileUrl(filename) { if (url.indexOf('/api/v1/files/get') !== -1) { url = filename.split('/api/v1/files/get')[1]; } - url = getWindowLocationOrigin() + '/api/v1/files/get' + url; + url = getWindowLocationOrigin() + '/api/v1/files/get' + url + '?' + getSessionIndex(); return url; } @@ -884,6 +908,14 @@ export function getFileName(path) { return split[split.length - 1]; } +export function getSessionIndex() { + if (global.window.mm_session_token_index >= 0) { + return 'session_token_index=' + global.window.mm_session_token_index; + } + + return ''; +} + // Generates a RFC-4122 version 4 compliant globally unique identifier. export function generateId() { // implementation taken from http://stackoverflow.com/a/2117523 |