228 lines
5.9 KiB
JavaScript
228 lines
5.9 KiB
JavaScript
import { ajax } from "discourse/lib/ajax";
|
|
|
|
// We use this class to track how long posts in a topic are on the screen.
|
|
const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3;
|
|
const MAX_TRACKING_TIME = 1000 * 60 * 6;
|
|
const ANON_MAX_TOPIC_IDS = 5;
|
|
|
|
export default class {
|
|
constructor(topicTrackingState, siteSettings, session, currentUser) {
|
|
this.topicTrackingState = topicTrackingState;
|
|
this.siteSettings = siteSettings;
|
|
this.session = session;
|
|
this.currentUser = currentUser;
|
|
this.reset();
|
|
}
|
|
|
|
start(topicId, topicController) {
|
|
const currentTopicId = this._topicId;
|
|
if (currentTopicId && currentTopicId !== topicId) {
|
|
this.tick();
|
|
this.flush();
|
|
}
|
|
|
|
this.reset();
|
|
|
|
// Create an interval timer if we don't have one.
|
|
if (!this._interval) {
|
|
this._interval = setInterval(() => this.tick(), 1000);
|
|
$(window).on("scroll.screentrack", this.scrolled.bind(this));
|
|
}
|
|
|
|
this._topicId = topicId;
|
|
this._topicController = topicController;
|
|
}
|
|
|
|
stop() {
|
|
// already stopped no need to "extra stop"
|
|
if (!this._topicId) {
|
|
return;
|
|
}
|
|
|
|
$(window).off("scroll.screentrack", this.scrolled);
|
|
this.tick();
|
|
this.flush();
|
|
this.reset();
|
|
|
|
this._topicId = null;
|
|
this._topicController = null;
|
|
|
|
if (this._interval) {
|
|
clearInterval(this._interval);
|
|
this._interval = null;
|
|
}
|
|
}
|
|
|
|
setOnscreen(onscreen, readOnscreen) {
|
|
this._onscreen = onscreen;
|
|
this._readOnscreen = readOnscreen;
|
|
}
|
|
|
|
// Reset our timers
|
|
reset() {
|
|
const now = new Date().getTime();
|
|
this._lastTick = now;
|
|
this._lastScrolled = now;
|
|
this._lastFlush = 0;
|
|
this._timings = {};
|
|
this._totalTimings = {};
|
|
this._topicTime = 0;
|
|
this._onscreen = [];
|
|
this._readOnscreen = [];
|
|
this._readPosts = {};
|
|
this._inProgress = false;
|
|
}
|
|
|
|
scrolled() {
|
|
this._lastScrolled = new Date().getTime();
|
|
}
|
|
|
|
registerAnonCallback(cb) {
|
|
this._anonCallback = cb;
|
|
}
|
|
|
|
flush() {
|
|
const newTimings = {};
|
|
const totalTimings = this._totalTimings;
|
|
|
|
const timings = this._timings;
|
|
Object.keys(this._timings).forEach(postNumber => {
|
|
const time = timings[postNumber];
|
|
totalTimings[postNumber] = totalTimings[postNumber] || 0;
|
|
|
|
if (time > 0 && totalTimings[postNumber] < MAX_TRACKING_TIME) {
|
|
totalTimings[postNumber] += time;
|
|
newTimings[postNumber] = time;
|
|
}
|
|
timings[postNumber] = 0;
|
|
});
|
|
|
|
const topicId = parseInt(this._topicId, 10);
|
|
let highestSeen = 0;
|
|
|
|
Object.keys(newTimings).forEach(postNumber => {
|
|
highestSeen = Math.max(highestSeen, parseInt(postNumber, 10));
|
|
});
|
|
|
|
const highestSeenByTopic = this.session.get("highestSeenByTopic");
|
|
if ((highestSeenByTopic[topicId] || 0) < highestSeen) {
|
|
highestSeenByTopic[topicId] = highestSeen;
|
|
}
|
|
|
|
this.topicTrackingState.updateSeen(topicId, highestSeen);
|
|
|
|
if (!$.isEmptyObject(newTimings)) {
|
|
if (this.currentUser) {
|
|
this._inProgress = true;
|
|
|
|
ajax("/topics/timings", {
|
|
data: {
|
|
timings: newTimings,
|
|
topic_time: this._topicTime,
|
|
topic_id: topicId
|
|
},
|
|
cache: false,
|
|
type: "POST",
|
|
headers: {
|
|
"X-SILENCE-LOGGER": "true"
|
|
}
|
|
})
|
|
.then(() => {
|
|
const controller = this._topicController;
|
|
if (controller) {
|
|
const postNumbers = Object.keys(newTimings).map(v =>
|
|
parseInt(v, 10)
|
|
);
|
|
controller.readPosts(topicId, postNumbers);
|
|
}
|
|
})
|
|
.catch(e => {
|
|
const error = e.jqXHR;
|
|
if (
|
|
error.status === 405 &&
|
|
error.responseJSON.error_type === "read_only"
|
|
)
|
|
return;
|
|
})
|
|
.finally(() => {
|
|
this._inProgress = false;
|
|
this._lastFlush = 0;
|
|
});
|
|
} else if (this._anonCallback) {
|
|
// Anonymous viewer - save to localStorage
|
|
const storage = this.keyValueStore;
|
|
|
|
// Save total time
|
|
const existingTime = storage.getInt("anon-topic-time");
|
|
storage.setItem("anon-topic-time", existingTime + this._topicTime);
|
|
|
|
// Save unique topic IDs up to a max
|
|
let topicIds = storage.get("anon-topic-ids");
|
|
if (topicIds) {
|
|
topicIds = topicIds.split(",").map(e => parseInt(e));
|
|
} else {
|
|
topicIds = [];
|
|
}
|
|
if (
|
|
topicIds.indexOf(topicId) === -1 &&
|
|
topicIds.length < ANON_MAX_TOPIC_IDS
|
|
) {
|
|
topicIds.push(topicId);
|
|
storage.setItem("anon-topic-ids", topicIds.join(","));
|
|
}
|
|
|
|
// Inform the observer
|
|
this._anonCallback();
|
|
|
|
// No need to call controller.readPosts()
|
|
}
|
|
|
|
this._topicTime = 0;
|
|
}
|
|
|
|
this._lastFlush = 0;
|
|
}
|
|
|
|
tick() {
|
|
const now = new Date().getTime();
|
|
|
|
// If the user hasn't scrolled the browser in a long time, stop tracking time read
|
|
const sinceScrolled = now - this._lastScrolled;
|
|
if (sinceScrolled > PAUSE_UNLESS_SCROLLED) {
|
|
return;
|
|
}
|
|
|
|
const diff = now - this._lastTick;
|
|
this._lastFlush += diff;
|
|
this._lastTick = now;
|
|
|
|
const totalTimings = this._totalTimings;
|
|
const timings = this._timings;
|
|
const nextFlush = this.siteSettings.flush_timings_secs * 1000;
|
|
|
|
const rush = Object.keys(timings).some(postNumber => {
|
|
return (
|
|
timings[postNumber] > 0 &&
|
|
!totalTimings[postNumber] &&
|
|
!this._readPosts[postNumber]
|
|
);
|
|
});
|
|
|
|
if (!this._inProgress && (this._lastFlush > nextFlush || rush)) {
|
|
this.flush();
|
|
}
|
|
|
|
if (Discourse.get("hasFocus")) {
|
|
this._topicTime += diff;
|
|
|
|
this._onscreen.forEach(
|
|
postNumber => (timings[postNumber] = (timings[postNumber] || 0) + diff)
|
|
);
|
|
|
|
this._readOnscreen.forEach(postNumber => {
|
|
this._readPosts[postNumber] = true;
|
|
});
|
|
}
|
|
}
|
|
}
|