summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/center_panel.jsx33
-rw-r--r--web/react/components/channel_invite_modal.jsx13
-rw-r--r--web/react/components/channel_loader.jsx4
-rw-r--r--web/react/components/channel_members_modal.jsx14
-rw-r--r--web/react/components/create_post.jsx11
-rw-r--r--web/react/components/post.jsx34
-rw-r--r--web/react/components/post_focus_view.jsx110
-rw-r--r--web/react/components/post_info.jsx69
-rw-r--r--web/react/components/posts_view.jsx91
-rw-r--r--web/react/components/posts_view_container.jsx129
-rw-r--r--web/react/components/rhs_thread.jsx2
-rw-r--r--web/react/components/search_results_item.jsx34
-rw-r--r--web/react/components/sidebar.jsx4
13 files changed, 348 insertions, 200 deletions
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index c2ecf4fa2..3c6a36ad4 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -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>
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 6d3203ae5..0518ccb86 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -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});
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index e29c659c7..c8f1196a8 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -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
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index 08ad95091..f07fc166a 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -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});
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 0a2979e21..f7f63fb92 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -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 = {};
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 5b61c711c..278261e22 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -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
};
diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx
new file mode 100644
index 000000000..5c6ad6c28
--- /dev/null
+++ b/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 = {
+};
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index edd63decd..bc6e8470d 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -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>
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 5b36ecbc5..5e374b877 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -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
};
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index c71ef401e..367d3687e 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -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'
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 4f453f76b..61f138539 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -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;
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 2202831a3..da422fe1b 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -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() {
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 77d222436..30422ff7d 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -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());