Implementing Permalinks and jumping to post from search. Performance
improvements.
Этот коммит содержится в:
@@ -4,11 +4,13 @@
|
||||
import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx';
|
||||
import CreatePost from './create_post.jsx';
|
||||
import PostsViewContainer from './posts_view_container.jsx';
|
||||
import PostFocusView from './post_focus_view.jsx';
|
||||
import ChannelHeader from './channel_header.jsx';
|
||||
import Navbar from './navbar.jsx';
|
||||
import FileUploadOverlay from './file_upload_overlay.jsx';
|
||||
|
||||
import PreferenceStore from '../stores/preference_store.jsx';
|
||||
import ChannelStore from '../stores/channel_store.jsx';
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
|
||||
import Constants from '../utils/constants.jsx';
|
||||
@@ -20,26 +22,48 @@ export default class CenterPanel extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
|
||||
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
|
||||
this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS};
|
||||
this.state = {
|
||||
showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS,
|
||||
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
}
|
||||
onPreferenceChange() {
|
||||
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
|
||||
this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
|
||||
}
|
||||
onChannelChange() {
|
||||
this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
|
||||
}
|
||||
render() {
|
||||
let postsContainer;
|
||||
let createPost;
|
||||
if (this.state.showTutorialScreens) {
|
||||
postsContainer = <TutorialIntroScreens />;
|
||||
createPost = null;
|
||||
} else if (this.state.showPostFocus) {
|
||||
postsContainer = <PostFocusView />;
|
||||
createPost = null;
|
||||
} else {
|
||||
postsContainer = <PostsViewContainer />;
|
||||
createPost = (
|
||||
<div
|
||||
className='post-create__container'
|
||||
id='post-create'
|
||||
>
|
||||
<CreatePost />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -62,12 +86,7 @@ export default class CenterPanel extends React.Component {
|
||||
<ChannelHeader />
|
||||
</div>
|
||||
{postsContainer}
|
||||
<div
|
||||
className='post-create__container'
|
||||
id='post-create'
|
||||
>
|
||||
<CreatePost />
|
||||
</div>
|
||||
{createPost}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,17 @@ export default class ChannelInviteModal extends React.Component {
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(this.props, nextProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(this.state, nextState)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
getStateFromStores() {
|
||||
function getId(user) {
|
||||
return user.id;
|
||||
@@ -105,7 +116,7 @@ export default class ChannelInviteModal extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({inviteError: null, memberIds, nonmembers});
|
||||
AsyncClient.getChannelExtraInfo(true);
|
||||
AsyncClient.getChannelExtraInfo();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({inviteError: err.message});
|
||||
|
||||
@@ -27,8 +27,8 @@ export default class ChannelLoader extends React.Component {
|
||||
componentDidMount() {
|
||||
/* Initial aysnc loads */
|
||||
AsyncClient.getPosts(ChannelStore.getCurrentId());
|
||||
AsyncClient.getChannels(true, true);
|
||||
AsyncClient.getChannelExtraInfo(true);
|
||||
AsyncClient.getChannels();
|
||||
AsyncClient.getChannelExtraInfo();
|
||||
AsyncClient.findTeams();
|
||||
AsyncClient.getMyTeam();
|
||||
setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
|
||||
|
||||
@@ -25,6 +25,17 @@ export default class ChannelMembersModal extends React.Component {
|
||||
state.showInviteModal = false;
|
||||
this.state = state;
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(this.props, nextProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(this.state, nextState)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
getStateFromStores() {
|
||||
const users = UserStore.getActiveOnlyProfiles();
|
||||
const memberList = ChannelStore.getCurrentExtraInfo().members;
|
||||
@@ -74,6 +85,7 @@ export default class ChannelMembersModal extends React.Component {
|
||||
if ($(window).width() > 768) {
|
||||
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
|
||||
}
|
||||
this.onChange();
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show && !prevProps.show) {
|
||||
@@ -130,7 +142,7 @@ export default class ChannelMembersModal extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({memberList, nonmemberList});
|
||||
AsyncClient.getChannelExtraInfo(true);
|
||||
AsyncClient.getChannelExtraInfo();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({inviteError: err.message});
|
||||
|
||||
@@ -8,6 +8,7 @@ import FilePreview from './file_preview.jsx';
|
||||
import TutorialTip from './tutorial/tutorial_tip.jsx';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
import * as Client from '../utils/client.jsx';
|
||||
import * as AsyncClient from '../utils/async_client.jsx';
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
@@ -19,6 +20,7 @@ import PreferenceStore from '../stores/preference_store.jsx';
|
||||
import SocketStore from '../stores/socket_store.jsx';
|
||||
|
||||
import Constants from '../utils/constants.jsx';
|
||||
|
||||
const Preferences = Constants.Preferences;
|
||||
const TutorialSteps = Constants.TutorialSteps;
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
@@ -176,9 +178,7 @@ export default class CreatePost extends React.Component {
|
||||
|
||||
const channel = ChannelStore.get(this.state.channelId);
|
||||
|
||||
PostStore.storePendingPost(post);
|
||||
PostStore.storeDraft(channel.id, null);
|
||||
PostStore.jumpPostsViewToBottom();
|
||||
EventHelpers.emitUserPostedEvent(post);
|
||||
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
|
||||
|
||||
Client.createPost(post, channel,
|
||||
@@ -190,10 +190,7 @@ export default class CreatePost extends React.Component {
|
||||
member.last_viewed_at = Date.now();
|
||||
ChannelStore.setChannelMember(member);
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST,
|
||||
post: data
|
||||
});
|
||||
EventHelpers.emitPostRecievedEvent(data);
|
||||
},
|
||||
(err) => {
|
||||
const state = {};
|
||||
|
||||
@@ -105,7 +105,7 @@ export default class Post extends React.Component {
|
||||
} else {
|
||||
commentRootId = post.id;
|
||||
}
|
||||
for (let postId in posts) {
|
||||
for (const postId in posts) {
|
||||
if (posts[postId].root_id === commentRootId) {
|
||||
commentCount += 1;
|
||||
}
|
||||
@@ -114,53 +114,58 @@ export default class Post extends React.Component {
|
||||
return commentCount;
|
||||
}
|
||||
render() {
|
||||
var post = this.props.post;
|
||||
var parentPost = this.props.parentPost;
|
||||
var posts = this.props.posts;
|
||||
const post = this.props.post;
|
||||
const parentPost = this.props.parentPost;
|
||||
const posts = this.props.posts;
|
||||
|
||||
if (!post.props) {
|
||||
post.props = {};
|
||||
}
|
||||
|
||||
var type = 'Post';
|
||||
let type = 'Post';
|
||||
if (post.root_id && post.root_id.length > 0) {
|
||||
type = 'Comment';
|
||||
}
|
||||
|
||||
const commentCount = this.getCommentCount(this.props);
|
||||
|
||||
var rootUser;
|
||||
let rootUser;
|
||||
if (this.props.sameRoot) {
|
||||
rootUser = 'same--root';
|
||||
} else {
|
||||
rootUser = 'other--root';
|
||||
}
|
||||
|
||||
var postType = '';
|
||||
let postType = '';
|
||||
if (type !== 'Post') {
|
||||
postType = 'post--comment';
|
||||
} else if (commentCount > 0) {
|
||||
postType = 'post--root';
|
||||
}
|
||||
|
||||
var currentUserCss = '';
|
||||
let currentUserCss = '';
|
||||
if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) {
|
||||
currentUserCss = 'current--user';
|
||||
}
|
||||
|
||||
var userProfile = UserStore.getProfile(post.user_id);
|
||||
const userProfile = UserStore.getProfile(post.user_id);
|
||||
|
||||
var timestamp = UserStore.getCurrentUser().update_at;
|
||||
let timestamp = UserStore.getCurrentUser().update_at;
|
||||
if (userProfile) {
|
||||
timestamp = userProfile.update_at;
|
||||
}
|
||||
|
||||
var sameUserClass = '';
|
||||
let sameUserClass = '';
|
||||
if (this.props.sameUser) {
|
||||
sameUserClass = 'same--user';
|
||||
}
|
||||
|
||||
var profilePic = null;
|
||||
let shouldHighlightClass = '';
|
||||
if (this.props.shouldHighlight) {
|
||||
shouldHighlightClass = 'post--highlight';
|
||||
}
|
||||
|
||||
let profilePic = null;
|
||||
if (!this.props.hideProfilePic) {
|
||||
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
|
||||
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
|
||||
@@ -182,7 +187,7 @@ export default class Post extends React.Component {
|
||||
<div>
|
||||
<div
|
||||
id={'post_' + post.id}
|
||||
className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}
|
||||
className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass}
|
||||
>
|
||||
<div className='post__content'>
|
||||
<div className='post__img'>{profilePic}</div>
|
||||
@@ -218,5 +223,6 @@ Post.propTypes = {
|
||||
sameUser: React.PropTypes.bool,
|
||||
sameRoot: React.PropTypes.bool,
|
||||
hideProfilePic: React.PropTypes.bool,
|
||||
isLastComment: React.PropTypes.bool
|
||||
isLastComment: React.PropTypes.bool,
|
||||
shouldHighlight: React.PropTypes.bool
|
||||
};
|
||||
|
||||
110
web/react/components/post_focus_view.jsx
Обычный файл
110
web/react/components/post_focus_view.jsx
Обычный файл
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostsView from './posts_view.jsx';
|
||||
|
||||
import PostStore from '../stores/post_store.jsx';
|
||||
import ChannelStore from '../stores/channel_store.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
|
||||
export default class PostFocusView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
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);
|
||||
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
|
||||
this.state = {
|
||||
scrollType: PostsView.SCROLL_TYPE_POST,
|
||||
scrollPostId: focusedPostId,
|
||||
postList: PostStore.getVisiblePosts(focusedPostId),
|
||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
PostStore.addChangeListener(this.onPostsChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
PostStore.removeChangeListener(this.onPostsChange);
|
||||
}
|
||||
|
||||
onChannelChange() {
|
||||
this.setState({
|
||||
scrollType: PostsView.SCROLL_TYPE_POST
|
||||
});
|
||||
}
|
||||
|
||||
onPostsChange() {
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
if (focusedPostId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
scrollPostId: focusedPostId,
|
||||
postList: PostStore.getVisiblePosts(focusedPostId),
|
||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
|
||||
});
|
||||
}
|
||||
|
||||
handlePostsViewScroll() {
|
||||
this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
|
||||
}
|
||||
|
||||
loadMorePostsTop() {
|
||||
EventHelpers.emitLoadMorePostsFocusedTopEvent();
|
||||
}
|
||||
|
||||
loadMorePostsBottom() {
|
||||
EventHelpers.emitLoadMorePostsFocusedBottomEvent();
|
||||
}
|
||||
|
||||
getIntroMessage() {
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<h4 className='channel-intro__title'>{'Beginning of Channel'}</h4>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const postsToHighlight = {};
|
||||
postsToHighlight[this.state.scrollPostId] = true;
|
||||
|
||||
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}
|
||||
introText={this.getIntroMessage()}
|
||||
messageSeparatorTime={0}
|
||||
postsToHighlight={postsToHighlight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
PostFocusView.defaultProps = {
|
||||
};
|
||||
|
||||
PostFocusView.propTypes = {
|
||||
};
|
||||
@@ -3,20 +3,28 @@
|
||||
|
||||
import DeletePostModal from './delete_post_modal.jsx';
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
import * as utils from '../utils/utils.jsx';
|
||||
import TeamStore from '../stores/team_store.jsx';
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
import TimeSince from './time_since.jsx';
|
||||
|
||||
import Constants from '../utils/constants.jsx';
|
||||
|
||||
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
||||
const Popover = ReactBootstrap.Popover;
|
||||
|
||||
export default class PostInfo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state = {
|
||||
copiedLink: false
|
||||
};
|
||||
|
||||
this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this);
|
||||
}
|
||||
createDropdown() {
|
||||
var post = this.props.post;
|
||||
var isOwner = UserStore.getCurrentId() === post.user_id;
|
||||
var isAdmin = utils.isAdmin(UserStore.getCurrentUser().roles);
|
||||
var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
|
||||
|
||||
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) {
|
||||
return '';
|
||||
@@ -113,6 +121,21 @@ export default class PostInfo extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
handlePermalinkCopy() {
|
||||
const textBox = $(ReactDOM.findDOMNode(this.refs.permalinkbox));
|
||||
textBox.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
this.setState({copiedLink: true});
|
||||
} else {
|
||||
this.setState({copiedLink: false});
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({copiedLink: false});
|
||||
}
|
||||
}
|
||||
render() {
|
||||
var post = this.props.post;
|
||||
var comments = '';
|
||||
@@ -143,6 +166,37 @@ export default class PostInfo extends React.Component {
|
||||
|
||||
var dropdown = this.createDropdown();
|
||||
|
||||
const permalink = TeamStore.getCurrentTeamUrl() + '/pl/' + post.id;
|
||||
const copyButtonText = this.state.copiedLink ? (<div>{'Copy '}<i className='fa fa-check'/></div>) : 'Copy';
|
||||
const permalinkOverlay = (
|
||||
<Popover
|
||||
id='permalink-overlay'
|
||||
className='permalink-popover'
|
||||
placement='left'
|
||||
title=''
|
||||
>
|
||||
<div className='form-inline'>
|
||||
<input
|
||||
type='text'
|
||||
readOnly='true'
|
||||
ref='permalinkbox'
|
||||
className='permalink-text form-control no-resize min-height input-large'
|
||||
rows='1'
|
||||
value={permalink}
|
||||
/>
|
||||
<button
|
||||
data-copy-btn='true'
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={this.handlePermalinkCopy}
|
||||
data-clipboard-text={permalink}
|
||||
>
|
||||
{copyButtonText}
|
||||
</button>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className='post__header post__header--info'>
|
||||
<li className='col'>
|
||||
@@ -152,6 +206,15 @@ export default class PostInfo extends React.Component {
|
||||
</li>
|
||||
<li className='col col__reply'>
|
||||
{comments}
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
placement='left'
|
||||
rootClose={true}
|
||||
overlay={permalinkOverlay}
|
||||
>
|
||||
<i className={'permalink-icon fa fa-link ' + showCommentClass}/>
|
||||
</OverlayTrigger>
|
||||
|
||||
<div className='dropdown'>
|
||||
{dropdown}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
import Post from './post.jsx';
|
||||
import Constants from '../utils/constants.jsx';
|
||||
@@ -13,6 +14,7 @@ export default class PostsView extends React.Component {
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.isAtBottom = this.isAtBottom.bind(this);
|
||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
||||
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
|
||||
this.createPosts = this.createPosts.bind(this);
|
||||
this.updateScrolling = this.updateScrolling.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
@@ -27,12 +29,15 @@ export default class PostsView extends React.Component {
|
||||
static get SCROLL_TYPE_BOTTOM() {
|
||||
return 2;
|
||||
}
|
||||
static get SIDEBAR_OPEN() {
|
||||
static get SCROLL_TYPE_SIDEBAR_OPEN() {
|
||||
return 3;
|
||||
}
|
||||
static get SCROLL_TYPE_NEW_MESSAGE() {
|
||||
return 4;
|
||||
}
|
||||
static get SCROLL_TYPE_POST() {
|
||||
return 5;
|
||||
}
|
||||
isAtBottom() {
|
||||
return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
|
||||
}
|
||||
@@ -47,15 +52,22 @@ export default class PostsView extends React.Component {
|
||||
}
|
||||
}
|
||||
this.wasAtBottom = this.isAtBottom();
|
||||
if (!this.jumpToPostNode && childNodes.length > 0) {
|
||||
this.jumpToPostNode = childNodes[childNodes.length - 1];
|
||||
}
|
||||
|
||||
// --- --------
|
||||
|
||||
this.props.postViewScrolled(this.isAtBottom());
|
||||
this.prevScrollHeight = this.refs.postlist.scrollHeight;
|
||||
this.prevOffsetTop = this.jumpToPostNode.offsetTop;
|
||||
}
|
||||
loadMorePostsTop() {
|
||||
this.props.loadMorePostsTopClicked();
|
||||
}
|
||||
loadMorePostsBottom() {
|
||||
this.props.loadMorePostsBottomClicked();
|
||||
}
|
||||
createPosts(posts, order) {
|
||||
const postCtls = [];
|
||||
let previousPostDay = new Date(0);
|
||||
@@ -63,12 +75,7 @@ export default class PostsView extends React.Component {
|
||||
|
||||
let renderedLastViewed = false;
|
||||
|
||||
let numToDisplay = this.props.numPostsToDisplay;
|
||||
if (order.length - 1 < numToDisplay) {
|
||||
numToDisplay = order.length - 1;
|
||||
}
|
||||
|
||||
for (let i = numToDisplay; i >= 0; i--) {
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const post = posts[order[i]];
|
||||
const parentPost = posts[post.parent_id];
|
||||
const prevPost = posts[order[i + 1]];
|
||||
@@ -113,6 +120,8 @@ export default class PostsView extends React.Component {
|
||||
|
||||
const keyPrefix = post.id ? post.id : i;
|
||||
|
||||
const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
|
||||
|
||||
const postCtl = (
|
||||
<Post
|
||||
key={keyPrefix + 'postKey'}
|
||||
@@ -124,6 +133,8 @@ export default class PostsView extends React.Component {
|
||||
posts={posts}
|
||||
hideProfilePic={hideProfilePic}
|
||||
isLastComment={isLastComment}
|
||||
shouldHighlight={shouldHighlight}
|
||||
onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -185,9 +196,12 @@ export default class PostsView extends React.Component {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
}
|
||||
});
|
||||
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPost) {
|
||||
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPost]);
|
||||
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
|
||||
if (postNode == null) {
|
||||
return;
|
||||
}
|
||||
postNode.scrollIntoView();
|
||||
if (this.refs.postlist.scrollTop === postNode.offsetTop) {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
|
||||
@@ -195,7 +209,7 @@ export default class PostsView extends React.Component {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop);
|
||||
}
|
||||
});
|
||||
} else if (this.props.scrollType === PostsView.SIDEBAR_OPEN) {
|
||||
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_SIDEBAR_OPEN) {
|
||||
// If we are at the bottom then stay there
|
||||
if (this.wasAtBottom) {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
@@ -211,7 +225,10 @@ export default class PostsView extends React.Component {
|
||||
}
|
||||
} else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
|
||||
// Only need to jump if we added posts to the top.
|
||||
if (this.jumpToPostNode && (this.jumpToPostNode.offsetTop !== this.prevOffsetTop)) {
|
||||
this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -219,14 +236,18 @@ export default class PostsView extends React.Component {
|
||||
this.updateScrolling();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.updateScrolling();
|
||||
if (this.props.postList != null) {
|
||||
this.updateScrolling();
|
||||
}
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.updateScrolling();
|
||||
if (this.props.postList != null) {
|
||||
this.updateScrolling();
|
||||
}
|
||||
}
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (this.props.isActive !== nextProps.isActive) {
|
||||
@@ -235,15 +256,12 @@ export default class PostsView extends React.Component {
|
||||
if (this.props.postList !== nextProps.postList) {
|
||||
return true;
|
||||
}
|
||||
if (this.props.scrollPost !== nextProps.scrollPost) {
|
||||
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.numPostsToDisplay !== nextProps.numPostsToDisplay) {
|
||||
return true;
|
||||
}
|
||||
if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
|
||||
return true;
|
||||
}
|
||||
@@ -256,7 +274,8 @@ export default class PostsView extends React.Component {
|
||||
render() {
|
||||
let posts = [];
|
||||
let order = [];
|
||||
let moreMessages;
|
||||
let moreMessagesTop;
|
||||
let moreMessagesBottom;
|
||||
let postElements;
|
||||
let activeClass = 'inactive';
|
||||
if (this.props.postList != null) {
|
||||
@@ -264,10 +283,10 @@ export default class PostsView extends React.Component {
|
||||
order = this.props.postList.order;
|
||||
|
||||
// Create intro message or top loadmore link
|
||||
if (order.length >= this.props.numPostsToDisplay) {
|
||||
moreMessages = (
|
||||
if (this.props.showMoreMessagesTop) {
|
||||
moreMessagesTop = (
|
||||
<a
|
||||
ref='loadmore'
|
||||
ref='loadmoretop'
|
||||
className='more-messages-text theme'
|
||||
href='#'
|
||||
onClick={this.loadMorePostsTop}
|
||||
@@ -276,7 +295,23 @@ export default class PostsView extends React.Component {
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
moreMessages = this.props.introText;
|
||||
moreMessagesTop = this.props.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}
|
||||
>
|
||||
{'Load more messages'}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
moreMessagesBottom = null;
|
||||
}
|
||||
|
||||
// Create post elements
|
||||
@@ -299,8 +334,9 @@ export default class PostsView extends React.Component {
|
||||
ref='postlistcontent'
|
||||
className='post-list__content'
|
||||
>
|
||||
{moreMessages}
|
||||
{moreMessagesTop}
|
||||
{postElements}
|
||||
{moreMessagesBottom}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,11 +349,14 @@ PostsView.defaultProps = {
|
||||
PostsView.propTypes = {
|
||||
isActive: React.PropTypes.bool,
|
||||
postList: React.PropTypes.object,
|
||||
scrollPost: React.PropTypes.string,
|
||||
scrollPostId: React.PropTypes.string,
|
||||
scrollType: React.PropTypes.number,
|
||||
postViewScrolled: React.PropTypes.func.isRequired,
|
||||
loadMorePostsTopClicked: React.PropTypes.func.isRequired,
|
||||
numPostsToDisplay: React.PropTypes.number,
|
||||
loadMorePostsBottomClicked: React.PropTypes.func.isRequired,
|
||||
showMoreMessagesTop: React.PropTypes.bool,
|
||||
showMoreMessagesBottom: React.PropTypes.bool,
|
||||
introText: React.PropTypes.element,
|
||||
messageSeparatorTime: React.PropTypes.number
|
||||
messageSeparatorTime: React.PropTypes.number,
|
||||
postsToHighlight: React.PropTypes.object
|
||||
};
|
||||
|
||||
@@ -9,12 +9,9 @@ import ChannelStore from '../stores/channel_store.jsx';
|
||||
import PostStore from '../stores/post_store.jsx';
|
||||
|
||||
import * as Utils from '../utils/utils.jsx';
|
||||
import * as Client from '../utils/client.jsx';
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import * as AsyncClient from '../utils/async_client.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
|
||||
import Constants from '../utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
|
||||
|
||||
@@ -27,27 +24,26 @@ export default class PostsViewContainer extends React.Component {
|
||||
this.onPostsChange = this.onPostsChange.bind(this);
|
||||
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
|
||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
||||
this.postsLoaded = this.postsLoaded.bind(this);
|
||||
this.postsLoadedFailure = this.postsLoadedFailure.bind(this);
|
||||
this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
|
||||
|
||||
const currentChannelId = ChannelStore.getCurrentId();
|
||||
const state = {
|
||||
scrollType: PostsView.SCROLL_TYPE_BOTTOM,
|
||||
scrollPost: null,
|
||||
numPostsToDisplay: Constants.POST_CHUNK_SIZE
|
||||
scrollPost: null
|
||||
};
|
||||
if (currentChannelId) {
|
||||
Object.assign(state, {
|
||||
currentChannelIndex: 0,
|
||||
channels: [currentChannelId],
|
||||
postLists: [this.getChannelPosts(currentChannelId)]
|
||||
postLists: [this.getChannelPosts(currentChannelId)],
|
||||
atTop: [PostStore.getVisibilityAtTop(currentChannelId)]
|
||||
});
|
||||
} else {
|
||||
Object.assign(state, {
|
||||
currentChannelIndex: null,
|
||||
channels: [],
|
||||
postLists: []
|
||||
postLists: [],
|
||||
atTop: []
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,24 +74,21 @@ export default class PostsViewContainer extends React.Component {
|
||||
});
|
||||
break;
|
||||
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
|
||||
this.setState({scrollType: PostsView.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]) {
|
||||
// Dirty hack
|
||||
this.forceUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
PostStore.clearUnseenDeletedPosts(channelId);
|
||||
|
||||
let lastViewed = Number.MAX_VALUE;
|
||||
const member = ChannelStore.getMember(channelId);
|
||||
if (member != null) {
|
||||
@@ -107,115 +100,45 @@ export default class PostsViewContainer extends React.Component {
|
||||
newIndex = channels.length;
|
||||
channels.push(channelId);
|
||||
postLists[newIndex] = this.getChannelPosts(channelId);
|
||||
atTop[newIndex] = PostStore.getVisibilityAtTop(channelId);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
currentChannelIndex: newIndex,
|
||||
currentLastViewed: lastViewed,
|
||||
scrollType: PostsView.SCROLL_TYPE_NEW_MESSAGE,
|
||||
channels,
|
||||
postLists});
|
||||
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});
|
||||
this.setState({channels, postLists, atTop});
|
||||
}
|
||||
onPostsChange() {
|
||||
const channels = this.state.channels;
|
||||
const postLists = this.state.postLists.slice();
|
||||
const newPostsView = this.getChannelPosts(channels[this.state.currentChannelIndex]);
|
||||
const atTop = this.state.atTop.slice();
|
||||
const currentChannelId = channels[this.state.currentChannelIndex];
|
||||
const newPostsView = this.getChannelPosts(currentChannelId);
|
||||
|
||||
postLists[this.state.currentChannelIndex] = newPostsView;
|
||||
this.setState({postLists});
|
||||
atTop[this.state.currentChannelIndex] = PostStore.getVisibilityAtTop(currentChannelId);
|
||||
this.setState({postLists, atTop});
|
||||
}
|
||||
getChannelPosts(id) {
|
||||
const postList = PostStore.getPosts(id);
|
||||
|
||||
if (postList != null) {
|
||||
const deletedPosts = PostStore.getUnseenDeletedPosts(id);
|
||||
|
||||
if (deletedPosts && Object.keys(deletedPosts).length > 0) {
|
||||
for (const pid in deletedPosts) {
|
||||
if (deletedPosts.hasOwnProperty(pid)) {
|
||||
postList.posts[pid] = deletedPosts[pid];
|
||||
postList.order.unshift(pid);
|
||||
}
|
||||
}
|
||||
|
||||
postList.order.sort((a, b) => {
|
||||
if (postList.posts[a].create_at > postList.posts[b].create_at) {
|
||||
return -1;
|
||||
}
|
||||
if (postList.posts[a].create_at < postList.posts[b].create_at) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
const pendingPostList = PostStore.getPendingPosts(id);
|
||||
|
||||
if (pendingPostList) {
|
||||
postList.order = pendingPostList.order.concat(postList.order);
|
||||
for (const ppid in pendingPostList.posts) {
|
||||
if (pendingPostList.posts.hasOwnProperty(ppid)) {
|
||||
postList.posts[ppid] = pendingPostList.posts[ppid];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return postList;
|
||||
return PostStore.getVisiblePosts(id);
|
||||
}
|
||||
loadMorePostsTop() {
|
||||
const postLists = this.state.postLists;
|
||||
const channels = this.state.channels;
|
||||
const currentChannelId = channels[this.state.currentChannelIndex];
|
||||
const currentPostList = postLists[this.state.currentChannelIndex];
|
||||
|
||||
this.setState({numPostsToDisplay: this.state.numPostsToDisplay + Constants.POST_CHUNK_SIZE});
|
||||
|
||||
Client.getPostsPage(
|
||||
currentChannelId,
|
||||
currentPostList.order.length,
|
||||
Constants.POST_CHUNK_SIZE,
|
||||
this.postsLoaded,
|
||||
this.postsLoadedFailure
|
||||
);
|
||||
}
|
||||
postsLoaded(data) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.order.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postLists = this.state.postLists;
|
||||
const currentPostList = postLists[this.state.currentChannelIndex];
|
||||
const channels = this.state.channels;
|
||||
const currentChannelId = channels[this.state.currentChannelIndex];
|
||||
|
||||
var newPostList = {};
|
||||
newPostList.posts = Object.assign(currentPostList.posts, data.posts);
|
||||
newPostList.order = currentPostList.order.concat(data.order);
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POSTS,
|
||||
id: currentChannelId,
|
||||
post_list: newPostList
|
||||
});
|
||||
|
||||
Client.getProfiles();
|
||||
}
|
||||
postsLoadedFailure(err) {
|
||||
AsyncClient.dispatchError(err, 'getPosts');
|
||||
EventHelpers.emitLoadMorePostsEvent();
|
||||
}
|
||||
handlePostsViewScroll(atBottom) {
|
||||
if (atBottom) {
|
||||
@@ -246,15 +169,17 @@ export default class PostsViewContainer extends React.Component {
|
||||
isActive={isActive}
|
||||
postList={postLists[i]}
|
||||
scrollType={this.state.scrollType}
|
||||
scrollPost={this.state.scrollPost}
|
||||
scrollPostId={this.state.scrollPost}
|
||||
postViewScrolled={this.handlePostsViewScroll}
|
||||
loadMorePostsTopClicked={this.loadMorePostsTop}
|
||||
numPostsToDisplay={this.state.numPostsToDisplay}
|
||||
loadMorePostsBottomClicked={() => {}}
|
||||
showMoreMessagesTop={!this.state.atTop[this.state.currentChannelIndex]}
|
||||
showMoreMessagesBottom={false}
|
||||
introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null}
|
||||
messageSeparatorTime={this.state.currentLastViewed}
|
||||
/>
|
||||
);
|
||||
if ((!postLists[i] || !channel) && isActive) {
|
||||
if (!postLists[i] && isActive) {
|
||||
postListCtls.push(
|
||||
<LoadingScreen
|
||||
position='absolute'
|
||||
|
||||
@@ -94,7 +94,7 @@ export default class RhsThread extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id);
|
||||
var currentPosts = PostStore.getVisiblePosts(currentSelected.posts[currentSelected.order[0]].channel_id);
|
||||
|
||||
if (!currentPosts || currentPosts.order.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SearchStore from '../stores/search_store.jsx';
|
||||
import ChannelStore from '../stores/channel_store.jsx';
|
||||
import UserStore from '../stores/user_store.jsx';
|
||||
import UserProfile from './user_profile.jsx';
|
||||
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
|
||||
import * as utils from '../utils/utils.jsx';
|
||||
import * as client from '../utils/client.jsx';
|
||||
import * as AsyncClient from '../utils/async_client.jsx';
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import Constants from '../utils/constants.jsx';
|
||||
import * as TextFormatting from '../utils/text_formatting.jsx';
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
export default class SearchResultsItem extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -23,32 +18,7 @@ export default class SearchResultsItem extends React.Component {
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var self = this;
|
||||
|
||||
client.getPost(
|
||||
this.props.post.channel_id,
|
||||
this.props.post.id,
|
||||
function success(data) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
post_list: data,
|
||||
from_search: SearchStore.getSearchTerm()
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_SEARCH,
|
||||
results: null,
|
||||
is_mention_search: self.props.isMentionSearch
|
||||
});
|
||||
},
|
||||
function success(err) {
|
||||
AsyncClient.dispatchError(err, 'getPost');
|
||||
}
|
||||
);
|
||||
|
||||
var postChannel = ChannelStore.get(this.props.post.channel_id);
|
||||
|
||||
utils.switchChannel(postChannel);
|
||||
EventHelpers.emitPostFocusEvent(this.props.post.id);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -201,10 +201,6 @@ export default class Sidebar extends React.Component {
|
||||
});
|
||||
}
|
||||
updateScrollbar() {
|
||||
if (this.state.windowWidth > 768) {
|
||||
$('.nav-pills__container').perfectScrollbar();
|
||||
$('.nav-pills__container').perfectScrollbar('update');
|
||||
}
|
||||
}
|
||||
onChange() {
|
||||
this.setState(this.getStateFromStores());
|
||||
|
||||
Ссылка в новой задаче
Block a user