diff options
author | Joram Wilander <jwawilander@gmail.com> | 2016-05-27 16:01:28 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-05-27 16:01:28 -0400 |
commit | 6399a94ce221be3d15e7132654c28cd953075ec6 (patch) | |
tree | 4b1927fdd8374e8bd3cb809ecb720f2689043358 /webapp/components/posts_view.jsx | |
parent | ca9f348be6bf62fc888df9a710c9af155872528e (diff) | |
download | chat-6399a94ce221be3d15e7132654c28cd953075ec6.tar.gz chat-6399a94ce221be3d15e7132654c28cd953075ec6.tar.bz2 chat-6399a94ce221be3d15e7132654c28cd953075ec6.zip |
PLT-2672 Refactored posts view with caching (#3054)
* Refactored posts view to use view controller design
* Add post view caching
* Required updates after rebase
* Fixed bug where current channel not set yet was causing breakage
Diffstat (limited to 'webapp/components/posts_view.jsx')
-rw-r--r-- | webapp/components/posts_view.jsx | 630 |
1 files changed, 0 insertions, 630 deletions
diff --git a/webapp/components/posts_view.jsx b/webapp/components/posts_view.jsx deleted file mode 100644 index 64da4e67c..000000000 --- a/webapp/components/posts_view.jsx +++ /dev/null @@ -1,630 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; - -import Post from './post.jsx'; -import FloatingTimestamp from './floating_timestamp.jsx'; - -import * as GlobalActions from 'actions/global_actions.jsx'; - -import PreferenceStore from 'stores/preference_store.jsx'; -import UserStore from 'stores/user_store.jsx'; - -import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx'; - -import * as Utils from 'utils/utils.jsx'; -import DelayedAction from 'utils/delayed_action.jsx'; - -import Constants from 'utils/constants.jsx'; -const Preferences = Constants.Preferences; - -import {FormattedDate, FormattedMessage} from 'react-intl'; - -import React from 'react'; -import ReactDOM from 'react-dom'; - -export default class PostsView extends React.Component { - constructor(props) { - super(props); - - this.updateState = this.updateState.bind(this); - this.handleScroll = this.handleScroll.bind(this); - this.handleScrollStop = this.handleScrollStop.bind(this); - this.isAtBottom = this.isAtBottom.bind(this); - this.loadMorePostsTop = this.loadMorePostsTop.bind(this); - this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this); - this.createPosts = this.createPosts.bind(this); - this.updateScrolling = this.updateScrolling.bind(this); - this.handleResize = this.handleResize.bind(this); - this.scrollToBottom = this.scrollToBottom.bind(this); - this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this); - this.onUserChange = this.onUserChange.bind(this); - - this.jumpToPostNode = null; - this.wasAtBottom = true; - this.scrollHeight = 0; - - this.scrollStopAction = new DelayedAction(this.handleScrollStop); - - let profiles = UserStore.getProfiles(); - if (props.channel && props.channel.type === Constants.DM_CHANNEL) { - profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); - } - - this.state = { - displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'), - centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, - compactPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, - isScrolling: false, - topPostId: null, - currentUser: UserStore.getCurrentUser(), - profiles - }; - } - static get SCROLL_TYPE_FREE() { - return 1; - } - static get SCROLL_TYPE_BOTTOM() { - return 2; - } - static get SCROLL_TYPE_SIDEBAR_OPEN() { - return 3; - } - static get SCROLL_TYPE_NEW_MESSAGE() { - return 4; - } - static get SCROLL_TYPE_POST() { - return 5; - } - updateState() { - this.setState({ - displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'), - centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, - compactPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT - }); - } - onUserChange() { - let profiles = UserStore.getProfiles(); - if (this.props.channel && this.props.channel.type === Constants.DM_CHANNEL) { - profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); - } - this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))}); - } - isAtBottom() { - // consider the view to be at the bottom if it's within this many pixels of the bottom - const atBottomMargin = 10; - - return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - atBottomMargin; - } - handleScroll() { - // HACK FOR RHS -- REMOVE WHEN RHS DIES - const childNodes = this.refs.postlistcontent.childNodes; - for (let i = 0; i < childNodes.length; i++) { - // If the node is 1/3 down the page - if (childNodes[i].offsetTop > (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / 3))) { - this.jumpToPostNode = childNodes[i]; - break; - } - } - this.wasAtBottom = this.isAtBottom(); - if (!this.jumpToPostNode && childNodes.length > 0) { - this.jumpToPostNode = childNodes[childNodes.length - 1]; - } - - // --- -------- - - this.props.postViewScrolled(this.isAtBottom()); - this.prevScrollHeight = this.refs.postlist.scrollHeight; - this.prevOffsetTop = this.jumpToPostNode.offsetTop; - - this.updateFloatingTimestamp(); - - if (!this.state.isScrolling) { - this.setState({ - isScrolling: true - }); - } - - this.scrollStopAction.fireAfter(2000); - } - handleScrollStop() { - this.setState({ - isScrolling: false - }); - } - updateFloatingTimestamp() { - // skip this in non-mobile view since that's when the timestamp is visible - if ($(window).width() > 768) { - return; - } - - if (this.props.postList) { - // iterate through posts starting at the bottom since users are more likely to be viewing newer posts - for (let i = 0; i < this.props.postList.order.length; i++) { - const id = this.props.postList.order[i]; - const element = ReactDOM.findDOMNode(this.refs[id]); - - if (!element || element.offsetTop + element.clientHeight <= this.refs.postlist.scrollTop) { - // this post is off the top of the screen so the last one is at the top of the screen - let topPostId; - - if (i > 0) { - topPostId = this.props.postList.order[i - 1]; - } else { - // the first post we look at should always be on the screen, but handle that case anyway - topPostId = id; - } - - if (topPostId !== this.state.topPostId) { - this.setState({ - topPostId - }); - } - - break; - } - } - } - } - loadMorePostsTop() { - this.props.loadMorePostsTopClicked(); - } - loadMorePostsBottom() { - this.props.loadMorePostsBottomClicked(); - } - createPosts(posts, order) { - const postCtls = []; - let previousPostDay = new Date(0); - const userId = this.state.currentUser.id; - const profiles = this.state.profiles || {}; - - let renderedLastViewed = false; - - for (let i = order.length - 1; i >= 0; i--) { - const post = posts[order[i]]; - const parentPost = posts[post.parent_id]; - const prevPost = posts[order[i + 1]]; - const postUserId = Utils.isSystemMessage(post) ? '' : post.user_id; - - // If the post is a comment whose parent has been deleted, don't add it to the list. - if (parentPost && parentPost.state === Constants.POST_DELETED) { - continue; - } - - let sameUser = false; - let sameRoot = false; - let hideProfilePic = false; - - if (prevPost) { - const postIsComment = Utils.isComment(post); - const prevPostIsComment = Utils.isComment(prevPost); - const postFromWebhook = Boolean(post.props && post.props.from_webhook); - const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook); - const prevPostUserId = Utils.isSystemMessage(prevPost) ? '' : prevPost.user_id; - - // consider posts from the same user if: - // the previous post was made by the same user as the current post, - // the previous post was made within 5 minutes of the current post, - // the current post is not from a webhook - // the previous post is not from a webhook - if (prevPostUserId === postUserId && - post.create_at - prevPost.create_at <= 1000 * 60 * 5 && - !postFromWebhook && !prevPostFromWebhook) { - sameUser = true; - } - - // consider posts from the same root if: - // the current post is a comment, - // the current post has the same root as the previous post - if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) { - sameRoot = true; - } - - // consider posts from the same root if: - // the current post is not a comment, - // the previous post is not a comment, - // the previous post is from the same user - if (!postIsComment && !prevPostIsComment && sameUser) { - sameRoot = true; - } - - // hide the profile pic if: - // the previous post was made by the same user as the current post, - // the previous post is not a comment, - // the current post is not a comment, - // the previous post is not from a webhook - // the current post is not from a webhook - if (prevPostUserId === postUserId && - !prevPostIsComment && - !postIsComment && - !prevPostFromWebhook && - !postFromWebhook) { - hideProfilePic = true; - } - } - - // 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 - const isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); - - const keyPrefix = post.id ? post.id : i; - - const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id); - - let profile; - if (userId === post.user_id) { - profile = this.state.currentUser; - } else { - profile = profiles[post.user_id]; - } - - const postCtl = ( - <Post - key={keyPrefix + 'postKey'} - ref={post.id} - sameUser={sameUser} - sameRoot={sameRoot} - post={post} - parentPost={parentPost} - posts={posts} - hideProfilePic={hideProfilePic} - isLastComment={isLastComment} - shouldHighlight={shouldHighlight} - onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func - displayNameType={this.state.displayNameType} - user={profile} - currentUser={this.state.currentUser} - center={this.state.centerPosts} - compactDisplay={this.state.compactPosts} - /> - ); - - const 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'> - <FormattedDate - value={currentPostDay} - weekday='short' - month='short' - day='2-digit' - year='numeric' - /> - </div> - </div> - ); - } - - if (postUserId !== userId && - this.props.messageSeparatorTime !== 0 && - post.create_at > this.props.messageSeparatorTime && - !renderedLastViewed) { - renderedLastViewed = true; - - // Temporary fix to solve ie11 rendering issue - let newSeparatorId = ''; - if (!Utils.isBrowserIE()) { - newSeparatorId = 'new_message_' + post.id; - } - postCtls.push( - <div - id={newSeparatorId} - key='unviewed' - ref='newMessageSeparator' - className='new-separator' - > - <hr - className='separator__hr' - /> - <div className='separator__text'> - <FormattedMessage - id='posts_view.newMsg' - defaultMessage='New Messages' - /> - </div> - </div> - ); - } - postCtls.push(postCtl); - previousPostDay = currentPostDay; - } - - return postCtls; - } - updateScrolling() { - if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) { - this.scrollToBottom(); - } else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) { - window.setTimeout(window.requestAnimationFrame(() => { - // If separator exists scroll to it. Otherwise scroll to bottom. - if (this.refs.newMessageSeparator) { - var objDiv = this.refs.postlist; - objDiv.scrollTop = this.refs.newMessageSeparator.offsetTop; //scrolls node to top of Div - } else if (this.refs.postlist) { - this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; - } - }), 0); - } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) { - window.requestAnimationFrame(() => { - const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]); - if (postNode == null) { - return; - } - postNode.scrollIntoView(); - if (this.refs.postlist.scrollTop === postNode.offsetTop) { - this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3); - } else { - this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop); - } - }); - } else if (this.props.scrollType === PostsView.SCROLL_TYPE_SIDEBAR_OPEN) { - // If we are at the bottom then stay there - if (this.wasAtBottom) { - this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; - } else { - window.requestAnimationFrame(() => { - this.jumpToPostNode.scrollIntoView(); - if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) { - this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3); - } else { - this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop); - } - }); - } - } else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) { - window.requestAnimationFrame(() => { - // Only need to jump if we added posts to the top. - if (this.jumpToPostNode && (this.jumpToPostNode.offsetTop !== this.prevOffsetTop)) { - this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight); - } - }); - } - } - handleResize() { - this.updateScrolling(); - } - scrollToBottom() { - window.requestAnimationFrame(() => { - this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; - }); - } - scrollToBottomAnimated() { - var postList = $(this.refs.postlist); - postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500'); - } - - getArchivesIntroMessage() { - return ( - <div className='channel-intro'> - <h4 className='channel-intro__title'> - <FormattedMessage - id='post_focus_view.beginning' - defaultMessage='Beginning of Channel Archives' - /> - </h4> - </div> - ); - } - - componentDidMount() { - if (this.props.postList != null) { - this.updateScrolling(); - } - - if (this.props.isActive) { - PreferenceStore.addChangeListener(this.updateState); - UserStore.addChangeListener(this.onUserChange); - } - - if (this.props.channel) { - this.introText = createChannelIntroMessage(this.props.channel); - } else { - this.introText = this.getArchivesIntroMessage(); - } - - window.addEventListener('resize', this.handleResize); - } - componentWillUnmount() { - window.removeEventListener('resize', this.handleResize); - this.scrollStopAction.cancel(); - PreferenceStore.removeChangeListener(this.updateState); - UserStore.removeChangeListener(this.onUserChange); - } - componentDidUpdate() { - if (this.props.postList != null) { - this.updateScrolling(); - } - } - componentWillReceiveProps(nextProps) { - if (!this.props.isActive && nextProps.isActive) { - this.updateState(); - PreferenceStore.addChangeListener(this.updateState); - UserStore.addChangeListener(this.onUserChange); - } else if (this.props.isActive && !nextProps.isActive) { - PreferenceStore.removeChangeListener(this.updateState); - UserStore.removeChangeListener(this.onUserChange); - } - } - shouldComponentUpdate(nextProps, nextState) { - if (this.props.isActive !== nextProps.isActive) { - return true; - } - if (this.props.postList !== nextProps.postList) { - return true; - } - if (this.props.scrollPostId !== nextProps.scrollPostId) { - return true; - } - if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) { - return true; - } - if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) { - return true; - } - if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) { - return true; - } - if (nextState.displayNameType !== this.state.displayNameType) { - return true; - } - if (this.state.topPostId !== nextState.topPostId) { - return true; - } - if (this.state.isScrolling !== nextState.isScrolling) { - return true; - } - if (this.state.centerPosts !== nextState.centerPosts) { - return true; - } - if (this.state.compactPosts !== nextState.compactPosts) { - return true; - } - if (!Utils.areObjectsEqual(this.state.profiles, nextState.profiles)) { - return true; - } - - return false; - } - render() { - let posts = []; - let order = []; - let moreMessagesTop; - let moreMessagesBottom; - let postElements; - let activeClass = 'inactive'; - if (this.props.postList != null) { - posts = this.props.postList.posts; - order = this.props.postList.order; - - // Create intro message or top loadmore link - if (this.props.showMoreMessagesTop) { - moreMessagesTop = ( - <a - ref='loadmoretop' - className='more-messages-text theme' - href='#' - onClick={this.loadMorePostsTop} - > - <FormattedMessage - id='posts_view.loadMore' - defaultMessage='Load more messages' - /> - </a> - ); - } else { - moreMessagesTop = this.introText; - } - - // Give option to load more posts at bottom if nessisary - if (this.props.showMoreMessagesBottom) { - moreMessagesBottom = ( - <a - ref='loadmorebottom' - className='more-messages-text theme' - href='#' - onClick={this.loadMorePostsBottom} - > - <FormattedMessage id='posts_view.loadMore'/> - </a> - ); - } else { - moreMessagesBottom = null; - } - - // Create post elements - postElements = this.createPosts(posts, order); - - // Show ourselves if we are marked active - if (this.props.isActive) { - activeClass = ''; - } - } - - let topPostCreateAt = 0; - if (this.state.topPostId && this.props.postList.posts[this.state.topPostId]) { - topPostCreateAt = this.props.postList.posts[this.state.topPostId].create_at; - } - - return ( - <div className={activeClass}> - <FloatingTimestamp - isScrolling={this.state.isScrolling} - isMobile={$(window).width() > 768} - createAt={topPostCreateAt} - /> - <ScrollToBottomArrows - isScrolling={this.state.isScrolling} - atBottom={this.wasAtBottom} - onClick={this.scrollToBottomAnimated} - /> - <div - ref='postlist' - className='post-list-holder-by-time' - onScroll={this.handleScroll} - > - <div className='post-list__table'> - <div - ref='postlistcontent' - className='post-list__content' - > - {moreMessagesTop} - {postElements} - {moreMessagesBottom} - </div> - </div> - </div> - </div> - ); - } -} -PostsView.defaultProps = { -}; - -PostsView.propTypes = { - isActive: React.PropTypes.bool, - postList: React.PropTypes.object, - scrollPostId: React.PropTypes.string, - scrollType: React.PropTypes.number, - postViewScrolled: React.PropTypes.func.isRequired, - loadMorePostsTopClicked: React.PropTypes.func.isRequired, - loadMorePostsBottomClicked: React.PropTypes.func.isRequired, - showMoreMessagesTop: React.PropTypes.bool, - showMoreMessagesBottom: React.PropTypes.bool, - channel: React.PropTypes.object, - messageSeparatorTime: React.PropTypes.number, - postsToHighlight: React.PropTypes.object, - compactDisplay: React.PropTypes.bool -}; - -function ScrollToBottomArrows({isScrolling, atBottom, onClick}) { - // only show on mobile - if ($(window).width() > 768) { - return <noscript/>; - } - - let className = 'post-list__arrows'; - if (isScrolling && !atBottom) { - className += ' scrolling'; - } - - return ( - <div - className={className} - onClick={onClick} - > - <span dangerouslySetInnerHTML={{__html: Constants.SCROLL_BOTTOM_ICON}}/> - </div> - ); -} - -ScrollToBottomArrows.propTypes = { - isScrolling: React.PropTypes.bool.isRequired, - atBottom: React.PropTypes.bool.isRequired, - onClick: React.PropTypes.func.isRequired -}; |