diff options
Diffstat (limited to 'web/react')
21 files changed, 1437 insertions, 889 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..f6e34fda9 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) { @@ -105,14 +104,17 @@ module.exports = React.createClass({ this.lastTime = t; } }, - handleUserInput: function(messageText) { + handleUserInput: function(message) { + var messageText = utils.truncateText(message); + var newPostError = utils.checkMessageLengthError(messageText, this.state.postError, 'Comment length cannot exceed ' + Constants.MAX_POST_LEN + ' characters'); + var draft = PostStore.getCommentDraft(this.props.rootId); draft.message = messageText; PostStore.storeCommentDraft(this.props.rootId, draft); $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); $('.post-right__scroll').perfectScrollbar('update'); - this.setState({messageText: messageText}); + this.setState({messageText: messageText, postError: newPostError}); }, handleUploadStart: function(clientIds, channelId) { var draft = PostStore.getCommentDraft(this.props.rootId); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index efaa40577..73210c855 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(); @@ -131,9 +130,12 @@ module.exports = React.createClass({ this.lastTime = t; } }, - handleUserInput: function(messageText) { + handleUserInput: function(message) { + var messageText = utils.truncateText(message); + var newPostError = utils.checkMessageLengthError(messageText, this.state.postError, 'Message length cannot exceed ' + Constants.MAX_POST_LEN + ' characters'); + this.resizePostHolder(); - this.setState({messageText: messageText}); + this.setState({messageText: messageText, postError: newPostError}); var draft = PostStore.getCurrentDraft(); draft['message'] = messageText; 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..df692e1bb 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) { @@ -37,7 +39,9 @@ module.exports = React.createClass({ $(this.state.refocusId).focus(); }, handleEditInput: function(editText) { - this.setState({ editText: editText }); + var editMessage = utils.truncateText(editText); + var newError = utils.checkMessageLengthError(editMessage, this.state.error, 'New message length cannot exceed ' + Constants.MAX_POST_LEN + ' characters'); + this.setState({editText: editMessage, error: newError}); }, handleEditKeyPress: function(e) { if (e.which == 13 && !e.shiftKey && !e.altKey) { @@ -53,7 +57,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 +73,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/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..865a22dbd 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -15,124 +15,116 @@ 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.state = this.getStateFromStores(); + this.state.numToDisplay = Constants.POST_CHUNK_SIZE; } + 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 +155,101 @@ 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(); + setTimeout(this.scrollToBottom, 100); + } + 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'); } - $(postHolder).perfectScrollbar('update'); - }, - onChange: function() { - var newState = getStateFromStores(); + } + onChange() { + var newState = this.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 +261,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 +270,8 @@ module.exports = React.createClass({ } else if (msg.action === 'new_user') { AsyncClient.getProfiles(); } - }, - onTimeChange: function() { + } + onTimeChange() { if (!this.state.postList) { return; } @@ -273,271 +282,331 @@ 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; - - var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>; + for (var i = numToDisplay; i >= 0; i--) { + var post = posts[order[i]]; + var parentPost = posts[post.parent_id]; - var userStyle = {color: UserStore.getCurrentUser().props.theme}; + var sameUser = false; + var sameRoot = false; + var hideProfilePic = false; + var prevPost = posts[order[i + 1]]; - 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; - } + if (prevPost) { + sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5; - 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; - } - } + sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); - 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; - } - } - } + // 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 (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.'; - } + // 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} + /> + ); - 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) + '.'; - } + 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> + ); + } - 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> - ); - } + if (post.user_id !== userId && post.create_at > this.state.lastViewed && !renderedLastViewed) { + renderedLastViewed = true; + postCtls.push( + <div + id='new_message' + key='unviewed' + className='new-separator' + > + <hr + className='separator__hr' + /> + <div className='separator__text'>New Messages</div> + </div> + ); } + postCtls.push(postCtl); + previousPostDay = currentPostDay; } - var postCtls = []; + return postCtls; + } + loadMorePosts() { + if (this.state.postList == null) { + return; + } - if (posts) { - var previousPostDay = new Date(0); - var currentPostDay; - - 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) { + postCtls = this.createPosts(posts, order); } else { postCtls.push(<LoadingScreen position='absolute' />); } @@ -553,4 +622,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 fb45ad28e..000000000 --- a/web/react/components/post_right.jsx +++ /dev/null @@ -1,411 +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_SEARCH_TERM, - term: null, - do_search: false, - is_mention_search: false - }); - - 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_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/team_import_tab.jsx b/web/react/components/team_import_tab.jsx index c21701c0e..ecb3491b0 100644 --- a/web/react/components/team_import_tab.jsx +++ b/web/react/components/team_import_tab.jsx @@ -20,10 +20,22 @@ module.exports = React.createClass({ utils.importSlack(file, this.onImportSuccess, this.onImportFailure); }, render: function() { + var uploadHelpText = ( + <div> + <br/> + Slack does now allow you to export any of your files or images stored in slack. Private channels and direct message channels are also not exported. Therefore, no files, images, or private channels will be imported. + <br/> + Slack bot posts are not imported. + <br/> + Unable to import Slack channels that are not valid Mattermost channels. (ex underscores) + <br/> + </div> + ); var uploadSection = ( <SettingUpload title='Import from Slack' submit={this.doImportSlack} + helpText={uploadHelpText} fileTypesAccepted='.zip'/> ); @@ -39,12 +51,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/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/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..4b0b90dc7 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,58 @@ 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; + } + + 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..70220c71e 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -653,7 +653,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 +669,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..82fc3da22 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -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..f0cf17446 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -765,7 +765,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 +987,58 @@ 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; +}; + +// Gets text length consistent with maxlength property of textarea html tag +module.exports.getLengthOfTextInTextarea = function(messageText) { + // Need to get length with carriage returns counting as two characters to match textbox maxlength behavior + // unless ie10/ie11/edge which already do + + var len = messageText.length; + if (!module.exports.isBrowserIE() && !module.exports.isBrowserEdge()) { + len = messageText.replace(/\r(?!\n)|\n(?!\r)/g, '--').length; + } + + return len; +}; + +module.exports.checkMessageLengthError = function(message, currentError, newError) { + var updatedError = currentError; + var len = module.exports.getLengthOfTextInTextarea(message); + + if (!currentError && len >= Constants.MAX_POST_LEN) { + updatedError = newError; + } else if (currentError === newError && len < Constants.MAX_POST_LEN) { + updatedError = ''; + } + + return updatedError; +}; + +// Necessary due to issues with textarea max length and pasting newlines +module.exports.truncateText = function(message) { + var lengthDifference = module.exports.getLengthOfTextInTextarea(message) - message.length; + + if (lengthDifference > 0) { + return message.substring(0, Constants.MAX_POST_LEN - lengthDifference); + } + + return message.substring(0, Constants.MAX_POST_LEN); +}; + // Used to get the id of the other user from a DM channel module.exports.getUserIdFromChannelName = function(channel) { var ids = channel.name.split('__'); |