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 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'

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

@@ -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'
>

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

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

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

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

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

@@ -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 = (

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

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

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

@@ -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={{}}

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

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

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

@@ -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;
}
// 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 = '';
}
if (this.props.postList == null) {
return <div/>;
}
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;
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
};

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

@@ -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.
// 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});
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))});
}
loadMorePostsTop() {
GlobalActions.emitLoadMorePostsFocusedTopEvent();
}
loadMorePostsBottom() {
GlobalActions.emitLoadMorePostsFocusedBottomEvent();
onPostListScroll() {
this.setState({scrollType: ScrollTypes.FREE});
}
render() {
const postsToHighlight = {};
postsToHighlight[this.state.scrollPostId] = true;
if (!this.state.postList) {
return null;
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}
postListScrolled={this.onPostListScroll}
showMoreMessagesTop={!this.state.atTop}
showMoreMessagesBottom={!this.state.atBottom}
postsToHighlight={postsToHighlight}
/>
);
}
return (
<div id='post-list'>
<PostsView
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}
/>
{content}
</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 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 = (

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

@@ -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'
/>

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

@@ -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'
/>

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

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

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

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

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