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
Этот коммит содержится в:
Joram Wilander
2016-05-27 16:01:28 -04:00
коммит произвёл Christopher Speller
родитель ca9f348be6
Коммит 6399a94ce2
27 изменённых файлов: 646 добавлений и 560 удалений

Просмотреть файл

@@ -7,7 +7,7 @@ import React from 'react';
import ChannelHeader from 'components/channel_header.jsx'; import ChannelHeader from 'components/channel_header.jsx';
import FileUploadOverlay from 'components/file_upload_overlay.jsx'; import FileUploadOverlay from 'components/file_upload_overlay.jsx';
import CreatePost from 'components/create_post.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'; import ChannelStore from 'stores/channel_store.jsx';
@@ -70,7 +70,7 @@ export default class ChannelView extends React.Component {
<ChannelHeader <ChannelHeader
channelId={this.state.channelId} channelId={this.state.channelId}
/> />
<PostsViewContainer/> <PostViewCache/>
<div <div
className='post-create__container' className='post-create__container'
id='post-create' id='post-create'

Просмотреть файл

@@ -5,7 +5,7 @@ import $ from 'jquery';
import React from 'react'; import React from 'react';
import ChannelHeader from 'components/channel_header.jsx'; 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 ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
@@ -70,7 +70,7 @@ export default class PermalinkView extends React.Component {
<ChannelHeader <ChannelHeader
channelId={this.state.channelId} channelId={this.state.channelId}
/> />
<PostFocusView/> <PostFocusViewController/>
<div <div
id='archive-link-home' id='archive-link-home'
> >

Просмотреть файл

@@ -4,7 +4,7 @@
import PostStore from 'stores/post_store.jsx'; import PostStore from 'stores/post_store.jsx';
import ChannelStore from 'stores/channel_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 Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx';
@@ -16,7 +16,7 @@ import {FormattedMessage} from 'react-intl';
import React from 'react'; import React from 'react';
export default class PendingPostActions extends React.Component { export default class PendingPostOptions extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.retryPost = this.retryPost.bind(this); 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 post: React.PropTypes.object
}; };

Просмотреть файл

@@ -8,7 +8,8 @@ import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
import * as Utils from 'utils/utils.jsx'; 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'; import React from 'react';
@@ -55,7 +56,7 @@ export default class Post extends React.Component {
return true; return true;
} }
if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) { if (nextProps.commentCount !== this.props.commentCount) {
return true; return true;
} }
@@ -77,30 +78,9 @@ export default class Post extends React.Component {
return false; 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() { render() {
const post = this.props.post; const post = this.props.post;
const parentPost = this.props.parentPost; const parentPost = this.props.parentPost;
const posts = this.props.posts;
const mattermostLogo = Constants.MATTERMOST_ICON_SVG; const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
if (!post.props) { if (!post.props) {
@@ -112,7 +92,7 @@ export default class Post extends React.Component {
type = 'Comment'; type = 'Comment';
} }
const commentCount = this.getCommentCount(this.props); const commentCount = this.props.commentCount;
let rootUser; let rootUser;
if (this.props.sameRoot) { if (this.props.sameRoot) {
@@ -129,7 +109,7 @@ export default class Post extends React.Component {
} }
let currentUserCss = ''; 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'; currentUserCss = 'current--user';
} }
@@ -151,19 +131,19 @@ export default class Post extends React.Component {
} }
let systemMessageClass = ''; let systemMessageClass = '';
if (Utils.isSystemMessage(post)) { if (PostUtils.isSystemMessage(post)) {
systemMessageClass = 'post--system'; systemMessageClass = 'post--system';
} }
let profilePic = ( let profilePic = (
<img <img
src={Utils.getProfilePicSrcForPost(post, timestamp)} src={PostUtils.getProfilePicSrcForPost(post, timestamp)}
height='36' height='36'
width='36' width='36'
/> />
); );
if (Utils.isSystemMessage(post)) { if (PostUtils.isSystemMessage(post)) {
profilePic = ( profilePic = (
<span <span
className='icon' className='icon'
@@ -207,7 +187,6 @@ export default class Post extends React.Component {
post={post} post={post}
sameRoot={this.props.sameRoot} sameRoot={this.props.sameRoot}
parentPost={parentPost} parentPost={parentPost}
posts={posts}
handleCommentClick={this.handleCommentClick} handleCommentClick={this.handleCommentClick}
compactDisplay={this.props.compactDisplay} compactDisplay={this.props.compactDisplay}
/> />
@@ -221,7 +200,6 @@ export default class Post extends React.Component {
Post.propTypes = { Post.propTypes = {
post: React.PropTypes.object.isRequired, post: React.PropTypes.object.isRequired,
posts: React.PropTypes.object,
parentPost: React.PropTypes.object, parentPost: React.PropTypes.object,
user: React.PropTypes.object, user: React.PropTypes.object,
sameUser: React.PropTypes.bool, sameUser: React.PropTypes.bool,
@@ -233,5 +211,6 @@ Post.propTypes = {
hasProfiles: React.PropTypes.bool, hasProfiles: React.PropTypes.bool,
currentUser: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired,
center: React.PropTypes.bool, center: React.PropTypes.bool,
compactDisplay: React.PropTypes.bool compactDisplay: React.PropTypes.bool,
commentCount: React.PropTypes.number
}; };

Просмотреть файл

@@ -1,13 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // 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 UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.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'; import {FormattedMessage} from 'react-intl';
@@ -111,7 +111,7 @@ export default class PostBody extends React.Component {
let loading; let loading;
if (post.state === Constants.POST_FAILED) { if (post.state === Constants.POST_FAILED) {
postClass += ' post--fail'; postClass += ' post--fail';
loading = <PendingPostActions post={this.props.post}/>; loading = <PendingPostOptions post={this.props.post}/>;
} else if (post.state === Constants.POST_LOADING) { } else if (post.state === Constants.POST_LOADING) {
postClass += ' post-waiting'; postClass += ' post-waiting';
loading = ( loading = (

Просмотреть файл

@@ -4,7 +4,7 @@
import PostAttachmentList from './post_attachment_list.jsx'; import PostAttachmentList from './post_attachment_list.jsx';
import PostAttachmentOEmbed from './post_attachment_oembed.jsx'; import PostAttachmentOEmbed from './post_attachment_oembed.jsx';
import PostImage from './post_image.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 Constants from 'utils/constants.jsx';
import OEmbedProviders from './providers.json'; import OEmbedProviders from './providers.json';

Просмотреть файл

@@ -1,9 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // 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 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'; 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>; botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>;
} else if (Utils.isSystemMessage(post)) { } else if (PostUtils.isSystemMessage(post)) {
userProfile = ( userProfile = (
<UserProfile <UserProfile
user={{}} user={{}}

Просмотреть файл

@@ -3,7 +3,7 @@
import $ from 'jquery'; import $ from 'jquery';
import * as Utils from 'utils/utils.jsx'; 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 * as GlobalActions from 'actions/global_actions.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';

Просмотреть файл

@@ -5,30 +5,28 @@ import $ from 'jquery';
import Post from './post.jsx'; import Post from './post.jsx';
import FloatingTimestamp from './floating_timestamp.jsx'; import FloatingTimestamp from './floating_timestamp.jsx';
import ScrollToBottomArrows from './scroll_to_bottom_arrows.jsx';
import * as GlobalActions from 'actions/global_actions.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 {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import DelayedAction from 'utils/delayed_action.jsx'; import DelayedAction from 'utils/delayed_action.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
const Preferences = Constants.Preferences; const ScrollTypes = Constants.ScrollTypes;
import {FormattedDate, FormattedMessage} from 'react-intl'; import {FormattedDate, FormattedMessage} from 'react-intl';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
export default class PostsView extends React.Component { export default class PostList extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.updateState = this.updateState.bind(this);
this.handleScroll = this.handleScroll.bind(this); this.handleScroll = this.handleScroll.bind(this);
this.handleScrollStop = this.handleScrollStop.bind(this); this.handleScrollStop = this.handleScrollStop.bind(this);
this.isAtBottom = this.isAtBottom.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.handleResize = this.handleResize.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this); this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this); this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
this.onUserChange = this.onUserChange.bind(this);
this.jumpToPostNode = null; this.jumpToPostNode = null;
this.wasAtBottom = true; this.wasAtBottom = true;
@@ -47,62 +44,31 @@ export default class PostsView extends React.Component {
this.scrollStopAction = new DelayedAction(this.handleScrollStop); this.scrollStopAction = new DelayedAction(this.handleScrollStop);
let profiles = UserStore.getProfiles(); if (props.channel) {
if (props.channel && props.channel.type === Constants.DM_CHANNEL) { this.introText = createChannelIntroMessage(props.channel);
profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); } else {
this.introText = this.getArchivesIntroMessage();
} }
this.state = { 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, isScrolling: false,
topPostId: null, 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() { isAtBottom() {
// consider the view to be at the bottom if it's within this many pixels of the bottom // consider the view to be at the bottom if it's within this many pixels of the bottom
const atBottomMargin = 10; const atBottomMargin = 10;
return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - atBottomMargin; return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - atBottomMargin;
} }
handleScroll() { handleScroll() {
// HACK FOR RHS -- REMOVE WHEN RHS DIES // HACK FOR RHS -- REMOVE WHEN RHS DIES
const childNodes = this.refs.postlistcontent.childNodes; const childNodes = this.refs.postlistcontent.childNodes;
for (let i = 0; i < childNodes.length; i++) { for (let i = 0; i < childNodes.length; i++) {
// If the node is 1/3 down the page // 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]; this.jumpToPostNode = childNodes[i];
break; 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.prevScrollHeight = this.refs.postlist.scrollHeight;
this.prevOffsetTop = this.jumpToPostNode.offsetTop; 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() { handleScrollStop() {
this.setState({ this.setState({
isScrolling: false isScrolling: false
}); });
} }
updateFloatingTimestamp() { updateFloatingTimestamp() {
// skip this in non-mobile view since that's when the timestamp is visible // skip this in non-mobile view since that's when the timestamp is visible
if ($(window).width() > 768) { if (!Utils.isMobile()) {
return; 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 // 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++) { for (let i = 0; i < this.props.postList.order.length; i++) {
const id = this.props.postList.order[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) { 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 // 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() { loadMorePostsTop() {
this.props.loadMorePostsTopClicked(); GlobalActions.emitLoadMorePostsEvent();
} }
loadMorePostsBottom() { loadMorePostsBottom() {
this.props.loadMorePostsBottomClicked(); GlobalActions.emitLoadMorePostsFocusedBottomEvent();
} }
createPosts(posts, order) { createPosts(posts, order) {
const postCtls = []; const postCtls = [];
let previousPostDay = new Date(0); let previousPostDay = new Date(0);
const userId = this.state.currentUser.id; const userId = this.props.currentUser.id;
const profiles = this.state.profiles || {}; const profiles = this.props.profiles || {};
let renderedLastViewed = false; let renderedLastViewed = false;
@@ -185,7 +156,7 @@ export default class PostsView extends React.Component {
const post = posts[order[i]]; const post = posts[order[i]];
const parentPost = posts[post.parent_id]; const parentPost = posts[post.parent_id];
const prevPost = posts[order[i + 1]]; 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 the post is a comment whose parent has been deleted, don't add it to the list.
if (parentPost && parentPost.state === Constants.POST_DELETED) { if (parentPost && parentPost.state === Constants.POST_DELETED) {
@@ -197,11 +168,11 @@ export default class PostsView extends React.Component {
let hideProfilePic = false; let hideProfilePic = false;
if (prevPost) { if (prevPost) {
const postIsComment = Utils.isComment(post); const postIsComment = PostUtils.isComment(post);
const prevPostIsComment = Utils.isComment(prevPost); const prevPostIsComment = PostUtils.isComment(prevPost);
const postFromWebhook = Boolean(post.props && post.props.from_webhook); const postFromWebhook = Boolean(post.props && post.props.from_webhook);
const prevPostFromWebhook = Boolean(prevPost.props && prevPost.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: // 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 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 current post is not from a webhook
// the previous post is not from a webhook // the previous post is not from a webhook
if (prevPostUserId === postUserId && if (prevPostUserId === postUserId &&
post.create_at - prevPost.create_at <= 1000 * 60 * 5 && post.create_at - prevPost.create_at <= Constants.POST_COLLAPSE_TIMEOUT &&
!postFromWebhook && !prevPostFromWebhook) { !postFromWebhook && !prevPostFromWebhook) {
sameUser = true; 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 // 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 // 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; const keyPrefix = post.id ? post.id : i;
@@ -254,11 +225,24 @@ export default class PostsView extends React.Component {
let profile; let profile;
if (userId === post.user_id) { if (userId === post.user_id) {
profile = this.state.currentUser; profile = this.props.currentUser;
} else { } else {
profile = profiles[post.user_id]; 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 = ( const postCtl = (
<Post <Post
key={keyPrefix + 'postKey'} key={keyPrefix + 'postKey'}
@@ -267,16 +251,15 @@ export default class PostsView extends React.Component {
sameRoot={sameRoot} sameRoot={sameRoot}
post={post} post={post}
parentPost={parentPost} parentPost={parentPost}
posts={posts}
hideProfilePic={hideProfilePic} hideProfilePic={hideProfilePic}
isLastComment={isLastComment} isLastComment={isLastComment}
shouldHighlight={shouldHighlight} shouldHighlight={shouldHighlight}
onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.props.displayNameType}
displayNameType={this.state.displayNameType}
user={profile} user={profile}
currentUser={this.state.currentUser} currentUser={this.props.currentUser}
center={this.state.centerPosts} center={this.props.displayPostsInCenter}
compactDisplay={this.state.compactPosts} commentCount={commentCount}
compactDisplay={this.props.compactDisplay}
/> />
); );
@@ -302,8 +285,8 @@ export default class PostsView extends React.Component {
} }
if (postUserId !== userId && if (postUserId !== userId &&
this.props.messageSeparatorTime !== 0 && this.props.lastViewed !== 0 &&
post.create_at > this.props.messageSeparatorTime && post.create_at > this.props.lastViewed &&
!renderedLastViewed) { !renderedLastViewed) {
renderedLastViewed = true; renderedLastViewed = true;
@@ -337,10 +320,11 @@ export default class PostsView extends React.Component {
return postCtls; return postCtls;
} }
updateScrolling() { updateScrolling() {
if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) { if (this.props.scrollType === ScrollTypes.BOTTOM) {
this.scrollToBottom(); this.scrollToBottom();
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) { } else if (this.props.scrollType === ScrollTypes.NEW_MESSAGE) {
window.setTimeout(window.requestAnimationFrame(() => { window.setTimeout(window.requestAnimationFrame(() => {
// If separator exists scroll to it. Otherwise scroll to bottom. // If separator exists scroll to it. Otherwise scroll to bottom.
if (this.refs.newMessageSeparator) { if (this.refs.newMessageSeparator) {
@@ -350,7 +334,7 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
} }
}), 0); }), 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(() => { window.requestAnimationFrame(() => {
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]); const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
if (postNode == null) { if (postNode == null) {
@@ -358,12 +342,12 @@ export default class PostsView extends React.Component {
} }
postNode.scrollIntoView(); postNode.scrollIntoView();
if (this.refs.postlist.scrollTop === postNode.offsetTop) { 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 { } 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 we are at the bottom then stay there
if (this.wasAtBottom) { if (this.wasAtBottom) {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
@@ -371,9 +355,9 @@ export default class PostsView extends React.Component {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.jumpToPostNode.scrollIntoView(); this.jumpToPostNode.scrollIntoView();
if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) { 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 { } 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() { handleResize() {
this.updateScrolling(); this.updateScrolling();
} }
scrollToBottom() { scrollToBottom() {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight; this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
}); });
} }
scrollToBottomAnimated() { scrollToBottomAnimated() {
var postList = $(this.refs.postlist); var postList = $(this.refs.postlist);
postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500'); postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
@@ -417,145 +404,76 @@ export default class PostsView extends React.Component {
this.updateScrolling(); 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); window.addEventListener('resize', this.handleResize);
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
this.scrollStopAction.cancel(); this.scrollStopAction.cancel();
PreferenceStore.removeChangeListener(this.updateState);
UserStore.removeChangeListener(this.onUserChange);
} }
componentDidUpdate() { componentDidUpdate() {
if (this.props.postList != null) { if (this.props.postList != null) {
this.updateScrolling(); 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() { render() {
let posts = []; if (this.props.postList == null) {
let order = []; return <div/>;
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 = '';
}
} }
const posts = this.props.postList.posts;
const order = this.props.postList.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;
}
// 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; let topPostCreateAt = 0;
if (this.state.topPostId && this.props.postList.posts[this.state.topPostId]) { if (this.state.topPostId && this.props.postList.posts[this.state.topPostId]) {
topPostCreateAt = this.props.postList.posts[this.state.topPostId].create_at; topPostCreateAt = this.props.postList.posts[this.state.topPostId].create_at;
} }
return ( return (
<div className={activeClass}> <div>
<FloatingTimestamp <FloatingTimestamp
isScrolling={this.state.isScrolling} isScrolling={this.state.isScrolling}
isMobile={$(window).width() > 768} isMobile={Utils.isMobile()}
createAt={topPostCreateAt} createAt={topPostCreateAt}
/> />
<ScrollToBottomArrows <ScrollToBottomArrows
@@ -583,48 +501,24 @@ export default class PostsView extends React.Component {
); );
} }
} }
PostsView.defaultProps = {
PostList.defaultProps = {
lastViewed: 0
}; };
PostsView.propTypes = { PostList.propTypes = {
isActive: React.PropTypes.bool,
postList: React.PropTypes.object, postList: React.PropTypes.object,
profiles: React.PropTypes.object,
channel: React.PropTypes.object,
currentUser: React.PropTypes.object,
scrollPostId: React.PropTypes.string, scrollPostId: React.PropTypes.string,
scrollType: React.PropTypes.number, scrollType: React.PropTypes.number,
postViewScrolled: React.PropTypes.func.isRequired, postListScrolled: React.PropTypes.func.isRequired,
loadMorePostsTopClicked: React.PropTypes.func.isRequired,
loadMorePostsBottomClicked: React.PropTypes.func.isRequired,
showMoreMessagesTop: React.PropTypes.bool, showMoreMessagesTop: React.PropTypes.bool,
showMoreMessagesBottom: React.PropTypes.bool, showMoreMessagesBottom: React.PropTypes.bool,
channel: React.PropTypes.object, lastViewed: React.PropTypes.number,
messageSeparatorTime: React.PropTypes.number,
postsToHighlight: React.PropTypes.object, postsToHighlight: React.PropTypes.object,
displayNameType: React.PropTypes.string,
displayPostsInCenter: React.PropTypes.bool,
compactDisplay: 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
};

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -1,11 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // 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 PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_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'; import React from 'react';
@@ -15,17 +19,24 @@ export default class PostFocusView extends React.Component {
this.onChannelChange = this.onChannelChange.bind(this); this.onChannelChange = this.onChannelChange.bind(this);
this.onPostsChange = this.onPostsChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.onUserChange = this.onUserChange.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this);
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
const focusedPostId = PostStore.getFocusedPostId(); 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 = { this.state = {
scrollType: PostsView.SCROLL_TYPE_POST, postList: PostStore.getVisiblePosts(focusedPostId),
currentUser: UserStore.getCurrentUser(),
profiles,
scrollType: ScrollTypes.POST,
currentChannel: ChannelStore.getCurrentId().slice(), currentChannel: ChannelStore.getCurrentId().slice(),
scrollPostId: focusedPostId, scrollPostId: focusedPostId,
postList: PostStore.getVisiblePosts(focusedPostId),
atTop: PostStore.getVisibilityAtTop(focusedPostId), atTop: PostStore.getVisibilityAtTop(focusedPostId),
atBottom: PostStore.getVisibilityAtBottom(focusedPostId) atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
}; };
@@ -34,11 +45,13 @@ export default class PostFocusView extends React.Component {
componentDidMount() { componentDidMount() {
ChannelStore.addChangeListener(this.onChannelChange); ChannelStore.addChangeListener(this.onChannelChange);
PostStore.addChangeListener(this.onPostsChange); PostStore.addChangeListener(this.onPostsChange);
UserStore.addChangeListener(this.onUserChange);
} }
componentWillUnmount() { componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeChangeListener(this.onChannelChange);
PostStore.removeChangeListener(this.onPostsChange); PostStore.removeChangeListener(this.onPostsChange);
UserStore.removeChangeListener(this.onUserChange);
} }
onChannelChange() { onChannelChange() {
@@ -46,7 +59,7 @@ export default class PostFocusView extends React.Component {
if (this.state.currentChannel !== currentChannel) { if (this.state.currentChannel !== currentChannel) {
this.setState({ this.setState({
currentChannel: currentChannel.slice(), currentChannel: currentChannel.slice(),
scrollType: PostsView.SCROLL_TYPE_POST scrollType: ScrollTypes.POST
}); });
} }
} }
@@ -65,42 +78,50 @@ export default class PostFocusView extends React.Component {
}); });
} }
handlePostsViewScroll() { onUserChange() {
this.setState({scrollType: PostsView.SCROLL_TYPE_FREE}); 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))});
} }
loadMorePostsTop() { onPostListScroll() {
GlobalActions.emitLoadMorePostsFocusedTopEvent(); this.setState({scrollType: ScrollTypes.FREE});
}
loadMorePostsBottom() {
GlobalActions.emitLoadMorePostsFocusedBottomEvent();
} }
render() { render() {
const postsToHighlight = {}; const postsToHighlight = {};
postsToHighlight[this.state.scrollPostId] = true; postsToHighlight[this.state.scrollPostId] = true;
if (!this.state.postList) { let content;
return null; 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}
postListScrolled={this.onPostListScroll}
showMoreMessagesTop={!this.state.atTop}
showMoreMessagesBottom={!this.state.atBottom}
postsToHighlight={postsToHighlight}
/>
);
} }
return ( return (
<div id='post-list'> <div id='post-list'>
<PostsView {content}
key={'postfocusview'}
isActive={true}
postList={this.state.postList}
scrollType={this.state.scrollType}
scrollPostId={this.state.scrollPostId}
postViewScrolled={this.handlePostsViewScroll}
loadMorePostsTopClicked={this.loadMorePostsTop}
loadMorePostsBottomClicked={this.loadMorePostsBottom}
showMoreMessagesTop={!this.state.atTop}
showMoreMessagesBottom={!this.state.atBottom}
messageSeparatorTime={0}
postsToHighlight={postsToHighlight}
/>
</div> </div>
); );
} }

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -3,7 +3,7 @@
import UserProfile from './user_profile.jsx'; import UserProfile from './user_profile.jsx';
import FileAttachmentList from './file_attachment_list.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 TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_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) { if (post.state === Constants.POST_FAILED) {
postClass += ' post-fail'; postClass += ' post-fail';
loading = <PendingPostActions post={this.props.post}/>; loading = <PendingPostOptions post={this.props.post}/>;
} else if (post.state === Constants.POST_LOADING) { } else if (post.state === Constants.POST_LOADING) {
postClass += ' post-waiting'; postClass += ' post-waiting';
loading = ( loading = (

Просмотреть файл

@@ -6,11 +6,13 @@ import UserProfile from './user_profile.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
import FileAttachmentList from './file_attachment_list.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 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 Constants from 'utils/constants.jsx';
import {FormattedMessage, FormattedDate} from 'react-intl'; import {FormattedMessage, FormattedDate} from 'react-intl';
@@ -56,7 +58,7 @@ export default class RhsRootPost extends React.Component {
} }
var systemMessageClass = ''; var systemMessageClass = '';
if (Utils.isSystemMessage(post)) { if (PostUtils.isSystemMessage(post)) {
systemMessageClass = 'post--system'; systemMessageClass = 'post--system';
} }
@@ -188,7 +190,7 @@ export default class RhsRootPost extends React.Component {
} }
botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>; botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
} else if (Utils.isSystemMessage(post)) { } else if (PostUtils.isSystemMessage(post)) {
userProfile = ( userProfile = (
<UserProfile <UserProfile
user={{}} user={{}}
@@ -202,7 +204,7 @@ export default class RhsRootPost extends React.Component {
const profilePic = ( const profilePic = (
<img <img
className='post-profile-img' className='post-profile-img'
src={Utils.getProfilePicSrcForPost(post, timestamp)} src={PostUtils.getProfilePicSrcForPost(post, timestamp)}
height='36' height='36'
width='36' width='36'
/> />

Просмотреть файл

@@ -10,6 +10,7 @@ import * as GlobalActions from 'actions/global_actions.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
@@ -93,7 +94,7 @@ export default class SearchResultsItem extends React.Component {
<div className='post__content'> <div className='post__content'>
<div className='post__img'> <div className='post__img'>
<img <img
src={Utils.getProfilePicSrcForPost(post, timestamp)} src={PostUtils.getProfilePicSrcForPost(post, timestamp)}
height='36' height='36'
width='36' width='36'
/> />

Просмотреть файл

@@ -7,6 +7,7 @@ import Constants from 'utils/constants.jsx';
import UserStore from './user_store.jsx'; import UserStore from './user_store.jsx';
import ChannelStore from './channel_store.jsx'; import ChannelStore from './channel_store.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
const CHANGE_EVENT = 'change'; const CHANGE_EVENT = 'change';
@@ -26,7 +27,7 @@ class NotificationStoreClass extends EventEmitter {
handleRecievedPost(post, msgProps) { handleRecievedPost(post, msgProps) {
// Send desktop notification // 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 = []; let mentions = [];
if (msgProps.mentions) { if (msgProps.mentions) {
mentions = JSON.parse(msgProps.mentions); mentions = JSON.parse(msgProps.mentions);

Просмотреть файл

@@ -161,6 +161,14 @@ export default {
EPHEMERAL_MESSAGE: 'ephemeral_message' EPHEMERAL_MESSAGE: 'ephemeral_message'
}, },
ScrollTypes: {
FREE: 1,
BOTTOM: 2,
SIDEBBAR_OPEN: 3,
NEW_MESSAGE: 4,
POST: 5
},
//SPECIAL_MENTIONS: ['all', 'channel'], //SPECIAL_MENTIONS: ['all', 'channel'],
SPECIAL_MENTIONS: ['channel'], SPECIAL_MENTIONS: ['channel'],
CHARACTER_LIMIT: 4000, CHARACTER_LIMIT: 4000,
@@ -204,6 +212,9 @@ export default {
WEB_VIDEO_HEIGHT: 480, WEB_VIDEO_HEIGHT: 480,
MOBILE_VIDEO_WIDTH: 480, MOBILE_VIDEO_WIDTH: 480,
MOBILE_VIDEO_HEIGHT: 360, MOBILE_VIDEO_HEIGHT: 360,
MOBILE_SCREEN_WIDTH: 768,
SCROLL_DELAY: 2000,
SCROLL_PAGE_FRACTION: 3,
DEFAULT_CHANNEL: 'town-square', DEFAULT_CHANNEL: 'town-square',
DEFAULT_CHANNEL_UI_NAME: 'Town Square', DEFAULT_CHANNEL_UI_NAME: 'Town Square',
OFFTOPIC_CHANNEL: 'off-topic', OFFTOPIC_CHANNEL: 'off-topic',
@@ -738,5 +749,6 @@ export default {
DEFAULT_WEBHOOK_LOGO: logoWebhook, DEFAULT_WEBHOOK_LOGO: logoWebhook,
MHPNS: 'https://push.mattermost.com', MHPNS: 'https://push.mattermost.com',
MTPNS: 'http://push-test.mattermost.com', MTPNS: 'http://push-test.mattermost.com',
BOT_NAME: 'BOT' BOT_NAME: 'BOT',
POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5 // five minutes
}; };

32
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;
}

Просмотреть файл

@@ -909,14 +909,7 @@ export function isValidUsername(name) {
} }
export function isMobile() { export function isMobile() {
return window.innerWidth <= 768; return window.innerWidth <= Constants.MOBILE_SCREEN_WIDTH;
}
export function isComment(post) {
if ('root_id' in post) {
return post.root_id !== '' && post.root_id != null;
}
return false;
} }
export function getDirectTeammate(channelId) { export function getDirectTeammate(channelId) {
@@ -1315,10 +1308,6 @@ export function isFeatureEnabled(feature) {
return PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label); 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) { export function fillArray(value, length) {
const arr = []; const arr = [];
@@ -1379,18 +1368,3 @@ export function localizeMessage(id, defaultMessage) {
export function mod(a, b) { export function mod(a, b) {
return ((a % b) + b) % 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;
}