From 041d89b85a22b0a498a4176d0d26fd5dc84c33f9 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 26 Aug 2015 12:09:01 -0400 Subject: Refactored post handling/updating on both the client and server. --- web/react/components/channel_loader.jsx | 54 +- web/react/components/create_comment.jsx | 7 +- web/react/components/create_post.jsx | 4 +- web/react/components/delete_post_modal.jsx | 3 +- web/react/components/edit_post_modal.jsx | 2 +- web/react/components/post.jsx | 41 +- web/react/components/post_list.jsx | 831 ++++++++++++++++------------- web/react/components/post_right.jsx | 411 -------------- web/react/components/rhs_comment.jsx | 207 +++++++ web/react/components/rhs_header_post.jsx | 81 +++ web/react/components/rhs_root_post.jsx | 145 +++++ web/react/components/rhs_thread.jsx | 215 ++++++++ web/react/components/sidebar_right.jsx | 12 +- web/react/stores/post_store.jsx | 125 ++++- web/react/utils/async_client.jsx | 55 +- web/react/utils/client.jsx | 17 +- web/react/utils/utils.jsx | 2 +- web/sass-files/sass/partials/_post.scss | 3 + 18 files changed, 1351 insertions(+), 864 deletions(-) delete mode 100644 web/react/components/post_right.jsx create mode 100644 web/react/components/rhs_comment.jsx create mode 100644 web/react/components/rhs_header_post.jsx create mode 100644 web/react/components/rhs_root_post.jsx create mode 100644 web/react/components/rhs_thread.jsx (limited to 'web') diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 525b67b5c..0fa433383 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -5,63 +5,83 @@ to the server on page load. This is to prevent other React controls from spamming AsyncClient with requests. */ -var BrowserStore = require('../stores/browser_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var PostStore = require('../stores/post_store.jsx'); +var UserStore = require('../stores/user_store.jsx'); var Constants = require('../utils/constants.jsx'); +var utils = require('../utils/utils.jsx'); + module.exports = React.createClass({ componentDidMount: function() { - /* Start initial aysnc loads */ + /* Initial aysnc loads */ AsyncClient.getMe(); - AsyncClient.getPosts(true, ChannelStore.getCurrentId(), Constants.POST_CHUNK_SIZE); + AsyncClient.getPosts(ChannelStore.getCurrentId()); AsyncClient.getChannels(true, true); AsyncClient.getChannelExtraInfo(true); AsyncClient.findTeams(); AsyncClient.getStatuses(); AsyncClient.getMyTeam(); - /* End of async loads */ /* Perform pending post clean-up */ PostStore.clearPendingPosts(); - /* End pending post clean-up */ - /* Start interval functions */ + /* Set up interval functions */ setInterval( function pollStatuses() { AsyncClient.getStatuses(); }, 30000); - /* End interval functions */ - /* Start device tracking setup */ + /* Device tracking setup */ var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); if (iOS) { $('body').addClass('ios'); } - /* End device tracking setup */ - /* Start window active tracking setup */ + /* Set up tracking for whether the window is active */ window.isActive = true; - $(window).focus(function() { + $(window).focus(function windowFocus() { AsyncClient.updateLastViewedAt(); window.isActive = true; }); - $(window).blur(function() { + $(window).blur(function windowBlur() { window.isActive = false; }); - /* End window active tracking setup */ /* Start global change listeners setup */ - SocketStore.addChangeListener(this._onSocketChange); - /* End global change listeners setup */ + SocketStore.addChangeListener(this.onSocketChange); + + /* Update CSS classes to match user theme */ + var user = UserStore.getCurrentUser(); + + if (user.props && user.props.theme) { + utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';'); + utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';'); + utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';'); + utils.changeCss('.mention', 'background: ' + user.props.theme + ';'); + utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';'); + utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}'); + utils.changeCss('.search-item-container:hover', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';'); + } + + if (user.props.theme !== '#000000' && user.props.theme !== '#585858') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) + ';'); + utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;'); + } else if (user.props.theme === '#000000') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) + ';'); + $('.team__header').addClass('theme--black'); + } else if (user.props.theme === '#585858') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) + ';'); + $('.team__header').addClass('theme--gray'); + } }, - _onSocketChange: function(msg) { - if (msg && msg.user_id) { + onSocketChange: function(msg) { + if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { UserStore.setStatus(msg.user_id, 'online'); } }, diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 1de768872..c2b7e222f 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -29,8 +29,6 @@ module.exports = React.createClass({ return; } - this.setState({submitting: true, serverError: null}); - var post = {}; post.filenames = []; post.message = this.state.messageText; @@ -57,11 +55,10 @@ module.exports = React.createClass({ PostStore.storePendingPost(post); PostStore.storeCommentDraft(this.props.rootId, null); - this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); client.createPost(post, ChannelStore.getCurrent(), function(data) { - AsyncClient.getPosts(true, this.props.channelId); + AsyncClient.getPosts(this.props.channelId); var channel = ChannelStore.get(this.props.channelId); var member = ChannelStore.getMember(this.props.channelId); @@ -91,6 +88,8 @@ module.exports = React.createClass({ this.setState(state); }.bind(this) ); + + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); }, commentMsgKeyPress: function(e) { if (e.which === 13 && !e.shiftKey && !e.altKey) { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index efaa40577..9aef2dea4 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -82,7 +82,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 +112,6 @@ module.exports = React.createClass({ }.bind(this) ); } - - $('.post-list-holder-by-time').perfectScrollbar('update'); }, componentDidUpdate: function() { this.resizePostHolder(); diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 1b6a7e162..55d6f509c 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -44,7 +44,8 @@ module.exports = React.createClass({ } } } - AsyncClient.getPosts(true, this.state.channel_id); + PostStore.removePost(this.state.post_id, this.state.channel_id); + AsyncClient.getPosts(this.state.channel_id); }.bind(this), function(err) { AsyncClient.dispatchError(err, "deletePost"); diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index 2d865a45d..a801cfc1b 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -25,7 +25,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) { 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 ?
: 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 (
-
+
{ !this.props.hideProfilePic ? -
- +
+
: null } -
- +
+ - +
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 ( +
+
+ +
+
+ +
+

+ {'This is the start of your private message history with ' + teammateName + '.'}
+ {'Private messages and files shared here are not shown to people outside this area.'} +

+ + Set a description + +
); - }, - getInitialState: function() { - return getStateFromStores(); - }, - render: function() { - var order = []; - var posts; + } - var lastViewed = Number.MAX_VALUE; + return ( +
+

{'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.'}

+
+ ); + } + 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 ( +
+

Beginning of {channel.display_name}

+

+ Welcome to {channel.display_name}! +

+ This is the first channel {strings.Team}mates see when they +
+ sign up - use it for posting updates everyone needs to know. +

+ To create a new channel or join an existing one, go to +
+ the Left Hand Sidebar under “Channels” and click “More…”. +
+

+
+ ); + } + createOffTopicIntroMessage(channel) { + return ( +
+

Beginning of {channel.display_name}

+

+ {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'} +
+

+ + Set a description + +
+ ); + } + 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 = (This is the start of the {uiName} {uiType}, created by {creatorName} on {utils.displayDate(channel.create_at)}); + } else { + createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.'; } + return ( +
+

Beginning of {uiName}

+

+ {createMessage} + {memberMessage} +
+

+ + Set a description + + + Invite others to this {uiType} + +
+ ); + } + 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
; + var numToDisplay = this.state.numToDisplay; + if (order.length - 1 < numToDisplay) { + numToDisplay = order.length - 1; } - var channel = this.state.channel; - - var moreMessages =

