ScreenTrack refactor - removes logic from TopicView didInsertElement

This commit is contained in:
Robin Ward 2013-06-07 15:19:41 -04:00
parent 34e1f376f6
commit fa4cfa1269
6 changed files with 140 additions and 146 deletions

View File

@ -8,99 +8,102 @@
**/ **/
Discourse.ScreenTrack = Ember.Object.extend({ Discourse.ScreenTrack = Ember.Object.extend({
// Don't send events if we haven't scrolled in a long time init: function() {
PAUSE_UNLESS_SCROLLED: 1000 * 60 * 3, var screenTrack = this;
this.reset();
},
// After 6 minutes stop tracking read position on post start: function(topicId) {
MAX_TRACKING_TIME: 1000 * 60 * 6, // Create an interval timer if we don't have one.
if (!this.get('interval')) {
var screenTrack = this;
this.set('interval', setInterval(function () {
screenTrack.tick();
}, 1000));
}
totalTimings: {}, var currentTopicId = this.get('topicId');
if (currentTopicId && (currentTopicId !== topicId)) {
this.flush();
this.reset();
}
this.set('topicId', topicId);
},
// Elements to track stop: function() {
timings: {}, this.flush();
topicTime: 0, this.reset();
cancelled: false, this.set('topicId', null);
if (this.get('interval')) {
clearInterval(this.get('interval'));
this.set('interval', null);
}
},
track: function(elementId, postNumber) { track: function(elementId, postNumber) {
this.timings["#" + elementId] = { this.get('timings')["#" + elementId] = {
time: 0, time: 0,
postNumber: postNumber postNumber: postNumber
}; };
}, },
stopTracking: function(elementId) {
delete this.get('timings')['#' + elementId];
},
// Reset our timers // Reset our timers
reset: function() { reset: function() {
this.lastTick = new Date().getTime(); this.set('lastTick', new Date().getTime());
this.lastFlush = 0; this.set('lastScrolled', new Date().getTime());
this.cancelled = false; this.set('lastFlush', 0);
}, this.set('cancelled', false);
this.set('timings', {});
// Start tracking this.set('totalTimings', {});
start: function() { this.set('topicTime', 0);
var _this = this; this.set('cancelled', false);
this.reset();
this.lastScrolled = new Date().getTime();
this.interval = setInterval(function() {
return _this.tick();
}, 1000);
},
// Cancel and eject any tracking we have buffered
cancel: function() {
this.cancelled = true;
this.timings = {};
this.topicTime = 0;
clearInterval(this.interval);
this.interval = null;
},
// Stop tracking and flush buffered read records
stop: function() {
clearInterval(this.interval);
this.interval = null;
return this.flush();
}, },
scrolled: function() { scrolled: function() {
this.lastScrolled = new Date().getTime(); this.set('lastScrolled', new Date().getTime());
}, },
flush: function() { flush: function() {
var highestSeenByTopic, newTimings, topicId, if (this.get('cancelled')) { return; }
_this = this;
if (this.cancelled) {
return;
}
// We don't log anything unless we're logged in // We don't log anything unless we're logged in
if (!Discourse.User.current()) return; if (!Discourse.User.current()) return;
newTimings = {}; var newTimings = {};
Object.values(this.timings, function(timing) {
if (!_this.totalTimings[timing.postNumber])
_this.totalTimings[timing.postNumber] = 0;
if (timing.time > 0 && _this.totalTimings[timing.postNumber] < _this.MAX_TRACKING_TIME) { // Update our total timings
_this.totalTimings[timing.postNumber] += timing.time; var totalTimings = this.get('totalTimings');
Object.values(this.get('timings'), function(timing) {
if (!totalTimings[timing.postNumber])
totalTimings[timing.postNumber] = 0;
if (timing.time > 0 && totalTimings[timing.postNumber] < Discourse.ScreenTrack.MAX_TRACKING_TIME) {
totalTimings[timing.postNumber] += timing.time;
newTimings[timing.postNumber] = timing.time; newTimings[timing.postNumber] = timing.time;
} }
timing.time = 0; timing.time = 0;
}); });
topicId = this.get('topic_id');
var topicId = parseInt(this.get('topicId'), 10);
var highestSeen = 0; var highestSeen = 0;
$.each(newTimings, function(postNumber){ Object.keys(newTimings, function(postNumber) {
highestSeen = Math.max(highestSeen, parseInt(postNumber, 10)); highestSeen = Math.max(highestSeen, parseInt(postNumber, 10));
}); });
highestSeenByTopic = Discourse.get('highestSeenByTopic'); var highestSeenByTopic = Discourse.get('highestSeenByTopic');
if ((highestSeenByTopic[topicId] || 0) < highestSeen) { if ((highestSeenByTopic[topicId] || 0) < highestSeen) {
highestSeenByTopic[topicId] = highestSeen; highestSeenByTopic[topicId] = highestSeen;
} }
if (!Object.isEmpty(newTimings)) { if (!Object.isEmpty(newTimings)) {
Discourse.ajax('/topics/timings', { Discourse.ajax('/topics/timings', {
data: { data: {
timings: newTimings, timings: newTimings,
topic_time: this.topicTime, topic_time: this.get('topicTime'),
topic_id: topicId topic_id: topicId
}, },
cache: false, cache: false,
@ -109,37 +112,39 @@ Discourse.ScreenTrack = Ember.Object.extend({
'X-SILENCE-LOGGER': 'true' 'X-SILENCE-LOGGER': 'true'
} }
}); });
this.topicTime = 0;
this.set('topicTime', 0);
} }
this.lastFlush = 0; this.set('lastFlush', 0);
}, },
tick: function() { tick: function() {
// If the user hasn't scrolled the browser in a long time, stop tracking time read // If the user hasn't scrolled the browser in a long time, stop tracking time read
var diff, docViewBottom, docViewTop, sinceScrolled, var sinceScrolled = new Date().getTime() - this.get('lastScrolled');
_this = this; if (sinceScrolled > Discourse.ScreenTrack.PAUSE_UNLESS_SCROLLED) {
sinceScrolled = new Date().getTime() - this.lastScrolled;
if (sinceScrolled > this.PAUSE_UNLESS_SCROLLED) {
this.reset(); this.reset();
return; return;
} }
diff = new Date().getTime() - this.lastTick;
this.lastFlush += diff; var diff = new Date().getTime() - this.get('lastTick');
this.lastTick = new Date().getTime(); this.set('lastFlush', this.get('lastFlush') + diff);
if (this.lastFlush > (Discourse.SiteSettings.flush_timings_secs * 1000)) { this.set('lastTick', new Date().getTime());
if (this.get('lastFlush') > (Discourse.SiteSettings.flush_timings_secs * 1000)) {
this.flush(); this.flush();
} }
// Don't track timings if we're not in focus // Don't track timings if we're not in focus
if (!Discourse.get("hasFocus")) return; if (!Discourse.get("hasFocus")) return;
this.topicTime += diff; this.set('topicTime', this.get('topicTime') + diff);
docViewTop = $(window).scrollTop() + $('header').height(); var docViewTop = $(window).scrollTop() + $('header').height();
docViewBottom = docViewTop + $(window).height(); var docViewBottom = docViewTop + $(window).height();
// TODO: Eyeline has a smarter more accurate function here // 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.
return Object.keys(this.timings, function(id) { var screenTrack = this;
return Object.keys(this.get('timings'), function(id) {
var $element, elemBottom, elemTop, timing; var $element, elemBottom, elemTop, timing;
$element = $(id); $element = $(id);
if ($element.length === 1) { if ($element.length === 1) {
@ -148,11 +153,32 @@ Discourse.ScreenTrack = Ember.Object.extend({
// If part of the element is on the screen, increase the counter // If part of the element is on the screen, increase the counter
if (((docViewTop <= elemTop && elemTop <= docViewBottom)) || ((docViewTop <= elemBottom && elemBottom <= docViewBottom))) { if (((docViewTop <= elemTop && elemTop <= docViewBottom)) || ((docViewTop <= elemBottom && elemBottom <= docViewBottom))) {
timing = _this.timings[id]; timing = screenTrack.timings[id];
timing.time = timing.time + diff; timing.time = timing.time + diff;
} }
} }
}); });
} }
});
Discourse.ScreenTrack.reopenClass({
// Don't send events if we haven't scrolled in a long time
PAUSE_UNLESS_SCROLLED: 1000 * 60 * 3,
// After 6 minutes stop tracking read position on post
MAX_TRACKING_TIME: 1000 * 60 * 6,
/**
Returns a Screen Tracking singleton
**/
instance: function() {
if (this.screenTrack) { return this.screenTrack; }
this.screenTrack = Discourse.ScreenTrack.create();
return this.screenTrack;
}
}); });

