mirror of
https://github.com/flarum/core.git
synced 2025-07-12 04:16:24 +02:00
Extract PostStream state (#2160)
Co-authored-by: Franz Liedke <franz@develophp.org>
This commit is contained in:
committed by
GitHub
parent
f9c9b5d5e4
commit
6953d93c6d
@ -1,8 +1,6 @@
|
||||
import Component from '../../common/Component';
|
||||
import ScrollListener from '../../common/utils/ScrollListener';
|
||||
import PostLoading from './LoadingPost';
|
||||
import anchorScroll from '../../common/utils/anchorScroll';
|
||||
import evented from '../../common/utils/evented';
|
||||
import ReplyPlaceholder from './ReplyPlaceholder';
|
||||
import Button from '../../common/components/Button';
|
||||
|
||||
@ -13,183 +11,16 @@ import Button from '../../common/components/Button';
|
||||
* ### Props
|
||||
*
|
||||
* - `discussion`
|
||||
* - `includedPosts`
|
||||
* - `stream`
|
||||
* - `targetPost`
|
||||
* - `onPositionChange`
|
||||
*/
|
||||
class PostStream extends Component {
|
||||
export default class PostStream extends Component {
|
||||
init() {
|
||||
/**
|
||||
* The discussion to display the post stream for.
|
||||
*
|
||||
* @type {Discussion}
|
||||
*/
|
||||
this.discussion = this.props.discussion;
|
||||
|
||||
/**
|
||||
* Whether or not the infinite-scrolling auto-load functionality is
|
||||
* disabled.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.paused = false;
|
||||
this.stream = this.props.stream;
|
||||
|
||||
this.scrollListener = new ScrollListener(this.onscroll.bind(this));
|
||||
this.loadPageTimeouts = {};
|
||||
this.pagesLoading = 0;
|
||||
|
||||
this.show(this.props.includedPosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and scroll to a post with a certain number.
|
||||
*
|
||||
* @param {Integer|String} number The post number to go to. If 'reply', go to
|
||||
* the last post and scroll the reply preview into view.
|
||||
* @param {Boolean} noAnimation
|
||||
* @return {Promise}
|
||||
*/
|
||||
goToNumber(number, noAnimation) {
|
||||
// If we want to go to the reply preview, then we will go to the end of the
|
||||
// discussion and then scroll to the very bottom of the page.
|
||||
if (number === 'reply') {
|
||||
return this.goToLast().then(() => {
|
||||
$('html,body')
|
||||
.stop(true)
|
||||
.animate(
|
||||
{
|
||||
scrollTop: $(document).height() - $(window).height(),
|
||||
},
|
||||
'fast',
|
||||
() => {
|
||||
this.flashItem(this.$('.PostStream-item:last-child'));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.paused = true;
|
||||
|
||||
const promise = this.loadNearNumber(number);
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
return promise.then(() => {
|
||||
m.redraw(true);
|
||||
|
||||
this.scrollToNumber(number, noAnimation).done(this.unpause.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and scroll to a certain index within the discussion.
|
||||
*
|
||||
* @param {Integer} index
|
||||
* @param {Boolean} backwards Whether or not to load backwards from the given
|
||||
* index.
|
||||
* @param {Boolean} noAnimation
|
||||
* @return {Promise}
|
||||
*/
|
||||
goToIndex(index, backwards, noAnimation) {
|
||||
this.paused = true;
|
||||
|
||||
const promise = this.loadNearIndex(index);
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
return promise.then(() => {
|
||||
anchorScroll(this.$('.PostStream-item:' + (backwards ? 'last' : 'first')), () => m.redraw(true));
|
||||
|
||||
this.scrollToIndex(index, noAnimation, backwards).done(this.unpause.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and scroll up to the first post in the discussion.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
goToFirst() {
|
||||
return this.goToIndex(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and scroll down to the last post in the discussion.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
goToLast() {
|
||||
return this.goToIndex(this.count() - 1, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the stream so that it loads and includes the latest posts in the
|
||||
* discussion, if the end is being viewed.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
update() {
|
||||
if (!this.viewingEnd) return m.deferred().resolve().promise;
|
||||
|
||||
this.visibleEnd = this.count();
|
||||
|
||||
return this.loadRange(this.visibleStart, this.visibleEnd).then(() => m.redraw());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of posts in the discussion.
|
||||
*
|
||||
* @return {Integer}
|
||||
*/
|
||||
count() {
|
||||
return this.discussion.postIds().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that the given index is not outside of the possible range of
|
||||
* indexes in the discussion.
|
||||
*
|
||||
* @param {Integer} index
|
||||
* @protected
|
||||
*/
|
||||
sanitizeIndex(index) {
|
||||
return Math.max(0, Math.min(this.count(), index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the stream with the given array of posts.
|
||||
*
|
||||
* @param {Post[]} posts
|
||||
*/
|
||||
show(posts) {
|
||||
this.visibleStart = posts.length ? this.discussion.postIds().indexOf(posts[0].id()) : 0;
|
||||
this.visibleEnd = this.visibleStart + posts.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the stream so that a specific range of posts is displayed. If a range
|
||||
* is not specified, the first page of posts will be displayed.
|
||||
*
|
||||
* @param {Integer} [start]
|
||||
* @param {Integer} [end]
|
||||
*/
|
||||
reset(start, end) {
|
||||
this.visibleStart = start || 0;
|
||||
this.visibleEnd = this.sanitizeIndex(end || this.constructor.loadCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible page of posts.
|
||||
*
|
||||
* @return {Post[]}
|
||||
*/
|
||||
posts() {
|
||||
return this.discussion
|
||||
.postIds()
|
||||
.slice(this.visibleStart, this.visibleEnd)
|
||||
.map((id) => {
|
||||
const post = app.store.getById('posts', id);
|
||||
|
||||
return post && post.discussion() && typeof post.canEdit() !== 'undefined' ? post : null;
|
||||
});
|
||||
}
|
||||
|
||||
view() {
|
||||
@ -200,15 +31,13 @@ class PostStream extends Component {
|
||||
|
||||
let lastTime;
|
||||
|
||||
this.visibleEnd = this.sanitizeIndex(this.visibleEnd);
|
||||
this.viewingEnd = this.visibleEnd === this.count();
|
||||
|
||||
const posts = this.posts();
|
||||
const viewingEnd = this.stream.viewingEnd();
|
||||
const posts = this.stream.posts();
|
||||
const postIds = this.discussion.postIds();
|
||||
|
||||
const items = posts.map((post, i) => {
|
||||
let content;
|
||||
const attrs = { 'data-index': this.visibleStart + i };
|
||||
const attrs = { 'data-index': this.stream.visibleStart + i };
|
||||
|
||||
if (post) {
|
||||
const time = post.createdAt();
|
||||
@ -238,7 +67,7 @@ class PostStream extends Component {
|
||||
|
||||
lastTime = time;
|
||||
} else {
|
||||
attrs.key = 'post' + postIds[this.visibleStart + i];
|
||||
attrs.key = 'post' + postIds[this.stream.visibleStart + i];
|
||||
|
||||
content = PostLoading.component();
|
||||
}
|
||||
@ -250,10 +79,10 @@ class PostStream extends Component {
|
||||
);
|
||||
});
|
||||
|
||||
if (!this.viewingEnd && posts[this.visibleEnd - this.visibleStart - 1]) {
|
||||
if (!viewingEnd && posts[this.stream.visibleEnd - this.stream.visibleStart - 1]) {
|
||||
items.push(
|
||||
<div className="PostStream-loadMore" key="loadMore">
|
||||
<Button className="Button" onclick={this.loadNext.bind(this)}>
|
||||
<Button className="Button" onclick={this.stream.loadNext.bind(this.stream)}>
|
||||
{app.translator.trans('core.forum.post_stream.load_more_button')}
|
||||
</Button>
|
||||
</div>
|
||||
@ -262,7 +91,7 @@ class PostStream extends Component {
|
||||
|
||||
// If we're viewing the end of the discussion, the user can reply, and
|
||||
// is not already doing so, then show a 'write a reply' placeholder.
|
||||
if (this.viewingEnd && (!app.session.user || this.discussion.canReply())) {
|
||||
if (viewingEnd && (!app.session.user || this.discussion.canReply())) {
|
||||
items.push(
|
||||
<div className="PostStream-item" key="reply">
|
||||
{ReplyPlaceholder.component({ discussion: this.discussion })}
|
||||
@ -274,6 +103,8 @@ class PostStream extends Component {
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
this.triggerScroll();
|
||||
|
||||
if (isInitialized) return;
|
||||
|
||||
// This is wrapped in setTimeout due to the following Mithril issue:
|
||||
@ -286,201 +117,135 @@ class PostStream extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start scrolling, if appropriate, to a newly-targeted post.
|
||||
*/
|
||||
triggerScroll() {
|
||||
if (!this.props.targetPost) return;
|
||||
|
||||
const oldTarget = this.prevTarget;
|
||||
const newTarget = this.props.targetPost;
|
||||
|
||||
if (oldTarget) {
|
||||
if ('number' in oldTarget && oldTarget.number === newTarget.number) return;
|
||||
if ('index' in oldTarget && oldTarget.index === newTarget.index) return;
|
||||
}
|
||||
|
||||
if ('number' in newTarget) {
|
||||
this.scrollToNumber(newTarget.number, this.stream.noAnimationScroll);
|
||||
} else if ('index' in newTarget) {
|
||||
const backwards = newTarget.index === this.stream.count() - 1;
|
||||
this.scrollToIndex(newTarget.index, this.stream.noAnimationScroll, backwards);
|
||||
}
|
||||
|
||||
this.prevTarget = newTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the window is scrolled, check if either extreme of the post stream is
|
||||
* in the viewport, and if so, trigger loading the next/previous page.
|
||||
*
|
||||
* @param {Integer} top
|
||||
*/
|
||||
onscroll(top) {
|
||||
if (this.paused) return;
|
||||
|
||||
onscroll(top = window.pageYOffset) {
|
||||
if (this.stream.paused) return;
|
||||
const marginTop = this.getMarginTop();
|
||||
const viewportHeight = $(window).height() - marginTop;
|
||||
const viewportTop = top + marginTop;
|
||||
const loadAheadDistance = 300;
|
||||
|
||||
if (this.visibleStart > 0) {
|
||||
const $item = this.$('.PostStream-item[data-index=' + this.visibleStart + ']');
|
||||
if (this.stream.visibleStart > 0) {
|
||||
const $item = this.$('.PostStream-item[data-index=' + this.stream.visibleStart + ']');
|
||||
|
||||
if ($item.length && $item.offset().top > viewportTop - loadAheadDistance) {
|
||||
this.loadPrevious();
|
||||
this.stream.loadPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.visibleEnd < this.count()) {
|
||||
const $item = this.$('.PostStream-item[data-index=' + (this.visibleEnd - 1) + ']');
|
||||
if (this.stream.visibleEnd < this.stream.count()) {
|
||||
const $item = this.$('.PostStream-item[data-index=' + (this.stream.visibleEnd - 1) + ']');
|
||||
|
||||
if ($item.length && $item.offset().top + $item.outerHeight(true) < viewportTop + viewportHeight + loadAheadDistance) {
|
||||
this.loadNext();
|
||||
this.stream.loadNext();
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle calculation of our position (start/end numbers of posts in the
|
||||
// viewport) to 100ms.
|
||||
clearTimeout(this.calculatePositionTimeout);
|
||||
this.calculatePositionTimeout = setTimeout(this.calculatePosition.bind(this), 100);
|
||||
this.calculatePositionTimeout = setTimeout(this.calculatePosition.bind(this, top), 100);
|
||||
|
||||
this.updateScrubber(top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of posts.
|
||||
*/
|
||||
loadNext() {
|
||||
const start = this.visibleEnd;
|
||||
const end = (this.visibleEnd = this.sanitizeIndex(this.visibleEnd + this.constructor.loadCount));
|
||||
updateScrubber(top = window.pageYOffset) {
|
||||
const marginTop = this.getMarginTop();
|
||||
const viewportHeight = $(window).height() - marginTop;
|
||||
const viewportTop = top + marginTop;
|
||||
|
||||
// Unload the posts which are two pages back from the page we're currently
|
||||
// loading.
|
||||
const twoPagesAway = start - this.constructor.loadCount * 2;
|
||||
if (twoPagesAway > this.visibleStart && twoPagesAway >= 0) {
|
||||
this.visibleStart = twoPagesAway + this.constructor.loadCount + 1;
|
||||
// Before looping through all of the posts, we reset the scrollbar
|
||||
// properties to a 'default' state. These values reflect what would be
|
||||
// seen if the browser were scrolled right up to the top of the page,
|
||||
// and the viewport had a height of 0.
|
||||
const $items = this.$('.PostStream-item[data-index]');
|
||||
let index = $items.first().data('index') || 0;
|
||||
let visible = 0;
|
||||
let period = '';
|
||||
|
||||
if (this.loadPageTimeouts[twoPagesAway]) {
|
||||
clearTimeout(this.loadPageTimeouts[twoPagesAway]);
|
||||
this.loadPageTimeouts[twoPagesAway] = null;
|
||||
this.pagesLoading--;
|
||||
// Now loop through each of the items in the discussion. An 'item' is
|
||||
// either a single post or a 'gap' of one or more posts that haven't
|
||||
// been loaded yet.
|
||||
$items.each(function () {
|
||||
const $this = $(this);
|
||||
const top = $this.offset().top;
|
||||
const height = $this.outerHeight(true);
|
||||
|
||||
// If this item is above the top of the viewport, skip to the next
|
||||
// one. If it's below the bottom of the viewport, break out of the
|
||||
// loop.
|
||||
if (top + height < viewportTop) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadPage(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the previous page of posts.
|
||||
*/
|
||||
loadPrevious() {
|
||||
const end = this.visibleStart;
|
||||
const start = (this.visibleStart = this.sanitizeIndex(this.visibleStart - this.constructor.loadCount));
|
||||
|
||||
// Unload the posts which are two pages back from the page we're currently
|
||||
// loading.
|
||||
const twoPagesAway = start + this.constructor.loadCount * 2;
|
||||
if (twoPagesAway < this.visibleEnd && twoPagesAway <= this.count()) {
|
||||
this.visibleEnd = twoPagesAway;
|
||||
|
||||
if (this.loadPageTimeouts[twoPagesAway]) {
|
||||
clearTimeout(this.loadPageTimeouts[twoPagesAway]);
|
||||
this.loadPageTimeouts[twoPagesAway] = null;
|
||||
this.pagesLoading--;
|
||||
if (top > viewportTop + viewportHeight) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadPage(start, end, true);
|
||||
}
|
||||
// Work out how many pixels of this item are visible inside the viewport.
|
||||
// Then add the proportion of this item's total height to the index.
|
||||
const visibleTop = Math.max(0, viewportTop - top);
|
||||
const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);
|
||||
const visiblePost = visibleBottom - visibleTop;
|
||||
|
||||
/**
|
||||
* Load a page of posts into the stream and redraw.
|
||||
*
|
||||
* @param {Integer} start
|
||||
* @param {Integer} end
|
||||
* @param {Boolean} backwards
|
||||
*/
|
||||
loadPage(start, end, backwards) {
|
||||
const redraw = () => {
|
||||
if (start < this.visibleStart || end > this.visibleEnd) return;
|
||||
if (top <= viewportTop) {
|
||||
index = parseFloat($this.data('index')) + visibleTop / height;
|
||||
}
|
||||
|
||||
const anchorIndex = backwards ? this.visibleEnd - 1 : this.visibleStart;
|
||||
anchorScroll(`.PostStream-item[data-index="${anchorIndex}"]`, () => m.redraw(true));
|
||||
if (visiblePost > 0) {
|
||||
visible += visiblePost / height;
|
||||
}
|
||||
|
||||
this.unpause();
|
||||
};
|
||||
redraw();
|
||||
// If this item has a time associated with it, then set the
|
||||
// scrollbar's current period to a formatted version of this time.
|
||||
const time = $this.data('time');
|
||||
if (time) period = time;
|
||||
});
|
||||
|
||||
this.loadPageTimeouts[start] = setTimeout(
|
||||
() => {
|
||||
this.loadRange(start, end).then(() => {
|
||||
redraw();
|
||||
this.pagesLoading--;
|
||||
});
|
||||
this.loadPageTimeouts[start] = null;
|
||||
},
|
||||
this.pagesLoading ? 1000 : 0
|
||||
);
|
||||
|
||||
this.pagesLoading++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and inject the specified range of posts into the stream, without
|
||||
* clearing it.
|
||||
*
|
||||
* @param {Integer} start
|
||||
* @param {Integer} end
|
||||
* @return {Promise}
|
||||
*/
|
||||
loadRange(start, end) {
|
||||
const loadIds = [];
|
||||
const loaded = [];
|
||||
|
||||
this.discussion
|
||||
.postIds()
|
||||
.slice(start, end)
|
||||
.forEach((id) => {
|
||||
const post = app.store.getById('posts', id);
|
||||
|
||||
if (post && post.discussion() && typeof post.canEdit() !== 'undefined') {
|
||||
loaded.push(post);
|
||||
} else {
|
||||
loadIds.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return loadIds.length ? app.store.find('posts', loadIds) : m.deferred().resolve(loaded).promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the stream and load posts near a certain number. Returns a promise.
|
||||
* If the post with the given number is already loaded, the promise will be
|
||||
* resolved immediately.
|
||||
*
|
||||
* @param {Integer} number
|
||||
* @return {Promise}
|
||||
*/
|
||||
loadNearNumber(number) {
|
||||
if (this.posts().some((post) => post && Number(post.number()) === Number(number))) {
|
||||
return m.deferred().resolve().promise;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
|
||||
return app.store
|
||||
.find('posts', {
|
||||
filter: { discussion: this.discussion.id() },
|
||||
page: { near: number },
|
||||
})
|
||||
.then(this.show.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the stream and load posts near a certain index. A page of posts
|
||||
* surrounding the given index will be loaded. Returns a promise. If the given
|
||||
* index is already loaded, the promise will be resolved immediately.
|
||||
*
|
||||
* @param {Integer} index
|
||||
* @return {Promise}
|
||||
*/
|
||||
loadNearIndex(index) {
|
||||
if (index >= this.visibleStart && index <= this.visibleEnd) {
|
||||
return m.deferred().resolve().promise;
|
||||
}
|
||||
|
||||
const start = this.sanitizeIndex(index - this.constructor.loadCount / 2);
|
||||
const end = start + this.constructor.loadCount;
|
||||
|
||||
this.reset(start, end);
|
||||
|
||||
return this.loadRange(start, end).then(this.show.bind(this));
|
||||
this.stream.index = index + 1;
|
||||
this.stream.visible = visible;
|
||||
if (period) this.stream.description = dayjs(period).format('MMMM YYYY');
|
||||
}
|
||||
|
||||
/**
|
||||
* Work out which posts (by number) are currently visible in the viewport, and
|
||||
* fire an event with the information.
|
||||
*/
|
||||
calculatePosition() {
|
||||
calculatePosition(top = window.pageYOffset) {
|
||||
const marginTop = this.getMarginTop();
|
||||
const $window = $(window);
|
||||
const viewportHeight = $window.height() - marginTop;
|
||||
const scrollTop = $window.scrollTop() + marginTop;
|
||||
const viewportTop = top + marginTop;
|
||||
|
||||
let startNumber;
|
||||
let endNumber;
|
||||
|
||||
@ -488,12 +253,15 @@ class PostStream extends Component {
|
||||
const $item = $(this);
|
||||
const top = $item.offset().top;
|
||||
const height = $item.outerHeight(true);
|
||||
const visibleTop = Math.max(0, viewportTop - top);
|
||||
|
||||
const threeQuartersVisible = visibleTop / height < 0.75;
|
||||
const coversQuarterOfViewport = (height - visibleTop) / viewportHeight > 0.25;
|
||||
if (startNumber === undefined && (threeQuartersVisible || coversQuarterOfViewport)) {
|
||||
startNumber = $item.data('number');
|
||||
}
|
||||
|
||||
if (top + height > scrollTop) {
|
||||
if (!startNumber) {
|
||||
startNumber = endNumber = $item.data('number');
|
||||
}
|
||||
|
||||
if (top + height < scrollTop + viewportHeight) {
|
||||
if ($item.data('number')) {
|
||||
endNumber = $item.data('number');
|
||||
@ -503,7 +271,7 @@ class PostStream extends Component {
|
||||
});
|
||||
|
||||
if (startNumber) {
|
||||
this.trigger('positionChanged', startNumber || 1, endNumber);
|
||||
this.props.onPositionChange(startNumber || 1, endNumber, startNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@ -521,42 +289,46 @@ class PostStream extends Component {
|
||||
* Scroll down to a certain post by number and 'flash' it.
|
||||
*
|
||||
* @param {Integer} number
|
||||
* @param {Boolean} noAnimation
|
||||
* @param {Boolean} animate
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
scrollToNumber(number, noAnimation) {
|
||||
scrollToNumber(number, animate) {
|
||||
const $item = this.$(`.PostStream-item[data-number=${number}]`);
|
||||
|
||||
return this.scrollToItem($item, noAnimation).done(this.flashItem.bind(this, $item));
|
||||
return this.scrollToItem($item, animate).then(this.flashItem.bind(this, $item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll down to a certain post by index.
|
||||
*
|
||||
* @param {Integer} index
|
||||
* @param {Boolean} noAnimation
|
||||
* @param {Boolean} animate
|
||||
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
|
||||
* at the given index, instead of the top of it.
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
scrollToIndex(index, noAnimation, bottom) {
|
||||
scrollToIndex(index, animate, bottom) {
|
||||
const $item = this.$(`.PostStream-item[data-index=${index}]`);
|
||||
|
||||
return this.scrollToItem($item, noAnimation, true, bottom);
|
||||
return this.scrollToItem($item, animate, true, bottom).then(() => {
|
||||
if (index == this.stream.count() - 1) {
|
||||
this.flashItem(this.$('.PostStream-item:last-child'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll down to the given post.
|
||||
*
|
||||
* @param {jQuery} $item
|
||||
* @param {Boolean} noAnimation
|
||||
* @param {Boolean} animate
|
||||
* @param {Boolean} force Whether or not to force scrolling to the item, even
|
||||
* if it is already in the viewport.
|
||||
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
|
||||
* at the given index, instead of the top of it.
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
scrollToItem($item, noAnimation, force, bottom) {
|
||||
scrollToItem($item, animate, force, bottom) {
|
||||
const $container = $('html, body').stop(true);
|
||||
|
||||
if ($item.length) {
|
||||
@ -571,7 +343,7 @@ class PostStream extends Component {
|
||||
if (force || itemTop < scrollTop || itemBottom > scrollBottom) {
|
||||
const top = bottom ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
|
||||
|
||||
if (noAnimation) {
|
||||
if (!animate) {
|
||||
$container.scrollTop(top);
|
||||
} else if (top !== scrollTop) {
|
||||
$container.animate({ scrollTop: top }, 'fast');
|
||||
@ -579,7 +351,15 @@ class PostStream extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
return $container.promise();
|
||||
return Promise.all([$container.promise(), this.stream.loadPromise]).then(() => {
|
||||
this.updateScrubber();
|
||||
const index = $item.data('index');
|
||||
m.redraw(true);
|
||||
const scroll = index == 0 ? 0 : $(`.PostStream-item[data-index=${$item.data('index')}]`).offset().top - this.getMarginTop();
|
||||
$(window).scrollTop(scroll);
|
||||
this.calculatePosition();
|
||||
this.stream.paused = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -590,24 +370,4 @@ class PostStream extends Component {
|
||||
flashItem($item) {
|
||||
$item.addClass('flash').one('animationend webkitAnimationEnd', () => $item.removeClass('flash'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the stream's ability to auto-load posts on scroll.
|
||||
*/
|
||||
unpause() {
|
||||
this.paused = false;
|
||||
this.scrollListener.update();
|
||||
this.trigger('unpaused');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of posts to load per page.
|
||||
*
|
||||
* @type {Integer}
|
||||
*/
|
||||
PostStream.loadCount = 20;
|
||||
|
||||
Object.assign(PostStream.prototype, evented);
|
||||
|
||||
export default PostStream;
|
||||
|
Reference in New Issue
Block a user