diff options
Diffstat (limited to 'web/react/components')
53 files changed, 873 insertions, 331 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_post.jsx b/web/react/components/create_post.jsx index 035899592..055be112d 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(); @@ -51,7 +52,7 @@ export default class CreatePost extends React.Component { submitting: false, initialText: draft.messageText, windowWidth: Utils.windowWidth(), - windowHeigth: Utils.windowHeight() + windowHeight: Utils.windowHeight() }; } handleResize() { @@ -71,7 +72,7 @@ export default class CreatePost extends React.Component { return; } - if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeigth) { + if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeight) { this.resizePostHolder(); return; } @@ -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(); @@ -208,7 +221,7 @@ export default class CreatePost extends React.Component { PostStore.storeCurrentDraft(draft); } resizePostHolder() { - const height = this.state.windowHeigth - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; + const height = this.state.windowHeight - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; $('.post-list-holder-by-time').css('height', `${height}px`); if (this.state.windowWidth > 960) { $('#post_textbox').focus(); 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/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/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 4402745e1..3ceef478c 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -12,7 +12,7 @@ const UserStore = require('../stores/user_store.jsx'); const SocketStore = require('../stores/socket_store.jsx'); const PreferenceStore = require('../stores/preference_store.jsx'); -const utils = require('../utils/utils.jsx'); +const Utils = require('../utils/utils.jsx'); const Client = require('../utils/client.jsx'); const Constants = require('../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -40,11 +40,14 @@ export default class PostList extends React.Component { this.loadFirstPosts = this.loadFirstPosts.bind(this); this.activate = this.activate.bind(this); this.deactivate = this.deactivate.bind(this); - this.resize = this.resize.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; state.isFirstLoadComplete = false; + state.windowHeight = Utils.windowHeight(); this.state = state; } @@ -115,12 +118,7 @@ export default class PostList extends React.Component { const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); - $(window).resize(() => { - this.resize(); - if (!this.scrolled) { - this.scrollToBottom(); - } - }); + window.addEventListener('resize', this.handleResize); postHolder.on('scroll', () => { const position = postHolder.scrollTop() + postHolder.height() + 14; @@ -154,7 +152,7 @@ export default class PostList extends React.Component { this.loadFirstPosts(this.props.channelId); } - this.resize(); + this.resizePostList(); this.onChange(); this.scrollToBottom(); } @@ -164,7 +162,9 @@ export default class PostList extends React.Component { SocketStore.removeChangeListener(this.onSocketChange); PreferenceStore.removeChangeListener(this.onTimeChange); $('body').off('click.userpopover'); - $(window).off('resize'); + + window.removeEventListener('resize', this.handleResize); + var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); postHolder.off('scroll'); } @@ -173,6 +173,13 @@ export default class PostList extends React.Component { return; } + if (prevState.windowHeight !== this.state.windowHeight) { + this.resizePostList(); + if (!this.scrolled) { + this.scrollToBottom(); + } + } + $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); @@ -199,10 +206,11 @@ 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 && - !utils.isComment(firstPost)) { + !firstPost.props.from_webhook && + !Utils.isComment(firstPost)) { this.scrollToBottom(true); // the user clicked 'load more messages' @@ -231,10 +239,20 @@ export default class PostList extends React.Component { this.deactivate(); } } - resize() { + updateScroll() { + if (!this.scrolled) { + this.scrollToBottom(); + } + } + handleResize() { + this.setState({ + windowHeight: Utils.windowHeight() + }); + } + resizePostList() { const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); if ($('#create_post').length > 0) { - const height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; + const height = this.state.windowHeight - $('#create_post').height() - $('#error_bar').outerHeight() - 50; postHolder.css('height', height + 'px'); } } @@ -280,7 +298,7 @@ export default class PostList extends React.Component { onChange() { var newState = this.getStateFromStores(this.props.channelId); - if (!utils.areStatesEqual(newState.postList, this.state.postList)) { + if (!Utils.areStatesEqual(newState.postList, this.state.postList)) { this.setState(newState); } } @@ -310,7 +328,7 @@ export default class PostList extends React.Component { } } createDMIntroMessage(channel) { - var teammate = utils.getDirectTeammate(channel.id); + var teammate = Utils.getDirectTeammate(channel.id); if (teammate) { var teammateName = teammate.username; @@ -323,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' /> @@ -370,13 +388,13 @@ export default class PostList extends React.Component { createDefaultIntroMessage(channel) { return ( <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4> + <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4> <p className='channel-intro__content'> - Welcome to {channel.display_name}! + {'Welcome to ' + channel.display_name + '!'} <br/><br/> - This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know. + {'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'} <br/><br/> - To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”. + {'To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”.'} <br/> </p> </div> @@ -385,7 +403,7 @@ export default class PostList extends React.Component { createOffTopicIntroMessage(channel) { return ( <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4> + <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4> <p className='channel-intro__content'> {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'} <br/> @@ -399,7 +417,7 @@ export default class PostList extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>Set a description + <i className='fa fa-pencil'></i>{'Set a description'} </a> <a className='intro-links' @@ -407,7 +425,7 @@ export default class PostList extends React.Component { data-toggle='modal' data-target='#channel_invite' > - <i className='fa fa-user-plus'></i>Invite others to this channel + <i className='fa fa-user-plus'></i>{'Invite others to this channel'} </a> </div> ); @@ -422,7 +440,7 @@ export default class PostList extends React.Component { var members = ChannelStore.getExtraInfo(channel.id).members; for (var i = 0; i < members.length; i++) { - if (utils.isAdmin(members[i].roles)) { + if (Utils.isAdmin(members[i].roles)) { return members[i].username; } } @@ -443,14 +461,14 @@ export default class PostList extends React.Component { var createMessage; if (creatorName === '') { - createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.'; + createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + Utils.displayDate(channel.create_at) + '.'; } else { - createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{utils.displayDate(channel.create_at)}</strong></span>); + createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{Utils.displayDate(channel.create_at)}</strong></span>); } return ( <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {uiName}</h4> + <h4 className='channel-intro__title'>{'Beginning of ' + uiName}</h4> <p className='channel-intro__content'> {createMessage} {memberMessage} @@ -465,7 +483,7 @@ export default class PostList extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>Set a description + <i className='fa fa-pencil'></i>{'Set a description'} </a> <a className='intro-links' @@ -473,7 +491,7 @@ export default class PostList extends React.Component { data-toggle='modal' data-target='#channel_invite' > - <i className='fa fa-user-plus'></i>Invite others to this {uiType} + <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType} </a> </div> ); @@ -507,7 +525,7 @@ export default class PostList extends React.Component { if (prevPost) { sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5; - sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); + sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); // hide the profile pic if: // the previous post was made by the same user as the current post, @@ -516,8 +534,8 @@ export default class PostList extends React.Component { // the current post is not from a webhook // and the previous post is not from a webhook if ((prevPost.user_id === post.user_id) && - !utils.isComment(prevPost) && - !utils.isComment(post) && + !Utils.isComment(prevPost) && + !Utils.isComment(post) && (!post.props || !post.props.from_webhook) && (!prevPost.props || !prevPost.props.from_webhook)) { hideProfilePic = true; @@ -526,7 +544,7 @@ export default class PostList extends React.Component { // check if it's the last comment in a consecutive string of comments on the same post // it is the last comment if it is last post in the channel or the next post has a different root post - var isLastComment = utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); + var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); var postCtl = ( <Post @@ -539,10 +557,11 @@ export default class PostList extends React.Component { posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} + resize={this.updateScroll} /> ); - let currentPostDay = utils.getDateForUnixTicks(post.create_at); + const currentPostDay = Utils.getDateForUnixTicks(post.create_at); if (currentPostDay.toDateString() !== previousPostDay.toDateString()) { postCtls.push( <div @@ -558,9 +577,9 @@ export default class PostList extends React.Component { if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) { renderedLastViewed = true; - // Temporary fix to solve ie10/11 rendering issue + // Temporary fix to solve ie11 rendering issue let newSeparatorId = ''; - if (!utils.isBrowserIE()) { + if (!Utils.isBrowserIE()) { newSeparatorId = 'new_message_' + this.props.channelId; } postCtls.push( @@ -572,7 +591,7 @@ export default class PostList extends React.Component { <hr className='separator__hr' /> - <div className='separator__text'>New Messages</div> + <div className='separator__text'>{'New Messages'}</div> </div> ); } @@ -638,7 +657,7 @@ export default class PostList extends React.Component { order = this.state.postList.order; } - var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>; + var moreMessages = <p className='beginning-messages-text'>{'Beginning of Channel'}</p>; if (channel != null) { if (order.length >= this.state.numToDisplay) { moreMessages = ( @@ -648,7 +667,7 @@ export default class PostList extends React.Component { href='#' onClick={this.loadMorePosts} > - Load more messages + {'Load more messages'} </a> ); } else { 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..ed2c84057 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -183,8 +183,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; 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..70e559c30 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.'}); @@ -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 |