diff options
Diffstat (limited to 'web/react')
36 files changed, 1904 insertions, 1129 deletions
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 525b67b5c..0fa433383 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -5,63 +5,83 @@ to the server on page load. This is to prevent other React controls from spamming AsyncClient with requests. */ -var BrowserStore = require('../stores/browser_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var PostStore = require('../stores/post_store.jsx'); +var UserStore = require('../stores/user_store.jsx'); var Constants = require('../utils/constants.jsx'); +var utils = require('../utils/utils.jsx'); + module.exports = React.createClass({ componentDidMount: function() { - /* Start initial aysnc loads */ + /* Initial aysnc loads */ AsyncClient.getMe(); - AsyncClient.getPosts(true, ChannelStore.getCurrentId(), Constants.POST_CHUNK_SIZE); + AsyncClient.getPosts(ChannelStore.getCurrentId()); AsyncClient.getChannels(true, true); AsyncClient.getChannelExtraInfo(true); AsyncClient.findTeams(); AsyncClient.getStatuses(); AsyncClient.getMyTeam(); - /* End of async loads */ /* Perform pending post clean-up */ PostStore.clearPendingPosts(); - /* End pending post clean-up */ - /* Start interval functions */ + /* Set up interval functions */ setInterval( function pollStatuses() { AsyncClient.getStatuses(); }, 30000); - /* End interval functions */ - /* Start device tracking setup */ + /* Device tracking setup */ var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); if (iOS) { $('body').addClass('ios'); } - /* End device tracking setup */ - /* Start window active tracking setup */ + /* Set up tracking for whether the window is active */ window.isActive = true; - $(window).focus(function() { + $(window).focus(function windowFocus() { AsyncClient.updateLastViewedAt(); window.isActive = true; }); - $(window).blur(function() { + $(window).blur(function windowBlur() { window.isActive = false; }); - /* End window active tracking setup */ /* Start global change listeners setup */ - SocketStore.addChangeListener(this._onSocketChange); - /* End global change listeners setup */ + SocketStore.addChangeListener(this.onSocketChange); + + /* Update CSS classes to match user theme */ + var user = UserStore.getCurrentUser(); + + if (user.props && user.props.theme) { + utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';'); + utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';'); + utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';'); + utils.changeCss('.mention', 'background: ' + user.props.theme + ';'); + utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';'); + utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}'); + utils.changeCss('.search-item-container:hover', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';'); + } + + if (user.props.theme !== '#000000' && user.props.theme !== '#585858') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) + ';'); + utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;'); + } else if (user.props.theme === '#000000') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) + ';'); + $('.team__header').addClass('theme--black'); + } else if (user.props.theme === '#585858') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) + ';'); + $('.team__header').addClass('theme--gray'); + } }, - _onSocketChange: function(msg) { - if (msg && msg.user_id) { + onSocketChange: function(msg) { + if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { UserStore.setStatus(msg.user_id, 'online'); } }, diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 1de768872..c2b7e222f 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -29,8 +29,6 @@ module.exports = React.createClass({ return; } - this.setState({submitting: true, serverError: null}); - var post = {}; post.filenames = []; post.message = this.state.messageText; @@ -57,11 +55,10 @@ module.exports = React.createClass({ PostStore.storePendingPost(post); PostStore.storeCommentDraft(this.props.rootId, null); - this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); client.createPost(post, ChannelStore.getCurrent(), function(data) { - AsyncClient.getPosts(true, this.props.channelId); + AsyncClient.getPosts(this.props.channelId); var channel = ChannelStore.get(this.props.channelId); var member = ChannelStore.getMember(this.props.channelId); @@ -91,6 +88,8 @@ module.exports = React.createClass({ this.setState(state); }.bind(this) ); + + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); }, commentMsgKeyPress: function(e) { if (e.which === 13 && !e.shiftKey && !e.altKey) { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index efaa40577..b9142223f 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -18,6 +18,7 @@ var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; module.exports = React.createClass({ + displayName: 'CreatePost', lastTime: 0, handleSubmit: function(e) { e.preventDefault(); @@ -82,7 +83,7 @@ module.exports = React.createClass({ client.createPost(post, channel, function(data) { this.resizePostHolder(); - AsyncClient.getPosts(true); + AsyncClient.getPosts(); var member = ChannelStore.getMember(channel.id); member.msg_count = channel.total_msg_count; @@ -112,8 +113,6 @@ module.exports = React.createClass({ }.bind(this) ); } - - $('.post-list-holder-by-time').perfectScrollbar('update'); }, componentDidUpdate: function() { this.resizePostHolder(); diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 1b6a7e162..55d6f509c 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -44,7 +44,8 @@ module.exports = React.createClass({ } } } - AsyncClient.getPosts(true, this.state.channel_id); + PostStore.removePost(this.state.post_id, this.state.channel_id); + AsyncClient.getPosts(this.state.channel_id); }.bind(this), function(err) { AsyncClient.dispatchError(err, "deletePost"); diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index 2d865a45d..1c5a1ed5e 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -3,6 +3,8 @@ var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); +var Constants = require('../utils/constants.jsx'); +var utils = require('../utils/utils.jsx'); var Textbox = require('./textbox.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); @@ -25,7 +27,7 @@ module.exports = React.createClass({ Client.updatePost(updatedPost, function(data) { - AsyncClient.getPosts(true, this.state.channel_id); + AsyncClient.getPosts(this.state.channel_id); window.scrollTo(0, 0); }.bind(this), function(err) { @@ -36,8 +38,8 @@ module.exports = React.createClass({ $("#edit_post").modal('hide'); $(this.state.refocusId).focus(); }, - handleEditInput: function(editText) { - this.setState({ editText: editText }); + handleEditInput: function(editMessage) { + this.setState({editText: editMessage}); }, handleEditKeyPress: function(e) { if (e.which == 13 && !e.shiftKey && !e.altKey) { @@ -53,7 +55,7 @@ module.exports = React.createClass({ var self = this; $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({ editText: "", title: "", channel_id: "", post_id: "", comments: 0, refocusId: "" }); + self.setState({editText: "", title: "", channel_id: "", post_id: "", comments: 0, refocusId: "", error: ''}); }); $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { @@ -69,7 +71,7 @@ module.exports = React.createClass({ return { editText: "", title: "", post_id: "", channel_id: "", comments: 0, refocusId: "" }; }, render: function() { - var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null; + var error = this.state.error ? <div className='form-group has-error'><br /><label className='control-label'>{ this.state.error }</label></div> : <div className='form-group'><br /></div>; return ( <div className="modal fade edit-modal" ref="modal" id="edit_post" role="dialog" tabIndex="-1" aria-hidden="true"> diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index ab550d500..45e6c5e28 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -10,7 +10,7 @@ module.exports = React.createClass({ canSetState: false, propTypes: { // a list of file pathes displayed by the parent FileAttachmentList - filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, + filename: React.PropTypes.string.isRequired, // the index of this attachment preview in the parent FileAttachmentList index: React.PropTypes.number.isRequired, // the identifier of the modal dialog used to preview files @@ -22,9 +22,17 @@ module.exports = React.createClass({ return {fileSize: -1}; }, componentDidMount: function() { + this.loadFiles(); + }, + componentDidUpdate: function(prevProps) { + if (this.props.filename !== prevProps.filename) { + this.loadFiles(); + } + }, + loadFiles: function() { this.canSetState = true; - var filename = this.props.filenames[this.props.index]; + var filename = this.props.filename; if (filename) { var fileInfo = utils.splitFileLocation(filename); @@ -71,6 +79,10 @@ module.exports = React.createClass({ this.canSetState = false; }, shouldComponentUpdate: function(nextProps, nextState) { + if (!utils.areStatesEqual(nextProps, this.props)) { + return true; + } + // the only time this object should update is when it receives an updated file size which we can usually handle without re-rendering if (nextState.fileSize != this.state.fileSize) { if (this.refs.fileSize) { @@ -87,8 +99,7 @@ module.exports = React.createClass({ } }, render: function() { - var filenames = this.props.filenames; - var filename = filenames[this.props.index]; + var filename = this.props.filename; var fileInfo = utils.splitFileLocation(filename); var type = utils.getFileType(fileInfo.ext); diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx index b92442957..df4424d03 100644 --- a/web/react/components/file_attachment_list.jsx +++ b/web/react/components/file_attachment_list.jsx @@ -26,7 +26,7 @@ module.exports = React.createClass({ var postFiles = []; for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - postFiles.push(<FileAttachment key={i} filenames={filenames} index={i} modalId={modalId} handleImageClick={this.handleImageClick} />); + postFiles.push(<FileAttachment key={i} filename={filenames[i]} index={i} modalId={modalId} handleImageClick={this.handleImageClick} />); } return ( diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 678a2ff87..0f3aa42db 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -10,7 +10,9 @@ var Constants = require('../utils/constants.jsx'); export default class Login extends React.Component { constructor(props) { super(props); + this.handleSubmit = this.handleSubmit.bind(this); + this.state = {}; } handleSubmit(e) { @@ -96,19 +98,16 @@ export default class Login extends React.Component { var authServices = JSON.parse(this.props.authServices); var loginMessage = []; - if (authServices.indexOf(Constants.GITLAB_SERVICE) >= 0) { + if (authServices.indexOf(Constants.GITLAB_SERVICE) !== -1) { loginMessage.push( - <div className='form-group form-group--small'> - <span><a href={'/' + teamName + '/login/gitlab'}>{'Log in with GitLab'}</a></span> - </div> - ); - } - if (authServices.indexOf(Constants.GOOGLE_SERVICE) >= 0) { - loginMessage.push( - <div className='form-group form-group--small'> - <span><a href={'/' + teamName + '/login/google'}>{'Log in with Google'}</a></span> - </div> - ); + <a + className='btn btn-custom-login gitlab' + href={'/' + teamName + '/login/gitlab'} + > + <span className='icon' /> + <span>with GitLab</span> + </a> + ); } var errorClass = ''; @@ -116,15 +115,10 @@ export default class Login extends React.Component { errorClass = ' has-error'; } - return ( - <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 {config.SiteName}</h2> - <form onSubmit={this.handleSubmit}> - <div className={'form-group' + errorClass}> - {serverError} - </div> + var emailSignup; + if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { + emailSignup = ( + <div> <div className={'form-group' + errorClass}> <input autoFocus={focusEmail} @@ -154,13 +148,43 @@ export default class Login extends React.Component { Sign in </button> </div> + </div> + ); + } + + var forgotPassword; + if (loginMessage.length > 0 && emailSignup) { + loginMessage = ( + <div> {loginMessage} + <div className='or__container'> + <span>or</span> + </div> + </div> + ); + + forgotPassword = ( + <div className='form-group'> + <a href={'/' + teamName + '/reset_password'}>I forgot my password</a> + </div> + ); + } + + return ( + <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 {config.SiteName}</h2> + <form onSubmit={this.handleSubmit}> + <div className={'form-group' + errorClass}> + {serverError} + </div> + {loginMessage} + {emailSignup} <div className='form-group margin--extra form-group--small'> <span><a href='/find_team'>{'Find other ' + strings.TeamPlural}</a></span> </div> - <div className='form-group'> - <a href={'/' + teamName + '/reset_password'}>I forgot my password</a> - </div> + {forgotPassword} <div className='margin--extra'> <span>{'Want to create your own ' + strings.Team + '? '} <a diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx index 38c9ea76d..fc24a7cdc 100644 --- a/web/react/components/new_channel.jsx +++ b/web/react/components/new_channel.jsx @@ -84,6 +84,17 @@ module.exports = React.createClass({ var button = e.relatedTarget; self.setState({channelType: $(button).attr('data-channeltype')}); }); + $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', this.handleClose); + }, + componentWillUnmount: function() { + $(this.refs.modal.getDOMNode()).off('hidden.bs.modal', this.handleClose); + }, + handleClose: function() { + $(this.getDOMNode()).find('.form-control').each(function clearForms() { + this.value = ''; + }); + + this.setState({channelType: '', displayNameError: '', nameError: '', serverError: '', inValid: false}); }, getInitialState: function() { return {channelType: ''}; diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index cc2e37fa8..acc2b51d2 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -11,11 +11,12 @@ var ChannelStore = require('../stores/channel_store.jsx'); var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var ActionTypes = Constants.ActionTypes; +var utils = require('../utils/utils.jsx'); var PostInfo = require('./post_info.jsx'); module.exports = React.createClass({ - displayName: "Post", + displayName: 'Post', handleCommentClick: function(e) { e.preventDefault(); @@ -43,7 +44,7 @@ module.exports = React.createClass({ var post = this.props.post; client.createPost(post, post.channel_id, function(data) { - AsyncClient.getPosts(true); + AsyncClient.getPosts(); var channel = ChannelStore.get(post.channel_id); var member = ChannelStore.getMember(post.channel_id); @@ -67,6 +68,13 @@ module.exports = React.createClass({ PostStore.updatePendingPost(post); this.forceUpdate(); }, + shouldComponentUpdate: function(nextProps) { + if (!utils.areStatesEqual(nextProps.post, this.props.post)) { + return true; + } + + return false; + }, getInitialState: function() { return { }; }, @@ -90,16 +98,16 @@ module.exports = React.createClass({ var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null; - var rootUser = this.props.sameRoot ? "same--root" : "other--root"; + var rootUser = this.props.sameRoot ? 'same--root' : 'other--root'; - var postType = ""; - if (type != "Post"){ - postType = "post--comment"; + var postType = ''; + if (type != 'Post'){ + postType = 'post--comment'; } - var currentUserCss = ""; + var currentUserCss = ''; if (UserStore.getCurrentId() === post.user_id) { - currentUserCss = "current--user"; + currentUserCss = 'current--user'; } var userProfile = UserStore.getProfile(post.user_id); @@ -109,18 +117,23 @@ module.exports = React.createClass({ timestamp = userProfile.update_at; } + var sameUserClass = ''; + if (this.props.sameUser) { + sameUserClass = 'same--user'; + } + return ( <div> - <div id={post.id} className={"post " + this.props.sameUser + " " + rootUser + " " + postType + " " + currentUserCss}> + <div id={post.id} className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}> { !this.props.hideProfilePic ? - <div className="post-profile-img__container"> - <img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image?time=" + timestamp} height="36" width="36" /> + <div className='post-profile-img__container'> + <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' /> </div> : null } - <div className="post__content"> - <PostHeader ref="header" post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} /> + <div className='post__content'> + <PostHeader ref='header' post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} /> <PostBody post={post} sameRoot={this.props.sameRoot} parentPost={parentPost} posts={posts} handleCommentClick={this.handleCommentClick} retryPost={this.retryPost} /> - <PostInfo ref="info" post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} allowReply="true" /> + <PostInfo ref='info' post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} allowReply='true' /> </div> </div> </div> diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 7748f5c2a..5b0b1f79a 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -15,124 +15,118 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -function getStateFromStores() { - var channel = ChannelStore.getCurrent(); - - if (channel == null) { - channel = {}; +export default class PostList extends React.Component { + constructor() { + super(); + + this.gotMorePosts = false; + this.scrolled = false; + this.prevScrollTop = 0; + this.seenNewMessages = false; + this.isUserScroll = true; + this.userHasSeenNew = false; + + this.onChange = this.onChange.bind(this); + this.onTimeChange = this.onTimeChange.bind(this); + this.onSocketChange = this.onSocketChange.bind(this); + this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this); + this.loadMorePosts = this.loadMorePosts.bind(this); + this.loadFirstPosts = this.loadFirstPosts.bind(this); + + this.state = this.getStateFromStores(); + this.state.numToDisplay = Constants.POST_CHUNK_SIZE; + this.state.isFirstLoadComplete = false; } + getStateFromStores() { + var channel = ChannelStore.getCurrent(); - var postList = PostStore.getCurrentPosts(); - var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id); - - if (deletedPosts && Object.keys(deletedPosts).length > 0) { - for (var pid in deletedPosts) { - postList.posts[pid] = deletedPosts[pid]; - postList.order.unshift(pid); + if (channel == null) { + channel = {}; } - postList.order.sort(function postSort(a, b) { - if (postList.posts[a].create_at > postList.posts[b].create_at) { - return -1; - } - if (postList.posts[a].create_at < postList.posts[b].create_at) { - return 1; - } - return 0; - }); - } + var postList = PostStore.getCurrentPosts(); - var pendingPostList = PostStore.getPendingPosts(channel.id); + if (postList != null) { + var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id); - if (pendingPostList) { - postList.order = pendingPostList.order.concat(postList.order); - for (var ppid in pendingPostList.posts) { - postList.posts[ppid] = pendingPostList.posts[ppid]; - } - } + if (deletedPosts && Object.keys(deletedPosts).length > 0) { + for (var pid in deletedPosts) { + postList.posts[pid] = deletedPosts[pid]; + postList.order.unshift(pid); + } - return { - postList: postList, - channel: channel - }; -} + postList.order.sort(function postSort(a, b) { + if (postList.posts[a].create_at > postList.posts[b].create_at) { + return -1; + } + if (postList.posts[a].create_at < postList.posts[b].create_at) { + return 1; + } + return 0; + }); + } + + var pendingPostList = PostStore.getPendingPosts(channel.id); -module.exports = React.createClass({ - displayName: 'PostList', - scrollPosition: 0, - preventScrollTrigger: false, - gotMorePosts: false, - oldScrollHeight: 0, - oldZoom: 0, - scrolledToNew: false, - componentDidMount: function() { - var user = UserStore.getCurrentUser(); - if (user.props && user.props.theme) { - utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';'); - utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';'); - utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';'); - utils.changeCss('.mention', 'background: ' + user.props.theme + ';'); - utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';'); - utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}'); - utils.changeCss('.search-item-container:hover', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';'); - utils.changeCss('.nav-pills__unread-indicator', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';'); + if (pendingPostList) { + postList.order = pendingPostList.order.concat(postList.order); + for (var ppid in pendingPostList.posts) { + postList.posts[ppid] = pendingPostList.posts[ppid]; + } + } } - if (user.props.theme !== '#000000' && user.props.theme !== '#585858') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) + ';'); - utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;'); - } else if (user.props.theme === '#000000') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) + ';'); - $('.team__header').addClass('theme--black'); - } else if (user.props.theme === '#585858') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) + ';'); - $('.team__header').addClass('theme--gray'); + var lastViewed = Number.MAX_VALUE; + + if (ChannelStore.getCurrentMember() != null) { + lastViewed = ChannelStore.getCurrentMember().last_viewed_at; } + return { + postList: postList, + channel: channel, + lastViewed: lastViewed + }; + } + componentDidMount() { PostStore.addChangeListener(this.onChange); ChannelStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onTimeChange); SocketStore.addChangeListener(this.onSocketChange); - $('.post-list-holder-by-time').perfectScrollbar(); - - this.resize(); - - var postHolder = $('.post-list-holder-by-time')[0]; - this.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight(); - this.oldScrollHeight = postHolder.scrollHeight; - this.oldZoom = (window.outerWidth - 8) / window.innerWidth; + var postHolder = $('.post-list-holder-by-time'); $('.modal').on('show.bs.modal', function onShow() { $('.modal-body').css('overflow-y', 'auto'); $('.modal-body').css('max-height', $(window).height() * 0.7); }); - var self = this; $(window).resize(function resize() { - $(postHolder).perfectScrollbar('update'); - - // this only kind of works, detecting zoom in browsers is a nightmare - var newZoom = (window.outerWidth - 8) / window.innerWidth; + if ($('#create_post').length > 0) { + var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; + postHolder.css('height', height + 'px'); + } - if (self.scrollPosition >= postHolder.scrollHeight || (self.oldScrollHeight !== postHolder.scrollHeight && self.scrollPosition >= self.oldScrollHeight) || self.oldZoom !== newZoom) { - self.resize(); + if (!this.scrolled) { + this.scrollToBottom(); } + }.bind(this)); - self.oldZoom = newZoom; + postHolder.scroll(function scroll() { + var position = postHolder.scrollTop() + postHolder.height() + 14; + var bottom = postHolder[0].scrollHeight; - if ($('#create_post').length > 0) { - var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; - $('.post-list-holder-by-time').css('height', height + 'px'); + if (position >= bottom) { + this.scrolled = false; + } else { + this.scrolled = true; } - }); - $(postHolder).scroll(function scroll() { - if (!self.preventScrollTrigger) { - self.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight(); + if (this.isUserScroll) { + this.userHasSeenNew = true; } - self.preventScrollTrigger = false; - }); + this.isUserScroll = true; + }.bind(this)); $('body').on('click.userpopover', function popOver(e) { if ($(e.target).attr('data-toggle') !== 'popover' && @@ -163,76 +157,121 @@ module.exports = React.createClass({ $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); } }); - }, - componentDidUpdate: function() { - this.resize(); - var postHolder = $('.post-list-holder-by-time')[0]; - this.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight(); - this.oldScrollHeight = postHolder.scrollHeight; + + this.scrollToBottom(); + + if (this.state.channel.id != null) { + this.loadFirstPosts(this.state.channel.id); + } + } + componentDidUpdate(prevProps, prevState) { $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); - }, - componentWillUnmount: function() { + + if (this.state.postList == null || prevState.postList == null) { + this.scrollToBottom(); + return; + } + + var order = this.state.postList.order || []; + var posts = this.state.postList.posts || {}; + var oldOrder = prevState.postList.order || []; + var oldPosts = prevState.postList.posts || {}; + var userId = UserStore.getCurrentId(); + var firstPost = posts[order[0]] || {}; + var isNewPost = oldOrder.indexOf(order[0]) === -1; + + if (this.state.channel.id !== prevState.channel.id) { + this.scrollToBottom(); + } else if (oldOrder.length === 0) { + this.scrollToBottom(); + + // the user is scrolled to the bottom + } else if (!this.scrolled) { + this.scrollToBottom(); + + // there's a new post and + // it's by the user and not a comment + } else if (isNewPost && + userId === firstPost.user_id && + !utils.isComment(firstPost)) { + this.state.lastViewed = utils.getTimestamp(); + this.scrollToBottom(true); + + // the user clicked 'load more messages' + } else if (this.gotMorePosts) { + var lastPost = oldPosts[oldOrder[prevState.numToDisplay]]; + $('#' + lastPost.id)[0].scrollIntoView(); + } else { + this.scrollTo(this.prevScrollTop); + } + } + componentWillUpdate() { + var postHolder = $('.post-list-holder-by-time'); + this.prevScrollTop = postHolder.scrollTop(); + } + componentWillUnmount() { PostStore.removeChangeListener(this.onChange); ChannelStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onTimeChange); SocketStore.removeChangeListener(this.onSocketChange); $('body').off('click.userpopover'); $('.modal').off('show.bs.modal'); - }, - resize: function() { - var postHolder = $('.post-list-holder-by-time')[0]; - this.preventScrollTrigger = true; - if (this.gotMorePosts) { - this.gotMorePosts = false; - $(postHolder).scrollTop($(postHolder).scrollTop() + (postHolder.scrollHeight - this.oldScrollHeight)); - } else if ($('#new_message')[0] && !this.scrolledToNew) { - $(postHolder).scrollTop($(postHolder).scrollTop() + $('#new_message').offset().top - 63); - this.scrolledToNew = true; + } + scrollTo(val) { + this.isUserScroll = false; + var postHolder = $('.post-list-holder-by-time'); + postHolder[0].scrollTop = val; + } + scrollToBottom(force) { + this.isUserScroll = false; + var postHolder = $('.post-list-holder-by-time'); + if ($('#new_message')[0] && !this.userHasSeenNew && !force) { + $('#new_message')[0].scrollIntoView(); } else { - $(postHolder).scrollTop(postHolder.scrollHeight); + postHolder.addClass('hide-scroll'); + postHolder[0].scrollTop = postHolder[0].scrollHeight; + postHolder.removeClass('hide-scroll'); + } + } + loadFirstPosts(id) { + Client.getPosts( + id, + PostStore.getLatestUpdate(id), + function success() { + this.setState({isFirstLoadComplete: true}); + }.bind(this), + function fail() { + this.setState({isFirstLoadComplete: true}); + }.bind(this) + ); + } + onChange() { + var newState = this.getStateFromStores(); + + // Special case where the channel wasn't yet set in componentDidMount + if (!this.state.isFirstLoadComplete && this.state.channel.id == null && newState.channel.id != null) { + this.loadFirstPosts(newState.channel.id); } - $(postHolder).perfectScrollbar('update'); - }, - onChange: function() { - var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { - if (this.state.postList && this.state.postList.order) { - if (this.state.channel.id === newState.channel.id && this.state.postList.order.length !== newState.postList.order.length && newState.postList.order.length > Constants.POST_CHUNK_SIZE) { - this.gotMorePosts = true; - } - } if (this.state.channel.id !== newState.channel.id) { PostStore.clearUnseenDeletedPosts(this.state.channel.id); - this.scrolledToNew = false; + this.userHasSeenNew = false; + newState.numToDisplay = Constants.POST_CHUNK_SIZE; + } else { + newState.lastViewed = this.state.lastViewed; } + this.setState(newState); } - }, - onSocketChange: function(msg) { + } + onSocketChange(msg) { var postList; var post; - if (msg.action === 'posted') { + if (msg.action === 'posted' || msg.action === 'post_edited') { post = JSON.parse(msg.props.post); PostStore.storePost(post); - } else if (msg.action === 'post_edited') { - if (this.state.channel.id === msg.channel_id) { - postList = this.state.postList; - if (!(msg.props.post_id in postList.posts)) { - return; - } - - post = postList.posts[msg.props.post_id]; - post.message = msg.props.message; - - postList.posts[post.id] = post; - this.setState({postList: postList}); - - PostStore.storePosts(msg.channel_id, postList); - } else { - AsyncClient.getPosts(true, msg.channel_id); - } } else if (msg.action === 'post_deleted') { var activeRoot = $(document.activeElement).closest('.comment-create-body')[0]; var activeRootPostId = ''; @@ -244,16 +283,8 @@ module.exports = React.createClass({ postList = this.state.postList; PostStore.storeUnseenDeletedPost(post); - - if (postList.posts[post.id]) { - delete postList.posts[post.id]; - var index = postList.order.indexOf(post.id); - if (index > -1) { - postList.order.splice(index, 1); - } - - PostStore.storePosts(msg.channel_id, postList); - } + PostStore.removePost(post, true); + PostStore.emitChange(); if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) { $('#post_deleted').modal('show'); @@ -261,8 +292,8 @@ module.exports = React.createClass({ } else if (msg.action === 'new_user') { AsyncClient.getProfiles(); } - }, - onTimeChange: function() { + } + onTimeChange() { if (!this.state.postList) { return; } @@ -273,271 +304,337 @@ module.exports = React.createClass({ } this.refs[id].forceUpdateInfo(); } - }, - getMorePosts: function(e) { - e.preventDefault(); - - if (!this.state.postList) { - return; - } - - var posts = this.state.postList.posts; - var order = this.state.postList.order; - var channelId = this.state.channel.id; - - $(this.refs.loadmore.getDOMNode()).text('Retrieving more messages...'); - - var self = this; - var currentPos = $('.post-list').scrollTop; - - Client.getPosts( - channelId, - order.length, - Constants.POST_CHUNK_SIZE, - function success(data) { - $(self.refs.loadmore.getDOMNode()).text('Load more messages'); - - if (!data) { - return; - } + } + createDMIntroMessage(channel) { + var teammate = utils.getDirectTeammate(channel.id); - if (data.order.length === 0) { - return; - } + if (teammate) { + var teammateName = teammate.username; + if (teammate.nickname.length > 0) { + teammateName = teammate.nickname; + } - var postList = {}; - postList.posts = $.extend(posts, data.posts); - postList.order = order.concat(data.order); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_POSTS, - id: channelId, - postList: postList - }); - - Client.getProfiles(); - $('.post-list').scrollTop(currentPos); - }, - function fail(err) { - $(self.refs.loadmore.getDOMNode()).text('Load more messages'); - AsyncClient.dispatchError(err, 'getPosts'); - } + return ( + <div className='channel-intro'> + <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} + height='50' + width='50' + /> + </div> + <div className='channel-intro-profile'> + <strong><UserProfile userId={teammate.id} /></strong> + </div> + <p className='channel-intro-text'> + {'This is the start of your private message history with ' + teammateName + '.'}<br/> + {'Private messages and files shared here are not shown to people outside this area.'} + </p> + <a + className='intro-links' + href='#' + data-toggle='modal' + data-target='#edit_channel' + data-desc={channel.description} + data-title={channel.display_name} + data-channelid={channel.id} + > + <i className='fa fa-pencil'></i>Set a description + </a> + </div> ); - }, - getInitialState: function() { - return getStateFromStores(); - }, - render: function() { - var order = []; - var posts; + } - var lastViewed = Number.MAX_VALUE; + return ( + <div className='channel-intro'> + <p className='channel-intro-text'>{'This is the start of your private message history with this ' + strings.Team + 'mate. Private messages and files shared here are not shown to people outside this area.'}</p> + </div> + ); + } + createChannelIntroMessage(channel) { + if (channel.type === 'D') { + return this.createDMIntroMessage(channel); + } else if (ChannelStore.isDefault(channel)) { + return this.createDefaultIntroMessage(channel); + } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { + return this.createOffTopicIntroMessage(channel); + } else if (channel.type === 'O' || channel.type === 'P') { + return this.createStandardIntroMessage(channel); + } + } + createDefaultIntroMessage(channel) { + return ( + <div className='channel-intro'> + <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4> + <p className='channel-intro__content'> + Welcome to {channel.display_name}! + <br/><br/> + This is the first channel {strings.Team}mates see when they + <br/> + 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 + <br/> + the Left Hand Sidebar under “Channels” and click “More…”. + <br/> + </p> + </div> + ); + } + createOffTopicIntroMessage(channel) { + return ( + <div className='channel-intro'> + <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/> + </p> + <a + className='intro-links' + href='#' + data-toggle='modal' + data-target='#edit_channel' + data-desc={channel.description} + data-title={channel.display_name} + data-channelid={channel.id} + > + <i className='fa fa-pencil'></i>Set a description + </a> + </div> + ); + } + getChannelCreator(channel) { + if (channel.creator_id.length > 0) { + var creator = UserStore.getProfile(channel.creator_id); + if (creator) { + return creator.username; + } + } - if (ChannelStore.getCurrentMember() != null) { - lastViewed = ChannelStore.getCurrentMember().last_viewed_at; + var members = ChannelStore.getCurrentExtraInfo().members; + for (var i = 0; i < members.length; i++) { + if (members[i].roles.indexOf('admin') > -1) { + return members[i].username; + } + } + } + createStandardIntroMessage(channel) { + var uiName = channel.display_name; + var creatorName = ''; + + var uiType; + var memberMessage; + if (channel.type === 'P') { + uiType = 'private group'; + memberMessage = ' Only invited members can see this private group.'; + } else { + uiType = 'channel'; + memberMessage = ' Any member can join and read this channel.'; } - if (this.state.postList != null) { - posts = this.state.postList.posts; - order = this.state.postList.order; + var createMessage; + if (creatorName !== '') { + 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>); + } else { + createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.'; } + return ( + <div className='channel-intro'> + <h4 className='channel-intro__title'>Beginning of {uiName}</h4> + <p className='channel-intro__content'> + {createMessage} + {memberMessage} + <br/> + </p> + <a + className='intro-links' + href='#' + data-toggle='modal' + data-target='#edit_channel' + data-desc={channel.description} + data-title={channel.display_name} + data-channelid={channel.id} + > + <i className='fa fa-pencil'></i>Set a description + </a> + <a + className='intro-links' + href='#' + data-toggle='modal' + data-target='#channel_invite' + > + <i className='fa fa-user-plus'></i>Invite others to this {uiType} + </a> + </div> + ); + } + createPosts(posts, order) { + var postCtls = []; + var previousPostDay = new Date(0); + var userId = UserStore.getCurrentId(); + var renderedLastViewed = false; - var userId = ''; - if (UserStore.getCurrentId()) { - userId = UserStore.getCurrentId(); - } else { - return <div/>; + var numToDisplay = this.state.numToDisplay; + if (order.length - 1 < numToDisplay) { + numToDisplay = order.length - 1; } - var channel = this.state.channel; + for (var i = numToDisplay; i >= 0; i--) { + var post = posts[order[i]]; + var parentPost = posts[post.parent_id]; - var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>; + var sameUser = false; + var sameRoot = false; + var hideProfilePic = false; + var prevPost = posts[order[i + 1]]; - var userStyle = {color: UserStore.getCurrentUser().props.theme}; + if (prevPost) { + sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5; - if (channel != null) { - if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) { - moreMessages = <a ref='loadmore' className='more-messages-text theme' href='#' onClick={this.getMorePosts}>Load more messages</a>; - } else if (channel.type === 'D') { - var teammate = utils.getDirectTeammate(channel.id); - - if (teammate) { - var teammateName = teammate.username; - if (teammate.nickname.length > 0) { - teammateName = teammate.nickname; - } + sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); - moreMessages = ( - <div className='channel-intro'> - <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} height='50' width='50' /> - </div> - <div className='channel-intro-profile'> - <strong><UserProfile userId={teammate.id} /></strong> - </div> - <p className='channel-intro-text'> - This is the start of your private message history with <strong>{teammateName}</strong>.<br/> - Private messages and files shared here are not shown to people outside this area. - </p> - <a className='intro-links' href='#' style={userStyle} data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}><i className='fa fa-pencil'></i>Set a description</a> - </div> - ); - } else { - moreMessages = ( - <div className='channel-intro'> - <p className='channel-intro-text'>{'This is the start of your private message history with this ' + strings.Team + 'mate. Private messages and files shared here are not shown to people outside this area.'}</p> - </div> - ); - } - } else if (channel.type === 'P' || channel.type === 'O') { - var uiName = channel.display_name; - var creatorName = ''; - - if (channel.creator_id.length > 0) { - var creator = UserStore.getProfile(channel.creator_id); - if (creator) { - creatorName = creator.username; - } - } + // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post + hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post); + } - if (creatorName === '') { - var members = ChannelStore.getCurrentExtraInfo().members; - for (var i = 0; i < members.length; i++) { - if (members[i].roles.indexOf('admin') > -1) { - creatorName = members[i].username; - break; - } - } - } + // 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 postCtl = ( + <Post + key={post.id} + ref={post.id} + sameUser={sameUser} + sameRoot={sameRoot} + post={post} + parentPost={parentPost} + posts={posts} + hideProfilePic={hideProfilePic} + isLastComment={isLastComment} + /> + ); - if (ChannelStore.isDefault(channel)) { - moreMessages = ( - <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {uiName}</h4> - <p className='channel-intro__content'> - Welcome to <strong>{uiName}</strong>! - <br/><br/> - This is the first channel {strings.Team}mates see when they - <br/> - 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 - <br/> - the Left Hand Sidebar under “Channels” and click “More…”. - <br/> - </p> - </div> - ); - } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { - moreMessages = ( - <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {uiName}</h4> - <p className='channel-intro__content'> - This is the start of <strong>{uiName}</strong>, a channel for non-work-related conversations. - <br/> - </p> - <a className='intro-links' href='#' style={userStyle} data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={uiName} data-channelid={channel.id}><i className='fa fa-pencil'></i>Set a description</a> - </div> - ); - } else { - var uiType; - var memberMessage; - if (channel.type === 'P') { - uiType = 'private group'; - memberMessage = ' Only invited members can see this private group.'; - } else { - uiType = 'channel'; - memberMessage = ' Any member can join and read this channel.'; - } + let currentPostDay = utils.getDateForUnixTicks(post.create_at); + if (currentPostDay.toDateString() !== previousPostDay.toDateString()) { + postCtls.push( + <div + key={currentPostDay.toDateString()} + className='date-separator' + > + <hr className='separator__hr' /> + <div className='separator__text'>{currentPostDay.toDateString()}</div> + </div> + ); + } - var createMessage; - if (creatorName !== '') { - 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>); - } else { - createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.'; - } + if (post.user_id !== userId && post.create_at > this.state.lastViewed && !renderedLastViewed) { + renderedLastViewed = true; - moreMessages = ( - <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {uiName}</h4> - <p className='channel-intro__content'> - {createMessage} - {memberMessage} - <br/> - </p> - <a className='intro-links' href='#' style={userStyle} data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}><i className='fa fa-pencil'></i>Set a description</a> - <a className='intro-links' href='#' style={userStyle} data-toggle='modal' data-target='#channel_invite'><i className='fa fa-user-plus'></i>Invite others to this {uiType}</a> - </div> - ); + // Temporary fix to solve ie10/11 rendering issue + let newSeparatorId = ''; + if (!utils.isBrowserIE()) { + newSeparatorId = 'new_message'; } + postCtls.push( + <div + id={newSeparatorId} + key='unviewed' + className='new-separator' + > + <hr + className='separator__hr' + /> + <div className='separator__text'>New Messages</div> + </div> + ); } + postCtls.push(postCtl); + previousPostDay = currentPostDay; } - var postCtls = []; - - if (posts) { - var previousPostDay = new Date(0); - var currentPostDay; + return postCtls; + } + loadMorePosts() { + if (this.state.postList == null) { + return; + } - for (var i = order.length - 1; i >= 0; i--) { - var post = posts[order[i]]; - var parentPost = null; - if (post.parent_id) { - parentPost = posts[post.parent_id]; - } + var posts = this.state.postList.posts; + var order = this.state.postList.order; + var channelId = this.state.channel.id; - var sameUser = ''; - var sameRoot = false; - var hideProfilePic = false; - var prevPost; - if (i < order.length - 1) { - prevPost = posts[order[i + 1]]; - } + $(this.refs.loadmore.getDOMNode()).text('Retrieving more messages...'); - if (prevPost) { - if ((prevPost.user_id === post.user_id) && (post.create_at - prevPost.create_at <= 1000 * 60 * 5)) { - sameUser = 'same--user'; - } - sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); + Client.getPostsPage( + channelId, + order.length, + Constants.POST_CHUNK_SIZE, + function success(data) { + $(this.refs.loadmore.getDOMNode()).text('Load more messages'); + this.gotMorePosts = true; + this.setState({numToDisplay: this.state.numToDisplay + Constants.POST_CHUNK_SIZE}); - // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post - hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post); + if (!data) { + return; } - // 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); + if (data.order.length === 0) { + return; + } - var postCtl = ( - <Post ref={post.id} sameUser={sameUser} sameRoot={sameRoot} post={post} parentPost={parentPost} key={post.id} - posts={posts} hideProfilePic={hideProfilePic} isLastComment={isLastComment} - /> - ); + var postList = {}; + postList.posts = $.extend(posts, data.posts); + postList.order = order.concat(data.order); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POSTS, + id: channelId, + post_list: postList + }); + + Client.getProfiles(); + }.bind(this), + function fail(err) { + $(this.refs.loadmore.getDOMNode()).text('Load more messages'); + AsyncClient.dispatchError(err, 'getPosts'); + }.bind(this) + ); + } + render() { + var order = []; + var posts; + var channel = this.state.channel; - currentPostDay = utils.getDateForUnixTicks(post.create_at); - if (currentPostDay.toDateString() !== previousPostDay.toDateString()) { - postCtls.push( - <div key={currentPostDay.toDateString()} className='date-separator'> - <hr className='separator__hr' /> - <div className='separator__text'>{currentPostDay.toDateString()}</div> - </div> - ); - } + if (this.state.postList != null) { + posts = this.state.postList.posts; + order = this.state.postList.order; + } - if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) { - renderedLastViewed = true; - postCtls.push( - <div key='unviewed' className='new-separator'> - <hr id='new_message' className='separator__hr' /> - <div className='separator__text'>New Messages</div> - </div> - ); - } - postCtls.push(postCtl); - previousPostDay = currentPostDay; + var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>; + if (channel != null) { + if (order.length > this.state.numToDisplay) { + moreMessages = ( + <a + ref='loadmore' + className='more-messages-text theme' + href='#' + onClick={this.loadMorePosts} + > + Load more messages + </a> + ); + } else { + moreMessages = this.createChannelIntroMessage(channel); } + } + + var postCtls = []; + if (posts && this.state.isFirstLoadComplete) { + postCtls = this.createPosts(posts, order); } else { postCtls.push(<LoadingScreen position='absolute' />); } @@ -553,4 +650,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx deleted file mode 100644 index ac4c8a6d7..000000000 --- a/web/react/components/post_right.jsx +++ /dev/null @@ -1,404 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -var PostStore = require('../stores/post_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var UserProfile = require('./user_profile.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var utils = require('../utils/utils.jsx'); -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 client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var ActionTypes = Constants.ActionTypes; - -RhsHeaderPost = React.createClass({ - handleClose: function(e) { - e.preventDefault(); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_SEARCH, - results: null - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_POST_SELECTED, - results: null - }); - }, - handleBack: function(e) { - e.preventDefault(); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_SEARCH_TERM, - term: this.props.fromSearch, - do_search: true, - is_mention_search: this.props.isMentionSearch - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_POST_SELECTED, - results: null - }); - }, - render: function() { - var back; - if (this.props.fromSearch) { - back = <a href='#' onClick={this.handleBack} className='sidebar--right__back'><i className='fa fa-chevron-left'></i></a>; - } - - return ( - <div className='sidebar--right__header'> - <span className='sidebar--right__title'>{back}Message Details</span> - <button type='button' className='sidebar--right__close' aria-label='Close' onClick={this.handleClose}></button> - </div> - ); - } -}); - -RootPost = React.createClass({ - render: function() { - var post = this.props.post; - var message = utils.textToJsx(post.message); - var isOwner = UserStore.getCurrentId() === post.user_id; - var timestamp = UserStore.getProfile(post.user_id).update_at; - var channel = ChannelStore.get(post.channel_id); - - var type = 'Post'; - if (post.root_id.length > 0) { - type = 'Comment'; - } - - var currentUserCss = ''; - if (UserStore.getCurrentId() === post.user_id) { - currentUserCss = 'current--user'; - } - - var channelName; - if (channel) { - if (channel.type === 'D') { - channelName = 'Private Message'; - } else { - channelName = channel.display_name; - } - } - - var ownerOptions; - if (isOwner) { - ownerOptions = ( - <div> - <a href='#' className='dropdown-toggle theme' type='button' data-toggle='dropdown' aria-expanded='false' /> - <ul className='dropdown-menu' role='menu'> - <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#edit_post' data-refoucsid='#reply_textbox' data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>Edit</a></li> - <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#delete_post' data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={this.props.commentCount}>Delete</a></li> - </ul> - </div> - ); - } - - var fileAttachment; - if (post.filenames && post.filenames.length > 0) { - fileAttachment = ( - <FileAttachmentList - filenames={post.filenames} - modalId={'rhs_view_image_modal_' + post.id} - channelId={post.channel_id} - userId={post.user_id} /> - ); - } - - return ( - <div className={'post post--root ' + currentUserCss}> - <div className='post-right-channel__name'>{ channelName }</div> - <div className='post-profile-img__container'> - <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' /> - </div> - <div className='post__content'> - <ul className='post-header'> - <li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li> - <li className='post-header-col'><time className='post-right-root-time'>{utils.displayCommentDateTime(post.create_at)}</time></li> - <li className='post-header-col post-header__reply'> - <div className='dropdown'> - {ownerOptions} - </div> - </li> - </ul> - <div className='post-body'> - <p>{message}</p> - {fileAttachment} - </div> - </div> - <hr /> - </div> - ); - } -}); - -CommentPost = React.createClass({ - retryComment: function(e) { - e.preventDefault(); - - var post = this.props.post; - client.createPost(post, post.channel_id, - function success(data) { - AsyncClient.getPosts(true); - - var channel = ChannelStore.get(post.channel_id); - var member = ChannelStore.getMember(post.channel_id); - member.msg_count = channel.total_msg_count; - member.last_viewed_at = (new Date()).getTime(); - ChannelStore.setChannelMember(member); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_POST, - post: data - }); - }.bind(this), - function fail() { - post.state = Constants.POST_FAILED; - PostStore.updatePendingPost(post); - this.forceUpdate(); - }.bind(this) - ); - - post.state = Constants.POST_LOADING; - PostStore.updatePendingPost(post); - this.forceUpdate(); - }, - render: function() { - var post = this.props.post; - - var currentUserCss = ''; - if (UserStore.getCurrentId() === post.user_id) { - currentUserCss = 'current--user'; - } - - var isOwner = UserStore.getCurrentId() === post.user_id; - - var type = 'Post'; - if (post.root_id.length > 0) { - type = 'Comment'; - } - - var message = utils.textToJsx(post.message); - var timestamp = UserStore.getCurrentUser().update_at; - - var loading; - var postClass = ''; - if (post.state === Constants.POST_FAILED) { - postClass += ' post-fail'; - loading = <a className='theme post-retry pull-right' href='#' onClick={this.retryComment}>Retry</a>; - } else if (post.state === Constants.POST_LOADING) { - postClass += ' post-waiting'; - loading = <img className='post-loading-gif pull-right' src='/static/images/load.gif'/>; - } - - var ownerOptions; - if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { - ownerOptions = ( - <div className='dropdown' onClick={function(e){$('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50);}}> - <a href='#' className='dropdown-toggle theme' type='button' data-toggle='dropdown' aria-expanded='false' /> - <ul className='dropdown-menu' role='menu'> - <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#edit_post' data-refoucsid='#reply_textbox' data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>Edit</a></li> - <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#delete_post' data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={0}>Delete</a></li> - </ul> - </div> - ); - } - - var fileAttachment; - if (post.filenames && post.filenames.length > 0) { - fileAttachment = ( - <FileAttachmentList - filenames={post.filenames} - modalId={'rhs_comment_view_image_modal_' + post.id} - channelId={post.channel_id} - userId={post.user_id} /> - ); - } - - return ( - <div className={'post ' + currentUserCss}> - <div className='post-profile-img__container'> - <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' /> - </div> - <div className='post__content'> - <ul className='post-header'> - <li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li> - <li className='post-header-col'><time className='post-right-comment-time'>{utils.displayCommentDateTime(post.create_at)}</time></li> - <li className='post-header-col post-header__reply'> - {ownerOptions} - </li> - </ul> - <div className='post-body'> - <p className={postClass}>{loading}{message}</p> - {fileAttachment} - </div> - </div> - </div> - ); - } -}); - -function getStateFromStores() { - var postList = PostStore.getSelectedPost(); - if (!postList || postList.order.length < 1) { - return {postList: {}}; - } - - var channelId = postList.posts[postList.order[0]].channel_id; - var pendingPostList = PostStore.getPendingPosts(channelId); - - if (pendingPostList) { - for (var pid in pendingPostList.posts) { - postList.posts[pid] = pendingPostList.posts[pid]; - } - } - - return {postList: postList}; -} - -module.exports = React.createClass({ - componentDidMount: function() { - PostStore.addSelectedPostChangeListener(this.onChange); - PostStore.addChangeListener(this.onChangeAll); - UserStore.addStatusesChangeListener(this.onTimeChange); - this.resize(); - var self = this; - $(window).resize(function() { - self.resize(); - }); - }, - componentDidUpdate: function() { - $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); - $('.post-right__scroll').perfectScrollbar('update'); - this.resize(); - }, - componentWillUnmount: function() { - PostStore.removeSelectedPostChangeListener(this.onChange); - PostStore.removeChangeListener(this.onChangeAll); - UserStore.removeStatusesChangeListener(this.onTimeChange); - }, - onChange: function() { - if (this.isMounted()) { - var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { - this.setState(newState); - } - } - }, - onChangeAll: function() { - if (this.isMounted()) { - // if something was changed in the channel like adding a - // comment or post then lets refresh the sidebar list - var currentSelected = PostStore.getSelectedPost(); - if (!currentSelected || currentSelected.order.length === 0) { - return; - } - - var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id); - - if (!currentPosts || currentPosts.order.length === 0) { - return; - } - - if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) { - currentSelected.posts = {}; - for (var postId in currentPosts.posts) { - currentSelected.posts[postId] = currentPosts.posts[postId]; - } - - PostStore.storeSelectedPost(currentSelected); - } - - this.setState(getStateFromStores()); - } - }, - onTimeChange: function() { - for (var id in this.state.postList.posts) { - if (!this.refs[id]) { - continue; - } - this.refs[id].forceUpdate(); - } - }, - getInitialState: function() { - return getStateFromStores(); - }, - resize: function() { - var height = $(window).height() - $('#error_bar').outerHeight() - 100; - $('.post-right__scroll').css('height', height + 'px'); - $('.post-right__scroll').scrollTop(100000); - $('.post-right__scroll').perfectScrollbar(); - $('.post-right__scroll').perfectScrollbar('update'); - }, - render: function() { - var postList = this.state.postList; - - if (postList == null) { - return ( - <div></div> - ); - } - - var selectedPost = postList.posts[postList.order[0]]; - var rootPost = null; - - if (selectedPost.root_id === '') { - rootPost = selectedPost; - } else { - rootPost = postList.posts[selectedPost.root_id]; - } - - var postsArray = []; - - for (var postId in postList.posts) { - var cpost = postList.posts[postId]; - if (cpost.root_id === rootPost.id) { - postsArray.push(cpost); - } - } - - postsArray.sort(function postSort(a, b) { - if (a.create_at < b.create_at) { - return -1; - } - if (a.create_at > b.create_at) { - return 1; - } - return 0; - }); - - var currentId = UserStore.getCurrentId(); - var searchForm; - if (currentId != null) { - searchForm = <SearchBox />; - } - - 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 post={rootPost} commentCount={postsArray.length}/> - <div className='post-right-comments-container'> - {postsArray.map(function mapPosts(comPost) { - return <CommentPost ref={comPost.id} key={comPost.id} post={comPost} selected={(comPost.id === selectedPost.id)} />; - })} - </div> - <div className='post-create__container'> - <CreateComment channelId={rootPost.channel_id} rootId={rootPost.id} /> - </div> - </div> - </div> - </div> - ); - } -}); diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx new file mode 100644 index 000000000..7df2fed9e --- /dev/null +++ b/web/react/components/rhs_comment.jsx @@ -0,0 +1,207 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var PostStore = require('../stores/post_store.jsx'); +var ChannelStore = require('../stores/channel_store.jsx'); +var UserProfile = require('./user_profile.jsx'); +var UserStore = require('../stores/user_store.jsx'); +var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +var utils = require('../utils/utils.jsx'); +var Constants = require('../utils/constants.jsx'); +var FileAttachmentList = require('./file_attachment_list.jsx'); +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); +var ActionTypes = Constants.ActionTypes; + +export default class RhsComment extends React.Component { + constructor(props) { + super(props); + + this.retryComment = this.retryComment.bind(this); + + this.state = {}; + } + retryComment(e) { + e.preventDefault(); + + var post = this.props.post; + client.createPost(post, post.channel_id, + function success(data) { + AsyncClient.getPosts(post.channel_id); + + var channel = ChannelStore.get(post.channel_id); + var member = ChannelStore.getMember(post.channel_id); + member.msg_count = channel.total_msg_count; + member.last_viewed_at = (new Date()).getTime(); + ChannelStore.setChannelMember(member); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST, + post: data + }); + }, + function fail() { + post.state = Constants.POST_FAILED; + PostStore.updatePendingPost(post); + this.forceUpdate(); + }.bind(this) + ); + + post.state = Constants.POST_LOADING; + PostStore.updatePendingPost(post); + this.forceUpdate(); + } + shouldComponentUpdate(nextProps) { + if (!utils.areStatesEqual(nextProps.post, this.props.post)) { + return true; + } + + return false; + } + render() { + var post = this.props.post; + + var currentUserCss = ''; + if (UserStore.getCurrentId() === post.user_id) { + currentUserCss = 'current--user'; + } + + var isOwner = UserStore.getCurrentId() === post.user_id; + + var type = 'Post'; + if (post.root_id.length > 0) { + type = 'Comment'; + } + + var message = utils.textToJsx(post.message); + var timestamp = UserStore.getCurrentUser().update_at; + + var loading; + var postClass = ''; + if (post.state === Constants.POST_FAILED) { + postClass += ' post-fail'; + loading = ( + <a + className='theme post-retry pull-right' + href='#' + onClick={this.retryComment} + > + Retry + </a> + ); + } else if (post.state === Constants.POST_LOADING) { + postClass += ' post-waiting'; + loading = ( + <img + className='post-loading-gif pull-right' + src='/static/images/load.gif' + /> + ); + } + + var ownerOptions; + if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { + ownerOptions = ( + <div + className='dropdown' + onClick={ + function scroll() { + $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50); + } + } + > + <a + href='#' + className='dropdown-toggle theme' + type='button' + data-toggle='dropdown' + aria-expanded='false' + /> + <ul + className='dropdown-menu' + role='menu' + > + <li role='presentation'> + <a + href='#' + role='menuitem' + data-toggle='modal' + data-target='#edit_post' + data-title={type} + data-message={post.message} + data-postid={post.id} + data-channelid={post.channel_id} + > + Edit + </a> + </li> + <li role='presentation'> + <a + href='#' + role='menuitem' + data-toggle='modal' + data-target='#delete_post' + data-title={type} + data-postid={post.id} + data-channelid={post.channel_id} + data-comments={0} + > + Delete + </a> + </li> + </ul> + </div> + ); + } + + var fileAttachment; + if (post.filenames && post.filenames.length > 0) { + fileAttachment = ( + <FileAttachmentList + filenames={post.filenames} + modalId={'rhs_comment_view_image_modal_' + post.id} + channelId={post.channel_id} + userId={post.user_id} /> + ); + } + + return ( + <div className={'post ' + currentUserCss}> + <div className='post-profile-img__container'> + <img + className='post-profile-img' + src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} + height='36' + width='36' + /> + </div> + <div className='post__content'> + <ul className='post-header'> + <li className='post-header-col'> + <strong><UserProfile userId={post.user_id} /></strong> + </li> + <li className='post-header-col'> + <time className='post-right-comment-time'> + {utils.displayCommentDateTime(post.create_at)} + </time> + </li> + <li className='post-header-col post-header__reply'> + {ownerOptions} + </li> + </ul> + <div className='post-body'> + <p className={postClass}>{loading}{message}</p> + {fileAttachment} + </div> + </div> + </div> + ); + } +} + +RhsComment.defaultProps = { + post: null +}; +RhsComment.propTypes = { + post: React.PropTypes.object +}; diff --git a/web/react/components/rhs_header_post.jsx b/web/react/components/rhs_header_post.jsx new file mode 100644 index 000000000..4cf4231e9 --- /dev/null +++ b/web/react/components/rhs_header_post.jsx @@ -0,0 +1,81 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +var Constants = require('../utils/constants.jsx'); +var ActionTypes = Constants.ActionTypes; + +export default class RhsHeaderPost extends React.Component { + constructor(props) { + super(props); + + this.handleClose = this.handleClose.bind(this); + this.handleBack = this.handleBack.bind(this); + + this.state = {}; + } + handleClose(e) { + e.preventDefault(); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_SEARCH, + results: null + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST_SELECTED, + results: null + }); + } + handleBack(e) { + e.preventDefault(); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_SEARCH_TERM, + term: this.props.fromSearch, + do_search: true, + is_mention_search: this.props.isMentionSearch + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST_SELECTED, + results: null + }); + } + render() { + var back; + if (this.props.fromSearch) { + back = ( + <a + href='#' + onClick={this.handleBack} + className='sidebar--right__back' + > + <i className='fa fa-chevron-left'></i> + </a> + ); + } + + return ( + <div className='sidebar--right__header'> + <span className='sidebar--right__title'>{back}Message Details</span> + <button + type='button' + className='sidebar--right__close' + aria-label='Close' + onClick={this.handleClose} + > + </button> + </div> + ); + } +} + +RhsHeaderPost.defaultProps = { + isMentionSearch: false, + fromSearch: '' +}; +RhsHeaderPost.propTypes = { + isMentionSearch: React.PropTypes.bool, + fromSearch: React.PropTypes.string +}; diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx new file mode 100644 index 000000000..a407e6470 --- /dev/null +++ b/web/react/components/rhs_root_post.jsx @@ -0,0 +1,145 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var ChannelStore = require('../stores/channel_store.jsx'); +var UserProfile = require('./user_profile.jsx'); +var UserStore = require('../stores/user_store.jsx'); +var utils = require('../utils/utils.jsx'); +var FileAttachmentList = require('./file_attachment_list.jsx'); + +export default class RhsRootPost extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + shouldComponentUpdate(nextProps) { + if (!utils.areStatesEqual(nextProps.post, this.props.post)) { + return true; + } + + return false; + } + render() { + var post = this.props.post; + var message = utils.textToJsx(post.message); + var isOwner = UserStore.getCurrentId() === post.user_id; + var timestamp = UserStore.getProfile(post.user_id).update_at; + var channel = ChannelStore.get(post.channel_id); + + var type = 'Post'; + if (post.root_id.length > 0) { + type = 'Comment'; + } + + var currentUserCss = ''; + if (UserStore.getCurrentId() === post.user_id) { + currentUserCss = 'current--user'; + } + + var channelName; + if (channel) { + if (channel.type === 'D') { + channelName = 'Private Message'; + } else { + channelName = channel.display_name; + } + } + + var ownerOptions; + if (isOwner) { + ownerOptions = ( + <div> + <a href='#' + className='dropdown-toggle theme' + type='button' + data-toggle='dropdown' + aria-expanded='false' + /> + <ul + className='dropdown-menu' + role='menu' + > + <li role='presentation'> + <a + href='#' + role='menuitem' + data-toggle='modal' + data-target='#edit_post' + data-title={type} + data-message={post.message} + data-postid={post.id} + data-channelid={post.channel_id} + > + Edit + </a> + </li> + <li role='presentation'> + <a + href='#' + role='menuitem' + data-toggle='modal' + data-target='#delete_post' + data-title={type} + data-postid={post.id} + data-channelid={post.channel_id} + data-comments={this.props.commentCount} + > + Delete + </a> + </li> + </ul> + </div> + ); + } + + var fileAttachment; + if (post.filenames && post.filenames.length > 0) { + fileAttachment = ( + <FileAttachmentList + filenames={post.filenames} + modalId={'rhs_view_image_modal_' + post.id} + channelId={post.channel_id} + userId={post.user_id} /> + ); + } + + return ( + <div className={'post post--root ' + currentUserCss}> + <div className='post-right-channel__name'>{channelName}</div> + <div className='post-profile-img__container'> + <img + className='post-profile-img' + src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} + height='36' + width='36' + /> + </div> + <div className='post__content'> + <ul className='post-header'> + <li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li> + <li className='post-header-col'><time className='post-right-root-time'>{utils.displayCommentDateTime(post.create_at)}</time></li> + <li className='post-header-col post-header__reply'> + <div className='dropdown'> + {ownerOptions} + </div> + </li> + </ul> + <div className='post-body'> + <p>{message}</p> + {fileAttachment} + </div> + </div> + <hr /> + </div> + ); + } +} + +RhsRootPost.defaultProps = { + post: null, + commentCount: 0 +}; +RhsRootPost.propTypes = { + post: React.PropTypes.object, + commentCount: React.PropTypes.number +}; diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx new file mode 100644 index 000000000..adddeccf0 --- /dev/null +++ b/web/react/components/rhs_thread.jsx @@ -0,0 +1,215 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var PostStore = require('../stores/post_store.jsx'); +var UserStore = require('../stores/user_store.jsx'); +var utils = require('../utils/utils.jsx'); +var SearchBox = require('./search_bar.jsx'); +var CreateComment = require('./create_comment.jsx'); +var RhsHeaderPost = require('./rhs_header_post.jsx'); +var RootPost = require('./rhs_root_post.jsx'); +var Comment = require('./rhs_comment.jsx'); +var Constants = require('../utils/constants.jsx'); +var FileUploadOverlay = require('./file_upload_overlay.jsx'); + +export default class RhsThread extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + this.onChangeAll = this.onChangeAll.bind(this); + this.onTimeChange = this.onTimeChange.bind(this); + + this.state = this.getStateFromStores(); + } + getStateFromStores() { + var postList = PostStore.getSelectedPost(); + if (!postList || postList.order.length < 1) { + return {postList: {}}; + } + + var channelId = postList.posts[postList.order[0]].channel_id; + var pendingPostList = PostStore.getPendingPosts(channelId); + + if (pendingPostList) { + for (var pid in pendingPostList.posts) { + postList.posts[pid] = pendingPostList.posts[pid]; + } + } + + return {postList: postList}; + } + componentDidMount() { + PostStore.addSelectedPostChangeListener(this.onChange); + PostStore.addChangeListener(this.onChangeAll); + UserStore.addStatusesChangeListener(this.onTimeChange); + this.resize(); + $(window).resize(function resize() { + this.resize(); + }.bind(this)); + } + componentDidUpdate() { + $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); + $('.post-right__scroll').perfectScrollbar('update'); + this.resize(); + } + componentWillUnmount() { + PostStore.removeSelectedPostChangeListener(this.onChange); + PostStore.removeChangeListener(this.onChangeAll); + UserStore.removeStatusesChangeListener(this.onTimeChange); + } + onChange() { + var newState = this.getStateFromStores(); + if (!utils.areStatesEqual(newState, this.state)) { + this.setState(newState); + } + } + onChangeAll() { + // if something was changed in the channel like adding a + // comment or post then lets refresh the sidebar list + var currentSelected = PostStore.getSelectedPost(); + if (!currentSelected || currentSelected.order.length === 0) { + return; + } + + var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id); + + if (!currentPosts || currentPosts.order.length === 0) { + return; + } + + if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) { + currentSelected.posts = {}; + for (var postId in currentPosts.posts) { + currentSelected.posts[postId] = currentPosts.posts[postId]; + } + + PostStore.storeSelectedPost(currentSelected); + } + + var newState = this.getStateFromStores(); + if (!utils.areStatesEqual(newState, this.state)) { + this.setState(newState); + } + } + onTimeChange() { + for (var id in this.state.postList.posts) { + if (!this.refs[id]) { + continue; + } + this.refs[id].forceUpdate(); + } + } + resize() { + var height = $(window).height() - $('#error_bar').outerHeight() - 100; + $('.post-right__scroll').css('height', height + 'px'); + $('.post-right__scroll').scrollTop(100000); + $('.post-right__scroll').perfectScrollbar(); + $('.post-right__scroll').perfectScrollbar('update'); + } + render() { + var postList = this.state.postList; + + if (postList == null) { + return ( + <div></div> + ); + } + + var selectedPost = postList.posts[postList.order[0]]; + var rootPost = null; + + if (selectedPost.root_id === '') { + rootPost = selectedPost; + } else { + rootPost = postList.posts[selectedPost.root_id]; + } + + var postsArray = []; + + for (var postId in postList.posts) { + var cpost = postList.posts[postId]; + if (cpost.root_id === rootPost.id) { + postsArray.push(cpost); + } + } + + // sort failed posts to bottom, followed by pending, and then regular posts + postsArray.sort(function postSort(a, b) { + if ((a.state === Constants.POST_LOADING || a.state === Constants.POST_FAILED) && (b.state !== Constants.POST_LOADING && b.state !== Constants.POST_FAILED)) { + return 1; + } + if ((a.state !== Constants.POST_LOADING && a.state !== Constants.POST_FAILED) && (b.state === Constants.POST_LOADING || b.state === Constants.POST_FAILED)) { + return -1; + } + + if (a.state === Constants.POST_LOADING && b.state === Constants.POST_FAILED) { + return -1; + } + if (a.state === Constants.POST_FAILED && b.state === Constants.POST_LOADING) { + return 1; + } + + if (a.create_at < b.create_at) { + return -1; + } + if (a.create_at > b.create_at) { + return 1; + } + return 0; + }); + + var currentId = UserStore.getCurrentId(); + var searchForm; + if (currentId != null) { + searchForm = <SearchBox />; + } + + 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 + post={rootPost} + commentCount={postsArray.length} + /> + <div className='post-right-comments-container'> + {postsArray.map(function mapPosts(comPost) { + return ( + <Comment + ref={comPost.id} + key={comPost.id} + post={comPost} + selected={(comPost.id === selectedPost.id)} + /> + ); + })} + </div> + <div className='post-create__container'> + <CreateComment + channelId={rootPost.channel_id} + rootId={rootPost.id} + /> + </div> + </div> + </div> + </div> + ); + } +} + +RhsThread.defaultProps = { + fromSearch: '', + isMentionSearch: false +}; +RhsThread.propTypes = { + fromSearch: React.PropTypes.string, + isMentionSearch: React.PropTypes.bool +}; diff --git a/web/react/components/setting_upload.jsx b/web/react/components/setting_upload.jsx index 02789f5dd..596324308 100644 --- a/web/react/components/setting_upload.jsx +++ b/web/react/components/setting_upload.jsx @@ -8,7 +8,8 @@ module.exports = React.createClass({ submit: React.PropTypes.func.isRequired, fileTypesAccepted: React.PropTypes.string.isRequired, clientError: React.PropTypes.string, - serverError: React.PropTypes.string + serverError: React.PropTypes.string, + helpText: React.PropTypes.string }, getInitialState: function() { return { @@ -38,14 +39,6 @@ module.exports = React.createClass({ this.setState({clientError: 'No file selected.'}); } }, - doCancel: function(e) { - e.preventDefault(); - this.refs.uploadinput.getDOMNode().value = ''; - this.setState({ - clientError: '', - serverError: '' - }); - }, onFileSelect: function(e) { var filename = $(e.target).val(); if (filename.substring(3, 11) === 'fakepath') { @@ -70,6 +63,7 @@ module.exports = React.createClass({ return ( <ul className='section-max'> <li className='col-xs-12 section-title'>{this.props.title}</li> + <li className='col-xs-offset-3'>{this.props.helpText}</li> <li className='col-xs-offset-3 col-xs-8'> <ul className='setting-list'> <li className='setting-list-item'> @@ -79,12 +73,6 @@ module.exports = React.createClass({ onClick={this.doSubmit}> Import </a> - <a - className='btn btn-sm btn-link theme' - href='#' - onClick={this.doCancel}> - Cancel - </a> <div className='file-status file-name hide'></div> {serverError} {clientError} diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index d79505e9e..8dd192893 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -64,7 +64,8 @@ function getStateFromStores() { var tempChannel = {}; tempChannel.fake = true; tempChannel.name = channelName; - tempChannel.display_name = utils.getDisplayName(teammate); + tempChannel.display_name = teammate.username; + tempChannel.teammate_username = teammate.username; tempChannel.status = UserStore.getStatus(teammate.id); tempChannel.last_post_at = 0; tempChannel.total_msg_count = 0; diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx index 8334b345b..df75e3adf 100644 --- a/web/react/components/sidebar_right.jsx +++ b/web/react/components/sidebar_right.jsx @@ -1,11 +1,9 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - -var SearchResults =require('./search_results.jsx'); -var PostRight =require('./post_right.jsx'); +var SearchResults = require('./search_results.jsx'); +var RhsThread = require('./rhs_thread.jsx'); var PostStore = require('../stores/post_store.jsx'); -var Constants = require('../utils/constants.jsx'); var utils = require('../utils/utils.jsx'); function getStateFromStores(from_search) { @@ -39,8 +37,8 @@ module.exports = React.createClass({ } }, resize: function() { - $(".post-list-holder-by-time").scrollTop(100000); - $(".post-list-holder-by-time").perfectScrollbar('update'); + var postHolder = $('.post-list-holder-by-time'); + postHolder[0].scrollTop = postHolder[0].scrollHeight - 224; }, getInitialState: function() { return getStateFromStores(); @@ -72,7 +70,7 @@ module.exports = React.createClass({ content = <SearchResults isMentionSearch={this.state.is_mention_search} />; } else if (this.state.post_right_visible) { - content = <PostRight fromSearch={this.state.from_search} isMentionSearch={this.state.is_mention_search} />; + content = <RhsThread fromSearch={this.state.from_search} isMentionSearch={this.state.is_mention_search} />; } return ( diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index d221ca840..615bc4ef2 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -15,7 +15,6 @@ module.exports = React.createClass({ var inviteLink = ''; var teamSettingsLink = ''; var manageLink = ''; - var renameLink = ''; var currentUser = UserStore.getCurrentUser(); var isAdmin = false; @@ -48,11 +47,6 @@ module.exports = React.createClass({ <a href='#' data-toggle='modal' data-target='#team_members'><i className='glyphicon glyphicon-wrench'></i>Manage Team</a> </li> ); - renameLink = ( - <li> - <a href='#' data-toggle='modal' data-target='#rename_team_link'><i className='glyphicon glyphicon-pencil'></i>Rename</a> - </li> - ); } var siteName = ''; @@ -77,7 +71,6 @@ module.exports = React.createClass({ {inviteLink} {teamLink} {manageLink} - {renameLink} <li><a href='#' onClick={this.handleLogoutClick}><i className='glyphicon glyphicon-log-out'></i>Logout</a></li> <li className='divider'></li> <li><a target='_blank' href='/static/help/configure_links.html'><i className='glyphicon glyphicon-question-sign'></i>Help</a></li> diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index edd48e0b9..13640b1e5 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -1,69 +1,49 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); - -module.exports = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - var team = {}; - var state = { server_error: "" }; - - team.email = this.refs.email.getDOMNode().value.trim().toLowerCase(); - if (!team.email || !utils.isEmail(team.email)) { - state.email_error = "Please enter a valid email address"; - state.inValid = true; +var ChoosePage = require('./team_signup_choose_auth.jsx'); +var EmailSignUpPage = require('./team_signup_with_email.jsx'); +var SSOSignupPage = require('./team_signup_with_sso.jsx'); +var Constants = require('../utils/constants.jsx'); + +export default class TeamSignUp extends React.Component { + constructor(props) { + super(props); + + this.updatePage = this.updatePage.bind(this); + + if (props.services.length === 1) { + if (props.services[0] === Constants.EMAIL_SERVICE) { + this.state = {page: 'email', service: ''}; + } else { + this.state = {page: 'service', service: props.services[0]}; + } + } else { + this.state = {page: 'choose', service: ''}; } - else { - state.email_error = ""; - } - - if (state.inValid) { - this.setState(state); - return; + } + updatePage(page, service) { + this.setState({page: page, service: service}); + } + render() { + if (this.state.page === 'email') { + return <EmailSignUpPage />; + } else if (this.state.page === 'service' && this.state.service !== '') { + return <SSOSignupPage service={this.state.service} />; + } else { + return ( + <ChoosePage + services={this.props.services} + updatePage={this.updatePage} + /> + ); } - - client.signupTeam(team.email, - function(data) { - if (data["follow_link"]) { - window.location.href = data["follow_link"]; - } - else { - window.location.href = "/signup_team_confirm/?email=" + encodeURIComponent(team.email); - } - }.bind(this), - function(err) { - state.server_error = err.message; - this.setState(state); - }.bind(this) - ); - }, - getInitialState: function() { - return { }; - }, - render: function() { - - var email_error = this.state.email_error ? <label className='control-label'>{ this.state.email_error }</label> : null; - var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null; - - return ( - <form role="form" onSubmit={this.handleSubmit}> - <div className={ email_error ? "form-group has-error" : "form-group" }> - <input autoFocus={true} type="email" ref="email" className="form-control" placeholder="Email Address" maxLength="128" /> - { email_error } - </div> - { server_error } - <div className="form-group"> - <button className="btn btn-md btn-primary" type="submit">Sign up</button> - </div> - <div className="form-group margin--extra-2x"> - <span><a href="/find_team">{"Find my " + strings.Team}</a></span> - </div> - </form> - ); } -}); - - +} + +TeamSignUp.defaultProps = { + services: [] +}; +TeamSignUp.propTypes = { + services: React.PropTypes.array +}; diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 0393e0413..2080cc191 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -162,16 +162,35 @@ module.exports = React.createClass({ ); } - if (authServices.indexOf(Constants.GOOGLE_SERVICE) >= 0) { - signupMessage.push( - <a className='btn btn-custom-login google' href={'/' + this.props.teamName + '/signup/google' + window.location.search}> - <span className='icon' /> - <span>with Google</span> - </a> - ); + var emailSignup; + if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { + emailSignup = ( + <div> + <div className='inner__content'> + {email} + {yourEmailIs} + <div className='margin--extra'> + <h5><strong>Choose your username</strong></h5> + <div className={nameDivStyle}> + <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' /> + {nameError} + <p className='form__hint'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</p> + </div> + </div> + <div className='margin--extra'> + <h5><strong>Choose your password</strong></h5> + <div className={passwordDivStyle}> + <input type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> + {passwordError} + </div> + </div> + </div> + <p className='margin--extra'><button type='submit' onClick={this.handleSubmit} className='btn-primary btn'>Create Account</button></p> + </div> + ); } - if (signupMessage.length > 0) { + if (signupMessage.length > 0 && emailSignup) { signupMessage = ( <div> {signupMessage} @@ -196,26 +215,7 @@ module.exports = React.createClass({ <h2 className='signup-team__subdomain'>on {config.SiteName}</h2> <h4 className='color--light'>Let's create your account</h4> {signupMessage} - <div className='inner__content'> - {email} - {yourEmailIs} - <div className='margin--extra'> - <h5><strong>Choose your username</strong></h5> - <div className={nameDivStyle}> - <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' /> - {nameError} - <p className='form__hint'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</p> - </div> - </div> - <div className='margin--extra'> - <h5><strong>Choose your password</strong></h5> - <div className={passwordDivStyle}> - <input type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> - {passwordError} - </div> - </div> - </div> - <p className='margin--extra'><button type='submit' onClick={this.handleSubmit} className='btn-primary btn'>Create Account</button></p> + {emailSignup} {serverError} {termsDisclaimer} </form> diff --git a/web/react/components/signup_user_oauth.jsx b/web/react/components/signup_user_oauth.jsx deleted file mode 100644 index 8b2800bde..000000000 --- a/web/react/components/signup_user_oauth.jsx +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - - -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var BrowserStore = require('../stores/browser_store.jsx'); - -module.exports = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - - if (!this.state.user.username) { - this.setState({name_error: "This field is required", email_error: "", password_error: "", server_error: ""}); - return; - } - - var username_error = utils.isValidUsername(this.state.user.username); - if (username_error === "Cannot use a reserved word as a username.") { - this.setState({name_error: "This username is reserved, please choose a new one.", email_error: "", password_error: "", server_error: ""}); - return; - } else if (username_error) { - this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.", email_error: "", password_error: "", server_error: ""}); - return; - } - - this.setState({name_error: "", server_error: ""}); - - this.state.user.allow_marketing = this.refs.email_service.getDOMNode().checked; - - var user = this.state.user; - client.createUser(user, "", "", - function(data) { - client.track('signup', 'signup_user_oauth_02'); - UserStore.setCurrentUser(data); - UserStore.setLastEmail(data.email); - - window.location.href = '/' + this.props.teamName + '/login/' + user.auth_service + '?login_hint=' + user.email; - }.bind(this), - function(err) { - this.state.server_error = err.message; - this.setState(this.state); - }.bind(this) - ); - }, - handleChange: function() { - var user = this.state.user; - user.username = this.refs.name.getDOMNode().value; - this.setState({ user: user }); - }, - getInitialState: function() { - var user = JSON.parse(this.props.user); - return { user: user }; - }, - render: function() { - - client.track('signup', 'signup_user_oauth_01'); - - var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null; - var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null; - - var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is <b>{ this.state.user.email }.</b></span>; - - return ( - <div> - <img className="signup-team-logo" src="/static/images/logo.png" /> - <h4>Welcome to { config.SiteName }</h4> - <p>{"To continue signing up with " + this.state.user.auth_service + ", please register a username."}</p> - <p>Your username can be made of lowercase letters and numbers.</p> - <label className="control-label">Username</label> - <div className={ name_error ? "form-group has-error" : "form-group" }> - <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" value={this.state.user.username} onChange={this.handleChange} /> - { name_error } - </div> - <p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others."}</p> - <p>{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.</p> - <div className="checkbox"><label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service. </label></div> - <p><button onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p> - { server_error } - <p>By proceeding to create your account and use { config.SiteName }, you agree to our <a href={ config.TermsLink }>Terms of Service</a> and <a href={ config.PrivacyLink }>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p> - </div> - ); - } -}); - - diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx index c21701c0e..e3415d7f4 100644 --- a/web/react/components/team_import_tab.jsx +++ b/web/react/components/team_import_tab.jsx @@ -20,10 +20,20 @@ module.exports = React.createClass({ utils.importSlack(file, this.onImportSuccess, this.onImportFailure); }, render: function() { + var uploadHelpText = ( + <div> + <br/> + Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team's public channels. + <br/><br/> + The Slack import to Mattermost is in "Preview". Slack bot posts and channels with underscores do not yet import. + <br/><br/> + </div> + ); var uploadSection = ( <SettingUpload title='Import from Slack' submit={this.doImportSlack} + helpText={uploadHelpText} fileTypesAccepted='.zip'/> ); @@ -39,12 +49,12 @@ module.exports = React.createClass({ break; case 'done': messageSection = ( - <p className="confirm-import alert alert-success"><i className="fa fa-check"></i> Import sucessfull: <a href={this.state.link} download='MattermostImportSummery.txt'>View Summery</a></p> + <p className="confirm-import alert alert-success"><i className="fa fa-check"></i> Import successful: <a href={this.state.link} download='MattermostImportSummary.txt'>View Summary</a></p> ); break; case 'fail': messageSection = ( - <p className="confirm-import alert alert-warning"><i className="fa fa-warning"></i> Import failure: <a href={this.state.link} download='MattermostImportSummery.txt'>View Summery</a></p> + <p className="confirm-import alert alert-warning"><i className="fa fa-warning"></i> Import failure: <a href={this.state.link} download='MattermostImportSummary.txt'>View Summary</a></p> ); break; } diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx new file mode 100644 index 000000000..92ade5d24 --- /dev/null +++ b/web/react/components/team_signup_choose_auth.jsx @@ -0,0 +1,70 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Constants = require('../utils/constants.jsx'); + +export default class ChooseAuthPage extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { + var buttons = []; + if (this.props.services.indexOf(Constants.GITLAB_SERVICE) !== -1) { + buttons.push( + <a + className='btn btn-custom-login gitlab btn-full' + href='#' + onClick={ + function clickGit(e) { + e.preventDefault(); + this.props.updatePage('service', Constants.GITLAB_SERVICE); + }.bind(this) + } + > + <span className='icon' /> + <span>Create new {strings.Team} with GitLab Account</span> + </a> + ); + } + + if (this.props.services.indexOf(Constants.EMAIL_SERVICE) !== -1) { + buttons.push( + <a + className='btn btn-custom-login email btn-full' + href='#' + onClick={ + function clickEmail(e) { + e.preventDefault(); + this.props.updatePage('email', ''); + }.bind(this) + } + > + <span className='fa fa-envelope' /> + <span>Create new {strings.Team} with email address</span> + </a> + ); + } + + if (buttons.length === 0) { + buttons = <span>No sign-up methods configured, please contact your system administrator.</span>; + } + + return ( + <div> + {buttons} + <div className='form-group margin--extra-2x'> + <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + </div> + </div> + ); + } +} + +ChooseAuthPage.defaultProps = { + services: [] +}; +ChooseAuthPage.propTypes = { + services: React.PropTypes.array, + updatePage: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx index e4f35f100..bbe82a5c2 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/team_signup_password_page.jsx @@ -31,31 +31,35 @@ module.exports = React.createClass({ teamSignup.user.allow_marketing = true; delete teamSignup.wizard; - // var ctl = this; - client.createTeamFromSignup(teamSignup, function success() { client.track('signup', 'signup_team_08_complete'); var props = this.props; - $('#sign-up-button').button('reset'); - props.state.wizard = 'finished'; - props.updateParent(props.state, true); + + client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, + function(data) { + UserStore.setLastEmail(teamSignup.team.email); + UserStore.setCurrentUser(teamSignup.user); + if (this.props.hash > 0) { + BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); + } - window.location.href = utils.getWindowLocationOrigin() + '/' + props.state.team.name + '/login?email=' + encodeURIComponent(teamSignup.team.email); + $('#sign-up-button').button('reset'); + props.state.wizard = 'finished'; + props.updateParent(props.state, true); - // client.loginByEmail(teamSignup.team.domain, teamSignup.team.email, teamSignup.user.password, - // function(data) { - // TeamStore.setLastName(teamSignup.team.domain); - // UserStore.setLastEmail(teamSignup.team.email); - // UserStore.setCurrentUser(data); - // window.location.href = '/channels/town-square'; - // }.bind(ctl), - // function(err) { - // this.setState({nameError: err.message}); - // }.bind(ctl) - // ); + window.location.href = '/'; + }.bind(this), + function(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}); + } + }.bind(this) + ); }.bind(this), function error(err) { this.setState({serverError: err.message}); diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx new file mode 100644 index 000000000..c7204880f --- /dev/null +++ b/web/react/components/team_signup_with_email.jsx @@ -0,0 +1,82 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); + +export default class EmailSignUpPage extends React.Component { + constructor() { + super(); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = {}; + } + handleSubmit(e) { + e.preventDefault(); + var team = {}; + var state = {serverError: ''}; + + team.email = this.refs.email.getDOMNode().value.trim().toLowerCase(); + if (!team.email || !utils.isEmail(team.email)) { + state.emailError = 'Please enter a valid email address'; + state.inValid = true; + } else { + state.emailError = ''; + } + + if (state.inValid) { + this.setState(state); + return; + } + + client.signupTeam(team.email, + function success(data) { + if (data.follow_link) { + window.location.href = data.follow_link; + } else { + window.location.href = '/signup_team_confirm/?email=' + encodeURIComponent(team.email); + } + }, + function fail(err) { + state.serverError = err.message; + this.setState(state); + }.bind(this) + ); + } + render() { + return ( + <form + role='form' + onSubmit={this.handleSubmit} + > + <div className='form-group'> + <input + autoFocus={true} + type='email' + ref='email' + className='form-control' + placeholder='Email Address' + maxLength='128' + /> + </div> + <div className='form-group'> + <button + className='btn btn-md btn-primary' + type='submit' + > + Sign up + </button> + </div> + <div className='form-group margin--extra-2x'> + <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + </div> + </form> + ); + } +} + +EmailSignUpPage.defaultProps = { +}; +EmailSignUpPage.propTypes = { +}; diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx new file mode 100644 index 000000000..6cb62efc7 --- /dev/null +++ b/web/react/components/team_signup_with_sso.jsx @@ -0,0 +1,125 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); +var Constants = require('../utils/constants.jsx'); + +export default class SSOSignUpPage extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.nameChange = this.nameChange.bind(this); + + this.state = {name: ''}; + } + handleSubmit(e) { + e.preventDefault(); + var team = {}; + var state = this.state; + state.nameError = null; + state.serverError = null; + + team.display_name = this.state.name; + + if (team.display_name.length <= 3) { + return; + } + + if (!team.display_name) { + state.nameError = 'Please enter a team name'; + this.setState(state); + return; + } + + team.name = utils.cleanUpUrlable(team.display_name); + team.type = 'O'; + + client.createTeamWithSSO(team, + this.props.service, + function success(data) { + if (data.follow_link) { + window.location.href = data.follow_link; + } else { + window.location.href = '/'; + } + }, + function fail(err) { + state.serverError = err.message; + this.setState(state); + }.bind(this) + ); + } + nameChange() { + this.setState({name: this.refs.teamname.getDOMNode().value.trim()}); + } + render() { + var nameError = null; + var nameDivClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameDivClass += ' has-error'; + } + + var serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + var disabled = false; + if (this.state.name.length <= 3) { + disabled = true; + } + + var button = null; + + if (this.props.service === Constants.GITLAB_SERVICE) { + button = ( + <a + className='btn btn-custom-login gitlab btn-full' + href='#' + onClick={this.handleSubmit} + disabled={disabled} + > + <span className='icon'/> + <span>Create {strings.Team} with GitLab Account</span> + </a> + ); + } + + return ( + <form + role='form' + onSubmit={this.handleSubmit} + > + <div className={nameDivClass}> + <input + autoFocus={true} + type='text' + ref='teamname' + className='form-control' + placeholder='Enter name of new team' + maxLength='128' + onChange={this.nameChange} + /> + {nameError} + </div> + <div className='form-group'> + {button} + {serverError} + </div> + <div className='form-group margin--extra-2x'> + <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + </div> + </form> + ); + } +} + +SSOSignUpPage.defaultProps = { + service: '' +}; +SSOSignUpPage.propTypes = { + service: React.PropTypes.string +}; diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index b5c5cc564..efd2dd810 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -257,7 +257,7 @@ module.exports = React.createClass({ return ( <div ref='wrapper' className='textarea-wrapper'> <CommandList ref='commands' addCommand={this.addCommand} channelId={this.props.channelId} /> - <textarea id={this.props.id} ref='message' className={'form-control custom-textarea ' + this.state.connection} spellCheck='true' autoComplete='off' autoCorrect='off' rows='1' placeholder={this.props.createMessage} value={this.props.messageText} onInput={this.handleChange} onChange={this.handleChange} onKeyPress={this.handleKeyPress} onKeyDown={this.handleKeyDown} onFocus={this.handleFocus} onBlur={this.handleBlur} onPaste={this.handlePaste} /> + <textarea id={this.props.id} ref='message' className={'form-control custom-textarea ' + this.state.connection} spellCheck='true' autoComplete='off' autoCorrect='off' rows='1' maxLength={Constants.MAX_POST_LEN} placeholder={this.props.createMessage} value={this.props.messageText} onInput={this.handleChange} onChange={this.handleChange} onKeyPress={this.handleKeyPress} onKeyDown={this.handleKeyDown} onFocus={this.handleFocus} onBlur={this.handleBlur} onPaste={this.handlePaste} /> </div> ); } diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index 37c441d4f..4b58025ac 100644 --- a/web/react/pages/signup_team.jsx +++ b/web/react/pages/signup_team.jsx @@ -5,11 +5,13 @@ var SignupTeam = require('../components/signup_team.jsx'); var AsyncClient = require('../utils/async_client.jsx'); -global.window.setup_signup_team_page = function() { +global.window.setup_signup_team_page = function(authServices) { AsyncClient.getConfig(); + var services = JSON.parse(authServices); + React.render( - <SignupTeam />, + <SignupTeam services={services} />, document.getElementById('signup-team') ); }; diff --git a/web/react/pages/signup_user_oauth.jsx b/web/react/pages/signup_user_oauth.jsx deleted file mode 100644 index 6a0707702..000000000 --- a/web/react/pages/signup_user_oauth.jsx +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -var SignupUserOAuth = require('../components/signup_user_oauth.jsx'); - -global.window.setup_signup_user_oauth_page = function(user, team_name, team_display_name) { - React.render( - <SignupUserOAuth user={user} teamName={team_name} teamDisplayName={team_display_name} />, - document.getElementById('signup-user-complete') - ); -}; diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 2fffb17d0..4038814d2 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -99,8 +99,52 @@ var PostStore = assign({}, EventEmitter.prototype, { } return null; }, - storePosts: function storePosts(channelId, posts) { - this.pStorePosts(channelId, posts); + storePosts: function storePosts(channelId, newPostList) { + if (isPostListNull(newPostList)) { + return; + } + + var postList = makePostListNonNull(PostStore.getPosts(channelId)); + + for (var pid in newPostList.posts) { + var np = newPostList.posts[pid]; + if (np.delete_at === 0) { + postList.posts[pid] = np; + if (postList.order.indexOf(pid) === -1) { + postList.order.push(pid); + } + } else { + if (pid in postList.posts) { + delete postList.posts[pid]; + } + + var index = postList.order.indexOf(pid); + if (index !== -1) { + postList.order.splice(index, 1); + } + } + } + + postList.order.sort(function postSort(a, b) { + if (postList.posts[a].create_at > postList.posts[b].create_at) { + return -1; + } + if (postList.posts[a].create_at < postList.posts[b].create_at) { + return 1; + } + + return 0; + }); + + var latestUpdate = 0; + for (var pid in postList.posts) { + if (postList.posts[pid].update_at > latestUpdate) { + latestUpdate = postList.posts[pid].update_at; + } + } + + this.storeLatestUpdate(channelId, latestUpdate); + this.pStorePosts(channelId, postList); this.emitChange(); }, pStorePosts: function pStorePosts(channelId, posts) { @@ -115,9 +159,7 @@ var PostStore = assign({}, EventEmitter.prototype, { }, pStorePost: function(post) { var postList = PostStore.getPosts(post.channel_id); - if (!postList) { - return; - } + postList = makePostListNonNull(postList); if (post.pending_post_id !== '') { this.removePendingPost(post.channel_id, post.pending_post_id); @@ -132,13 +174,28 @@ var PostStore = assign({}, EventEmitter.prototype, { this.pStorePosts(post.channel_id, postList); }, + removePost: function(postId, channelId) { + var postList = PostStore.getPosts(channelId); + if (isPostListNull(postList)) { + return; + } + + if (postId in postList.posts) { + delete postList.posts[postId]; + } + + var index = postList.order.indexOf(postId); + if (index !== -1) { + postList.order.splice(index, 1); + } + + this.pStorePosts(channelId, postList); + }, storePendingPost: function(post) { post.state = Constants.POST_LOADING; var postList = this.getPendingPosts(post.channel_id); - if (!postList) { - postList = {posts: {}, order: []}; - } + postList = makePostListNonNull(postList); postList.posts[post.pending_post_id] = post; postList.order.unshift(post.pending_post_id); @@ -200,15 +257,13 @@ var PostStore = assign({}, EventEmitter.prototype, { }, _removePendingPost: function(channelId, pendingPostId) { var postList = this.getPendingPosts(channelId); - if (!postList) { - return; - } + postList = makePostListNonNull(postList); if (pendingPostId in postList.posts) { delete postList.posts[pendingPostId]; } var index = postList.order.indexOf(pendingPostId); - if (index >= 0) { + if (index !== -1) { postList.order.splice(index, 1); } @@ -221,9 +276,7 @@ var PostStore = assign({}, EventEmitter.prototype, { }, updatePendingPost: function(post) { var postList = this.getPendingPosts(post.channel_id); - if (!postList) { - postList = {posts: {}, order: []}; - } + postList = makePostListNonNull(postList); if (postList.order.indexOf(post.pending_post_id) === -1) { return; @@ -293,6 +346,12 @@ var PostStore = assign({}, EventEmitter.prototype, { BrowserStore.setItem(key, value); } }); + }, + storeLatestUpdate: function(channelId, time) { + BrowserStore.setItem('latest_post_' + channelId, time); + }, + getLatestUpdate: function(channelId) { + return BrowserStore.getItem('latest_post_' + channelId, 0); } }); @@ -301,8 +360,7 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) { switch (action.type) { case ActionTypes.RECIEVED_POSTS: - PostStore.pStorePosts(action.id, action.post_list); - PostStore.emitChange(); + PostStore.storePosts(action.id, makePostListNonNull(action.post_list)); break; case ActionTypes.RECIEVED_POST: PostStore.pStorePost(action.post); @@ -331,3 +389,36 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) { }); module.exports = PostStore; + +function makePostListNonNull(pl) { + var postList = pl; + if (postList == null) { + postList = {order: [], posts: {}}; + } + + if (postList.order == null) { + postList.order = []; + } + + if (postList.posts == null) { + postList.posts = {}; + } + + return postList; +} + +function isPostListNull(pl) { + if (pl == null) { + return true; + } + + if (pl.posts == null) { + return true; + } + + if (pl.order == null) { + return true; + } + + return false; +} diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 349fe9021..c03a0230b 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -344,14 +344,14 @@ module.exports.search = function(terms) { ); } -module.exports.getPosts = function(force, id, maxPosts) { +module.exports.getPostsPage = function(force, id, maxPosts) { if (PostStore.getCurrentPosts() == null || force) { var channelId = id; if (channelId == null) { channelId = ChannelStore.getCurrentId(); } - if (isCallInProgress('getPosts_' + channelId)) { + if (isCallInProgress('getPostsPage_' + channelId)) { return; } @@ -371,9 +371,9 @@ module.exports.getPosts = function(force, id, maxPosts) { } if (channelId != null) { - callTracker['getPosts_' + channelId] = utils.getTimestamp(); + callTracker['getPostsPage_' + channelId] = utils.getTimestamp(); - client.getPosts( + client.getPostsPage( channelId, 0, numPosts, @@ -389,15 +389,63 @@ module.exports.getPosts = function(force, id, maxPosts) { module.exports.getProfiles(); }, function(err) { - dispatchError(err, 'getPosts'); + dispatchError(err, 'getPostsPage'); }, function() { - callTracker['getPosts_' + channelId] = 0; + callTracker['getPostsPage_' + channelId] = 0; } ); } } +}; + +function getPosts(id) { + var channelId = id; + if (channelId == null) { + if (ChannelStore.getCurrentId() == null) { + return; + } + channelId = ChannelStore.getCurrentId(); + } + + if (isCallInProgress('getPosts_' + channelId)) { + return; + } + + if (PostStore.getCurrentPosts() == null) { + module.exports.getPostsPage(true, id, Constants.POST_CHUNK_SIZE); + return; + } + + var latestUpdate = PostStore.getLatestUpdate(channelId); + + callTracker['getPosts_' + channelId] = utils.getTimestamp(); + + client.getPosts( + channelId, + latestUpdate, + function success(data, textStatus, xhr) { + if (xhr.status === 304 || !data) { + return; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POSTS, + id: channelId, + post_list: data + }); + + module.exports.getProfiles(); + }, + function fail(err) { + dispatchError(err, 'getPosts'); + }, + function complete() { + callTracker['getPosts_' + channelId] = 0; + } + ); } +module.exports.getPosts = getPosts; function getMe() { if (isCallInProgress('getMe')) { diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 13d6c3f54..082f82a08 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -70,6 +70,21 @@ module.exports.createTeamFromSignup = function(teamSignup, success, error) { }); }; +module.exports.createTeamWithSSO = function(team, service, success, error) { + $.ajax({ + url: '/api/v1/teams/create_with_sso/' + service, + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(team), + success: success, + error: function onError(xhr, status, err) { + var e = handleError('createTeamWithSSO', xhr, status, err); + error(e); + } + }); +}; + module.exports.createUser = function(user, data, emailHash, success, error) { $.ajax({ url: '/api/v1/users/create?d=' + encodeURIComponent(data) + '&h=' + encodeURIComponent(emailHash), @@ -653,7 +668,7 @@ module.exports.executeCommand = function(channelId, command, suggest, success, e }); }; -module.exports.getPosts = function(channelId, offset, limit, success, error, complete) { +module.exports.getPostsPage = function(channelId, offset, limit, success, error, complete) { $.ajax({ cache: false, url: '/api/v1/channels/' + channelId + '/posts/' + offset + '/' + limit, @@ -669,6 +684,21 @@ module.exports.getPosts = function(channelId, offset, limit, success, error, com }); }; +module.exports.getPosts = function(channelId, since, success, error, complete) { + $.ajax({ + url: '/api/v1/channels/' + channelId + '/posts/' + since, + dataType: 'json', + type: 'GET', + ifModified: true, + success: success, + error: function onError(xhr, status, err) { + var e = handleError('getPosts', xhr, status, err); + error(e); + }, + complete: complete + }); +}; + module.exports.getPost = function(channelId, postId, success, error) { $.ajax({ cache: false, diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 8239a4a69..8721ced7c 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -60,7 +60,7 @@ module.exports = { DEFAULT_CHANNEL: 'town-square', OFFTOPIC_CHANNEL: 'off-topic', GITLAB_SERVICE: 'gitlab', - GOOGLE_SERVICE: 'google', + EMAIL_SERVICE: 'email', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_LOADING: 'loading', @@ -92,6 +92,7 @@ module.exports = { ], MONTHS: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], MAX_DMS: 20, + MAX_POST_LEN: 4000, ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>", OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>", MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>", diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index f19dd2b47..14a5b2f40 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -286,10 +286,10 @@ function getYoutubeEmbed(link) { return; } var metadata = data.items[0].snippet; + $('.video-type.' + youtubeId).html("Youtube - ") $('.video-uploader.' + youtubeId).html(metadata.channelTitle); $('.video-title.' + youtubeId).find('a').html(metadata.title); $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time')[0].scrollHeight); - $('.post-list-holder-by-time').perfectScrollbar('update'); } if (config.GoogleDeveloperKey) { @@ -304,9 +304,11 @@ function getYoutubeEmbed(link) { return ( <div className='post-comment'> - <h4 className='video-type'>YouTube</h4> + <h4> + <span className={'video-type ' + youtubeId}>YouTube</span> + <span className={'video-title ' + youtubeId}><a href={link}></a></span> + </h4> <h4 className={'video-uploader ' + youtubeId}></h4> - <h4 className={'video-title ' + youtubeId}><a href={link}></a></h4> <div className='video-div embed-responsive-item' id={youtubeId} onClick={onClick}> <div className='embed-responsive embed-responsive-4by3 video-div__placeholder'> <div id={youtubeId} className='video-thumbnail__container'> @@ -457,9 +459,21 @@ module.exports.textToJsx = function(text, options) { var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here var explicitMention = mentionRegex.exec(trimWord); - if ((trimWord.toLowerCase().indexOf(searchTerm) > -1 || word.toLowerCase().indexOf(searchTerm) > -1) && searchTerm != '') { - - highlightSearchClass = ' search-highlight'; + if (searchTerm !== '') { + let searchWords = searchTerm.split(' '); + for (let idx in searchWords) { + let searchWord = searchWords[idx]; + if (searchWord === word.toLowerCase() || searchWord === trimWord.toLowerCase()) { + highlightSearchClass = ' search-highlight'; + break; + } else if (searchWord.charAt(searchWord.length - 1) === '*') { + let searchWordPrefix = searchWord.slice(0,-1); + if (trimWord.toLowerCase().indexOf(searchWordPrefix) > -1 || word.toLowerCase().indexOf(searchWordPrefix) > -1) { + highlightSearchClass = ' search-highlight'; + break; + } + } + } } if (explicitMention && @@ -765,7 +779,7 @@ function switchChannel(channel, teammateName) { AsyncClient.getChannels(true, true, true); AsyncClient.getChannelExtraInfo(true); - AsyncClient.getPosts(true, channel.id, Constants.POST_CHUNK_SIZE); + AsyncClient.getPosts(channel.id); $('.inner__wrap').removeClass('move--right'); $('.sidebar--left').removeClass('move--right'); @@ -987,6 +1001,21 @@ module.exports.isBrowserFirefox = function() { return navigator && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('firefox') > -1; }; +// Checks if browser is IE10 or IE11 +module.exports.isBrowserIE = function() { + if (window.navigator && window.navigator.userAgent) { + var ua = window.navigator.userAgent; + + return ua.indexOf('Trident/7.0') > 0 || ua.indexOf('Trident/6.0') > 0; + } + + return false; +}; + +module.exports.isBrowserEdge = function() { + return window.naviagtor && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('edge') > -1; +}; + // Used to get the id of the other user from a DM channel module.exports.getUserIdFromChannelName = function(channel) { var ids = channel.name.split('__'); |