This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
Sam Saffron 7068b58e85 PERF: flush topic timings less frequently
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)
2019-04-18 17:31:08 +10:00

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");
}
});