Merge pull request #623 from mattermost/plt-35

PLT-35 Add post list container to hold mounted post lists for faster rendering/channel switching.
This commit is contained in:
Christopher Speller
2015-09-09 08:22:06 -04:00
8 changed files with 251 additions and 107 deletions

View File

@@ -64,9 +64,14 @@ export default class ChannelHeader extends React.Component {
handleLeave() {
Client.leaveChannel(this.state.channel.id,
function handleLeaveSuccess() {
AppDispatcher.handleViewAction({
type: ActionTypes.LEAVE_CHANNEL,
id: this.state.channel.id
});
const townsquare = ChannelStore.getByName('town-square');
Utils.switchChannel(townsquare);
},
}.bind(this),
function handleLeaveError(err) {
AsyncClient.dispatchError(err, 'handleLeave');
}

View File

@@ -17,6 +17,8 @@ export default class ChannelLoader extends React.Component {
constructor(props) {
super(props);
this.intervalId = null;
this.onSocketChange = this.onSocketChange.bind(this);
this.state = {};
@@ -35,10 +37,12 @@ export default class ChannelLoader extends React.Component {
PostStore.clearPendingPosts();
/* Set up interval functions */
setInterval(
this.intervalId = setInterval(
function pollStatuses() {
AsyncClient.getStatuses();
}, 30000);
},
30000
);
/* Device tracking setup */
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
@@ -49,12 +53,12 @@ export default class ChannelLoader extends React.Component {
/* Set up tracking for whether the window is active */
window.isActive = true;
$(window).focus(function windowFocus() {
$(window).on('focus', function windowFocus() {
AsyncClient.updateLastViewedAt();
window.isActive = true;
});
$(window).blur(function windowBlur() {
$(window).on('blur', function windowBlur() {
window.isActive = false;
});
@@ -84,6 +88,54 @@ export default class ChannelLoader extends React.Component {
Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +10) + ';');
$('.team__header').addClass('theme--gray');
}
/* Setup global mouse events */
$('body').on('click.userpopover', function popOver(e) {
if ($(e.target).attr('data-toggle') !== 'popover' &&
$(e.target).parents('.popover.in').length === 0) {
$('.user-popover').popover('hide');
}
});
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
}
});
/* Setup modal events */
$('.modal').on('show.bs.modal', function onShow() {
$('.modal-body').css('overflow-y', 'auto');
$('.modal-body').css('max-height', $(window).height() * 0.7);
});
}
componentWillUnmount() {
clearInterval(this.intervalId);
$(window).off('focus');
$(window).off('blur');
SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover');
$('body').off('mouseenter mouseleave', '.post');
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
$('.modal').off('show.bs.modal');
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {

View File

@@ -18,8 +18,8 @@ var ActionTypes = Constants.ActionTypes;
import {strings} from '../utils/config.js';
export default class PostList extends React.Component {
constructor() {
super();
constructor(props) {
super(props);
this.gotMorePosts = false;
this.scrolled = false;
@@ -27,6 +27,7 @@ export default class PostList extends React.Component {
this.seenNewMessages = false;
this.isUserScroll = true;
this.userHasSeenNew = false;
this.loadInProgress = false;
this.onChange = this.onChange.bind(this);
this.onTimeChange = this.onTimeChange.bind(this);
@@ -34,22 +35,19 @@ export default class PostList extends React.Component {
this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this);
this.loadMorePosts = this.loadMorePosts.bind(this);
this.loadFirstPosts = this.loadFirstPosts.bind(this);
this.activate = this.activate.bind(this);
this.deactivate = this.deactivate.bind(this);
this.resize = this.resize.bind(this);
this.state = this.getStateFromStores();
this.state = this.getStateFromStores(props.channelId);
this.state.numToDisplay = Constants.POST_CHUNK_SIZE;
this.state.isFirstLoadComplete = false;
}
getStateFromStores() {
var channel = ChannelStore.getCurrent();
if (channel == null) {
channel = {};
}
var postList = PostStore.getCurrentPosts();
getStateFromStores(id) {
var postList = PostStore.getPosts(id);
if (postList != null) {
var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id);
var deletedPosts = PostStore.getUnseenDeletedPosts(id);
if (deletedPosts && Object.keys(deletedPosts).length > 0) {
for (var pid in deletedPosts) {
@@ -70,7 +68,7 @@ export default class PostList extends React.Component {
});
}
var pendingPostList = PostStore.getPendingPosts(channel.id);
var pendingPostList = PostStore.getPendingPosts(id);
if (pendingPostList) {
postList.order = pendingPostList.order.concat(postList.order);
@@ -82,43 +80,42 @@ export default class PostList extends React.Component {
}
}
var lastViewed = Number.MAX_VALUE;
if (ChannelStore.getCurrentMember() != null) {
lastViewed = ChannelStore.getCurrentMember().last_viewed_at;
}
return {
postList: postList,
channel: channel,
lastViewed: lastViewed
postList: postList
};
}
componentDidMount() {
if (this.props.isActive) {
this.activate();
this.loadFirstPosts(this.props.channelId);
}
}
componentWillUnmount() {
this.deactivate();
}
activate() {
this.gotMorePosts = false;
this.scrolled = false;
this.prevScrollTop = 0;
this.seenNewMessages = false;
this.isUserScroll = true;
this.userHasSeenNew = false;
PostStore.clearUnseenDeletedPosts(this.props.channelId);
PostStore.addChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onTimeChange);
SocketStore.addChangeListener(this.onSocketChange);
var postHolder = $('.post-list-holder-by-time');
$('.modal').on('show.bs.modal', function onShow() {
$('.modal-body').css('overflow-y', 'auto');
$('.modal-body').css('max-height', $(window).height() * 0.7);
});
$(window).resize(function resize() {
if ($('#create_post').length > 0) {
var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
postHolder.css('height', height + 'px');
}
var postHolder = $(React.findDOMNode(this.refs.postlist));
$(window).on('resize.' + this.props.channelId, function resize() {
this.resize();
if (!this.scrolled) {
this.scrollToBottom();
}
}.bind(this));
postHolder.scroll(function scroll() {
postHolder.on('scroll', function scroll() {
var position = postHolder.scrollTop() + postHolder.height() + 14;
var bottom = postHolder[0].scrollHeight;
@@ -134,41 +131,25 @@ export default class PostList extends React.Component {
this.isUserScroll = true;
}.bind(this));
$('body').on('click.userpopover', function popOver(e) {
if ($(e.target).attr('data-toggle') !== 'popover' &&
$(e.target).parents('.popover.in').length === 0) {
$('.user-popover').popover('hide');
}
});
$('.post-list__content div .post').removeClass('post--last');
$('.post-list__content div:last-child .post').addClass('post--last');
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
}
});
this.scrollToBottom();
if (this.state.channel.id != null) {
this.loadFirstPosts(this.state.channel.id);
if (!this.state.isFirstLoadComplete) {
this.loadFirstPosts(this.props.channelId);
}
this.resize();
this.onChange();
this.scrollToBottom();
}
deactivate() {
PostStore.removeChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onTimeChange);
SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover');
$(window).off('resize.' + this.props.channelId);
var postHolder = $(React.findDOMNode(this.refs.postlist));
postHolder.off('scroll');
}
componentDidUpdate(prevProps, prevState) {
$('.post-list__content div .post').removeClass('post--last');
@@ -187,7 +168,7 @@ export default class PostList extends React.Component {
var firstPost = posts[order[0]] || {};
var isNewPost = oldOrder.indexOf(order[0]) === -1;
if (this.state.channel.id !== prevState.channel.id) {
if (this.props.isActive && !prevProps.isActive) {
this.scrollToBottom();
} else if (oldOrder.length === 0) {
this.scrollToBottom();
@@ -201,37 +182,43 @@ export default class PostList extends React.Component {
} else if (isNewPost &&
userId === firstPost.user_id &&
!utils.isComment(firstPost)) {
this.state.lastViewed = utils.getTimestamp();
this.scrollToBottom(true);
// the user clicked 'load more messages'
} else if (this.gotMorePosts) {
var lastPost = oldPosts[oldOrder[prevState.numToDisplay]];
$('#' + lastPost.id)[0].scrollIntoView();
this.gotMorePosts = false;
} else {
this.scrollTo(this.prevScrollTop);
}
}
componentWillUpdate() {
var postHolder = $('.post-list-holder-by-time');
var postHolder = $(React.findDOMNode(this.refs.postlist));
this.prevScrollTop = postHolder.scrollTop();
}
componentWillUnmount() {
PostStore.removeChangeListener(this.onChange);
ChannelStore.removeChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onTimeChange);
SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover');
$('.modal').off('show.bs.modal');
componentWillReceiveProps(nextProps) {
if (nextProps.isActive === true && this.props.isActive === false) {
this.activate();
} else if (nextProps.isActive === false && this.props.isActive === true) {
this.deactivate();
}
}
resize() {
const postHolder = $(React.findDOMNode(this.refs.postlist));
if ($('#create_post').length > 0) {
const height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
postHolder.css('height', height + 'px');
}
}
scrollTo(val) {
this.isUserScroll = false;
var postHolder = $('.post-list-holder-by-time');
var postHolder = $(React.findDOMNode(this.refs.postlist));
postHolder[0].scrollTop = val;
}
scrollToBottom(force) {
this.isUserScroll = false;
var postHolder = $('.post-list-holder-by-time');
var postHolder = $(React.findDOMNode(this.refs.postlist));
if ($('#new_message')[0] && !this.userHasSeenNew && !force) {
$('#new_message')[0].scrollIntoView();
} else {
@@ -241,34 +228,32 @@ export default class PostList extends React.Component {
}
}
loadFirstPosts(id) {
if (this.loadInProgress) {
return;
}
if (this.props.channelId == null) {
return;
}
this.loadInProgress = true;
Client.getPosts(
id,
PostStore.getLatestUpdate(id),
function success() {
this.loadInProgress = false;
this.setState({isFirstLoadComplete: true});
}.bind(this),
function fail() {
this.loadInProgress = false;
this.setState({isFirstLoadComplete: true});
}.bind(this)
);
}
onChange() {
var newState = this.getStateFromStores();
// Special case where the channel wasn't yet set in componentDidMount
if (!this.state.isFirstLoadComplete && this.state.channel.id == null && newState.channel.id != null) {
this.loadFirstPosts(newState.channel.id);
}
if (!utils.areStatesEqual(newState, this.state)) {
if (this.state.channel.id !== newState.channel.id) {
PostStore.clearUnseenDeletedPosts(this.state.channel.id);
this.userHasSeenNew = false;
newState.numToDisplay = Constants.POST_CHUNK_SIZE;
} else {
newState.lastViewed = this.state.lastViewed;
}
var newState = this.getStateFromStores(this.props.channelId);
if (!utils.areStatesEqual(newState.postList, this.state.postList)) {
this.setState(newState);
}
}
@@ -424,7 +409,7 @@ export default class PostList extends React.Component {
}
}
var members = ChannelStore.getCurrentExtraInfo().members;
var members = ChannelStore.getExtraInfo(channel.id).members;
for (var i = 0; i < members.length; i++) {
if (members[i].roles.indexOf('admin') > -1) {
return members[i].username;
@@ -488,6 +473,11 @@ export default class PostList extends React.Component {
var userId = UserStore.getCurrentId();
var renderedLastViewed = false;
var lastViewed = Number.MAX_VALUE;
if (ChannelStore.getMember(this.props.channelId) != null) {
lastViewed = ChannelStore.getMember(this.props.channelId).last_viewed_at;
}
var numToDisplay = this.state.numToDisplay;
if (order.length - 1 < numToDisplay) {
@@ -543,7 +533,7 @@ export default class PostList extends React.Component {
);
}
if (post.user_id !== userId && post.create_at > this.state.lastViewed && !renderedLastViewed) {
if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) {
renderedLastViewed = true;
// Temporary fix to solve ie10/11 rendering issue
@@ -577,7 +567,7 @@ export default class PostList extends React.Component {
var posts = this.state.postList.posts;
var order = this.state.postList.order;
var channelId = this.state.channel.id;
var channelId = this.props.channelId;
$(React.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...');
@@ -619,7 +609,7 @@ export default class PostList extends React.Component {
render() {
var order = [];
var posts;
var channel = this.state.channel;
var channel = ChannelStore.get(this.props.channelId);
if (this.state.postList != null) {
posts = this.state.postList.posts;
@@ -628,7 +618,7 @@ export default class PostList extends React.Component {
var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>;
if (channel != null) {
if (order.length > this.state.numToDisplay) {
if (order.length >= this.state.numToDisplay) {
moreMessages = (
<a
ref='loadmore'
@@ -655,10 +645,15 @@ export default class PostList extends React.Component {
/>);
}
var activeClass = '';
if (!this.props.isActive) {
activeClass = 'inactive';
}
return (
<div
ref='postlist'
className='post-list-holder-by-time'
className={'post-list-holder-by-time ' + activeClass}
>
<div className='post-list__table'>
<div className='post-list__content'>
@@ -670,3 +665,12 @@ export default class PostList extends React.Component {
);
}
}
PostList.defaultProps = {
isActive: false,
channelId: null
};
PostList.propTypes = {
isActive: React.PropTypes.bool,
channelId: React.PropTypes.string
};

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
const PostList = require('./post_list.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
export default class PostListContainer extends React.Component {
constructor() {
super();
this.onChange = this.onChange.bind(this);
this.onLeave = this.onLeave.bind(this);
let currentChannelId = ChannelStore.getCurrentId();
if (currentChannelId) {
this.state = {currentChannelId: currentChannelId, postLists: [currentChannelId]};
} else {
this.state = {currentChannelId: null, postLists: []};
}
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addLeaveListener(this.onLeave);
}
onChange() {
let channelId = ChannelStore.getCurrentId();
if (channelId === this.state.currentChannelId) {
return;
}
let postLists = this.state.postLists;
if (postLists.indexOf(channelId) === -1) {
postLists.push(channelId);
}
this.setState({currentChannelId: channelId, postLists: postLists});
}
onLeave(id) {
let postLists = this.state.postLists;
var index = postLists.indexOf(id);
if (index !== -1) {
postLists.splice(index, 1);
}
}
render() {
let postLists = this.state.postLists;
let channelId = this.state.currentChannelId;
let postListCtls = [];
for (let i = 0; i <= this.state.postLists.length - 1; i++) {
postListCtls.push(
<PostList
channelId={postLists[i]}
isActive={postLists[i] === channelId}
/>
);
}
return (
<div>{postListCtls}</div>
);
}
}

View File

@@ -5,7 +5,7 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Navbar = require('../components/navbar.jsx');
var Sidebar = require('../components/sidebar.jsx');
var ChannelHeader = require('../components/channel_header.jsx');
var PostList = require('../components/post_list.jsx');
var PostListContainer = require('../components/post_list_container.jsx');
var CreatePost = require('../components/create_post.jsx');
var SidebarRight = require('../components/sidebar_right.jsx');
var SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
@@ -159,7 +159,7 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
);
React.render(
<PostList />,
<PostListContainer />,
document.getElementById('post-list')
);

View File

@@ -10,6 +10,7 @@ var ActionTypes = Constants.ActionTypes;
var BrowserStore = require('../stores/browser_store.jsx');
var CHANGE_EVENT = 'change';
var LEAVE_EVENT = 'leave';
var MORE_CHANGE_EVENT = 'change';
var EXTRA_INFO_EVENT = 'extra_info';
@@ -48,6 +49,15 @@ class ChannelStoreClass extends EventEmitter {
removeExtraInfoChangeListener(callback) {
this.removeListener(EXTRA_INFO_EVENT, callback);
}
emitLeave(id) {
this.emit(LEAVE_EVENT, id);
}
addLeaveListener(callback) {
this.on(LEAVE_EVENT, callback);
}
removeLeaveListener(callback) {
this.removeListener(LEAVE_EVENT, callback);
}
findFirstBy(field, value) {
var channels = this.pGetChannels();
for (var i = 0; i < channels.length; i++) {
@@ -272,6 +282,10 @@ ChannelStore.dispatchToken = AppDispatcher.register(function handleAction(payloa
ChannelStore.emitExtraInfoChange();
break;
case ActionTypes.LEAVE_CHANNEL:
ChannelStore.emitLeave(action.id);
break;
default:
break;
}

View File

@@ -9,6 +9,7 @@ module.exports = {
CLICK_CHANNEL: null,
CREATE_CHANNEL: null,
LEAVE_CHANNEL: null,
RECIEVED_CHANNELS: null,
RECIEVED_CHANNEL: null,
RECIEVED_MORE_CHANNELS: null,

View File

@@ -143,6 +143,12 @@ body.ios {
&.hide-scroll::-webkit-scrollbar {
width: 0px !important;
}
&.inactive {
display: none;
}
&.active {
display: inline;
}
}
.post-list__table {
display: table;