View File

@ -293,18 +293,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
this.get('content').convertArchetype('regular'); this.get('content').convertArchetype('regular');
}, },
startTracking: function() {
var screenTrack = Discourse.ScreenTrack.create({ topic_id: this.get('content.id') });
screenTrack.start();
this.set('content.screenTrack', screenTrack);
},
stopTracking: function() {
var screenTrack = this.get('content.screenTrack');
if (screenTrack) screenTrack.stop();
this.set('content.screenTrack', null);
},
// Toggle the star on the topic // Toggle the star on the topic
toggleStar: function(e) { toggleStar: function(e) {
this.get('content').toggleStar(); this.get('content').toggleStar();

View File

@ -92,6 +92,7 @@ Discourse.TopicRoute = Discourse.Route.extend({
topicController.set('multiSelect', false); topicController.set('multiSelect', false);
this.controllerFor('composer').set('topic', null); this.controllerFor('composer').set('topic', null);
Discourse.ScreenTrack.instance().stop();
if (headerController = this.controllerFor('header')) { if (headerController = this.controllerFor('header')) {
headerController.set('topic', null); headerController.set('topic', null);
@ -103,7 +104,11 @@ Discourse.TopicRoute = Discourse.Route.extend({
controller.set('model', model); controller.set('model', model);
this.controllerFor('header').set('topic', model); this.controllerFor('header').set('topic', model);
this.controllerFor('composer').set('topic', model); this.controllerFor('composer').set('topic', model);
Discourse.TopicTrackingState.current().trackIncoming('all');
controller.subscribe(); controller.subscribe();
// We reset screen tracking every time a topic is entered
Discourse.ScreenTrack.instance().start(model.get('id'));
} }
}); });

View File

@ -11,8 +11,11 @@ Discourse.EmbeddedPostView = Discourse.View.extend({
classNames: ['reply'], classNames: ['reply'],
didInsertElement: function() { didInsertElement: function() {
var postView = this.get('postView') || this.get('parentView.postView'); Discourse.ScreenTrack.instance().track(this.get('elementId'), this.get('post.post_number'));
postView.get('screenTrack').track(this.get('elementId'), this.get('post.post_number')); },
willDestroyElement: function() {
Discourse.ScreenTrack.instance().stopTracking(this.get('elementId'));
} }
}); });