Beginning of Channel

; + 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 = Load more messages; - } 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 = ( -
-
- -
-
- -
-

- This is the start of your private message history with {teammateName}.
- Private messages and files shared here are not shown to people outside this area. -

- Set a description -
- ); - } else { - moreMessages = ( -
-

{'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.'}

-
- ); - } - } 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 = ( -
-

Beginning of {uiName}

-

- Welcome to {uiName}! -

- This is the first channel {strings.Team}mates see when they -
- sign up - use it for posting updates everyone needs to know. -

- To create a new channel or join an existing one, go to -
- the Left Hand Sidebar under “Channels” and click “More…”. -
-

-
- ); - } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { - moreMessages = ( -
-

Beginning of {uiName}

-

- This is the start of {uiName}, a channel for non-work-related conversations. -
-

- Set a description -
- ); - } 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 = ( + + ); - var createMessage; - if (creatorName !== '') { - createMessage = (This is the start of the {uiName} {uiType}, created by {creatorName} on {utils.displayDate(channel.create_at)}); - } 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( +
+
+
{currentPostDay.toDateString()}
+
+ ); + } - moreMessages = ( -
-

Beginning of {uiName}

-

- {createMessage} - {memberMessage} -
-

- Set a description - Invite others to this {uiType} -
- ); - } + if (post.user_id !== userId && post.create_at > this.state.lastViewed && !renderedLastViewed) { + renderedLastViewed = true; + postCtls.push( +
+
+
New Messages
+
+ ); } + 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 = ( - - ); + 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( -
-
-
{currentPostDay.toDateString()}
-
- ); - } + 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( -
-
-
New Messages
-
- ); - } - postCtls.push(postCtl); - previousPostDay = currentPostDay; + var moreMessages =

