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 | |
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
27 files changed, 640 insertions, 554 deletions
diff --git a/webapp/components/channel_view.jsx b/webapp/components/channel_view.jsx index 6511d960a..ff101bca7 100644 --- a/webapp/components/channel_view.jsx +++ b/webapp/components/channel_view.jsx @@ -7,7 +7,7 @@ import React from 'react'; import ChannelHeader from 'components/channel_header.jsx'; import FileUploadOverlay from 'components/file_upload_overlay.jsx'; import CreatePost from 'components/create_post.jsx'; -import PostsViewContainer from 'components/posts_view_container.jsx'; +import PostViewCache from 'components/post_view/post_view_cache.jsx'; import ChannelStore from 'stores/channel_store.jsx'; @@ -70,7 +70,7 @@ export default class ChannelView extends React.Component { <ChannelHeader channelId={this.state.channelId} /> - <PostsViewContainer/> + <PostViewCache/> <div className='post-create__container' id='post-create' diff --git a/webapp/components/permalink_view.jsx b/webapp/components/permalink_view.jsx index c2019cb49..8f443bc05 100644 --- a/webapp/components/permalink_view.jsx +++ b/webapp/components/permalink_view.jsx @@ -5,7 +5,7 @@ import $ from 'jquery'; import React from 'react'; import ChannelHeader from 'components/channel_header.jsx'; -import PostFocusView from 'components/post_focus_view.jsx'; +import PostFocusViewController from 'components/post_view/post_focus_view_controller.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -70,7 +70,7 @@ export default class PermalinkView extends React.Component { <ChannelHeader channelId={this.state.channelId} /> - <PostFocusView/> + <PostFocusViewController/> <div id='archive-link-home' > diff --git a/webapp/components/floating_timestamp.jsx b/webapp/components/post_view/components/floating_timestamp.jsx index 8974c62c5..8974c62c5 100644 --- a/webapp/components/floating_timestamp.jsx +++ b/webapp/components/post_view/components/floating_timestamp.jsx diff --git a/webapp/components/pending_post_actions.jsx b/webapp/components/post_view/components/pending_post_options.jsx index 7528ef207..536ec541c 100644 --- a/webapp/components/pending_post_actions.jsx +++ b/webapp/components/post_view/components/pending_post_options.jsx @@ -4,7 +4,7 @@ import PostStore from 'stores/post_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import Client from 'utils/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; @@ -16,7 +16,7 @@ import {FormattedMessage} from 'react-intl'; import React from 'react'; -export default class PendingPostActions extends React.Component { +export default class PendingPostOptions extends React.Component { constructor(props) { super(props); this.retryPost = this.retryPost.bind(this); @@ -87,6 +87,6 @@ export default class PendingPostActions extends React.Component { } } -PendingPostActions.propTypes = { +PendingPostOptions.propTypes = { post: React.PropTypes.object }; diff --git a/webapp/components/post.jsx b/webapp/components/post_view/components/post.jsx index 2b28d442c..0bf4680fe 100644 --- a/webapp/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -8,7 +8,8 @@ import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; import * as Utils from 'utils/utils.jsx'; -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import * as PostUtils from 'utils/post_utils.jsx'; +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import React from 'react'; @@ -55,7 +56,7 @@ export default class Post extends React.Component { return true; } - if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) { + if (nextProps.commentCount !== this.props.commentCount) { return true; } @@ -77,30 +78,9 @@ export default class Post extends React.Component { return false; } - getCommentCount(props) { - const post = props.post; - const parentPost = props.parentPost; - const posts = props.posts; - - let commentCount = 0; - let commentRootId; - if (parentPost) { - commentRootId = post.root_id; - } else { - commentRootId = post.id; - } - for (const postId in posts) { - if (posts[postId].root_id === commentRootId) { - commentCount += 1; - } - } - - return commentCount; - } render() { const post = this.props.post; const parentPost = this.props.parentPost; - const posts = this.props.posts; const mattermostLogo = Constants.MATTERMOST_ICON_SVG; if (!post.props) { @@ -112,7 +92,7 @@ export default class Post extends React.Component { type = 'Comment'; } - const commentCount = this.getCommentCount(this.props); + const commentCount = this.props.commentCount; let rootUser; if (this.props.sameRoot) { @@ -129,7 +109,7 @@ export default class Post extends React.Component { } let currentUserCss = ''; - if (this.props.currentUser.id === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) { + if (this.props.currentUser.id === post.user_id && !post.props.from_webhook && !PostUtils.isSystemMessage(post)) { currentUserCss = 'current--user'; } @@ -151,19 +131,19 @@ export default class Post extends React.Component { } let systemMessageClass = ''; - if (Utils.isSystemMessage(post)) { + if (PostUtils.isSystemMessage(post)) { systemMessageClass = 'post--system'; } let profilePic = ( <img - src={Utils.getProfilePicSrcForPost(post, timestamp)} + src={PostUtils.getProfilePicSrcForPost(post, timestamp)} height='36' width='36' /> ); - if (Utils.isSystemMessage(post)) { + if (PostUtils.isSystemMessage(post)) { profilePic = ( <span className='icon' @@ -207,7 +187,6 @@ export default class Post extends React.Component { post={post} sameRoot={this.props.sameRoot} parentPost={parentPost} - posts={posts} handleCommentClick={this.handleCommentClick} compactDisplay={this.props.compactDisplay} /> @@ -221,7 +200,6 @@ export default class Post extends React.Component { Post.propTypes = { post: React.PropTypes.object.isRequired, - posts: React.PropTypes.object, parentPost: React.PropTypes.object, user: React.PropTypes.object, sameUser: React.PropTypes.bool, @@ -233,5 +211,6 @@ Post.propTypes = { hasProfiles: React.PropTypes.bool, currentUser: React.PropTypes.object.isRequired, center: React.PropTypes.bool, - compactDisplay: React.PropTypes.bool + compactDisplay: React.PropTypes.bool, + commentCount: React.PropTypes.number }; diff --git a/webapp/components/post_attachment.jsx b/webapp/components/post_view/components/post_attachment.jsx index 8b5ff91f2..8b5ff91f2 100644 --- a/webapp/components/post_attachment.jsx +++ b/webapp/components/post_view/components/post_attachment.jsx diff --git a/webapp/components/post_attachment_list.jsx b/webapp/components/post_view/components/post_attachment_list.jsx index 7da9efbee..7da9efbee 100644 --- a/webapp/components/post_attachment_list.jsx +++ b/webapp/components/post_view/components/post_attachment_list.jsx diff --git a/webapp/components/post_attachment_oembed.jsx b/webapp/components/post_view/components/post_attachment_oembed.jsx index 359c7cc35..359c7cc35 100644 --- a/webapp/components/post_attachment_oembed.jsx +++ b/webapp/components/post_view/components/post_attachment_oembed.jsx diff --git a/webapp/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx index 415052d96..3c57db0cd 100644 --- a/webapp/components/post_body.jsx +++ b/webapp/components/post_view/components/post_body.jsx @@ -1,13 +1,13 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import FileAttachmentList from './file_attachment_list.jsx'; +import FileAttachmentList from 'components/file_attachment_list.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; -import PendingPostActions from './pending_post_actions.jsx'; +import PendingPostOptions from './pending_post_options.jsx'; import {FormattedMessage} from 'react-intl'; @@ -111,7 +111,7 @@ export default class PostBody extends React.Component { let loading; if (post.state === Constants.POST_FAILED) { postClass += ' post--fail'; - loading = <PendingPostActions post={this.props.post}/>; + loading = <PendingPostOptions post={this.props.post}/>; } else if (post.state === Constants.POST_LOADING) { postClass += ' post-waiting'; loading = ( diff --git a/webapp/components/post_body_additional_content.jsx b/webapp/components/post_view/components/post_body_additional_content.jsx index cdb735b47..deabaaa9b 100644 --- a/webapp/components/post_body_additional_content.jsx +++ b/webapp/components/post_view/components/post_body_additional_content.jsx @@ -4,7 +4,7 @@ import PostAttachmentList from './post_attachment_list.jsx'; import PostAttachmentOEmbed from './post_attachment_oembed.jsx'; import PostImage from './post_image.jsx'; -import YoutubeVideo from './youtube_video.jsx'; +import YoutubeVideo from 'components/youtube_video.jsx'; import Constants from 'utils/constants.jsx'; import OEmbedProviders from './providers.json'; diff --git a/webapp/components/post_header.jsx b/webapp/components/post_view/components/post_header.jsx index 6fae092e5..3e7650d7f 100644 --- a/webapp/components/post_header.jsx +++ b/webapp/components/post_view/components/post_header.jsx @@ -1,9 +1,10 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import UserProfile from './user_profile.jsx'; +import UserProfile from 'components/user_profile.jsx'; import PostInfo from './post_info.jsx'; -import * as Utils from 'utils/utils.jsx'; + +import * as PostUtils from 'utils/post_utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -33,7 +34,7 @@ export default class PostHeader extends React.Component { } botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>; - } else if (Utils.isSystemMessage(post)) { + } else if (PostUtils.isSystemMessage(post)) { userProfile = ( <UserProfile user={{}} diff --git a/webapp/components/post_image.jsx b/webapp/components/post_view/components/post_image.jsx index d1d1a6c7a..d1d1a6c7a 100644 --- a/webapp/components/post_image.jsx +++ b/webapp/components/post_view/components/post_image.jsx diff --git a/webapp/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx index e3b80e45c..cc8c0a842 100644 --- a/webapp/components/post_info.jsx +++ b/webapp/components/post_view/components/post_info.jsx @@ -3,7 +3,7 @@ import $ from 'jquery'; import * as Utils from 'utils/utils.jsx'; -import TimeSince from './time_since.jsx'; +import TimeSince from 'components/time_since.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; diff --git a/webapp/components/posts_view.jsx b/webapp/components/post_view/components/post_list.jsx index 64da4e67c..288609cd9 100644 --- a/webapp/components/posts_view.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -5,30 +5,28 @@ import $ from 'jquery'; import Post from './post.jsx'; import FloatingTimestamp from './floating_timestamp.jsx'; +import ScrollToBottomArrows from './scroll_to_bottom_arrows.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 * as PostUtils from 'utils/post_utils.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; import Constants from 'utils/constants.jsx'; -const Preferences = Constants.Preferences; +const ScrollTypes = Constants.ScrollTypes; import {FormattedDate, FormattedMessage} from 'react-intl'; import React from 'react'; import ReactDOM from 'react-dom'; -export default class PostsView extends React.Component { +export default class PostList 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); @@ -39,7 +37,6 @@ export default class PostsView extends React.Component { 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; @@ -47,62 +44,31 @@ export default class PostsView extends React.Component { 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()); + if (props.channel) { + this.introText = createChannelIntroMessage(props.channel); + } else { + this.introText = this.getArchivesIntroMessage(); } 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 + 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'), - 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))) { + if (childNodes[i].offsetTop > (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION))) { this.jumpToPostNode = childNodes[i]; break; } @@ -114,7 +80,7 @@ export default class PostsView extends React.Component { // --- -------- - this.props.postViewScrolled(this.isAtBottom()); + this.props.postListScrolled(this.isAtBottom()); this.prevScrollHeight = this.refs.postlist.scrollHeight; this.prevOffsetTop = this.jumpToPostNode.offsetTop; @@ -126,16 +92,18 @@ export default class PostsView extends React.Component { }); } - this.scrollStopAction.fireAfter(2000); + this.scrollStopAction.fireAfter(Constants.SCROLL_DELAY); } + handleScrollStop() { this.setState({ isScrolling: false }); } + updateFloatingTimestamp() { // skip this in non-mobile view since that's when the timestamp is visible - if ($(window).width() > 768) { + if (!Utils.isMobile()) { return; } @@ -143,7 +111,7 @@ export default class PostsView extends React.Component { // 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]); + const element = 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 @@ -167,17 +135,20 @@ export default class PostsView extends React.Component { } } } + loadMorePostsTop() { - this.props.loadMorePostsTopClicked(); + GlobalActions.emitLoadMorePostsEvent(); } + loadMorePostsBottom() { - this.props.loadMorePostsBottomClicked(); + GlobalActions.emitLoadMorePostsFocusedBottomEvent(); } + createPosts(posts, order) { const postCtls = []; let previousPostDay = new Date(0); - const userId = this.state.currentUser.id; - const profiles = this.state.profiles || {}; + const userId = this.props.currentUser.id; + const profiles = this.props.profiles || {}; let renderedLastViewed = false; @@ -185,7 +156,7 @@ export default class PostsView extends React.Component { 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; + const postUserId = PostUtils.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) { @@ -197,11 +168,11 @@ export default class PostsView extends React.Component { let hideProfilePic = false; if (prevPost) { - const postIsComment = Utils.isComment(post); - const prevPostIsComment = Utils.isComment(prevPost); + const postIsComment = PostUtils.isComment(post); + const prevPostIsComment = PostUtils.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; + const prevPostUserId = PostUtils.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, @@ -209,7 +180,7 @@ export default class PostsView extends React.Component { // 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 && + post.create_at - prevPost.create_at <= Constants.POST_COLLAPSE_TIMEOUT && !postFromWebhook && !prevPostFromWebhook) { sameUser = true; } @@ -246,7 +217,7 @@ export default class PostsView extends React.Component { // 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 isLastComment = PostUtils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); const keyPrefix = post.id ? post.id : i; @@ -254,11 +225,24 @@ export default class PostsView extends React.Component { let profile; if (userId === post.user_id) { - profile = this.state.currentUser; + profile = this.props.currentUser; } else { profile = profiles[post.user_id]; } + let commentCount = 0; + let commentRootId; + if (parentPost) { + commentRootId = post.root_id; + } else { + commentRootId = post.id; + } + for (const postId in posts) { + if (posts[postId].root_id === commentRootId) { + commentCount += 1; + } + } + const postCtl = ( <Post key={keyPrefix + 'postKey'} @@ -267,16 +251,15 @@ export default class PostsView extends React.Component { 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} + displayNameType={this.props.displayNameType} user={profile} - currentUser={this.state.currentUser} - center={this.state.centerPosts} - compactDisplay={this.state.compactPosts} + currentUser={this.props.currentUser} + center={this.props.displayPostsInCenter} + commentCount={commentCount} + compactDisplay={this.props.compactDisplay} /> ); @@ -302,8 +285,8 @@ export default class PostsView extends React.Component { } if (postUserId !== userId && - this.props.messageSeparatorTime !== 0 && - post.create_at > this.props.messageSeparatorTime && + this.props.lastViewed !== 0 && + post.create_at > this.props.lastViewed && !renderedLastViewed) { renderedLastViewed = true; @@ -337,10 +320,11 @@ export default class PostsView extends React.Component { return postCtls; } + updateScrolling() { - if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) { + if (this.props.scrollType === ScrollTypes.BOTTOM) { this.scrollToBottom(); - } else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) { + } else if (this.props.scrollType === ScrollTypes.NEW_MESSAGE) { window.setTimeout(window.requestAnimationFrame(() => { // If separator exists scroll to it. Otherwise scroll to bottom. if (this.refs.newMessageSeparator) { @@ -350,7 +334,7 @@ export default class PostsView extends React.Component { this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; } }), 0); - } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) { + } else if (this.props.scrollType === ScrollTypes.POST && this.props.scrollPostId) { window.requestAnimationFrame(() => { const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]); if (postNode == null) { @@ -358,12 +342,12 @@ export default class PostsView extends React.Component { } postNode.scrollIntoView(); if (this.refs.postlist.scrollTop === postNode.offsetTop) { - this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3); + this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION); } else { - this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop); + this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION) + (this.refs.postlist.scrollTop - postNode.offsetTop); } }); - } else if (this.props.scrollType === PostsView.SCROLL_TYPE_SIDEBAR_OPEN) { + } else if (this.props.scrollType === ScrollTypes.SIDEBAR_OPEN) { // If we are at the bottom then stay there if (this.wasAtBottom) { this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; @@ -371,9 +355,9 @@ export default class PostsView extends React.Component { window.requestAnimationFrame(() => { this.jumpToPostNode.scrollIntoView(); if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) { - this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3); + this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION); } else { - this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop); + this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / Constants.SCROLL_PAGE_FRACTION) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop); } }); } @@ -386,14 +370,17 @@ export default class PostsView extends React.Component { }); } } + 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'); @@ -417,145 +404,76 @@ export default class PostsView extends React.Component { 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; - } + if (this.props.postList == null) { + return <div/>; + } - // 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; - } + const posts = this.props.postList.posts; + const order = this.props.postList.order; - // Create post elements - postElements = this.createPosts(posts, order); + // Create intro message or top loadmore link + let moreMessagesTop; + 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; + } - // Show ourselves if we are marked active - if (this.props.isActive) { - activeClass = ''; - } + // Give option to load more posts at bottom if necessary + let moreMessagesBottom; + if (this.props.showMoreMessagesBottom) { + moreMessagesBottom = ( + <a + ref='loadmorebottom' + className='more-messages-text theme' + href='#' + onClick={this.loadMorePostsBottom} + > + <FormattedMessage id='posts_view.loadMore'/> + </a> + ); } + // Create post elements + const postElements = this.createPosts(posts, order); + 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}> + <div> <FloatingTimestamp isScrolling={this.state.isScrolling} - isMobile={$(window).width() > 768} + isMobile={Utils.isMobile()} createAt={topPostCreateAt} /> <ScrollToBottomArrows @@ -583,48 +501,24 @@ export default class PostsView extends React.Component { ); } } -PostsView.defaultProps = { + +PostList.defaultProps = { + lastViewed: 0 }; -PostsView.propTypes = { - isActive: React.PropTypes.bool, +PostList.propTypes = { postList: React.PropTypes.object, + profiles: React.PropTypes.object, + channel: React.PropTypes.object, + currentUser: 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, + postListScrolled: React.PropTypes.func.isRequired, showMoreMessagesTop: React.PropTypes.bool, showMoreMessagesBottom: React.PropTypes.bool, - channel: React.PropTypes.object, - messageSeparatorTime: React.PropTypes.number, + lastViewed: React.PropTypes.number, postsToHighlight: React.PropTypes.object, + displayNameType: React.PropTypes.string, + displayPostsInCenter: React.PropTypes.bool, 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 -}; diff --git a/webapp/components/providers.json b/webapp/components/post_view/components/providers.json index b5899c225..b5899c225 100644 --- a/webapp/components/providers.json +++ b/webapp/components/post_view/components/providers.json diff --git a/webapp/components/post_view/components/scroll_to_bottom_arrows.jsx b/webapp/components/post_view/components/scroll_to_bottom_arrows.jsx new file mode 100644 index 000000000..461ca3358 --- /dev/null +++ b/webapp/components/post_view/components/scroll_to_bottom_arrows.jsx @@ -0,0 +1,37 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; + +import Constants from 'utils/constants.jsx'; + +import React from 'react'; + +export default class ScrollToBottomArrows extends React.Component { + render() { + // only show on mobile + if ($(window).width() > 768) { + return <noscript/>; + } + + let className = 'post-list__arrows'; + if (this.props.isScrolling && !this.props.atBottom) { + className += ' scrolling'; + } + + return ( + <div + className={className} + onClick={this.props.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 +}; diff --git a/webapp/components/post_focus_view.jsx b/webapp/components/post_view/post_focus_view_controller.jsx index 30a2f9d72..7c1da6566 100644 --- a/webapp/components/post_focus_view.jsx +++ b/webapp/components/post_view/post_focus_view_controller.jsx @@ -1,11 +1,15 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import PostsView from './posts_view.jsx'; +import PostList from './components/post_list.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; import PostStore from 'stores/post_store.jsx'; +import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; + +import Constants from 'utils/constants.jsx'; +const ScrollTypes = Constants.ScrollTypes; import React from 'react'; @@ -15,17 +19,24 @@ export default class PostFocusView extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this); - this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); - this.loadMorePostsTop = this.loadMorePostsTop.bind(this); - this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this); + this.onUserChange = this.onUserChange.bind(this); + this.onPostListScroll = this.onPostListScroll.bind(this); const focusedPostId = PostStore.getFocusedPostId(); + const channel = ChannelStore.getCurrent(); + let profiles = UserStore.getProfiles(); + if (channel && channel.type === Constants.DM_CHANNEL) { + profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); + } + this.state = { - scrollType: PostsView.SCROLL_TYPE_POST, + postList: PostStore.getVisiblePosts(focusedPostId), + currentUser: UserStore.getCurrentUser(), + profiles, + scrollType: ScrollTypes.POST, currentChannel: ChannelStore.getCurrentId().slice(), scrollPostId: focusedPostId, - postList: PostStore.getVisiblePosts(focusedPostId), atTop: PostStore.getVisibilityAtTop(focusedPostId), atBottom: PostStore.getVisibilityAtBottom(focusedPostId) }; @@ -34,11 +45,13 @@ export default class PostFocusView extends React.Component { componentDidMount() { ChannelStore.addChangeListener(this.onChannelChange); PostStore.addChangeListener(this.onPostsChange); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); PostStore.removeChangeListener(this.onPostsChange); + UserStore.removeChangeListener(this.onUserChange); } onChannelChange() { @@ -46,7 +59,7 @@ export default class PostFocusView extends React.Component { if (this.state.currentChannel !== currentChannel) { this.setState({ currentChannel: currentChannel.slice(), - scrollType: PostsView.SCROLL_TYPE_POST + scrollType: ScrollTypes.POST }); } } @@ -65,42 +78,50 @@ export default class PostFocusView extends React.Component { }); } - handlePostsViewScroll() { - this.setState({scrollType: PostsView.SCROLL_TYPE_FREE}); - } - - loadMorePostsTop() { - GlobalActions.emitLoadMorePostsFocusedTopEvent(); + onUserChange() { + const channel = ChannelStore.getCurrent(); + let profiles = UserStore.getProfiles(); + if (channel && channel.type === Constants.DM_CHANNEL) { + profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); + } + this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))}); } - loadMorePostsBottom() { - GlobalActions.emitLoadMorePostsFocusedBottomEvent(); + onPostListScroll() { + this.setState({scrollType: ScrollTypes.FREE}); } render() { const postsToHighlight = {}; postsToHighlight[this.state.scrollPostId] = true; - if (!this.state.postList) { - return null; - } - - return ( - <div id='post-list'> - <PostsView - key={'postfocusview'} - isActive={true} + let content; + if (this.state.postList == null) { + content = ( + <LoadingScreen + position='absolute' + key='loading' + /> + ); + } else { + content = ( + <PostList postList={this.state.postList} + currentUser={this.state.currentUser} + profiles={this.state.profiles} scrollType={this.state.scrollType} scrollPostId={this.state.scrollPostId} - postViewScrolled={this.handlePostsViewScroll} - loadMorePostsTopClicked={this.loadMorePostsTop} - loadMorePostsBottomClicked={this.loadMorePostsBottom} + postListScrolled={this.onPostListScroll} showMoreMessagesTop={!this.state.atTop} showMoreMessagesBottom={!this.state.atBottom} - messageSeparatorTime={0} postsToHighlight={postsToHighlight} /> + ); + } + + return ( + <div id='post-list'> + {content} </div> ); } diff --git a/webapp/components/post_view/post_view_cache.jsx b/webapp/components/post_view/post_view_cache.jsx new file mode 100644 index 000000000..8876ae461 --- /dev/null +++ b/webapp/components/post_view/post_view_cache.jsx @@ -0,0 +1,85 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information + +import PostViewController from './post_view_controller.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; + +import React from 'react'; + +const MAXIMUM_CACHED_VIEWS = 5; + +export default class PostViewCache extends React.Component { + constructor(props) { + super(props); + + this.onChannelChange = this.onChannelChange.bind(this); + + const channel = ChannelStore.getCurrent(); + + this.state = { + currentChannelId: channel.id, + channels: [channel] + }; + } + + componentDidMount() { + ChannelStore.addChangeListener(this.onChannelChange); + } + + componentWillUnmount() { + ChannelStore.removeChangeListener(this.onChannelChange); + } + + onChannelChange() { + const channels = Object.assign([], this.state.channels); + const currentChannel = ChannelStore.getCurrent(); + + if (currentChannel == null) { + return; + } + + // make sure current channel really changed + if (currentChannel.id === this.state.currentChannelId) { + return; + } + + if (channels.length > MAXIMUM_CACHED_VIEWS) { + channels.shift(); + } + + const index = channels.map((c) => c.id).indexOf(currentChannel.id); + if (index !== -1) { + channels.splice(index, 1); + } + + channels.push(currentChannel); + + this.setState({ + currentChannelId: currentChannel.id, + channels + }); + } + + render() { + const channels = this.state.channels; + const currentChannelId = this.state.currentChannelId; + + let postViews = []; + for (let i = 0; i < channels.length; i++) { + postViews.push( + <PostViewController + key={'postviewcontroller_' + channels[i].id} + channel={channels[i]} + active={channels[i].id === currentChannelId} + /> + ); + } + + return ( + <div id='post-list'> + {postViews} + </div> + ); + } +} diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx new file mode 100644 index 000000000..0898a9ce6 --- /dev/null +++ b/webapp/components/post_view/post_view_controller.jsx @@ -0,0 +1,264 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import PostList from './components/post_list.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; + +import PreferenceStore from 'stores/preference_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import PostStore from 'stores/post_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; + +import Constants from 'utils/constants.jsx'; +const Preferences = Constants.Preferences; +const ScrollTypes = Constants.ScrollTypes; + +import React from 'react'; + +export default class PostViewController extends React.Component { + constructor(props) { + super(props); + + this.onPreferenceChange = this.onPreferenceChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); + this.onPostsChange = this.onPostsChange.bind(this); + this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this); + this.onPostListScroll = this.onPostListScroll.bind(this); + this.onActivate = this.onActivate.bind(this); + this.onDeactivate = this.onDeactivate.bind(this); + + const channel = props.channel; + let profiles = UserStore.getProfiles(); + if (channel && channel.type === Constants.DM_CHANNEL) { + profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); + } + + let lastViewed = Number.MAX_VALUE; + const member = ChannelStore.getMember(channel.id); + if (member != null) { + lastViewed = member.last_viewed_at; + } + + this.state = { + channel, + postList: PostStore.getVisiblePosts(channel.id), + currentUser: UserStore.getCurrentUser(), + profiles, + atTop: PostStore.getVisibilityAtTop(channel.id), + lastViewed, + scrollType: ScrollTypes.BOTTOM, + displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'), + displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, + compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT + }; + } + + componentDidMount() { + if (this.props.active) { + this.onActivate(); + } + } + + componentWillUnmount() { + if (this.props.active) { + this.onDeactivate(); + } + } + + onPreferenceChange() { + this.setState({ + displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'), + displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, + compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT + }); + } + + onUserChange() { + const channel = this.state.channel; + let profiles = UserStore.getProfiles(); + if (channel && channel.type === Constants.DM_CHANNEL) { + profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); + } + this.setState({currentUser: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(profiles))}); + } + + onPostsChange() { + this.setState({ + postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(this.state.channel.id))), + atTop: PostStore.getVisibilityAtTop(this.state.channel.id) + }); + } + + onActivate() { + PreferenceStore.addChangeListener(this.onPreferenceChange); + UserStore.addChangeListener(this.onUserChange); + PostStore.addChangeListener(this.onPostsChange); + PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest); + } + + onDeactivate() { + PreferenceStore.removeChangeListener(this.onPreferenceChange); + UserStore.removeChangeListener(this.onUserChange); + PostStore.removeChangeListener(this.onPostsChange); + PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest); + } + + componentWillReceiveProps(nextProps) { + if (this.props.active && !nextProps.active) { + this.onDeactivate(); + } else if (!this.props.active && nextProps.active) { + this.onActivate(); + + const channel = nextProps.channel; + + let lastViewed = Number.MAX_VALUE; + const member = ChannelStore.getMember(channel.id); + if (member != null) { + lastViewed = member.last_viewed_at; + } + + let profiles = UserStore.getProfiles(); + if (channel && channel.type === Constants.DM_CHANNEL) { + profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); + } + + this.setState({ + channel, + lastViewed, + profiles: JSON.parse(JSON.stringify(profiles)), + postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(channel.id))), + displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'), + displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, + compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, + scrollType: ScrollTypes.BOTTOM + }); + } + } + + onPostsViewJumpRequest(type, postId) { + switch (type) { + case Constants.PostsViewJumpTypes.BOTTOM: + this.setState({scrollType: ScrollTypes.BOTTOM}); + break; + case Constants.PostsViewJumpTypes.POST: + this.setState({ + scrollType: ScrollTypes.POST, + scrollPostId: postId + }); + break; + case Constants.PostsViewJumpTypes.SIDEBAR_OPEN: + this.setState({scrollType: ScrollTypes.SIDEBAR_OPEN}); + break; + } + } + + onPostListScroll(atBottom) { + if (atBottom) { + this.setState({scrollType: ScrollTypes.BOTTOM}); + } else { + this.setState({scrollType: ScrollTypes.FREE}); + } + } + + shouldComponentUpdate(nextProps, nextState) { + if (nextProps.active !== this.props.active) { + return true; + } + + if (nextState.atTop !== this.state.atTop) { + return true; + } + + if (nextState.displayNameType !== this.state.displayNameType) { + return true; + } + + if (nextState.displayPostsInCenter !== this.state.displayPostsInCenter) { + return true; + } + + if (nextState.compactDisplay !== this.state.compactDisplay) { + return true; + } + + if (nextState.lastViewed !== this.state.lastViewed) { + return true; + } + + if (nextState.showMoreMessagesTop !== this.state.showMoreMessagesTop) { + return true; + } + + if (nextState.scrollType !== this.state.scrollType) { + return true; + } + + if (nextState.scrollPostId !== this.state.scrollPostId) { + return true; + } + + if (nextProps.channel.id !== this.props.channel.id) { + return true; + } + + if (!Utils.areObjectsEqual(nextState.currentUser, this.state.currentUser)) { + return true; + } + + if (!Utils.areObjectsEqual(nextState.postList, this.state.postList)) { + return true; + } + + if (!Utils.areObjectsEqual(nextState.profiles, this.state.profiles)) { + return true; + } + + return false; + } + + render() { + let content; + if (this.state.postList == null) { + content = ( + <LoadingScreen + position='absolute' + key='loading' + /> + ); + } else { + content = ( + <PostList + postList={this.state.postList} + profiles={this.state.profiles} + channel={this.state.channel} + currentUser={this.state.currentUser} + showMoreMessagesTop={!this.state.atTop} + scrollType={this.state.scrollType} + scrollPostId={this.state.scrollPostId} + postListScrolled={this.onPostListScroll} + displayNameType={this.state.displayNameType} + displayPostsInCenter={this.state.displayPostsInCenter} + compactDisplay={this.state.compactDisplay} + /> + ); + } + + let activeClass = ''; + if (!this.props.active) { + activeClass = 'inactive'; + } + + return ( + <div className={activeClass}> + {content} + </div> + ); + } +} + +PostViewController.propTypes = { + channel: React.PropTypes.object, + active: React.PropTypes.bool +}; diff --git a/webapp/components/posts_view_container.jsx b/webapp/components/posts_view_container.jsx deleted file mode 100644 index 3f8a44cc3..000000000 --- a/webapp/components/posts_view_container.jsx +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import $ from 'jquery'; - -import PostsView from './posts_view.jsx'; -import LoadingScreen from './loading_screen.jsx'; - -import ChannelStore from 'stores/channel_store.jsx'; -import PostStore from 'stores/post_store.jsx'; - -import * as GlobalActions from 'actions/global_actions.jsx'; - -import Constants from 'utils/constants.jsx'; - -import React from 'react'; - -const MAXIMUM_CACHED_VIEWS = 3; - -export default class PostsViewContainer extends React.Component { - constructor() { - super(); - - this.onChannelChange = this.onChannelChange.bind(this); - this.onChannelLeave = this.onChannelLeave.bind(this); - this.onPostsChange = this.onPostsChange.bind(this); - this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); - this.loadMorePostsTop = this.loadMorePostsTop.bind(this); - this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); - - const currentChannelId = ChannelStore.getCurrentId(); - const state = { - scrollType: PostsView.SCROLL_TYPE_BOTTOM, - scrollPost: null - }; - if (currentChannelId) { - let lastViewed = Date.now(); - const member = ChannelStore.getMember(currentChannelId); - if (member) { - lastViewed = member.last_viewed_at; - } - - Object.assign(state, { - currentChannelIndex: 0, - channels: [currentChannelId], - postLists: [this.getChannelPosts(currentChannelId)], - atTop: [PostStore.getVisibilityAtTop(currentChannelId)], - currentLastViewed: lastViewed - }); - } else { - Object.assign(state, { - currentChannelIndex: null, - channels: [], - postLists: [], - atTop: [], - currentLastViewed: Date.now() - }); - } - - state.showInviteModal = false; - this.state = state; - } - componentDidMount() { - ChannelStore.addChangeListener(this.onChannelChange); - ChannelStore.addLeaveListener(this.onChannelLeave); - PostStore.addChangeListener(this.onPostsChange); - PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest); - $('body').addClass('app__body'); - } - componentWillUnmount() { - ChannelStore.removeChangeListener(this.onChannelChange); - ChannelStore.removeLeaveListener(this.onChannelLeave); - PostStore.removeChangeListener(this.onPostsChange); - PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest); - $('body').removeClass('app__body'); - } - handlePostsViewJumpRequest(type, post) { - switch (type) { - case Constants.PostsViewJumpTypes.BOTTOM: - this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM}); - break; - case Constants.PostsViewJumpTypes.POST: - this.setState({ - scrollType: PostsView.SCROLL_TYPE_POST, - scrollPost: post - }); - break; - case Constants.PostsViewJumpTypes.SIDEBAR_OPEN: - this.setState({scrollType: PostsView.SCROLL_TYPE_SIDEBAR_OPEN}); - break; - } - } - onChannelChange() { - const postLists = this.state.postLists.slice(); - const atTop = this.state.atTop.slice(); - const channels = this.state.channels.slice(); - const channelId = ChannelStore.getCurrentId(); - - // Has the channel really changed? - if (channelId === channels[this.state.currentChannelIndex]) { - return; - } - - let lastViewed = Number.MAX_VALUE; - const member = ChannelStore.getMember(channelId); - if (member != null) { - lastViewed = member.last_viewed_at; - } - - let newIndex = channels.indexOf(channelId); - if (newIndex === -1) { - if (channels.length >= MAXIMUM_CACHED_VIEWS) { - channels.shift(); - atTop.shift(); - postLists.shift(); - } - - newIndex = channels.length; - channels.push(channelId); - atTop[newIndex] = PostStore.getVisibilityAtTop(channelId); - } - - // make sure we have the latest posts from the store - postLists[newIndex] = this.getChannelPosts(channelId); - - this.setState({ - currentChannelIndex: newIndex, - currentLastViewed: lastViewed, - scrollType: PostsView.SCROLL_TYPE_NEW_MESSAGE, - channels, - postLists, - atTop}); - } - onChannelLeave(id) { - const postLists = this.state.postLists.slice(); - const channels = this.state.channels.slice(); - const atTop = this.state.atTop.slice(); - const index = channels.indexOf(id); - if (index !== -1) { - postLists.splice(index, 1); - channels.splice(index, 1); - atTop.splice(index, 1); - } - this.setState({channels, postLists, atTop}); - } - onPostsChange() { - const channels = this.state.channels; - const postLists = this.state.postLists.slice(); - const atTop = this.state.atTop.slice(); - const currentChannelId = channels[this.state.currentChannelIndex]; - const newPostsView = this.getChannelPosts(currentChannelId); - - postLists[this.state.currentChannelIndex] = newPostsView; - atTop[this.state.currentChannelIndex] = PostStore.getVisibilityAtTop(currentChannelId); - this.setState({postLists, atTop}); - } - getChannelPosts(id) { - return PostStore.getVisiblePosts(id); - } - loadMorePostsTop() { - GlobalActions.emitLoadMorePostsEvent(); - } - handlePostsViewScroll(atBottom) { - if (atBottom) { - this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM}); - } else { - this.setState({scrollType: PostsView.SCROLL_TYPE_FREE}); - } - } - render() { - const postLists = this.state.postLists; - const channels = this.state.channels; - const currentChannelId = channels[this.state.currentChannelIndex]; - const channel = ChannelStore.get(currentChannelId); - - if (!channel) { - return null; - } - - const postListCtls = []; - for (let i = 0; i < channels.length; i++) { - const isActive = (channels[i] === currentChannelId); - postListCtls.push( - <PostsView - key={'postsviewkey' + channels[i]} - isActive={isActive} - postList={postLists[i]} - scrollType={this.state.scrollType} - scrollPostId={this.state.scrollPost} - postViewScrolled={this.handlePostsViewScroll} - loadMorePostsTopClicked={this.loadMorePostsTop} - loadMorePostsBottomClicked={() => { - // Do Nothing - }} - showMoreMessagesTop={!this.state.atTop[this.state.currentChannelIndex]} - showMoreMessagesBottom={false} - channel={channel} - messageSeparatorTime={this.state.currentLastViewed} - /> - ); - if (!postLists[i] && isActive) { - postListCtls.push( - <LoadingScreen - position='absolute' - key='loading' - /> - ); - } - } - - return ( - <div id='post-list'> - {postListCtls} - </div> - ); - } -} diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index a771803b8..a980a8227 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -3,7 +3,7 @@ import UserProfile from './user_profile.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; -import PendingPostActions from './pending_post_actions.jsx'; +import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; @@ -168,7 +168,7 @@ export default class RhsComment extends React.Component { if (post.state === Constants.POST_FAILED) { postClass += ' post-fail'; - loading = <PendingPostActions post={this.props.post}/>; + loading = <PendingPostOptions post={this.props.post}/>; } else if (post.state === Constants.POST_LOADING) { postClass += ' post-waiting'; loading = ( diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 02fc4fc59..051d68f34 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -6,11 +6,13 @@ import UserProfile from './user_profile.jsx'; import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; -import * as Utils from 'utils/utils.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; -import PostBodyAdditionalContent from './post_body_additional_content.jsx'; +import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; +import * as Utils from 'utils/utils.jsx'; +import * as PostUtils from 'utils/post_utils.jsx'; + import Constants from 'utils/constants.jsx'; import {FormattedMessage, FormattedDate} from 'react-intl'; @@ -56,7 +58,7 @@ export default class RhsRootPost extends React.Component { } var systemMessageClass = ''; - if (Utils.isSystemMessage(post)) { + if (PostUtils.isSystemMessage(post)) { systemMessageClass = 'post--system'; } @@ -188,7 +190,7 @@ export default class RhsRootPost extends React.Component { } botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>; - } else if (Utils.isSystemMessage(post)) { + } else if (PostUtils.isSystemMessage(post)) { userProfile = ( <UserProfile user={{}} @@ -202,7 +204,7 @@ export default class RhsRootPost extends React.Component { const profilePic = ( <img className='post-profile-img' - src={Utils.getProfilePicSrcForPost(post, timestamp)} + src={PostUtils.getProfilePicSrcForPost(post, timestamp)} height='36' width='36' /> diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index 708b148d8..3fe13878e 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -10,6 +10,7 @@ import * as GlobalActions from 'actions/global_actions.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; import * as Utils from 'utils/utils.jsx'; +import * as PostUtils from 'utils/post_utils.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -93,7 +94,7 @@ export default class SearchResultsItem extends React.Component { <div className='post__content'> <div className='post__img'> <img - src={Utils.getProfilePicSrcForPost(post, timestamp)} + src={PostUtils.getProfilePicSrcForPost(post, timestamp)} height='36' width='36' /> diff --git a/webapp/stores/notification_store.jsx b/webapp/stores/notification_store.jsx index 7d7039780..202b24432 100644 --- a/webapp/stores/notification_store.jsx +++ b/webapp/stores/notification_store.jsx @@ -7,6 +7,7 @@ import Constants from 'utils/constants.jsx'; import UserStore from './user_store.jsx'; import ChannelStore from './channel_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import * as PostUtils from 'utils/post_utils.jsx'; const ActionTypes = Constants.ActionTypes; const CHANGE_EVENT = 'change'; @@ -26,7 +27,7 @@ class NotificationStoreClass extends EventEmitter { handleRecievedPost(post, msgProps) { // Send desktop notification - if ((UserStore.getCurrentId() !== post.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) { + if ((UserStore.getCurrentId() !== post.user_id || post.props.from_webhook === 'true') && !PostUtils.isSystemMessage(post)) { let mentions = []; if (msgProps.mentions) { mentions = JSON.parse(msgProps.mentions); diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 0e2ae07ea..f1af112a9 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -161,6 +161,14 @@ export default { EPHEMERAL_MESSAGE: 'ephemeral_message' }, + ScrollTypes: { + FREE: 1, + BOTTOM: 2, + SIDEBBAR_OPEN: 3, + NEW_MESSAGE: 4, + POST: 5 + }, + //SPECIAL_MENTIONS: ['all', 'channel'], SPECIAL_MENTIONS: ['channel'], CHARACTER_LIMIT: 4000, @@ -204,6 +212,9 @@ export default { WEB_VIDEO_HEIGHT: 480, MOBILE_VIDEO_WIDTH: 480, MOBILE_VIDEO_HEIGHT: 360, + MOBILE_SCREEN_WIDTH: 768, + SCROLL_DELAY: 2000, + SCROLL_PAGE_FRACTION: 3, DEFAULT_CHANNEL: 'town-square', DEFAULT_CHANNEL_UI_NAME: 'Town Square', OFFTOPIC_CHANNEL: 'off-topic', @@ -738,5 +749,6 @@ export default { DEFAULT_WEBHOOK_LOGO: logoWebhook, MHPNS: 'https://push.mattermost.com', MTPNS: 'http://push-test.mattermost.com', - BOT_NAME: 'BOT' + BOT_NAME: 'BOT', + POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5 // five minutes }; diff --git a/webapp/utils/post_utils.jsx b/webapp/utils/post_utils.jsx new file mode 100644 index 000000000..f5111d72d --- /dev/null +++ b/webapp/utils/post_utils.jsx @@ -0,0 +1,32 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Client from 'utils/web_client.jsx'; + +import Constants from 'utils/constants.jsx'; + +export function isSystemMessage(post) { + return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0); +} + +export function isComment(post) { + if ('root_id' in post) { + return post.root_id !== '' && post.root_id != null; + } + return false; +} + +export function getProfilePicSrcForPost(post, timestamp) { + let src = Client.getUsersRoute() + '/' + post.user_id + '/image?time=' + timestamp; + if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { + if (post.props.override_icon_url) { + src = post.props.override_icon_url; + } else { + src = Constants.DEFAULT_WEBHOOK_LOGO; + } + } else if (isSystemMessage(post)) { + src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE; + } + + return src; +} diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 978f231df..9b0e370bf 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -909,14 +909,7 @@ export function isValidUsername(name) { } export function isMobile() { - return window.innerWidth <= 768; -} - -export function isComment(post) { - if ('root_id' in post) { - return post.root_id !== '' && post.root_id != null; - } - return false; + return window.innerWidth <= Constants.MOBILE_SCREEN_WIDTH; } export function getDirectTeammate(channelId) { @@ -1315,10 +1308,6 @@ export function isFeatureEnabled(feature) { return PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label); } -export function isSystemMessage(post) { - return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0); -} - export function fillArray(value, length) { const arr = []; @@ -1379,18 +1368,3 @@ export function localizeMessage(id, defaultMessage) { export function mod(a, b) { return ((a % b) + b) % b; } - -export function getProfilePicSrcForPost(post, timestamp) { - let src = Client.getUsersRoute() + '/' + post.user_id + '/image?time=' + timestamp; - if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { - if (post.props.override_icon_url) { - src = post.props.override_icon_url; - } else { - src = Constants.DEFAULT_WEBHOOK_LOGO; - } - } else if (isSystemMessage(post)) { - src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE; - } - - return src; -} |