diff options
Diffstat (limited to 'web/react/components')
20 files changed, 351 insertions, 249 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 16768a119..a19e5c16e 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -13,21 +13,23 @@ function getStateFromStoresForAudits() { } module.exports = React.createClass({ + displayName: 'AccessHistoryModal', componentDidMount: function() { - UserStore.addAuditsChangeListener(this._onChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function(e) { + UserStore.addAuditsChangeListener(this.onListenerChange); + $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function() { AsyncClient.getAudits(); }); var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { + $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function() { + $('#user_settings').modal('show'); self.setState({moreInfo: []}); }); }, componentWillUnmount: function() { - UserStore.removeAuditsChangeListener(this._onChange); + UserStore.removeAuditsChangeListener(this.onListenerChange); }, - _onChange: function() { + onListenerChange: function() { var newState = getStateFromStoresForAudits(); if (!utils.areStatesEqual(newState.audits, this.state.audits)) { this.setState(newState); @@ -61,6 +63,21 @@ module.exports = React.createClass({ currentAudit.session_id = 'N/A (Login attempt)'; } + var moreInfo = (<a href='#' className='theme' onClick={this.handleMoreInfo.bind(this, i)}>More info</a>); + if (this.state.moreInfo[i]) { + moreInfo = ( + <div> + <div>{'Session ID: ' + currentAudit.session_id}</div> + <div>{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}</div> + </div> + ); + } + + var divider = null; + if (i < this.state.audits.length - 1) { + divider = (<div className='divider-light'></div>) + } + accessList[i] = ( <div className='access-history__table'> <div className='access__date'>{newDate}</div> @@ -68,25 +85,21 @@ module.exports = React.createClass({ <div className='report__time'>{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'})}</div> <div className='report__info'> <div>{'IP: ' + currentAudit.ip_address}</div> - {this.state.moreInfo[i] ? - <div> - <div>{'Session ID: ' + currentAudit.session_id}</div> - <div>{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}</div> - </div> - : - <a href='#' className='theme' onClick={this.handleMoreInfo.bind(this, i)}>More info</a> - } + {moreInfo} </div> - {i < this.state.audits.length - 1 ? - <div className='divider-light'/> - : - null - } + {divider} </div> </div> ); } + var content; + if (this.state.audits.loading) { + content = (<LoadingScreen />); + } else { + content = (<form role='form'>{accessList}</form>); + } + return ( <div> <div className='modal fade' ref='modal' id='access-history' tabIndex='-1' role='dialog' aria-hidden='true'> @@ -97,13 +110,7 @@ module.exports = React.createClass({ <h4 className='modal-title' id='myModalLabel'>Access History</h4> </div> <div ref='modalBody' className='modal-body'> - {!this.state.audits.loading ? - <form role='form'> - {accessList} - </form> - : - <LoadingScreen /> - } + {content} </div> </div> </div> diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index f28f0d5f1..1192a72bc 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -10,40 +10,41 @@ var utils = require('../utils/utils.jsx'); function getStateFromStoresForSessions() { return { sessions: UserStore.getSessions(), - server_error: null, - client_error: null + serverError: null, + clientError: null }; } module.exports = React.createClass({ + displayName: 'ActivityLogModal', submitRevoke: function(altId) { - var self = this; Client.revokeSession(altId, function(data) { AsyncClient.getSessions(); }.bind(this), function(err) { - state = getStateFromStoresForSessions(); - state.server_error = err; + var state = getStateFromStoresForSessions(); + state.serverError = err; this.setState(state); }.bind(this) ); }, componentDidMount: function() { - UserStore.addSessionsChangeListener(this._onChange); + UserStore.addSessionsChangeListener(this.onListenerChange); $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function (e) { AsyncClient.getSessions(); }); var self = this; $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({ moreInfo: [] }); + $('#user_settings').modal('show'); + self.setState({moreInfo: []}); }); }, componentWillUnmount: function() { - UserStore.removeSessionsChangeListener(this._onChange); + UserStore.removeSessionsChangeListener(this.onListenerChange); }, - _onChange: function() { + onListenerChange: function() { var newState = getStateFromStoresForSessions(); if (!utils.areStatesEqual(newState.sessions, this.state.sessions)) { this.setState(newState); @@ -52,7 +53,7 @@ module.exports = React.createClass({ handleMoreInfo: function(index) { var newMoreInfo = this.state.moreInfo; newMoreInfo[index] = true; - this.setState({ moreInfo: newMoreInfo }); + this.setState({moreInfo: newMoreInfo}); }, getInitialState: function() { var initialState = getStateFromStoresForSessions(); @@ -61,68 +62,76 @@ module.exports = React.createClass({ }, render: function() { var activityList = []; - var server_error = this.state.server_error ? this.state.server_error : null; + var serverError = this.state.serverError; + + // Squash any false-y value for server error into null + if (!serverError) { + serverError = null; + } for (var i = 0; i < this.state.sessions.length; i++) { var currentSession = this.state.sessions[i]; var lastAccessTime = new Date(currentSession.last_activity_at); var firstAccessTime = new Date(currentSession.create_at); - var devicePicture = ""; + var devicePicture = ''; - if (currentSession.props.platform === "Windows") { - devicePicture = "fa fa-windows"; + if (currentSession.props.platform === 'Windows') { + devicePicture = 'fa fa-windows'; } - else if (currentSession.props.platform === "Macintosh" || currentSession.props.platform === "iPhone") { - devicePicture = "fa fa-apple"; + else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') { + devicePicture = 'fa fa-apple'; } - else if (currentSession.props.platform === "Linux") { - devicePicture = "fa fa-linux"; + else if (currentSession.props.platform === 'Linux') { + devicePicture = 'fa fa-linux'; + } + + var moreInfo; + if (this.state.moreInfo[i]) { + moreInfo = ( + <div> + <div>{'First time active: ' + firstAccessTime.toDateString() + ', ' + lastAccessTime.toLocaleTimeString()}</div> + <div>{'OS: ' + currentSession.props.os}</div> + <div>{'Browser: ' + currentSession.props.browser}</div> + <div>{'Session ID: ' + currentSession.alt_id}</div> + </div> + ); + } else { + moreInfo = (<a className='theme' href='#' onClick={this.handleMoreInfo.bind(this, i)}>More info</a>); } activityList[i] = ( - <div className="activity-log__table"> - <div className="activity-log__report"> - <div className="report__platform"><i className={devicePicture} />{currentSession.props.platform}</div> - <div className="report__info"> - <div>{"Last activity: " + lastAccessTime.toDateString() + ", " + lastAccessTime.toLocaleTimeString()}</div> - { this.state.moreInfo[i] ? - <div> - <div>{"First time active: " + firstAccessTime.toDateString() + ", " + lastAccessTime.toLocaleTimeString()}</div> - <div>{"OS: " + currentSession.props.os}</div> - <div>{"Browser: " + currentSession.props.browser}</div> - <div>{"Session ID: " + currentSession.alt_id}</div> - </div> - : - <a className="theme" href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> - } + <div className='activity-log__table'> + <div className='activity-log__report'> + <div className='report__platform'><i className={devicePicture} />{currentSession.props.platform}</div> + <div className='report__info'> + <div>{'Last activity: ' + lastAccessTime.toDateString() + ', ' + lastAccessTime.toLocaleTimeString()}</div> + {moreInfo} </div> </div> - <div className="activity-log__action"><button onClick={this.submitRevoke.bind(this, currentSession.alt_id)} className="btn btn-primary">Logout</button></div> + <div className='activity-log__action'><button onClick={this.submitRevoke.bind(this, currentSession.alt_id)} className='btn btn-primary'>Logout</button></div> </div> ); } + var content; + if (this.state.sessions.loading) { + content = (<LoadingScreen />); + } else { + content = (<form role='form'>{activityList}</form>); + } + return ( <div> - <div className="modal fade" ref="modal" id="activity-log" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog modal-lg"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" id="myModalLabel">Active Sessions</h4> + <div className='modal fade' ref='modal' id='activity-log' tabIndex='-1' role='dialog' aria-hidden='true'> + <div className='modal-dialog modal-lg'> + <div className='modal-content'> + <div className='modal-header'> + <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> + <h4 className='modal-title' id='myModalLabel'>Active Sessions</h4> </div> - <p className="session-help-text">Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the "Logout" button below to end a session.</p> - <div ref="modalBody" className="modal-body"> - { !this.state.sessions.loading ? - <div> - <form role="form"> - { activityList } - </form> - { server_error } - </div> - : - <LoadingScreen /> - } + <p className='session-help-text'>Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the 'Logout' button below to end a session.</p> + <div ref='modalBody' className='modal-body'> + {content} </div> </div> </div> diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 4d64e2b94..90a776791 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -156,7 +156,7 @@ module.exports = React.createClass({ } var channel = this.state.channel; - var description = utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true, noTextFormatting: true}); + var description = utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true}); var popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>); var channelTitle = channel.display_name; var currentId = UserStore.getCurrentId(); diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index a0a018025..885efab7a 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -122,16 +122,20 @@ module.exports = React.createClass({ this.setState({uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']}); }, handleUploadError: function(err, clientId) { - var draft = PostStore.getCommentDraft(this.props.rootId); + if (clientId !== -1) { + var draft = PostStore.getCommentDraft(this.props.rootId); - var index = draft['uploadsInProgress'].indexOf(clientId); - if (index !== -1) { - draft['uploadsInProgress'].splice(index, 1); - } + var index = draft['uploadsInProgress'].indexOf(clientId); + if (index !== -1) { + draft['uploadsInProgress'].splice(index, 1); + } - PostStore.storeCommentDraft(this.props.rootId, draft); + PostStore.storeCommentDraft(this.props.rootId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + } else { + this.setState({serverError: err}); + } }, clearPreviews: function() { this.setState({previews: []}); @@ -184,7 +188,6 @@ module.exports = React.createClass({ </div> ); } - var allowTextFormatting = config.AllowTextFormatting; var postError = null; if (this.state.postError) { @@ -205,10 +208,6 @@ module.exports = React.createClass({ if (postError) { postFooterClassName += ' has-error'; } - var extraInfo = <MsgTyping channelId={this.props.channelId} parentId={this.props.rootId} />; - if (this.state.messageText.split(' ').length > 1 && allowTextFormatting) { - extraInfo = <span className='msg-format-help'>_<em>italics</em>_ *<strong>bold</strong>* `<code className='code-info'>code</code>`</span>; - } return ( <form onSubmit={this.handleSubmit}> @@ -227,9 +226,11 @@ module.exports = React.createClass({ getFileCount={this.getFileCount} onUploadStart={this.handleUploadStart} onFileUpload={this.handleFileUploadComplete} - onUploadError={this.handleUploadError} /> + onUploadError={this.handleUploadError} + postType='comment' + channelId={this.props.channelId} /> </div> - {extraInfo} + <MsgTyping channelId={this.props.channelId} parentId={this.props.rootId} /> <div className={postFooterClassName}> <input type='button' className='btn btn-primary comment-btn pull-right' value='Add Comment' onClick={this.handleSubmit} /> {postError} diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 3e1faba7d..377e7bd34 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -145,16 +145,20 @@ module.exports = React.createClass({ this.setState({uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']}); }, handleUploadError: function(err, clientId) { - var draft = PostStore.getDraft(this.state.channelId); + if (clientId !== -1) { + var draft = PostStore.getDraft(this.state.channelId); - var index = draft['uploadsInProgress'].indexOf(clientId); - if (index !== -1) { - draft['uploadsInProgress'].splice(index, 1); - } + var index = draft['uploadsInProgress'].indexOf(clientId); + if (index !== -1) { + draft['uploadsInProgress'].splice(index, 1); + } - PostStore.storeDraft(this.state.channelId, draft); + PostStore.storeDraft(this.state.channelId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + } else { + this.setState({serverError: err}); + } }, removePreview: function(id) { var previews = this.state.previews; @@ -224,7 +228,6 @@ module.exports = React.createClass({ </div> ); } - var allowTextFormatting = config.AllowTextFormatting; var postError = null; if (this.state.postError) { @@ -245,10 +248,6 @@ module.exports = React.createClass({ if (postError) { postFooterClassName += ' has-error'; } - var extraInfo = <MsgTyping channelId={this.state.channel_id} parentId='' />; - if (this.state.messageText.split(' ').length > 1 && allowTextFormatting) { - extraInfo = <span className='msg-typing'>_<em>italics</em>_ *<strong>bold</strong>* `<code className='code-info'>code</code>`</span>; - } return ( <form id='create_post' ref='topDiv' role='form' onSubmit={this.handleSubmit}> @@ -267,13 +266,15 @@ module.exports = React.createClass({ getFileCount={this.getFileCount} onUploadStart={this.handleUploadStart} onFileUpload={this.handleFileUploadComplete} - onUploadError={this.handleUploadError} /> + onUploadError={this.handleUploadError} + postType='post' + channelId='' /> </div> <div className={postFooterClassName}> {postError} {serverError} {preview} - {extraInfo} + <MsgTyping channelId={this.state.channelId} parentId=''/> </div> </div> </form> diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index b7ea5734f..c36c908d2 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -113,6 +113,14 @@ module.exports = React.createClass({ fileSizeString = utils.fileSizeToString(this.state.fileSize); } + var filenameString = decodeURIComponent(utils.getFileName(filename)); + var trimmedFilename; + if (filenameString.length > 35) { + trimmedFilename = filenameString.substring(0, Math.min(35, filenameString.length)) + "..."; + } else { + trimmedFilename = filenameString; + } + return ( <div className="post-image__column" key={filename}> <a className="post-image__thumbnail" href="#" onClick={this.props.handleImageClick} @@ -120,7 +128,7 @@ module.exports = React.createClass({ {thumbnail} </a> <div className="post-image__details"> - <div className="post-image__name">{decodeURIComponent(utils.getFileName(filename))}</div> + <div data-toggle="tooltip" title={filenameString} className="post-image__name">{trimmedFilename}</div> <div> <span className="post-image__type">{fileInfo.ext.toUpperCase()}</span> <span className="post-image__size">{fileSizeString}</span> diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index c1fab669c..7497ec330 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -12,7 +12,9 @@ module.exports = React.createClass({ onUploadError: React.PropTypes.func, getFileCount: React.PropTypes.func, onFileUpload: React.PropTypes.func, - onUploadStart: React.PropTypes.func + onUploadStart: React.PropTypes.func, + channelId: React.PropTypes.string, + postType: React.PropTypes.string }, getInitialState: function() { return {requests: {}}; @@ -21,7 +23,7 @@ module.exports = React.createClass({ var element = $(this.refs.fileInput.getDOMNode()); var files = element.prop('files'); - var channelId = ChannelStore.getCurrentId(); + var channelId = this.props.channelId || ChannelStore.getCurrentId(); this.props.onUploadError(null); @@ -61,8 +63,8 @@ module.exports = React.createClass({ this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); var requests = this.state.requests; - for (var i = 0; i < parsedData.client_ids.length; i++) { - delete requests[parsedData.client_ids[i]]; + for (var j = 0; j < parsedData.client_ids.length; j++) { + delete requests[parsedData.client_ids[j]]; } this.setState({requests: requests}); }.bind(this), @@ -87,10 +89,94 @@ module.exports = React.createClass({ } } catch(e) {} }, + handleDrop: function(e) { + this.props.onUploadError(null); + + var files = e.originalEvent.dataTransfer.files; + var channelId = this.props.channelId || ChannelStore.getCurrentId(); + + if (typeof files !== 'string' && files.length) { + var numFiles = files.length; + + var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId), numFiles); + + if (numFiles > numToUpload) { + this.props.onUploadError('Uploads limited to ' + Constants.MAX_UPLOAD_FILES + ' files maximum. Please use additional posts for more files.'); + } + + for (var i = 0; i < files.length && i < numToUpload; i++) { + if (files[i].size > Constants.MAX_FILE_SIZE) { + this.props.onUploadError('Files must be no more than ' + Constants.MAX_FILE_SIZE / 1000000 + ' MB'); + continue; + } + + // generate a unique id that can be used by other components to refer back to this file upload + var clientId = utils.generateId(); + + // Prepare data to be uploaded. + var formData = new FormData(); + formData.append('channel_id', channelId); + formData.append('files', files[i], files[i].name); + formData.append('client_ids', clientId); + + var request = client.uploadFile(formData, + function(data) { + var parsedData = $.parseJSON(data); + this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); + + var requests = this.state.requests; + for (var j = 0; j < parsedData.client_ids.length; j++) { + delete requests[parsedData.client_ids[j]]; + } + this.setState({requests: requests}); + }.bind(this), + function(err) { + this.props.onUploadError(err, clientId); + }.bind(this) + ); + + var requests = this.state.requests; + requests[clientId] = request; + this.setState({requests: requests}); + + this.props.onUploadStart([clientId], channelId); + } + } else { + this.props.onUploadError('Invalid file upload', -1); + } + }, componentDidMount: function() { var inputDiv = this.refs.input.getDOMNode(); var self = this; + if (this.props.postType === 'post') { + $('.row.main').dragster({ + enter: function() { + $('.center-file-overlay').removeClass('hidden'); + }, + leave: function() { + $('.center-file-overlay').addClass('hidden'); + }, + drop: function(dragsterEvent, e) { + $('.center-file-overlay').addClass('hidden'); + self.handleDrop(e); + } + }); + } else if (this.props.postType === 'comment') { + $('.post-right__container').dragster({ + enter: function() { + $('.right-file-overlay').removeClass('hidden'); + }, + leave: function() { + $('.right-file-overlay').addClass('hidden'); + }, + drop: function(dragsterEvent, e) { + $('.right-file-overlay').addClass('hidden'); + self.handleDrop(e); + } + }); + } + document.addEventListener('paste', function(e) { var textarea = $(inputDiv.parentNode.parentNode).find('.custom-textarea')[0]; @@ -133,14 +219,13 @@ module.exports = React.createClass({ continue; } - var channelId = ChannelStore.getCurrentId(); + var channelId = this.props.channelId || ChannelStore.getCurrentId(); // generate a unique id that can be used by other components to refer back to this file upload var clientId = utils.generateId(); var formData = new FormData(); formData.append('channel_id', channelId); - var d = new Date(); var hour; if (d.getHours() < 10) { @@ -165,8 +250,8 @@ module.exports = React.createClass({ self.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); var requests = self.state.requests; - for (var i = 0; i < parsedData.client_ids.length; i++) { - delete requests[parsedData.client_ids[i]]; + for (var j = 0; j < parsedData.client_ids.length; j++) { + delete requests[parsedData.client_ids[j]]; } self.setState({requests: requests}); }, diff --git a/web/react/components/file_upload_overlay.jsx b/web/react/components/file_upload_overlay.jsx new file mode 100644 index 000000000..f35556371 --- /dev/null +++ b/web/react/components/file_upload_overlay.jsx @@ -0,0 +1,26 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +module.exports = React.createClass({ + displayName: 'FileUploadOverlay', + propTypes: { + overlayType: React.PropTypes.string + }, + render: function() { + var overlayClass = 'file-overlay hidden'; + if (this.props.overlayType === 'right') { + overlayClass += ' right-file-overlay'; + } else if (this.props.overlayType === 'center') { + overlayClass += ' center-file-overlay'; + } + + return ( + <div className={overlayClass}> + <div> + <i className='fa fa-upload'></i> + <span>Drop a file to upload it.</span> + </div> + </div> + ); + } +}); diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index c395bb500..f9eacf094 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -49,7 +49,7 @@ module.exports = React.createClass({ var redirect = utils.getUrlParameter('redirect'); if (redirect) { - window.location.pathname = decodeURI(redirect); + window.location.pathname = decodeURIComponent(redirect); } else { window.location.pathname = '/' + name + '/channels/town-square'; } diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index e72a2d001..f099c67ab 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -11,12 +11,6 @@ var ActionTypes = Constants.ActionTypes; module.exports = React.createClass({ displayName: "Post", - componentDidMount: function() { - $('.modal').on('show.bs.modal', function () { - $('.modal-body').css('overflow-y', 'auto'); - $('.modal-body').css('max-height', $(window).height() * 0.7); - }); - }, handleCommentClick: function(e) { e.preventDefault(); diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index fab6833e6..860c96d84 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -4,69 +4,59 @@ var FileAttachmentList = require('./file_attachment_list.jsx'); var UserStore = require('../stores/user_store.jsx'); var utils = require('../utils/utils.jsx'); -var formatText = require('../../static/js/marked/lib/marked.js'); module.exports = React.createClass({ componentWillReceiveProps: function(nextProps) { var linkData = utils.extractLinks(nextProps.post.message); - this.setState({links: linkData.links, message: linkData.text}); + this.setState({ links: linkData["links"], message: linkData["text"] }); }, getInitialState: function() { var linkData = utils.extractLinks(this.props.post.message); - return {links: linkData.links, message: linkData.text}; + return { links: linkData["links"], message: linkData["text"] }; }, render: function() { var post = this.props.post; var filenames = this.props.post.filenames; var parentPost = this.props.parentPost; var inner = utils.textToJsx(this.state.message); - var allowTextFormatting = config.AllowTextFormatting; - var comment = ''; - var postClass = ''; + var comment = ""; + var reply = ""; + var postClass = ""; if (parentPost) { var profile = UserStore.getProfile(parentPost.user_id); - var apostrophe = ''; - var name = '...'; + var apostrophe = ""; + var name = "..."; if (profile != null) { if (profile.username.slice(-1) === 's') { apostrophe = "'"; } else { apostrophe = "'s"; } - name = <a className='theme' onClick={utils.searchForTerm.bind(this, profile.username)}>{profile.username}</a>; + name = <a className="theme" onClick={function(){ utils.searchForTerm(profile.username); }}>{profile.username}</a>; } - var message = ''; - if (parentPost.message) { - message = utils.replaceHtmlEntities(parentPost.message); + var message = "" + if(parentPost.message) { + message = utils.replaceHtmlEntities(parentPost.message) } else if (parentPost.filenames.length) { message = parentPost.filenames[0].split('/').pop(); if (parentPost.filenames.length === 2) { - message += ' plus 1 other file'; + message += " plus 1 other file"; } else if (parentPost.filenames.length > 2) { - message += ' plus ' + (parentPost.filenames.length - 1) + ' other files'; + message += " plus " + (parentPost.filenames.length - 1) + " other files"; } } - if (allowTextFormatting) { - message = formatText(message, {sanitize: true, mangle: false, gfm: true, breaks: true, tables: false, smartypants: true, renderer: utils.customMarkedRenderer({disable: true})}); - comment = ( - <p className='post-link'> - <span>Commented on {name}{apostrophe} message: <a className='theme' onClick={this.props.handleCommentClick} dangerouslySetInnerHTML={{__html: message}} /></span> - </p> - ); - } else { - comment = ( - <p className='post-link'> - <span>Commented on {name}{apostrophe} message: <a className='theme' onClick={this.props.handleCommentClick}>{message}</a></span> - </p> - ); - } + comment = ( + <p className="post-link"> + <span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick}>{message}</a></span> + </p> + ); - postClass += ' post-comment'; + postClass += " post-comment"; } var embed; @@ -74,26 +64,18 @@ module.exports = React.createClass({ embed = utils.getEmbed(this.state.links[0]); } - var innerHolder = <p key={post.id + '_message'} className={postClass}><span>{inner}</span></p>; - if (allowTextFormatting) { - innerHolder = <div key={post.id + '_message'} className={postClass}><span>{inner}</span></div>; - } - - var fileAttachmentHolder = ''; - if (filenames && filenames.length > 0) { - fileAttachmentHolder = (<FileAttachmentList - filenames={filenames} - modalId={'view_image_modal_' + post.id} - channelId={post.channel_id} - userId={post.user_id} />); - } - return ( - <div className='post-body'> - {comment} - {innerHolder} - {fileAttachmentHolder} - {embed} + <div className="post-body"> + { comment } + <p key={post.id+"_message"} className={postClass}><span>{inner}</span></p> + { filenames && filenames.length > 0 ? + <FileAttachmentList + filenames={filenames} + modalId={"view_image_modal_" + post.id} + channelId={post.channel_id} + userId={post.user_id} /> + : "" } + { embed } </div> ); } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index ad7f4a8bf..fa74d4295 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -69,6 +69,12 @@ module.exports = React.createClass({ this.oldScrollHeight = post_holder.scrollHeight; this.oldZoom = (window.outerWidth - 8) / window.innerWidth; + $('.modal').on('show.bs.modal', function () { + $('.modal-body').css('overflow-y', 'auto'); + $('.modal-body').css('max-height', $(window).height() * 0.7); + }); + + // Timeout exists for the DOM to fully render before making changes var self = this; $(window).resize(function(){ $(post_holder).perfectScrollbar('update'); @@ -140,6 +146,7 @@ module.exports = React.createClass({ UserStore.removeStatusesChangeListener(this._onTimeChange); SocketStore.removeChangeListener(this._onSocketChange); $('body').off('click.userpopover'); + $('.modal').off('show.bs.modal') }, resize: function() { var post_holder = $(".post-list-holder-by-time")[0]; @@ -197,10 +204,7 @@ module.exports = React.createClass({ var post = post_list.posts[msg.props.post_id]; post.message = msg.props.message; - post.lastEditDate = Date.now(); - post_list.posts[post.id] = post; - this.setState({ post_list: post_list }); PostStore.storePosts(msg.channel_id, post_list); @@ -433,13 +437,8 @@ module.exports = React.createClass({ // 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 postKey = post.id; - if (post.lastEditDate) { - postKey += post.lastEditDate; - } - var postCtl = ( - <Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={postKey} + <Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} /> ); diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index 19e4cf67a..e46979ff7 100644 --- a/web/react/components/post_right.jsx +++ b/web/react/components/post_right.jsx @@ -11,6 +11,7 @@ var SearchBox =require('./search_bar.jsx'); var CreateComment = require( './create_comment.jsx' ); var Constants = require('../utils/constants.jsx'); var FileAttachmentList = require('./file_attachment_list.jsx'); +var FileUploadOverlay = require('./file_upload_overlay.jsx'); var ActionTypes = Constants.ActionTypes; RhsHeaderPost = React.createClass({ @@ -56,7 +57,6 @@ RhsHeaderPost = React.createClass({ RootPost = React.createClass({ render: function() { - var allowTextFormatting = config.AllowTextFormatting; var post = this.props.post; var message = utils.textToJsx(post.message); var isOwner = UserStore.getCurrentId() == post.user_id; @@ -77,11 +77,6 @@ RootPost = React.createClass({ channelName = (channel.type === 'D') ? "Private Message" : channel.display_name; } - var messageHolder = <p>{message}</p>; - if (allowTextFormatting) { - messageHolder = <div>{message}</div>; - } - return ( <div className={"post post--root " + currentUserCss}> <div className="post-right-channel__name">{ channelName }</div> @@ -107,7 +102,7 @@ RootPost = React.createClass({ </li> </ul> <div className="post-body"> - {messageHolder} + <p>{message}</p> { post.filenames && post.filenames.length > 0 ? <FileAttachmentList filenames={post.filenames} @@ -125,7 +120,6 @@ RootPost = React.createClass({ CommentPost = React.createClass({ render: function() { - var allowTextFormatting = config.AllowTextFormatting; var post = this.props.post; var commentClass = "post"; @@ -145,11 +139,6 @@ CommentPost = React.createClass({ var message = utils.textToJsx(post.message); var timestamp = UserStore.getCurrentUser().update_at; - var messageHolder = <p>{message}</p>; - if (allowTextFormatting) { - messageHolder = <div>{message}</div>; - } - return ( <div className={commentClass + " " + currentUserCss}> <div className="post-profile-img__container"> @@ -172,7 +161,7 @@ CommentPost = React.createClass({ </li> </ul> <div className="post-body"> - {messageHolder} + <p>{message}</p> { post.filenames && post.filenames.length > 0 ? <FileAttachmentList filenames={post.filenames} @@ -285,22 +274,11 @@ module.exports = React.createClass({ root_post = post_list.posts[selected_post.root_id]; } - var rootPostKey = root_post.id - if (root_post.lastEditDate) { - rootPostKey += root_post.lastEditDate; - } - var posts_array = []; for (var postId in post_list.posts) { var cpost = post_list.posts[postId]; if (cpost.root_id == root_post.id) { - var cpostKey = cpost.id - if (cpost.lastEditDate) { - cpostKey += cpost.lastEditDate; - } - - cpost.cpostKey = cpostKey; posts_array.push(cpost); } } @@ -319,14 +297,16 @@ module.exports = React.createClass({ return ( <div className="post-right__container"> + <FileUploadOverlay + overlayType='right' /> <div className="search-bar__container sidebar--right__search-header">{searchForm}</div> <div className="sidebar-right__body"> <RhsHeaderPost fromSearch={this.props.fromSearch} isMentionSearch={this.props.isMentionSearch} /> <div className="post-right__scroll"> - <RootPost key={rootPostKey} post={root_post} commentCount={posts_array.length}/> + <RootPost post={root_post} commentCount={posts_array.length}/> <div className="post-right-comments-container"> { posts_array.map(function(cpost) { - return <CommentPost ref={cpost.id} key={cpost.cpostKey} post={cpost} selected={ (cpost.id == selected_post.id) } /> + return <CommentPost ref={cpost.id} key={cpost.id} post={cpost} selected={ (cpost.id == selected_post.id) } /> })} </div> <div className="post-create__container"> diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index 8f6bd861a..643ad112b 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -84,8 +84,6 @@ var SearchItem = React.createClass({ channelName = (channel.type === 'D') ? "Private Message" : channel.display_name; } - var searchItemKey = Date.now().toString(); - return ( <div className="search-item-container post" onClick={this.handleClick}> <div className="search-channel__name">{ channelName }</div> @@ -101,7 +99,7 @@ var SearchItem = React.createClass({ </time> </li> </ul> - <div key={this.props.key + searchItemKey} className="search-item-snippet"><span>{message}</span></div> + <div className="search-item-snippet"><span>{message}</span></div> </div> </div> ); @@ -133,7 +131,6 @@ module.exports = React.createClass({ if (this.isMounted()) { var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { - newState.last_edit_time = Date.now(); this.setState(newState); } } @@ -155,11 +152,6 @@ module.exports = React.createClass({ var noResults = (!results || !results.order || !results.order.length); var searchTerm = PostStore.getSearchTerm(); - var searchItemKey = ""; - if (this.state.last_edit_time) { - searchItemKey += this.state.last_edit_time.toString(); - } - return ( <div className="sidebar--right__content"> <div className="search-bar__container sidebar--right__search-header">{searchForm}</div> @@ -170,7 +162,7 @@ module.exports = React.createClass({ { noResults ? <div className="sidebar--right__subheader">No results</div> : results.order.map(function(id) { var post = results.posts[id]; - return <SearchItem key={searchItemKey + post.id} post={post} term={searchTerm} isMentionSearch={this.props.isMentionSearch} /> + return <SearchItem key={post.id} post={post} term={searchTerm} isMentionSearch={this.props.isMentionSearch} /> }, this) } diff --git a/web/react/components/setting_item_min.jsx b/web/react/components/setting_item_min.jsx index 2209c74d1..3c87e416e 100644 --- a/web/react/components/setting_item_min.jsx +++ b/web/react/components/setting_item_min.jsx @@ -2,12 +2,23 @@ // See License.txt for license information. module.exports = React.createClass({ + displayName: 'SettingsItemMin', + propTypes: { + title: React.PropTypes.string, + disableOpen: React.PropTypes.bool, + updateSection: React.PropTypes.func, + describe: React.PropTypes.string + }, render: function() { + var editButton = ''; + if (!this.props.disableOpen) { + editButton = <li className='col-sm-2 section-edit'><a className='section-edit theme' href='#' onClick={this.props.updateSection}>Edit</a></li>; + } return ( - <ul className="section-min"> - <li className="col-sm-10 section-title">{this.props.title}</li> - <li className="col-sm-2 section-edit"><a className="section-edit theme" href="#" onClick={this.props.updateSection}>Edit</a></li> - <li className="col-sm-7 section-describe">{this.props.describe}</li> + <ul className='section-min'> + <li className='col-sm-10 section-title'>{this.props.title}</li> + {editButton} + <li className='col-sm-7 section-describe'>{this.props.describe}</li> </ul> ); } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 988ef4a9c..a8496b385 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -11,7 +11,6 @@ var BrowserStore = require('../stores/browser_store.jsx'); var utils = require('../utils/utils.jsx'); var SidebarHeader = require('./sidebar_header.jsx'); var SearchBox = require('./search_bar.jsx'); -var formatText = require('../../static/js/marked/lib/marked.js'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; @@ -129,6 +128,7 @@ module.exports = React.createClass({ ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); + TeamStore.addChangeListener(this.onChange); SocketStore.addChangeListener(this.onSocketChange); $('.nav-pills__container').perfectScrollbar(); @@ -147,6 +147,7 @@ module.exports = React.createClass({ ChannelStore.removeChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange); + TeamStore.removeChangeListener(this.onChange); SocketStore.removeChangeListener(this.onSocketChange); }, onChange: function() { @@ -210,11 +211,6 @@ module.exports = React.createClass({ utils.notifyMe(title, username + ' did something new', channel); } } else { - var allowTextFormatting = config.AllowTextFormatting; - if (allowTextFormatting) { - notifyText = formatText(notifyText, {sanitize: false, mangle: false, gfm: true, breaks: true, tables: false, smartypants: true, renderer: utils.customMarkedRenderer({disable: true})}); - } - notifyText = utils.replaceHtmlEntities(notifyText); utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); } if (!user.notify_props || user.notify_props.desktop_sound === 'true') { @@ -354,15 +350,16 @@ module.exports = React.createClass({ // set up click handler to switch channels (or create a new channel for non-existant ones) var clickHandler = null; - var href; + var href = '#'; + var teamURL = TeamStore.getCurrentTeamUrl(); if (!channel.fake) { clickHandler = function(e) { e.preventDefault(); utils.switchChannel(channel); }; - href = '#'; - } else { - href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + } + if (channel.fake && teamURL){ + href = teamURL + '/channels/' + channel.name; } return ( diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index cc3f255ee..761c06e74 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -96,7 +96,7 @@ var NavbarDropdown = React.createClass({ <span className='dropdown__icon' dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> </a> <ul className='dropdown-menu' role='menu'> - <li><a href='#' data-toggle='modal' data-target='#user_settings1'>Account Settings</a></li> + <li><a href='#' data-toggle='modal' data-target='#user_settings'>Account Settings</a></li> {teamSettings} {inviteLink} {teamLink} diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 2439719a1..d221ca840 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -72,7 +72,7 @@ module.exports = React.createClass({ <div className='nav-pills__container'> <ul className='nav nav-pills nav-stacked'> - <li><a href='#' data-toggle='modal' data-target='#user_settings1'><i className='glyphicon glyphicon-cog'></i>Account Settings</a></li> + <li><a href='#' data-toggle='modal' data-target='#user_settings'><i className='glyphicon glyphicon-cog'></i>Account Settings</a></li> {teamSettingsLink} {inviteLink} {teamLink} diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 1a0c313d3..8f29bbe57 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -13,6 +13,7 @@ var assign = require('object-assign'); function getNotificationsStateFromStores() { var user = UserStore.getCurrentUser(); + var soundNeeded = !utils.isBrowserFirefox(); var sound = (!user.notify_props || user.notify_props.desktop_sound == undefined) ? "true" : user.notify_props.desktop_sound; var desktop = (!user.notify_props || user.notify_props.desktop == undefined) ? "all" : user.notify_props.desktop; var email = (!user.notify_props || user.notify_props.email == undefined) ? "true" : user.notify_props.email; @@ -58,7 +59,7 @@ function getNotificationsStateFromStores() { } } - return { notify_level: desktop, enable_email: email, enable_sound: sound, username_key: username_key, mention_key: mention_key, custom_keys: custom_keys, custom_keys_checked: custom_keys.length > 0, first_name_key: first_name_key, all_key: all_key, channel_key: channel_key }; + return { notify_level: desktop, enable_email: email, soundNeeded: soundNeeded, enable_sound: sound, username_key: username_key, mention_key: mention_key, custom_keys: custom_keys, custom_keys_checked: custom_keys.length > 0, first_name_key: first_name_key, all_key: all_key, channel_key: channel_key }; } @@ -105,11 +106,11 @@ var NotificationsTab = React.createClass({ }, componentDidMount: function() { UserStore.addChangeListener(this._onChange); - $('#user_settings1').on('hidden.bs.modal', this.handleClose); + $('#user_settings').on('hidden.bs.modal', this.handleClose); }, componentWillUnmount: function() { UserStore.removeChangeListener(this._onChange); - $('#user_settings1').off('hidden.bs.modal', this.handleClose); + $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); }, _onChange: function() { @@ -235,7 +236,7 @@ var NotificationsTab = React.createClass({ } var soundSection; - if (this.props.activeSection === 'sound') { + if (this.props.activeSection === 'sound' && this.state.soundNeeded) { var soundActive = ["",""]; if (this.state.enable_sound === "false") { soundActive[1] = "active"; @@ -265,7 +266,9 @@ var NotificationsTab = React.createClass({ ); } else { var describe = ""; - if (this.state.enable_sound === "false") { + if (!this.state.soundNeeded) { + describe = "Please configure notification sounds in your browser settings" + } else if (this.state.enable_sound === "false") { describe = "Off"; } else { describe = "On"; @@ -276,6 +279,7 @@ var NotificationsTab = React.createClass({ title="Desktop notification sounds" describe={describe} updateSection={function(){self.props.updateSection("sound");}} + disableOpen = {!this.state.soundNeeded} /> ); } @@ -513,27 +517,34 @@ var SecurityTab = React.createClass({ this.setState({confirmPassword: e.target.value}); }, handleHistoryOpen: function() { - $('#user_settings1').modal('hide'); + this.setState({willReturn: true}); + $("#user_settings").modal('hide'); }, handleDevicesOpen: function() { - $('#user_settings1').modal('hide'); + this.setState({willReturn: true}); + $("#user_settings").modal('hide'); }, handleClose: function() { $(this.getDOMNode()).find('.form-control').each(function() { this.value = ''; }); this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); - this.props.updateTab('general'); + + if (!this.state.willReturn) { + this.props.updateTab('general'); + } else { + this.setState({willReturn: false}); + } }, componentDidMount: function() { - $('#user_settings1').on('hidden.bs.modal', this.handleClose); + $('#user_settings').on('hidden.bs.modal', this.handleClose); }, componentWillUnmount: function() { - $('#user_settings1').off('hidden.bs.modal', this.handleClose); + $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); }, getInitialState: function() { - return {currentPassword: '', newPassword: '', confirmPassword: ''}; + return {currentPassword: '', newPassword: '', confirmPassword: '', willReturn: false}; }, render: function() { var serverError = this.state.serverError ? this.state.serverError : null; @@ -811,10 +822,10 @@ var GeneralTab = React.createClass({ this.props.updateSection(''); }, componentDidMount: function() { - $('#user_settings1').on('hidden.bs.modal', this.handleClose); + $('#user_settings').on('hidden.bs.modal', this.handleClose); }, componentWillUnmount: function() { - $('#user_settings1').off('hidden.bs.modal', this.handleClose); + $('#user_settings').off('hidden.bs.modal', this.handleClose); }, getInitialState: function() { var user = this.props.user; @@ -1093,7 +1104,7 @@ var AppearanceTab = React.createClass({ if (this.props.activeSection === "theme") { $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); } - $('#user_settings1').on('hidden.bs.modal', this.handleClose); + $('#user_settings').on('hidden.bs.modal', this.handleClose); }, componentDidUpdate: function() { if (this.props.activeSection === "theme") { @@ -1102,7 +1113,7 @@ var AppearanceTab = React.createClass({ } }, componentWillUnmount: function() { - $('#user_settings1').off('hidden.bs.modal', this.handleClose); + $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); }, getInitialState: function() { diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings_modal.jsx index 702e7ad7a..7181c4020 100644 --- a/web/react/components/user_settings_modal.jsx +++ b/web/react/components/user_settings_modal.jsx @@ -32,7 +32,7 @@ module.exports = React.createClass({ tabs.push({name: "appearance", ui_name: "Appearance", icon: "glyphicon glyphicon-wrench"}); return ( - <div className="modal fade" ref="modal" id="user_settings1" role="dialog" tabIndex="-1" aria-hidden="true"> + <div className="modal fade" ref="modal" id="user_settings" role="dialog" tabIndex="-1" aria-hidden="true"> <div className="modal-dialog settings-modal"> <div className="modal-content"> <div className="modal-header"> @@ -64,4 +64,3 @@ module.exports = React.createClass({ ); } }); - |