View File

@ -17,17 +17,6 @@ Discourse.PostView = Discourse.View.extend({
'parentPost:replies-above'], 'parentPost:replies-above'],
postBinding: 'content', postBinding: 'content',
// TODO really we should do something cleaner here... this makes it work in debug but feels really messy
screenTrack: function() {
var parentView = this.get('parentView');
var screenTrack = null;
while (parentView && !screenTrack) {
screenTrack = parentView.get('screenTrack');
parentView = parentView.get('parentView');
}
return screenTrack;
}.property('parentView'),
postTypeClass: function() { postTypeClass: function() {
return this.get('post.post_type') === Discourse.Site.instance().get('post_types.moderator_action') ? 'moderator' : 'regular'; return this.get('post.post_type') === Discourse.Site.instance().get('post_types.moderator_action') ? 'moderator' : 'regular';
}.property('post.post_type'), }.property('post.post_type'),
@ -213,7 +202,11 @@ Discourse.PostView = Discourse.View.extend({
}); });
}, },
didInsertElement: function(e) { willDestroyElement: function() {
Discourse.ScreenTrack.instance().stopTracking(this.$().prop('id'));
},
didInsertElement: function() {
var $post = this.$(); var $post = this.$();
var post = this.get('post'); var post = this.get('post');
var postNumber = post.get('scrollToAfterInsert'); var postNumber = post.get('scrollToAfterInsert');
@ -233,10 +226,8 @@ Discourse.PostView = Discourse.View.extend({
} }
this.showLinkCounts(); this.showLinkCounts();
var screenTrack = this.get('screenTrack'); // Track this post
if (screenTrack) { Discourse.ScreenTrack.instance().track(this.$().prop('id'), this.get('post.post_number'));
screenTrack.track(this.$().prop('id'), this.get('post.post_number'));
}
// Add syntax highlighting // Add syntax highlighting
Discourse.SyntaxHighlighting.apply($post); Discourse.SyntaxHighlighting.apply($post);

View File

@ -48,8 +48,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
}.observes('progressPosition', 'topic.filtered_posts_count', 'topic.loaded'), }.observes('progressPosition', 'topic.filtered_posts_count', 'topic.loaded'),
updateTitle: function() { updateTitle: function() {
var title; var title = this.get('topic.title');
title = this.get('topic.title');
if (title) return Discourse.set('title', title); if (title) return Discourse.set('title', title);
}.observes('topic.loaded', 'topic.title'), }.observes('topic.loaded', 'topic.title'),
@ -95,19 +94,15 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
// This view is being removed. Shut down operations // This view is being removed. Shut down operations
willDestroyElement: function() { willDestroyElement: function() {
var screenTrack, controller;
this.unbindScrolling(); this.unbindScrolling();
$(window).unbind('resize.discourse-on-scroll'); $(window).unbind('resize.discourse-on-scroll');
controller = this.get('controller'); // Unbind link tracking
controller.set('onPostRendered', null); this.$().off('mouseup.discourse-redirect', '.cooked a, a.track-link');
screenTrack = this.get('screenTrack'); this.get('controller').set('onPostRendered', null);
if (screenTrack) {
screenTrack.stop();
}
this.set('screenTrack', null);
this.resetExamineDockCache(); this.resetExamineDockCache();
// this happens after route exit, stuff could have trickled in // this happens after route exit, stuff could have trickled in
@ -115,8 +110,9 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
}, },
didInsertElement: function(e) { didInsertElement: function(e) {
var topicView = this;
this.bindScrolling({debounce: 0}); this.bindScrolling({debounce: 0});
var topicView = this;
$(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(false); }); $(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(false); });
var controller = this.get('controller'); var controller = this.get('controller');
@ -124,19 +120,11 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
topicView.postsRendered.apply(topicView); topicView.postsRendered.apply(topicView);
}); });
// Insert our screen tracker
var screenTrack = Discourse.ScreenTrack.create({ topic_id: this.get('topic.id') });
screenTrack.start();
this.set('screenTrack', screenTrack);
this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) { this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
return Discourse.ClickTrack.trackClick(e); return Discourse.ClickTrack.trackClick(e);
}); });
this.updatePosition(true); this.updatePosition(true);
// Watch all incoming topic changes
this.get('topicTrackingState').trackIncoming("all");
}, },
debounceLoadSuggested: Discourse.debounce(function(lookup){ debounceLoadSuggested: Discourse.debounce(function(lookup){
@ -177,8 +165,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
}, 50), }, 50),
resetRead: function(e) { resetRead: function(e) {
this.get('screenTrack').cancel(); Discourse.ScreenTrack.instance().reset();
this.set('screenTrack', null);
this.get('controller').unsubscribe(); this.get('controller').unsubscribe();
var topicView = this; var topicView = this;
@ -205,11 +192,10 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
// Called for every post seen, returns the post number // Called for every post seen, returns the post number
postSeen: function($post) { postSeen: function($post) {
var post, postNumber, screenTrack; var post = this.getPost($post);
post = this.getPost($post);
if (post) { if (post) {
postNumber = post.get('post_number'); var postNumber = post.get('post_number');
if (postNumber > (this.get('topic.last_read_post_number') || 0)) { if (postNumber > (this.get('topic.last_read_post_number') || 0)) {
this.set('topic.last_read_post_number', postNumber); this.set('topic.last_read_post_number', postNumber);
} }
@ -401,11 +387,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
this.set('progressPosition', post.get('index')); this.set('progressPosition', post.get('index'));
}, },
nonUrgentPositionUpdate: Discourse.debounce(function(opts){ nonUrgentPositionUpdate: Discourse.debounce(function(opts) {
var screenTrack = this.get('screenTrack'); Discourse.ScreenTrack.instance().scrolled();
if(opts.userActive && screenTrack) {
screenTrack.scrolled();
}
this.set('controller.currentPost', opts.currentPost); this.set('controller.currentPost', opts.currentPost);
},500), },500),
@ -414,16 +397,12 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
}, },
updatePosition: function(userActive) { updatePosition: function(userActive) {
var $lastPost, firstLoaded, lastPostOffset, offset,
title, info, rows, screenTrack, _this, currentPost;
_this = this;
rows = $('.topic-post');
var rows = $('.topic-post');
if (!rows || rows.length === 0) { return; } if (!rows || rows.length === 0) { return; }
info = Discourse.Eyeline.analyze(rows);
// if we have no rows // if we have no rows
var info = Discourse.Eyeline.analyze(rows);
if(!info) { return; } if(!info) { return; }
// top on screen // top on screen
@ -432,8 +411,9 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
} }
// bottom of screen // bottom of screen
var currentPost;
if(info.bottom === rows.length-1) { if(info.bottom === rows.length-1) {
currentPost = _this.postSeen($(rows[info.bottom])); currentPost = this.postSeen($(rows[info.bottom]));
this.nextPage($(rows[info.bottom])); this.nextPage($(rows[info.bottom]));
} }
@ -441,8 +421,9 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
this.updateDock(Ember.View.views[rows[info.bottom].id]); this.updateDock(Ember.View.views[rows[info.bottom].id]);
// mark everything on screen read // mark everything on screen read
var topicView = this;
$.each(info.onScreen,function(){ $.each(info.onScreen,function(){
var seen = _this.postSeen($(rows[this])); var seen = topicView.postSeen($(rows[this]));
currentPost = currentPost || seen; currentPost = currentPost || seen;
}); });
@ -461,10 +442,10 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
console.error("can't update position "); console.error("can't update position ");
} }
offset = window.pageYOffset || $('html').scrollTop(); var offset = window.pageYOffset || $('html').scrollTop();
firstLoaded = this.get('firstPostLoaded'); var firstLoaded = this.get('firstPostLoaded');
if (!this.docAt) { if (!this.docAt) {
title = $('#topic-title'); var title = $('#topic-title');
if (title && title.length === 1) { if (title && title.length === 1) {
this.docAt = title.offset().top; this.docAt = title.offset().top;
} }
@ -478,8 +459,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
} }
// there is a whole bunch of caching we could add here // there is a whole bunch of caching we could add here
$lastPost = $('.last-post'); var $lastPost = $('.last-post');
lastPostOffset = $lastPost.offset(); var lastPostOffset = $lastPost.offset();
if (!lastPostOffset) return; if (!lastPostOffset) return;
if (offset >= (lastPostOffset.top + $lastPost.height()) - $(window).height()) { if (offset >= (lastPostOffset.top + $lastPost.height()) - $(window).height()) {
@ -499,7 +480,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
return Discourse.TopicTrackingState.current(); return Discourse.TopicTrackingState.current();
}.property(), }.property(),
browseMoreMessage: (function() { browseMoreMessage: function() {
var category, opts; var category, opts;
opts = { opts = {
@ -534,7 +515,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
} else { } else {
return Ember.String.i18n("topic.read_more", opts); return Ember.String.i18n("topic.read_more", opts);
} }
}).property('topicTrackingState.messageCount') }.property('topicTrackingState.messageCount')
}); });