From 12896bd23eeba79884245c1c29fdc568cf21a7fa Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 14 Mar 2016 08:50:46 -0400 Subject: Converting to Webpack. Stage 1. --- webapp/components/posts_view.jsx | 608 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 webapp/components/posts_view.jsx (limited to 'webapp/components/posts_view.jsx') diff --git a/webapp/components/posts_view.jsx b/webapp/components/posts_view.jsx new file mode 100644 index 000000000..8b4b0c662 --- /dev/null +++ b/webapp/components/posts_view.jsx @@ -0,0 +1,608 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import PreferenceStore from 'stores/preference_store.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; +import * as Utils from 'utils/utils.jsx'; +import Post from './post.jsx'; +import Constants from 'utils/constants.jsx'; +import DelayedAction from 'utils/delayed_action.jsx'; + +import {FormattedDate, FormattedMessage} from 'react-intl'; + +const Preferences = Constants.Preferences; + +import React from 'react'; + +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.jumpToPostNode = null; + this.wasAtBottom = true; + this.scrollHeight = 0; + + this.scrollStopAction = new DelayedAction(this.handleScrollStop); + + this.state = { + displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'), + isScrolling: false, + topPostId: null + }; + } + 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')}); + } + 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.props.currentUser.id; + const profiles = this.props.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; + let prevWebhookName = ''; + if (prevPost.props && prevPost.props.override_username) { + prevWebhookName = prevPost.props.override_username; + } + let curWebhookName = ''; + if (post.props && post.props.override_username) { + curWebhookName = post.props.override_username; + } + + // 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 previous post and current post are both from webhooks or both not, + // the previous post and current post have the same webhook usernames + if (prevPostUserId === postUserId && + post.create_at - prevPost.create_at <= 1000 * 60 * 5 && + postFromWebhook === prevPostFromWebhook && + prevWebhookName === curWebhookName) { + 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 and current post are both from webhooks or both not, + // the previous post and current post have the same webhook usernames + if (prevPostUserId === postUserId && + !prevPostIsComment && + !postIsComment && + postFromWebhook === prevPostFromWebhook && + prevWebhookName === curWebhookName) { + 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 (this.props.currentUser.id === post.user_id) { + profile = this.props.currentUser; + } else { + profile = profiles[post.user_id]; + } + + const postCtl = ( + GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func + displayNameType={this.state.displayNameType} + user={profile} + currentUser={this.props.currentUser} + /> + ); + + const currentPostDay = Utils.getDateForUnixTicks(post.create_at); + if (currentPostDay.toDateString() !== previousPostDay.toDateString()) { + postCtls.push( +
+
+
+ +
+
+ ); + } + + 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( +
+
+
+ +
+
+ ); + } + 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.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; + } + }); + } 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'); + } + componentDidMount() { + if (this.props.postList != null) { + this.updateScrolling(); + } + window.addEventListener('resize', this.handleResize); + } + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize); + } + componentDidUpdate() { + if (this.props.postList != null) { + this.updateScrolling(); + } + } + componentWillReceiveProps(nextProps) { + if (!this.props.isActive && nextProps.isActive) { + this.updateState(); + PreferenceStore.addChangeListener(this.updateState); + } else if (this.props.isActive && !nextProps.isActive) { + PreferenceStore.removeChangeListener(this.updateState); + } + } + 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 (!Utils.areObjectsEqual(this.props.profiles, nextProps.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 = ( + + + + ); + } else { + moreMessagesTop = this.props.introText; + } + + // Give option to load more posts at bottom if nessisary + if (this.props.showMoreMessagesBottom) { + moreMessagesBottom = ( + + + + ); + } else { + moreMessagesBottom = null; + } + + // Create post elements + postElements = this.createPosts(posts, order); + + // Show ourselves if we are marked active + if (this.props.isActive) { + activeClass = ''; + } + } + + let topPost = null; + if (this.state.topPostId) { + topPost = this.props.postList.posts[this.state.topPostId]; + } + + return ( +
+ + +
+
+
+ {moreMessagesTop} + {postElements} + {moreMessagesBottom} +
+
+
+
+ ); + } +} +PostsView.defaultProps = { +}; + +PostsView.propTypes = { + isActive: React.PropTypes.bool, + postList: React.PropTypes.object, + profiles: React.PropTypes.object.isRequired, + 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, + introText: React.PropTypes.element, + messageSeparatorTime: React.PropTypes.number, + postsToHighlight: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired +}; + +function FloatingTimestamp({isScrolling, post}) { + // only show on mobile + if ($(window).width() > 768) { + return