// We use this class to track how long posts in a topic are on the screen. import Singleton from 'discourse/mixins/singleton'; const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3, MAX_TRACKING_TIME = 1000 * 60 * 6; const ScreenTrack = Ember.Object.extend({ init() { this.reset(); // TODO: Move `ScreenTrack` to injection and remove this this.set('topicTrackingState', Discourse.__container__.lookup('topic-tracking-state:main')); }, start(topicId, topicController) { const currentTopicId = this.get('topicId'); if (currentTopicId && (currentTopicId !== topicId)) { this.tick(); this.flush(); } this.reset(); // Create an interval timer if we don't have one. if (!this.get('interval')) { const self = this; this.set('interval', setInterval(function () { self.tick(); }, 1000)); $(window).on('scroll.screentrack', function(){self.scrolled()}); } this.set('topicId', topicId); this.set('topicController', topicController); }, stop() { if(!this.get('topicId')) { // already stopped no need to "extra stop" return; } $(window).off('scroll.screentrack'); this.tick(); this.flush(); this.reset(); this.set('topicId', null); this.set('topicController', null); if (this.get('interval')) { clearInterval(this.get('interval')); this.set('interval', null); } }, track(elementId, postNumber) { this.get('timings')["#" + elementId] = { time: 0, postNumber: postNumber }; }, stopTracking(elementId) { delete this.get('timings')['#' + elementId]; }, // Reset our timers reset() { this.setProperties({ lastTick: new Date().getTime(), lastScrolled: new Date().getTime(), lastFlush: 0, cancelled: false, timings: {}, totalTimings: {}, topicTime: 0 }); }, scrolled() { this.set('lastScrolled', new Date().getTime()); }, flush() { if (this.get('cancelled')) { return; } // We don't log anything unless we're logged in if (!Discourse.User.current()) return; const newTimings = {}, totalTimings = this.get('totalTimings'), self = this; _.each(this.get('timings'), function(timing) { if (!totalTimings[timing.postNumber]) totalTimings[timing.postNumber] = 0; if (timing.time > 0 && totalTimings[timing.postNumber] < MAX_TRACKING_TIME) { totalTimings[timing.postNumber] += timing.time; newTimings[timing.postNumber] = timing.time; } timing.time = 0; }); const topicId = parseInt(this.get('topicId'), 10); let highestSeen = 0; _.each(newTimings, function(time,postNumber) { highestSeen = Math.max(highestSeen, parseInt(postNumber, 10)); }); const highestSeenByTopic = Discourse.Session.currentProp('highestSeenByTopic'); if ((highestSeenByTopic[topicId] || 0) < highestSeen) { highestSeenByTopic[topicId] = highestSeen; } this.topicTrackingState.updateSeen(topicId, highestSeen); if (!$.isEmptyObject(newTimings)) { Discourse.ajax('/topics/timings', { data: { timings: newTimings, topic_time: this.get('topicTime'), topic_id: topicId }, cache: false, type: 'POST', headers: { 'X-SILENCE-LOGGER': 'true' } }).then(function(){ const controller = self.get('topicController'); if(controller){ const postNumbers = Object.keys(newTimings).map(function(v){ return parseInt(v,10); }); controller.readPosts(topicId, postNumbers); } }); this.set('topicTime', 0); } this.set('lastFlush', 0); }, tick() { // If the user hasn't scrolled the browser in a long time, stop tracking time read const sinceScrolled = new Date().getTime() - this.get('lastScrolled'); if (sinceScrolled > PAUSE_UNLESS_SCROLLED) { return; } const diff = new Date().getTime() - this.get('lastTick'); this.set('lastFlush', this.get('lastFlush') + diff); this.set('lastTick', new Date().getTime()); const totalTimings = this.get('totalTimings'), timings = this.get('timings'); const nextFlush = Discourse.SiteSettings.flush_timings_secs * 1000; // rush new post numbers const rush = _.any(_.filter(timings, function(t){return t.time>0;}), function(t){ return !totalTimings[t.postNumber]; }); if (this.get('lastFlush') > nextFlush || rush) { this.flush(); } // Don't track timings if we're not in focus if (!Discourse.get("hasFocus")) return; this.set('topicTime', this.get('topicTime') + diff); const docViewTop = $(window).scrollTop() + $('header').height(), docViewBottom = docViewTop + $(window).height(); // TODO: Eyeline has a smarter more accurate function here. It's bad to do jQuery // in a model like component, so we should refactor this out later. _.each(this.get('timings'),function(timing,id) { const $element = $(id); if ($element.length === 1) { const elemTop = $element.offset().top, elemBottom = elemTop + $element.height(); // If part of the element is on the screen, increase the counter if (((docViewTop <= elemTop && elemTop <= docViewBottom)) || ((docViewTop <= elemBottom && elemBottom <= docViewBottom))) { timing.time = timing.time + diff; } } }); } }); ScreenTrack.reopenClass(Singleton); export default ScreenTrack;