This is a first step of a performance optimisation, more will follow Previously we did not properly account for previously read topics while "rushing" marking times on posts. The new mechanism now avoids "rushing" sending timings to server if all the posts were read. Also to alleviate some server load we only "ping" the server with old timings once a minute (it used to be every 20 seconds)
336 lines
9.2 KiB
JavaScript
336 lines
9.2 KiB
JavaScript
import DiscourseURL from "discourse/lib/url";
|
|
import MountWidget from "discourse/components/mount-widget";
|
|
import { cloak, uncloak } from "discourse/widgets/post-stream";
|
|
import { isWorkaroundActive } from "discourse/lib/safari-hacks";
|
|
import offsetCalculator from "discourse/lib/offset-calculator";
|
|
|
|
function findTopView($posts, viewportTop, postsWrapperTop, min, max) {
|
|
if (max < min) {
|
|
return min;
|
|
}
|
|
|
|
while (max > min) {
|
|
const mid = Math.floor((min + max) / 2);
|
|
const $post = $($posts[mid]);
|
|
const viewBottom = $post.offset().top - postsWrapperTop + $post.height();
|
|
|
|
if (viewBottom > viewportTop) {
|
|
max = mid - 1;
|
|
} else {
|
|
min = mid + 1;
|
|
}
|
|
}
|
|
|
|
return min;
|
|
}
|
|
|
|
export default MountWidget.extend({
|
|
widget: "post-stream",
|
|
_topVisible: null,
|
|
_bottomVisible: null,
|
|
_currentPost: null,
|
|
_currentVisible: null,
|
|
_currentPercent: null,
|
|
|
|
buildArgs() {
|
|
return this.getProperties(
|
|
"posts",
|
|
"canCreatePost",
|
|
"multiSelect",
|
|
"gaps",
|
|
"selectedQuery",
|
|
"selectedPostsCount",
|
|
"searchService"
|
|
);
|
|
},
|
|
|
|
beforePatch() {
|
|
const $body = $(document);
|
|
this.prevHeight = $body.height();
|
|
this.prevScrollTop = $body.scrollTop();
|
|
},
|
|
|
|
afterPatch() {
|
|
const $body = $(document);
|
|
const height = $body.height();
|
|
const scrollTop = $body.scrollTop();
|
|
|
|
// This hack is for when swapping out many cloaked views at once
|
|
// when using keyboard navigation. It could suddenly move the scroll
|
|
if (this.prevHeight === height && scrollTop !== this.prevScrollTop) {
|
|
$body.scrollTop(this.prevScrollTop);
|
|
}
|
|
},
|
|
|
|
scrolled() {
|
|
if (this.isDestroyed || this.isDestroying) {
|
|
return;
|
|
}
|
|
if (isWorkaroundActive()) {
|
|
return;
|
|
}
|
|
|
|
// We use this because watching videos fullscreen in Chrome was super buggy
|
|
// otherwise. Thanks to arrendek from q23 for the technique.
|
|
if (document.elementFromPoint(0, 0).tagName.toUpperCase() === "IFRAME") {
|
|
return;
|
|
}
|
|
|
|
const $w = $(window);
|
|
const windowHeight = window.innerHeight ? window.innerHeight : $w.height();
|
|
const slack = Math.round(windowHeight * 5);
|
|
const onscreen = [];
|
|
const nearby = [];
|
|
|
|
const windowTop = $w.scrollTop();
|
|
|
|
const postsWrapperTop = $(".posts-wrapper").offset().top;
|
|
const $posts = this.$(".onscreen-post, .cloaked-post");
|
|
const viewportTop = windowTop - slack;
|
|
const topView = findTopView(
|
|
$posts,
|
|
viewportTop,
|
|
postsWrapperTop,
|
|
0,
|
|
$posts.length - 1
|
|
);
|
|
|
|
let windowBottom = windowTop + windowHeight;
|
|
let viewportBottom = windowBottom + slack;
|
|
|
|
const bodyHeight = $("body").height();
|
|
if (windowBottom > bodyHeight) {
|
|
windowBottom = bodyHeight;
|
|
}
|
|
if (viewportBottom > bodyHeight) {
|
|
viewportBottom = bodyHeight;
|
|
}
|
|
|
|
let currentPost = null;
|
|
let percent = null;
|
|
|
|
const offset = offsetCalculator();
|
|
const topCheck = Math.ceil(windowTop + offset + 5);
|
|
|
|
// uncomment to debug the eyeline
|
|
/*
|
|
let $eyeline = $('.debug-eyeline');
|
|
if ($eyeline.length === 0) {
|
|
$('body').prepend('<div class="debug-eyeline"></div>');
|
|
$eyeline = $('.debug-eyeline');
|
|
}
|
|
$eyeline.css({ height: '5px', width: '100%', backgroundColor: 'blue', position: 'absolute', top: `${topCheck}px`, zIndex: 999999 });
|
|
*/
|
|
|
|
let allAbove = true;
|
|
let bottomView = topView;
|
|
let lastBottom = 0;
|
|
while (bottomView < $posts.length) {
|
|
const post = $posts[bottomView];
|
|
const $post = $(post);
|
|
|
|
if (!$post) {
|
|
break;
|
|
}
|
|
|
|
const viewTop = $post.offset().top;
|
|
const postHeight = $post.outerHeight(true);
|
|
const viewBottom = Math.ceil(viewTop + postHeight);
|
|
|
|
allAbove = allAbove && viewTop < topCheck;
|
|
|
|
if (viewTop > viewportBottom) {
|
|
break;
|
|
}
|
|
|
|
if (viewBottom >= windowTop && viewTop <= windowBottom) {
|
|
onscreen.push(bottomView);
|
|
}
|
|
|
|
if (
|
|
currentPost === null &&
|
|
((viewTop <= topCheck && viewBottom >= topCheck) ||
|
|
(lastBottom <= topCheck && viewTop >= topCheck))
|
|
) {
|
|
percent = (topCheck - viewTop) / postHeight;
|
|
currentPost = bottomView;
|
|
}
|
|
|
|
lastBottom = viewBottom;
|
|
nearby.push(bottomView);
|
|
bottomView++;
|
|
}
|
|
|
|
if (allAbove) {
|
|
if (percent === null) {
|
|
percent = 1.0;
|
|
}
|
|
if (currentPost === null) {
|
|
currentPost = bottomView - 1;
|
|
}
|
|
}
|
|
|
|
const posts = this.posts;
|
|
const refresh = cb => this.queueRerender(cb);
|
|
if (onscreen.length) {
|
|
const first = posts.objectAt(onscreen[0]);
|
|
if (this._topVisible !== first) {
|
|
this._topVisible = first;
|
|
const $body = $("body");
|
|
const elem = $posts[onscreen[0]];
|
|
const elemId = elem.id;
|
|
const $elem = $(elem);
|
|
const elemPos = $elem.position();
|
|
const distToElement = elemPos ? $body.scrollTop() - elemPos.top : 0;
|
|
|
|
const topRefresh = () => {
|
|
refresh(() => {
|
|
const $refreshedElem = $(`#${elemId}`);
|
|
|
|
// Quickly going back might mean the element is destroyed
|
|
const position = $refreshedElem.position();
|
|
if (position && position.top) {
|
|
let whereY = position.top + distToElement;
|
|
$("html, body").scrollTop(whereY);
|
|
|
|
// This seems weird, but somewhat infrequently a rerender
|
|
// will cause the browser to scroll to the top of the document
|
|
// in Chrome. This makes sure the scroll works correctly if that
|
|
// happens.
|
|
Ember.run.next(() => $("html, body").scrollTop(whereY));
|
|
}
|
|
});
|
|
};
|
|
this.topVisibleChanged({
|
|
post: first,
|
|
refresh: topRefresh
|
|
});
|
|
}
|
|
|
|
const last = posts.objectAt(onscreen[onscreen.length - 1]);
|
|
if (this._bottomVisible !== last) {
|
|
this._bottomVisible = last;
|
|
this.bottomVisibleChanged({ post: last, refresh });
|
|
}
|
|
|
|
const changedPost = this._currentPost !== currentPost;
|
|
if (changedPost) {
|
|
this._currentPost = currentPost;
|
|
const post = posts.objectAt(currentPost);
|
|
this.currentPostChanged({ post });
|
|
}
|
|
|
|
if (percent !== null) {
|
|
percent = Math.max(0.0, Math.min(1.0, percent));
|
|
|
|
if (changedPost || this._currentPercent !== percent) {
|
|
this._currentPercent = percent;
|
|
this.currentPostScrolled({ percent });
|
|
}
|
|
}
|
|
} else {
|
|
this._topVisible = null;
|
|
this._bottomVisible = null;
|
|
this._currentPost = null;
|
|
this._currentPercent = null;
|
|
}
|
|
|
|
const onscreenPostNumbers = [];
|
|
const readPostNumbers = [];
|
|
|
|
const prev = this._previouslyNearby;
|
|
const newPrev = {};
|
|
nearby.forEach(idx => {
|
|
const post = posts.objectAt(idx);
|
|
const postNumber = post.post_number;
|
|
|
|
delete prev[postNumber];
|
|
|
|
if (onscreen.indexOf(idx) !== -1) {
|
|
onscreenPostNumbers.push(postNumber);
|
|
if (post.read) {
|
|
readPostNumbers.push(postNumber);
|
|
}
|
|
}
|
|
newPrev[postNumber] = post;
|
|
uncloak(post, this);
|
|
});
|
|
|
|
Object.values(prev).forEach(node => cloak(node, this));
|
|
|
|
this._previouslyNearby = newPrev;
|
|
this.screenTrack.setOnscreen(onscreenPostNumbers, readPostNumbers);
|
|
},
|
|
|
|
_scrollTriggered() {
|
|
Ember.run.scheduleOnce("afterRender", this, this.scrolled);
|
|
},
|
|
|
|
_posted(staged) {
|
|
this.queueRerender(() => {
|
|
if (staged) {
|
|
const postNumber = staged.get("post_number");
|
|
DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true });
|
|
}
|
|
});
|
|
},
|
|
|
|
_refresh(args) {
|
|
if (args) {
|
|
if (args.id) {
|
|
this.dirtyKeys.keyDirty(`post-${args.id}`);
|
|
|
|
if (args.refreshLikes) {
|
|
this.dirtyKeys.keyDirty(`post-menu-${args.id}`, {
|
|
onRefresh: "refreshLikes"
|
|
});
|
|
}
|
|
} else if (args.force) {
|
|
this.dirtyKeys.forceAll();
|
|
}
|
|
}
|
|
this.queueRerender();
|
|
},
|
|
|
|
_debouncedScroll() {
|
|
Ember.run.debounce(this, this._scrollTriggered, 10);
|
|
},
|
|
|
|
didInsertElement() {
|
|
this._super(...arguments);
|
|
const debouncedScroll = () =>
|
|
Ember.run.debounce(this, this._scrollTriggered, 10);
|
|
|
|
this._previouslyNearby = {};
|
|
|
|
this.appEvents.on("post-stream:refresh", this, "_debouncedScroll");
|
|
$(document).bind("touchmove.post-stream", debouncedScroll);
|
|
$(window).bind("scroll.post-stream", debouncedScroll);
|
|
this._scrollTriggered();
|
|
|
|
this.appEvents.on("post-stream:posted", this, "_posted");
|
|
|
|
this.$().on("mouseenter.post-stream", "button.widget-button", e => {
|
|
$("button.widget-button").removeClass("d-hover");
|
|
$(e.target).addClass("d-hover");
|
|
});
|
|
|
|
this.$().on("mouseleave.post-stream", "button.widget-button", () => {
|
|
$("button.widget-button").removeClass("d-hover");
|
|
});
|
|
|
|
this.appEvents.on("post-stream:refresh", this, "_refresh");
|
|
},
|
|
|
|
willDestroyElement() {
|
|
this._super(...arguments);
|
|
$(document).unbind("touchmove.post-stream");
|
|
$(window).unbind("scroll.post-stream");
|
|
this.appEvents.off("post-stream:refresh", this, "_debouncedScroll");
|
|
this.$().off("mouseenter.post-stream");
|
|
this.$().off("mouseleave.post-stream");
|
|
this.appEvents.off("post-stream:refresh", this, "_refresh");
|
|
this.appEvents.off("post-stream:posted", this, "_posted");
|
|
}
|
|
});
|