From 9e8cd937908d5d2e730e94f761d6533eb2d95e28 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Wed, 18 Nov 2015 17:29:06 -0500 Subject: Implementing Permalinks and jumping to post from search. Performance improvements. --- web/react/components/center_panel.jsx | 33 +- web/react/components/channel_invite_modal.jsx | 13 +- web/react/components/channel_loader.jsx | 4 +- web/react/components/channel_members_modal.jsx | 14 +- web/react/components/create_post.jsx | 11 +- web/react/components/post.jsx | 34 +- web/react/components/post_focus_view.jsx | 110 ++++++ web/react/components/post_info.jsx | 69 +++- web/react/components/posts_view.jsx | 91 +++-- web/react/components/posts_view_container.jsx | 129 ++----- web/react/components/rhs_thread.jsx | 2 +- web/react/components/search_results_item.jsx | 34 +- web/react/components/sidebar.jsx | 4 - web/react/dispatcher/event_helpers.jsx | 83 ++++ web/react/pages/channel.jsx | 18 +- web/react/pages/home.jsx | 3 +- web/react/stores/channel_store.jsx | 86 +++-- web/react/stores/post_store.jsx | 509 ++++++++++++++++--------- web/react/stores/socket_store.jsx | 34 +- web/react/utils/async_client.jsx | 346 +++++++++-------- web/react/utils/client.jsx | 51 ++- web/react/utils/constants.jsx | 6 + web/react/utils/utils.jsx | 13 +- 23 files changed, 1095 insertions(+), 602 deletions(-) create mode 100644 web/react/components/post_focus_view.jsx create mode 100644 web/react/dispatcher/event_helpers.jsx (limited to 'web/react') 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 = ; + createPost = null; + } else if (this.state.showPostFocus) { + postsContainer = ; + createPost = null; } else { postsContainer = ; + createPost = ( +
+ +
+ ); } return ( @@ -62,12 +86,7 @@ export default class CenterPanel extends React.Component { {postsContainer} -
- -
+ {createPost} 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 {
{profilePic}
@@ -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 ( +
+

{'Beginning of Channel'}

+
+ ); + } + + render() { + const postsToHighlight = {}; + postsToHighlight[this.state.scrollPostId] = true; + + return ( +
+ +
+ ); + } +} +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 {
); } + 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 ? (
{'Copy '}
) : 'Copy'; + const permalinkOverlay = ( + +
+ + +
+
+ ); + return (
  • @@ -152,6 +206,15 @@ export default class PostInfo extends React.Component {
  • {comments} + + + +
    {dropdown}
    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 = ( 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 = ( ); } else { - moreMessages = this.props.introText; + moreMessagesTop = this.props.introText; + } + + // Give option to load more posts at bottom if nessisary + if (this.props.showMoreMessagesBottom) { + moreMessagesBottom = ( + + {'Load more messages'} + + ); + } 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}
@@ -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( 768) { - $('.nav-pills__container').perfectScrollbar(); - $('.nav-pills__container').perfectScrollbar('update'); - } } onChange() { this.setState(this.getStateFromStores()); diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx new file mode 100644 index 000000000..85329b38f --- /dev/null +++ b/web/react/dispatcher/event_helpers.jsx @@ -0,0 +1,83 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import PostStore from '../stores/post_store.jsx'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +import * as AsyncClient from '../utils/async_client.jsx'; +import * as Client from '../utils/client.jsx'; + +export function emitChannelClickEvent(channel) { + AsyncClient.getChannels(); + AsyncClient.getChannelExtraInfo(); + AsyncClient.updateLastViewedAt(); + AsyncClient.getPosts(channel.id); + + AppDispatcher.handleViewAction({ + type: ActionTypes.CLICK_CHANNEL, + name: channel.name, + id: channel.id + }); +} + +export function emitPostFocusEvent(postId) { + Client.getPostById( + postId, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_FOCUSED_POST, + postId, + post_list: data + }); + + AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + } + ); +} + +export function emitLoadMorePostsEvent() { + const id = ChannelStore.getCurrentId(); + loadMorePostsTop(id); +} + +export function emitLoadMorePostsFocusedTopEvent() { + const id = PostStore.getFocusedPostId(); + loadMorePostsTop(id); +} + +export function loadMorePostsTop(id) { + const earliestPostId = PostStore.getEarliestPost(id).id; + if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) { + AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE); + } +} + +export function emitLoadMorePostsFocusedBottomEvent() { + const id = PostStore.getFocusedPostId(); + const latestPostId = PostStore.getLatestPost(id).id; + AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); +} + +export function emitPostRecievedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST, + post + }); +} + +export function emitUserPostedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.CREATE_POST, + post + }); +} + +export function emitPostDeletedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.POST_DELETED, + post + }); +} diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 126942e65..5cc1be741 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import ChannelView from '../components/channel_view.jsx'; import ChannelLoader from '../components/channel_loader.jsx'; import ErrorBar from '../components/error_bar.jsx'; @@ -23,15 +22,14 @@ import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx import InviteMemberModal from '../components/invite_member_modal.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import Constants from '../utils/constants.jsx'; -var ActionTypes = Constants.ActionTypes; - -function setupChannelPage(props) { - AppDispatcher.handleViewAction({ - type: ActionTypes.CLICK_CHANNEL, - name: props.ChannelName, - id: props.ChannelId - }); +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; + +function setupChannelPage(props, team, channel) { + if (props.PostId === '') { + EventHelpers.emitChannelClickEvent(channel); + } else { + EventHelpers.emitPostFocusEvent(props.PostId); + } AsyncClient.getAllPreferences(); diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx index 2c1edaa3a..ff81c4994 100644 --- a/web/react/pages/home.jsx +++ b/web/react/pages/home.jsx @@ -1,12 +1,11 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ChannelStore from '../stores/channel_store.jsx'; import TeamStore from '../stores/team_store.jsx'; import Constants from '../utils/constants.jsx'; function setupHomePage() { - var last = ChannelStore.getLastVisitedName(); + var last = null; if (last == null || last.length === 0) { window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL; } else { diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index 1d481ada4..dec4926f5 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -8,8 +8,6 @@ var Utils; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import BrowserStore from '../stores/browser_store.jsx'; - const CHANGE_EVENT = 'change'; const LEAVE_EVENT = 'leave'; const MORE_CHANGE_EVENT = 'change'; @@ -21,7 +19,38 @@ class ChannelStoreClass extends EventEmitter { this.setMaxListeners(11); + this.emitChange = this.emitChange.bind(this); + this.addChangeListener = this.addChangeListener.bind(this); + this.removeChangeListener = this.removeChangeListener.bind(this); + this.emitMoreChange = this.emitMoreChange.bind(this); + this.addMoreChangeListener = this.addMoreChangeListener.bind(this); + this.removeMoreChangeListener = this.removeMoreChangeListener.bind(this); + this.emitExtraInfoChange = this.emitExtraInfoChange.bind(this); + this.addExtraInfoChangeListener = this.addExtraInfoChangeListener.bind(this); + this.removeExtraInfoChangeListener = this.removeExtraInfoChangeListener.bind(this); + this.emitLeave = this.emitLeave.bind(this); + this.addLeaveListener = this.addLeaveListener.bind(this); + this.removeLeaveListener = this.removeLeaveListener.bind(this); + this.findFirstBy = this.findFirstBy.bind(this); + this.get = this.get.bind(this); + this.getMember = this.getMember.bind(this); + this.getByName = this.getByName.bind(this); + this.pSetPostMode = this.pSetPostMode.bind(this); + this.getPostMode = this.getPostMode.bind(this); + this.currentId = null; + this.postMode = this.POST_MODE_CHANNEL; + this.channels = []; + this.channelMembers = {}; + this.moreChannels = {}; + this.moreChannels.loading = true; + this.extraInfos = {}; + } + get POST_MODE_CHANNEL() { + return 1; + } + get POST_MODE_FOCUS() { + return 2; } emitChange() { this.emit(CHANGE_EVENT); @@ -90,16 +119,6 @@ class ChannelStoreClass extends EventEmitter { setCurrentId(id) { this.currentId = id; } - setLastVisitedName(name) { - if (name == null) { - BrowserStore.removeItem('last_visited_name'); - } else { - BrowserStore.setItem('last_visited_name', name); - } - } - getLastVisitedName() { - return BrowserStore.getItem('last_visited_name'); - } resetCounts(id) { var cm = this.pGetChannelMembers(); for (var cmid in cm) { @@ -192,10 +211,10 @@ class ChannelStoreClass extends EventEmitter { this.pStoreChannels(channels); } pStoreChannels(channels) { - BrowserStore.setItem('channels', channels); + this.channels = channels; } pGetChannels() { - return BrowserStore.getItem('channels', []); + return this.channels; } pStoreChannelMember(channelMember) { var members = this.pGetChannelMembers(); @@ -203,49 +222,58 @@ class ChannelStoreClass extends EventEmitter { this.pStoreChannelMembers(members); } pStoreChannelMembers(channelMembers) { - BrowserStore.setItem('channel_members', channelMembers); + this.channelMembers = channelMembers; } pGetChannelMembers() { - return BrowserStore.getItem('channel_members', {}); + return this.channelMembers; } pStoreMoreChannels(channels) { - BrowserStore.setItem('more_channels', channels); + this.moreChannels = channels; } pGetMoreChannels() { - var channels = BrowserStore.getItem('more_channels'); - - if (channels == null) { - channels = {}; - channels.loading = true; - } - - return channels; + return this.moreChannels; } pStoreExtraInfos(extraInfos) { - BrowserStore.setItem('extra_infos', extraInfos); + this.extraInfos = extraInfos; } pGetExtraInfos() { - return BrowserStore.getItem('extra_infos', {}); + return this.extraInfos; } isDefault(channel) { return channel.name === Constants.DEFAULT_CHANNEL; } + + pSetPostMode(mode) { + this.postMode = mode; + } + + getPostMode() { + return this.postMode; + } } var ChannelStore = new ChannelStoreClass(); -ChannelStore.dispatchToken = AppDispatcher.register(function handleAction(payload) { +ChannelStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; var currentId; switch (action.type) { case ActionTypes.CLICK_CHANNEL: ChannelStore.setCurrentId(action.id); - ChannelStore.setLastVisitedName(action.name); ChannelStore.resetCounts(action.id); + ChannelStore.pSetPostMode(ChannelStore.POST_MODE_CHANNEL); ChannelStore.emitChange(); break; + case ActionTypes.RECIEVED_FOCUSED_POST: { + const post = action.post_list.posts[action.postId]; + ChannelStore.setCurrentId(post.channel_id); + ChannelStore.pSetPostMode(ChannelStore.POST_MODE_FOCUS); + ChannelStore.emitChange(); + break; + } + case ActionTypes.RECIEVED_CHANNELS: ChannelStore.pStoreChannels(action.channels); ChannelStore.pStoreChannelMembers(action.members); diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index ec01eef18..c76560c25 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -12,9 +12,10 @@ import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; const CHANGE_EVENT = 'change'; -const SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; +const FOCUSED_POST_CHANGE = 'focused_post_change'; const EDIT_POST_EVENT = 'edit_post'; const POSTS_VIEW_JUMP_EVENT = 'post_list_jump'; +const SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; class PostStoreClass extends EventEmitter { constructor() { @@ -24,10 +25,6 @@ class PostStoreClass extends EventEmitter { this.addChangeListener = this.addChangeListener.bind(this); this.removeChangeListener = this.removeChangeListener.bind(this); - this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this); - this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this); - this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this); - this.emitEditPost = this.emitEditPost.bind(this); this.addEditPostListener = this.addEditPostListener.bind(this); this.removeEditPostListener = this.removeEditPostListner.bind(this); @@ -36,27 +33,49 @@ class PostStoreClass extends EventEmitter { this.addPostsViewJumpListener = this.addPostsViewJumpListener.bind(this); this.removePostsViewJumpListener = this.removePostsViewJumpListener.bind(this); - this.getCurrentPosts = this.getCurrentPosts.bind(this); + this.emitPostFocused = this.emitPostFocused.bind(this); + this.addPostFocusedListener = this.addPostFocusedListener.bind(this); + this.removePostFocusedListener = this.removePostFocusedListener.bind(this); + + this.makePostsInfo = this.makePostsInfo.bind(this); + + this.getAllPosts = this.getAllPosts.bind(this); + this.getEarliestPost = this.getEarliestPost.bind(this); + this.getLatestPost = this.getLatestPost.bind(this); + this.getVisiblePosts = this.getVisiblePosts.bind(this); + this.getVisibilityAtTop = this.getVisibilityAtTop.bind(this); + this.getVisibilityAtBottom = this.getVisibilityAtBottom.bind(this); + this.requestVisibilityIncrease = this.requestVisibilityIncrease.bind(this); + this.getFocusedPostId = this.getFocusedPostId.bind(this); + this.storePosts = this.storePosts.bind(this); - this.pStorePosts = this.pStorePosts.bind(this); - this.getPosts = this.getPosts.bind(this); - this.getPost = this.getPost.bind(this); this.storePost = this.storePost.bind(this); - this.pStorePost = this.pStorePost.bind(this); + this.storeFocusedPost = this.storeFocusedPost.bind(this); + this.checkBounds = this.checkBounds.bind(this); + + this.clearFocusedPost = this.clearFocusedPost.bind(this); + this.clearChannelVisibility = this.clearChannelVisibility.bind(this); + this.removePost = this.removePost.bind(this); - this.storePendingPost = this.storePendingPost.bind(this); - this.pStorePendingPosts = this.pStorePendingPosts.bind(this); + this.getPendingPosts = this.getPendingPosts.bind(this); - this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this); - this.storeUnseenDeletedPosts = this.storeUnseenDeletedPosts.bind(this); - this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this); - this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this); + this.storePendingPost = this.storePendingPost.bind(this); this.removePendingPost = this.removePendingPost.bind(this); - this.pRemovePendingPost = this.pRemovePendingPost.bind(this); this.clearPendingPosts = this.clearPendingPosts.bind(this); this.updatePendingPost = this.updatePendingPost.bind(this); + + this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this); + this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this); + this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this); + + // These functions are bad and work should be done to remove this system when the RHS dies this.storeSelectedPost = this.storeSelectedPost.bind(this); this.getSelectedPost = this.getSelectedPost.bind(this); + this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this); + this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this); + this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this); + this.selectedPost = null; + this.getEmptyDraft = this.getEmptyDraft.bind(this); this.storeCurrentDraft = this.storeCurrentDraft.bind(this); this.getCurrentDraft = this.getCurrentDraft.bind(this); @@ -70,6 +89,9 @@ class PostStoreClass extends EventEmitter { this.getLatestUpdate = this.getLatestUpdate.bind(this); this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this); this.getCommentCount = this.getCommentCount.bind(this); + + this.postsInfo = {}; + this.currentFocusedPostId = null; } emitChange() { this.emit(CHANGE_EVENT); @@ -83,16 +105,16 @@ class PostStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT, callback); } - emitSelectedPostChange(fromSearch) { - this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); + emitPostFocused() { + this.emit(FOCUSED_POST_CHANGE); } - addSelectedPostChangeListener(callback) { - this.on(SELECTED_POST_CHANGE_EVENT, callback); + addPostFocusedListener(callback) { + this.on(FOCUSED_POST_CHANGE, callback); } - removeSelectedPostChangeListener(callback) { - this.removeListener(SELECTED_POST_CHANGE_EVENT, callback); + removePostFocusedListener(callback) { + this.removeListener(FOCUSED_POST_CHANGE, callback); } emitEditPost(post) { @@ -131,104 +153,157 @@ class PostStoreClass extends EventEmitter { this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null); } - getCurrentPosts() { - var currentId = ChannelStore.getCurrentId(); + // All this does is makes sure the postsInfo is not null for the specified channel + makePostsInfo(id) { + if (!this.postsInfo.hasOwnProperty(id)) { + this.postsInfo[id] = {}; + } + } - if (currentId != null) { - return this.getPosts(currentId); + getAllPosts(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return Object.assign({}, this.postsInfo[id].postList); } + return null; } - storePosts(channelId, newPostsView) { - if (isPostListNull(newPostsView)) { + + getEarliestPost(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[this.postsInfo[id].postList.order.length - 1]]; + } + + return null; + } + + getLatestPost(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[0]]; + } + + return null; + } + + getVisiblePosts(id) { + if (this.postsInfo.hasOwnProperty(id) && this.postsInfo[id].hasOwnProperty('postList')) { + const postList = JSON.parse(JSON.stringify(this.postsInfo[id].postList)); + + // Only limit visibility if we are not focused on a post + if (this.currentFocusedPostId === null) { + postList.order = postList.order.slice(0, this.postsInfo[id].endVisible); + } + + // Add pending posts + if (this.postsInfo[id].hasOwnProperty('pendingPosts')) { + Object.assign(postList.posts, this.postsInfo[id].pendingPosts.posts); + postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order); + } + + // Add delteted posts + if (this.postsInfo[id].hasOwnProperty('deletedPosts')) { + Object.assign(postList.posts, this.postsInfo[id].deletedPosts); + + for (const postID in this.postsInfo[id].deletedPosts) { + if (this.postsInfo[id].deletedPosts.hasOwnProperty(postID)) { + postList.order.push(postID); + } + } + + // Merge would be faster + 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; + }); + } + + return postList; + } + + return null; + } + + getVisibilityAtTop(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].atTop && this.postsInfo[id].endVisible >= this.postsInfo[id].postList.order.length; + } + + return false; + } + + getVisibilityAtBottom(id) { + if (this.postsInfo.hasOwnProperty(id)) { + return this.postsInfo[id].atBottom; + } + + return false; + } + + // Returns true if posts need to be fetched + requestVisibilityIncrease(id, ammount) { + const endVisible = this.postsInfo[id].endVisible; + const postList = this.postsInfo[id].postList; + if (this.getVisibilityAtTop(id)) { + return false; + } + this.postsInfo[id].endVisible += ammount; + this.emitChange(); + return endVisible + ammount > postList.order.length; + } + + getFocusedPostId() { + return this.currentFocusedPostId; + } + + storePosts(id, newPosts) { + if (isPostListNull(newPosts)) { return; } - var postList = makePostListNonNull(this.getPosts(channelId)); + const combinedPosts = makePostListNonNull(this.getAllPosts(id)); - for (const pid in newPostsView.posts) { - if (newPostsView.posts.hasOwnProperty(pid)) { - const np = newPostsView.posts[pid]; + for (const pid in newPosts.posts) { + if (newPosts.posts.hasOwnProperty(pid)) { + const np = newPosts.posts[pid]; if (np.delete_at === 0) { - postList.posts[pid] = np; - if (postList.order.indexOf(pid) === -1) { - postList.order.push(pid); + combinedPosts.posts[pid] = np; + if (combinedPosts.order.indexOf(pid) === -1) { + combinedPosts.order.push(pid); } } else { - if (pid in postList.posts) { - delete postList.posts[pid]; + if (pid in combinedPosts.posts) { + Reflect.deleteProperty(combinedPosts.posts, pid); } - const index = postList.order.indexOf(pid); + const index = combinedPosts.order.indexOf(pid); if (index !== -1) { - postList.order.splice(index, 1); + combinedPosts.order.splice(index, 1); } } } } - postList.order.sort((a, b) => { - if (postList.posts[a].create_at > postList.posts[b].create_at) { + combinedPosts.order.sort((a, b) => { + if (combinedPosts.posts[a].create_at > combinedPosts.posts[b].create_at) { return -1; } - if (postList.posts[a].create_at < postList.posts[b].create_at) { + if (combinedPosts.posts[a].create_at < combinedPosts.posts[b].create_at) { return 1; } return 0; }); - var latestUpdate = 0; - for (var pid in postList.posts) { - if (postList.posts[pid].update_at > latestUpdate) { - latestUpdate = postList.posts[pid].update_at; - } - } - - this.storeLatestUpdate(channelId, latestUpdate); - this.pStorePosts(channelId, postList); - this.emitChange(); - } - pStorePosts(channelId, posts) { - BrowserStore.setItem('posts_' + channelId, posts); - } - getPosts(channelId) { - return BrowserStore.getItem('posts_' + channelId); + this.makePostsInfo(id); + this.postsInfo[id].postList = combinedPosts; } - getPost(channelId, postId) { - return this.getPosts(channelId).posts[postId]; - } - getCurrentUsersLatestPost(channelId, rootId) { - const userId = UserStore.getCurrentId(); - var postList = makePostListNonNull(this.getPosts(channelId)); - var i = 0; - var len = postList.order.length; - var lastPost = null; - for (i; i < len; i++) { - let post = postList.posts[postList.order[i]]; - if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) { - if (rootId) { - if (post.root_id === rootId || post.id === rootId) { - lastPost = post; - break; - } - } else { - lastPost = post; - break; - } - } - } - - return lastPost; - } storePost(post) { - this.pStorePost(post); - this.emitChange(); - } - pStorePost(post) { - var postList = this.getPosts(post.channel_id); - postList = makePostListNonNull(postList); + const postList = makePostListNonNull(this.getAllPosts(post.channel_id)); if (post.pending_post_id !== '') { this.removePendingPost(post.channel_id, post.pending_post_id); @@ -241,65 +316,117 @@ class PostStoreClass extends EventEmitter { postList.order.unshift(post.id); } - this.pStorePosts(post.channel_id, postList); + this.makePostsInfo(post.channel_id); + this.postsInfo[post.channel_id].postList = postList; + } + + storeFocusedPost(postId, postList) { + const focusedPost = postList.posts[postId]; + if (!focusedPost) { + return; + } + this.currentFocusedPostId = postId; + this.storePosts(postId, postList); + } + + checkBounds(id, numRequested, postList, before) { + if (numRequested > postList.order.length) { + if (before) { + this.postsInfo[id].atTop = true; + } else { + this.postsInfo[id].atBottom = true; + } + } } - removePost(postId, channelId) { - var postList = this.getPosts(channelId); + + clearFocusedPost() { + if (this.currentFocusedPostId != null) { + Reflect.deleteProperty(this.postsInfo, this.currentFocusedPostId); + this.currentFocusedPostId = null; + } + } + + clearChannelVisibility(id, atBottom) { + this.makePostsInfo(id); + this.postsInfo[id].endVisible = Constants.POST_CHUNK_SIZE; + this.postsInfo[id].atTop = false; + this.postsInfo[id].atBottom = atBottom; + } + + removePost(post) { + const channelId = post.channel_id; + this.makePostsInfo(channelId); + const postList = this.postsInfo[channelId].postList; if (isPostListNull(postList)) { return; } - if (postId in postList.posts) { - delete postList.posts[postId]; + if (post.id in postList.posts) { + Reflect.deleteProperty(postList.posts, post.id); } - var index = postList.order.indexOf(postId); + const index = postList.order.indexOf(post.id); if (index !== -1) { postList.order.splice(index, 1); } - this.pStorePosts(channelId, postList); + this.postsInfo[channelId].postList = postList; } + + getPendingPosts(channelId) { + if (this.postsInfo.hasOwnProperty(channelId)) { + return this.postsInfo[channelId].pendingPosts; + } + + return null; + } + storePendingPost(post) { post.state = Constants.POST_LOADING; - var postList = this.getPendingPosts(post.channel_id); - postList = makePostListNonNull(postList); + const postList = makePostListNonNull(this.getPendingPosts(post.channel_id)); postList.posts[post.pending_post_id] = post; postList.order.unshift(post.pending_post_id); - this.pStorePendingPosts(post.channel_id, postList); + + this.makePostsInfo(post.channel_id); + this.postsInfo[post.channel_id].pendingPosts = postList; this.emitChange(); } - pStorePendingPosts(channelId, postList) { - var posts = postList.posts; - // sort failed posts to the bottom - postList.order.sort((a, b) => { - if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) { - return 1; - } - if (posts[a].state === Constants.POST_FAILED && posts[b].state === Constants.POST_LOADING) { - return -1; - } + removePendingPost(channelId, pendingPostId) { + const postList = makePostListNonNull(this.getPendingPosts(channelId)); - if (posts[a].create_at > posts[b].create_at) { - return -1; - } - if (posts[a].create_at < posts[b].create_at) { - return 1; - } + Reflect.deleteProperty(postList.posts, pendingPostId); + const index = postList.order.indexOf(pendingPostId); + if (index !== -1) { + postList.order.splice(index, 1); + } - return 0; - }); + this.postsInfo[channelId].pendingPosts = postList; + this.emitChange(); + } - BrowserStore.setGlobalItem('pending_posts_' + channelId, postList); + clearPendingPosts(channelId) { + if (this.postsInfo.hasOwnProperty(channelId)) { + Reflect.deleteProperty(this.postsInfo[channelId], 'pendingPosts'); + } } - getPendingPosts(channelId) { - return BrowserStore.getGlobalItem('pending_posts_' + channelId); + + updatePendingPost(post) { + const postList = makePostListNonNull(this.getPendingPosts(post.channel_id)); + + if (postList.order.indexOf(post.pending_post_id) === -1) { + return; + } + + postList.posts[post.pending_post_id] = post; + this.postsInfo[post.channel_id].pendingPosts = postList; + this.emitChange(); } + storeUnseenDeletedPost(post) { - var posts = this.getUnseenDeletedPosts(post.channel_id); + let posts = this.getUnseenDeletedPosts(post.channel_id); if (!posts) { posts = {}; @@ -310,58 +437,68 @@ class PostStoreClass extends EventEmitter { post.filenames = []; posts[post.id] = post; - this.storeUnseenDeletedPosts(post.channel_id, posts); - } - storeUnseenDeletedPosts(channelId, posts) { - BrowserStore.setItem('deleted_posts_' + channelId, posts); + this.postsInfo[post.channel_id].deletedPosts = posts; } + getUnseenDeletedPosts(channelId) { - return BrowserStore.getItem('deleted_posts_' + channelId); + if (this.postsInfo.hasOwnProperty(channelId)) { + return this.postsInfo[channelId].deletedPosts; + } + + return null; } + clearUnseenDeletedPosts(channelId) { - BrowserStore.setItem('deleted_posts_' + channelId, {}); + if (this.postsInfo.hasOwnProperty(channelId)) { + Reflect.deleteProperty(this.postsInfo[channelId], 'deletedPosts'); + } } - removePendingPost(channelId, pendingPostId) { - this.pRemovePendingPost(channelId, pendingPostId); - this.emitChange(); + + storeSelectedPost(postList) { + this.selectedPost = postList; } - pRemovePendingPost(channelId, pendingPostId) { - var postList = this.getPendingPosts(channelId); - postList = makePostListNonNull(postList); - if (pendingPostId in postList.posts) { - delete postList.posts[pendingPostId]; - } - var index = postList.order.indexOf(pendingPostId); - if (index !== -1) { - postList.order.splice(index, 1); - } + getSelectedPost() { + return this.selectedPost; + } - this.pStorePendingPosts(channelId, postList); + emitSelectedPostChange(fromSearch) { + this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); } - clearPendingPosts() { - BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => { - BrowserStore.removeItem(key); - }); + + addSelectedPostChangeListener(callback) { + this.on(SELECTED_POST_CHANGE_EVENT, callback); } - updatePendingPost(post) { - var postList = this.getPendingPosts(post.channel_id); - postList = makePostListNonNull(postList); - if (postList.order.indexOf(post.pending_post_id) === -1) { - return; + removeSelectedPostChangeListener(callback) { + this.removeListener(SELECTED_POST_CHANGE_EVENT, callback); + } + + getCurrentUsersLatestPost(channelId, rootId) { + const userId = UserStore.getCurrentId(); + var postList = makePostListNonNull(this.getAllPosts(channelId)); + var i = 0; + var len = postList.order.length; + var lastPost = null; + + for (i; i < len; i++) { + const post = postList.posts[postList.order[i]]; + if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) { + if (rootId) { + if (post.root_id === rootId || post.id === rootId) { + lastPost = post; + break; + } + } else { + lastPost = post; + break; + } + } } - postList.posts[post.pending_post_id] = post; - this.pStorePendingPosts(post.channel_id, postList); - this.emitChange(); - } - storeSelectedPost(postList) { - BrowserStore.setItem('select_post', postList); - } - getSelectedPost() { - return BrowserStore.getItem('select_post'); + return lastPost; } + getEmptyDraft() { return {message: '', uploadsInProgress: [], previews: []}; } @@ -402,16 +539,23 @@ class PostStoreClass extends EventEmitter { }); } storeLatestUpdate(channelId, time) { - BrowserStore.setItem('latest_post_' + channelId, time); + if (!this.postsInfo.hasOwnProperty(channelId)) { + this.postsInfo[channelId] = {}; + } + this.postsInfo[channelId].latestPost = time; } getLatestUpdate(channelId) { - return BrowserStore.getItem('latest_post_' + channelId, 0); + if (this.postsInfo.hasOwnProperty(channelId) && this.postsInfo[channelId].hasOwnProperty('latestPost')) { + return this.postsInfo[channelId].latestPost; + } + + return 0; } getCommentCount(post) { const posts = this.getPosts(post.channel_id).posts; let commentCount = 0; - for (let id in posts) { + for (const id in posts) { if (posts.hasOwnProperty(id)) { if (posts[id].root_id === post.id) { commentCount += 1; @@ -429,20 +573,45 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECIEVED_POSTS: - PostStore.storePosts(action.id, makePostListNonNull(action.post_list)); + case ActionTypes.RECIEVED_POSTS: { + const id = PostStore.currentFocusedPostId == null ? action.id : PostStore.currentFocusedPostId; + PostStore.checkBounds(id, action.numRequested, makePostListNonNull(action.post_list), action.before); + PostStore.storePosts(id, makePostListNonNull(action.post_list)); + PostStore.emitChange(); + break; + } + case ActionTypes.RECIEVED_FOCUSED_POST: + PostStore.clearChannelVisibility(action.postId, false); + PostStore.storeFocusedPost(action.postId, makePostListNonNull(action.post_list)); + PostStore.emitChange(); break; case ActionTypes.RECIEVED_POST: - PostStore.pStorePost(action.post); + PostStore.storePost(action.post); + PostStore.emitChange(); + break; + case ActionTypes.RECIEVED_EDIT_POST: + PostStore.emitEditPost(action); + PostStore.emitChange(); + break; + case ActionTypes.CLICK_CHANNEL: + PostStore.clearFocusedPost(); + PostStore.clearChannelVisibility(action.id, true); + PostStore.clearUnseenDeletedPosts(action.id); + break; + case ActionTypes.CREATE_POST: + PostStore.storePendingPost(action.post); + PostStore.storeDraft(action.post.channel_id, null); + PostStore.jumpPostsViewToBottom(); + break; + case ActionTypes.POST_DELETED: + PostStore.storeUnseenDeletedPost(action.post); + PostStore.removePost(action.post); PostStore.emitChange(); break; case ActionTypes.RECIEVED_POST_SELECTED: PostStore.storeSelectedPost(action.post_list); PostStore.emitSelectedPostChange(action.from_search); break; - case ActionTypes.RECIEVED_EDIT_POST: - PostStore.emitEditPost(action); - break; default: } }); diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index f2936c50a..2e0769cc4 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import UserStore from './user_store.jsx'; import PostStore from './post_store.jsx'; import ChannelStore from './channel_store.jsx'; @@ -11,9 +10,9 @@ import EventEmitter from 'events'; import * as Utils from '../utils/utils.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; const SocketEvents = Constants.SocketEvents; const CHANGE_EVENT = 'change'; @@ -91,10 +90,9 @@ class SocketStoreClass extends EventEmitter { }; conn.onmessage = (evt) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_MSG, - msg: JSON.parse(evt.data) - }); + const msg = JSON.parse(evt.data); + this.handleMessage(msg); + this.emitChange(msg); }; } } @@ -153,12 +151,12 @@ class SocketStoreClass extends EventEmitter { function handleNewPostEvent(msg) { // Store post const post = JSON.parse(msg.props.post); - PostStore.storePost(post); + EventHelpers.emitPostRecievedEvent(post); // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { if (window.isActive) { - AsyncClient.updateLastViewedAt(true); + AsyncClient.updateLastViewedAt(); } } else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) { AsyncClient.getChannel(msg.channel_id); @@ -237,20 +235,17 @@ function handlePostEditEvent(msg) { function handlePostDeleteEvent(msg) { const post = JSON.parse(msg.props.post); - - PostStore.storeUnseenDeletedPost(post); - PostStore.removePost(post, true); - PostStore.emitChange(); + EventHelpers.emitPostDeletedEvent(post); } function handleNewUserEvent() { AsyncClient.getProfiles(); - AsyncClient.getChannelExtraInfo(true); + AsyncClient.getChannelExtraInfo(); } function handleUserAddedEvent(msg) { if (ChannelStore.getCurrentId() === msg.channel_id) { - AsyncClient.getChannelExtraInfo(true); + AsyncClient.getChannelExtraInfo(); } if (UserStore.getCurrentId() === msg.user_id) { @@ -273,7 +268,7 @@ function handleUserRemovedEvent(msg) { $('#removed_from_channel').modal('show'); } } else if (ChannelStore.getCurrentId() === msg.channel_id) { - AsyncClient.getChannelExtraInfo(true); + AsyncClient.getChannelExtraInfo(); } } @@ -286,17 +281,12 @@ function handleChannelViewedEvent(msg) { var SocketStore = new SocketStoreClass(); -SocketStore.dispatchToken = AppDispatcher.register((payload) => { +/*SocketStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECIEVED_MSG: - SocketStore.handleMessage(action.msg); - SocketStore.emitChange(action.msg); - break; - default: } -}); + });*/ export default SocketStore; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index fac4cd009..8cf111d55 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -40,88 +40,42 @@ function isCallInProgress(callName) { return true; } -export function getChannels(force, updateLastViewed, checkVersion) { - var channels = ChannelStore.getAll(); - - if (channels.length === 0 || force) { - if (isCallInProgress('getChannels')) { - return; - } - - callTracker.getChannels = utils.getTimestamp(); +export function getChannels(checkVersion) { + if (isCallInProgress('getChannels')) { + return; + } - client.getChannels( - (data, textStatus, xhr) => { - callTracker.getChannels = 0; + callTracker.getChannels = utils.getTimestamp(); - if (checkVersion) { - var serverVersion = xhr.getResponseHeader('X-Version-ID'); + client.getChannels( + (data, textStatus, xhr) => { + callTracker.getChannels = 0; - if (!BrowserStore.getLastServerVersion()) { - BrowserStore.setLastServerVersion(serverVersion); - } + if (checkVersion) { + var serverVersion = xhr.getResponseHeader('X-Version-ID'); - if (serverVersion !== BrowserStore.getLastServerVersion()) { - BrowserStore.setLastServerVersion(serverVersion); - window.location.reload(true); - console.log('Detected version update refreshing the page'); //eslint-disable-line no-console - } + if (serverVersion !== BrowserStore.getLastServerVersion()) { + BrowserStore.setLastServerVersion(serverVersion); + window.location.reload(true); + console.log('Detected version update refreshing the page'); //eslint-disable-line no-console } - - if (xhr.status === 304 || !data) { - return; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_CHANNELS, - channels: data.channels, - members: data.members - }); - }, - (err) => { - callTracker.getChannels = 0; - dispatchError(err, 'getChannels'); } - ); - } else { - if (isCallInProgress('getChannelCounts')) { - return; - } - - callTracker.getChannelCounts = utils.getTimestamp(); - - client.getChannelCounts( - function getChannelCountsSuccess(data, textStatus, xhr) { - callTracker.getChannelCounts = 0; - - if (xhr.status === 304 || !data) { - return; - } - var countMap = data.counts; - var updateAtMap = data.update_times; - - for (var id in countMap) { - if ({}.hasOwnProperty.call(countMap, id)) { - var c = ChannelStore.get(id); - var count = countMap[id]; - var updateAt = updateAtMap[id]; - if (!c || c.total_msg_count !== count || updateAt > c.update_at) { - getChannel(id); - } - } - } - }, - function getChannelCountsFailure(err) { - callTracker.getChannelCounts = 0; - dispatchError(err, 'getChannelCounts'); + if (xhr.status === 304 || !data) { + return; } - ); - } - if (updateLastViewed && ChannelStore.getCurrentId() != null) { - updateLastViewedAt(); - } + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_CHANNELS, + channels: data.channels, + members: data.members + }); + }, + (err) => { + callTracker.getChannels = 0; + dispatchError(err, 'getChannels'); + } + ); } export function getChannel(id) { @@ -152,14 +106,14 @@ export function getChannel(id) { ); } -export function updateLastViewedAt(force) { +export function updateLastViewedAt() { const channelId = ChannelStore.getCurrentId(); if (channelId === null) { return; } - if (isCallInProgress(`updateLastViewed${channelId}`) && !force) { + if (isCallInProgress(`updateLastViewed${channelId}`)) { return; } @@ -205,40 +159,35 @@ export function getMoreChannels(force) { } } -export function getChannelExtraInfo(force) { - var channelId = ChannelStore.getCurrentId(); +export function getChannelExtraInfo() { + const channelId = ChannelStore.getCurrentId(); if (channelId != null) { if (isCallInProgress('getChannelExtraInfo_' + channelId)) { return; } - var minMembers = 0; - if (ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'D') { - minMembers = 1; - } - if (ChannelStore.getCurrentExtraInfo().members.length <= minMembers || force) { - callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp(); - client.getChannelExtraInfo( - channelId, - function getChannelExtraInfoSuccess(data, textStatus, xhr) { - callTracker['getChannelExtraInfo_' + channelId] = 0; - - if (xhr.status === 304 || !data) { - return; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO, - extra_info: data - }); - }, - function getChannelExtraInfoFailure(err) { - callTracker['getChannelExtraInfo_' + channelId] = 0; - dispatchError(err, 'getChannelExtraInfo'); + callTracker['getChannelExtraInfo_' + channelId] = utils.getTimestamp(); + + client.getChannelExtraInfo( + channelId, + (data, textStatus, xhr) => { + callTracker['getChannelExtraInfo_' + channelId] = 0; + + if (xhr.status === 304 || !data) { + return; } - ); - } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_CHANNEL_EXTRA_INFO, + extra_info: data + }); + }, + (err) => { + callTracker['getChannelExtraInfo_' + channelId] = 0; + dispatchError(err, 'getChannelExtraInfo'); + } + ); } } @@ -457,89 +406,92 @@ export function search(terms) { ); } -export function getPostsPage(force, id, maxPosts) { - if (PostStore.getCurrentPosts() == null || force) { - var channelId = id; +export function getPostsPage(id, maxPosts) { + let channelId = id; + if (channelId == null) { + channelId = ChannelStore.getCurrentId(); if (channelId == null) { - channelId = ChannelStore.getCurrentId(); - } - - if (isCallInProgress('getPostsPage_' + channelId)) { return; } + } - var postList = PostStore.getCurrentPosts(); + if (isCallInProgress('getPostsPage_' + channelId)) { + return; + } - var max = maxPosts; - if (max == null) { - max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS; - } + var postList = PostStore.getAllPosts(id); - // if we already have more than POST_CHUNK_SIZE posts, - // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE, - // with a max at maxPosts - var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE); - if (postList && postList.order.length > 0) { - numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE)); - } + var max = maxPosts; + if (max == null) { + max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS; + } + + // if we already have more than POST_CHUNK_SIZE posts, + // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE, + // with a max at maxPosts + var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE); + if (postList && postList.order.length > 0) { + numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE)); + } + + if (channelId != null) { + callTracker['getPostsPage_' + channelId] = utils.getTimestamp(); - if (channelId != null) { - callTracker['getPostsPage_' + channelId] = utils.getTimestamp(); - - client.getPostsPage( - channelId, - 0, - numPosts, - function getPostsPageSuccess(data, textStatus, xhr) { - if (xhr.status === 304 || !data) { - return; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_POSTS, - id: channelId, - post_list: data - }); - - getProfiles(); - }, - function getPostsPageFailure(err) { - dispatchError(err, 'getPostsPage'); - }, - function getPostsPageComplete() { - callTracker['getPostsPage_' + channelId] = 0; + client.getPostsPage( + channelId, + 0, + numPosts, + (data, textStatus, xhr) => { + if (xhr.status === 304 || !data) { + return; } - ); - } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POSTS, + id: channelId, + before: true, + numRequested: numPosts, + post_list: data + }); + + getProfiles(); + }, + (err) => { + dispatchError(err, 'getPostsPage'); + }, + () => { + callTracker['getPostsPage_' + channelId] = 0; + } + ); } } export function getPosts(id) { - var channelId = id; + let channelId = id; if (channelId == null) { - if (ChannelStore.getCurrentId() == null) { + channelId = ChannelStore.getCurrentId(); + if (channelId == null) { return; } - channelId = ChannelStore.getCurrentId(); } if (isCallInProgress('getPosts_' + channelId)) { return; } - if (PostStore.getCurrentPosts() == null) { - getPostsPage(true, id, Constants.POST_CHUNK_SIZE); + if (PostStore.getAllPosts(channelId) == null) { + getPostsPage(channelId, Constants.POST_CHUNK_SIZE); return; } - var latestUpdate = PostStore.getLatestUpdate(channelId); + const latestUpdate = PostStore.getLatestUpdate(channelId); callTracker['getPosts_' + channelId] = utils.getTimestamp(); client.getPosts( channelId, latestUpdate, - function success(data, textStatus, xhr) { + (data, textStatus, xhr) => { if (xhr.status === 304 || !data) { return; } @@ -547,20 +499,100 @@ export function getPosts(id) { AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_POSTS, id: channelId, + before: true, + numRequested: Constants.POST_CHUNK_SIZE, post_list: data }); getProfiles(); }, - function fail(err) { + (err) => { dispatchError(err, 'getPosts'); }, - function complete() { + () => { callTracker['getPosts_' + channelId] = 0; } ); } +export function getPostsBefore(postId, offset, numPost) { + const channelId = ChannelStore.getCurrentId(); + if (channelId == null) { + return; + } + + if (isCallInProgress('getPostsBefore_' + channelId)) { + return; + } + + client.getPostsBefore( + channelId, + postId, + offset, + numPost, + (data, textStatus, xhr) => { + if (xhr.status === 304 || !data) { + return; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POSTS, + id: channelId, + before: true, + numRequested: numPost, + post_list: data + }); + + getProfiles(); + }, + (err) => { + dispatchError(err, 'getPostsBefore'); + }, + () => { + callTracker['getPostsBefore_' + channelId] = 0; + } + ); +} + +export function getPostsAfter(postId, offset, numPost) { + const channelId = ChannelStore.getCurrentId(); + if (channelId == null) { + return; + } + + if (isCallInProgress('getPostsAfter_' + channelId)) { + return; + } + + client.getPostsAfter( + channelId, + postId, + offset, + numPost, + (data, textStatus, xhr) => { + if (xhr.status === 304 || !data) { + return; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POSTS, + id: channelId, + before: false, + numRequested: numPost, + post_list: data + }); + + getProfiles(); + }, + (err) => { + dispatchError(err, 'getPostsAfter'); + }, + () => { + callTracker['getPostsAfter_' + channelId] = 0; + } + ); +} + export function getMe() { if (isCallInProgress('getMe')) { return; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index e6c24aa9c..09e962161 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -820,7 +820,37 @@ export function getPosts(channelId, since, success, error, complete) { }); } -export function getPost(channelId, postId, success, error) { +export function getPostsBefore(channelId, post, offset, numPost, success, error, complete) { + $.ajax({ + url: '/api/v1/channels/' + channelId + '/post/' + post + '/before/' + offset + '/' + numPost, + dataType: 'json', + type: 'GET', + ifModified: false, + success, + error: function onError(xhr, status, err) { + var e = handleError('getPostsBefore', xhr, status, err); + error(e); + }, + complete: complete + }); +} + +export function getPostsAfter(channelId, post, offset, numPost, success, error, complete) { + $.ajax({ + url: '/api/v1/channels/' + channelId + '/post/' + post + '/after/' + offset + '/' + numPost, + dataType: 'json', + type: 'GET', + ifModified: false, + success, + error: function onError(xhr, status, err) { + var e = handleError('getPostsAfter', xhr, status, err); + error(e); + }, + complete: complete + }); +} + +export function getPost(channelId, postId, success, error, complete) { $.ajax({ cache: false, url: '/api/v1/channels/' + channelId + '/post/' + postId, @@ -831,7 +861,24 @@ export function getPost(channelId, postId, success, error) { error: function onError(xhr, status, err) { var e = handleError('getPost', xhr, status, err); error(e); - } + }, + complete + }); +} + +export function getPostById(postId, success, error, complete) { + $.ajax({ + cache: false, + url: '/api/v1/posts/' + postId, + dataType: 'json', + type: 'GET', + ifModified: false, + success, + error: function onError(xhr, status, err) { + var e = handleError('getPostById', xhr, status, err); + error(e); + }, + complete }); } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 958bfa8d2..1ac9a1b98 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -10,12 +10,17 @@ export default { CLICK_CHANNEL: null, CREATE_CHANNEL: null, LEAVE_CHANNEL: null, + CREATE_POST: null, + POST_DELETED: null, + RECIEVED_CHANNELS: null, RECIEVED_CHANNEL: null, RECIEVED_MORE_CHANNELS: null, RECIEVED_CHANNEL_EXTRA_INFO: null, + FOCUS_POST: null, RECIEVED_POSTS: null, + RECIEVED_FOCUSED_POST: null, RECIEVED_POST: null, RECIEVED_EDIT_POST: null, RECIEVED_SEARCH: null, @@ -99,6 +104,7 @@ export default { EMAIL_SERVICE: 'email', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, + POST_FOCUS_CONTEXT_RADIUS: 10, POST_LOADING: 'loading', POST_FAILED: 'failed', POST_DELETED: 'deleted', diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 668d8100f..764bdf763 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import UserStore from '../stores/user_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; @@ -839,23 +840,15 @@ export function isValidUsername(name) { } export function updateAddressBar(channelName) { - var teamURL = window.location.href.split('/channels')[0]; + const teamURL = TeamStore.getCurrentTeamUrl(); history.replaceState('data', '', teamURL + '/channels/' + channelName); } export function switchChannel(channel) { - AppDispatcher.handleViewAction({ - type: ActionTypes.CLICK_CHANNEL, - name: channel.name, - id: channel.id - }); + EventHelpers.emitChannelClickEvent(channel); updateAddressBar(channel.name); - AsyncClient.getChannels(true, true, true); - AsyncClient.getChannelExtraInfo(true); - AsyncClient.getPosts(channel.id); - $('.inner__wrap').removeClass('move--right'); $('.sidebar--left').removeClass('move--right'); -- cgit v1.2.3-1-g7c22