Beginning of Channel

; + if (channel != null) { + if (order.length > this.state.numToDisplay) { + moreMessages = ( + + Load more messages + + ); + } else { + moreMessages = this.createChannelIntroMessage(channel); } + } + + var postCtls = []; + if (posts) { + postCtls = this.createPosts(posts, order); } else { postCtls.push(); } @@ -553,4 +622,4 @@ module.exports = React.createClass({
); } -}); +} 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 = ; - } - - return ( -
- {back}Message Details - -
- ); - } -}); - -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 = ( -
- -
- ); - } - - var fileAttachment; - if (post.filenames && post.filenames.length > 0) { - fileAttachment = ( - - ); - } - - return ( -
-
{ channelName }
-
- -
-
-
    -
  • -
  • -
  • -
    - {ownerOptions} -
    -
  • -
-
-

{message}

- {fileAttachment} -
-
-
-
- ); - } -}); - -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 = Retry; - } else if (post.state === Constants.POST_LOADING) { - postClass += ' post-waiting'; - loading = ; - } - - var ownerOptions; - if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { - ownerOptions = ( - - ); - } - - var fileAttachment; - if (post.filenames && post.filenames.length > 0) { - fileAttachment = ( - - ); - } - - return ( -
-
- -
-
-
    -
  • -
  • -
  • - {ownerOptions} -
  • -
-
-

{loading}{message}

- {fileAttachment} -
-
-
- ); - } -}); - -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 ( -
- ); - } - - 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 = ; - } - - return ( -
- -
{searchForm}
-
- -
- -
- {postsArray.map(function mapPosts(comPost) { - return ; - })} -
-
- -
-
-
-
- ); - } -}); 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 = ( + + Retry + + ); + } else if (post.state === Constants.POST_LOADING) { + postClass += ' post-waiting'; + loading = ( + + ); + } + + var ownerOptions; + if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { + ownerOptions = ( + + ); + } + + var fileAttachment; + if (post.filenames && post.filenames.length > 0) { + fileAttachment = ( + + ); + } + + return ( +
+
+ +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + {ownerOptions} +
  • +
+
+

{loading}{message}

+ {fileAttachment} +
+
+
+ ); + } +} + +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 = ( + + + + ); + } + + return ( +
+ {back}Message Details + +
+ ); + } +} + +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 = ( + + ); + } + + var fileAttachment; + if (post.filenames && post.filenames.length > 0) { + fileAttachment = ( + + ); + } + + return ( +
+
{channelName}
+
+ +
+
+
    +
  • +
  • +
  • +
    + {ownerOptions} +
    +
  • +
+
+

{message}

+ {fileAttachment} +
+
+
+
+ ); + } +} + +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 ( +
+ ); + } + + 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 = ; + } + + return ( +
+ +
{searchForm}
+
+ +
+ +
+ {postsArray.map(function mapPosts(comPost) { + return ( + + ); + })} +
+
+ +
+
+
+
+ ); + } +} + +RhsThread.defaultProps = { + fromSearch: '', + isMentionSearch: false +}; +RhsThread.propTypes = { + fromSearch: React.PropTypes.string, + isMentionSearch: React.PropTypes.bool +}; 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 = ; } else if (this.state.post_right_visible) { - content = ; + content = ; } return ( 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/utils.jsx b/web/react/utils/utils.jsx index f19dd2b47..13989ad82 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'); diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index e665be6b9..0605e9c3b 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -139,6 +139,9 @@ body.ios { width: 100%; padding: 1em 0 0; position: relative; + &.hide-scroll::-webkit-scrollbar { + width: 0px !important; + } } .post-list__table { display: table; -- cgit v1.2.3-1-g7c22