refactor analysis of read posts and centralize logic

This commit is contained in:
Sam 2013-03-22 00:40:08 -07:00
parent a73c9dd530
commit e99f137316
4 changed files with 154 additions and 146 deletions

View file

@ -14,7 +14,91 @@
**/ **/
Discourse.Eyeline = function Eyeline(selector) { Discourse.Eyeline = function Eyeline(selector) {
this.selector = selector; this.selector = selector;
} };
/**
Call this to analyze the positions of all the nodes in a set
returns: a hash with top, bottom and onScreen items
{top: , bottom:, onScreen:}
**/
Discourse.Eyeline.analyze = function(rows) {
var current, goingUp, i, increment, offset,
winHeight, winOffset, detected, onScreen,
bottom, top, outerHeight;
if (rows.length === 0) return;
i = parseInt(rows.length / 2, 10);
increment = parseInt(rows.length / 4, 10);
goingUp = undefined;
winOffset = window.pageYOffset || $('html').scrollTop();
winHeight = window.innerHeight || $(window).height();
while (true) {
if (i === 0 || (i >= rows.length - 1)) {
break;
}
current = $(rows[i]);
offset = current.offset();
if (offset.top - winHeight < winOffset) {
if (offset.top + current.outerHeight() - window.innerHeight > winOffset) {
break;
} else {
i = i + increment;
if (goingUp !== undefined && increment === 1 && !goingUp) {
break;
}
goingUp = true;
}
} else {
i = i - increment;
if (goingUp !== undefined && increment === 1 && goingUp) {
break;
}
goingUp = false;
}
if (increment > 1) {
increment = parseInt(increment / 2, 10);
goingUp = undefined;
}
if (increment === 0) {
increment = 1;
goingUp = undefined;
}
}
onScreen = [];
bottom = i;
// quick analysis of whats on screen
while(true) {
if(i < 0) { break;}
current = $(rows[i]);
offset = current.offset();
outerHeight = current.outerHeight();
// on screen
if(offset.top > winOffset && offset.top + outerHeight < winOffset + winHeight) {
onScreen.unshift(i);
} else {
if(offset.top < winOffset) {
top = i;
break;
} else {
// bottom
}
}
i -=1;
}
return({top: top, bottom: bottom, onScreen: onScreen});
};
/** /**
Call this whenever you want to consider what is being seen by the browser Call this whenever you want to consider what is being seen by the browser
@ -25,9 +109,6 @@ Discourse.Eyeline.prototype.update = function() {
var $elements, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight, var $elements, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight,
_this = this; _this = this;
// before anything ... let us not do anything if we have no focus
if (!Discourse.get('hasFocus')) { return; }
docViewTop = $(window).scrollTop(); docViewTop = $(window).scrollTop();
windowHeight = $(window).height(); windowHeight = $(window).height();
docViewBottom = docViewTop + windowHeight; docViewBottom = docViewTop + windowHeight;

View file

@ -138,6 +138,9 @@ Discourse.ScreenTrack = Ember.Object.extend({
this.topicTime += diff; this.topicTime += diff;
docViewTop = $(window).scrollTop() + $('header').height(); docViewTop = $(window).scrollTop() + $('header').height();
docViewBottom = docViewTop + $(window).height(); docViewBottom = docViewTop + $(window).height();
// TODO: Eyeline has a smarter more accurate function here
return Object.keys(this.timings, function(id) { return Object.keys(this.timings, function(id) {
var $element, elemBottom, elemTop, timing; var $element, elemBottom, elemTop, timing;
$element = $(id); $element = $(id);

View file

@ -10,16 +10,27 @@
Discourse.Scrolling = Em.Mixin.create({ Discourse.Scrolling = Em.Mixin.create({
/** /**
Begin watching for scroll events. They will be called at max every 100ms. Begin watching for scroll events. By default they will be called at max every 100ms.
call with {debounce: N} for a diff time
@method bindScrolling @method bindScrolling
*/ */
bindScrolling: function() { bindScrolling: function(opts) {
var onScroll, var onScroll,
_this = this; _this = this;
onScroll = Discourse.debounce(function() { return _this.scrolled(); }, 100);
opts = opts || {debounce: 100};
if (opts.debounce) {
onScroll = Discourse.debounce(function() { return _this.scrolled(); }, 100);
} else {
onScroll = function(){ return _this.scrolled(); };
}
$(document).bind('touchmove.discourse', onScroll); $(document).bind('touchmove.discourse', onScroll);
$(window).bind('scroll.discourse', onScroll); $(window).bind('scroll.discourse', onScroll);
// resize is should also fire this cause it causes scrolling of sorts
}, },
/** /**

View file

@ -36,7 +36,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
progressWidth = ratio * totalWidth; progressWidth = ratio * totalWidth;
bg = $topicProgress.find('.bg'); bg = $topicProgress.find('.bg');
bg.stop(true, true); bg.stop(true, true);
currentWidth = bg.width() currentWidth = bg.width();
if (currentWidth === totalWidth) { if (currentWidth === totalWidth) {
bg.width(currentWidth - 1); bg.width(currentWidth - 1);
@ -61,13 +61,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
if (title) return Discourse.set('title', title); if (title) return Discourse.set('title', title);
}).observes('topic.loaded', 'topic.title'), }).observes('topic.loaded', 'topic.title'),
newPostsPresent: (function() {
if (this.get('topic.highest_post_number')) {
this.updateBar();
this.examineRead();
}
}).observes('topic.highest_post_number'),
currentPostChanged: (function() { currentPostChanged: (function() {
var current = this.get('controller.currentPost'); var current = this.get('controller.currentPost');
@ -112,6 +105,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
willDestroyElement: function() { willDestroyElement: function() {
var screenTrack, controller; var screenTrack, controller;
this.unbindScrolling(); this.unbindScrolling();
$(window).unbind('resize.discourse-on-scroll');
controller = this.get('controller'); controller = this.get('controller');
controller.unsubscribe(); controller.unsubscribe();
@ -123,22 +117,13 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
} }
this.set('screenTrack', null); this.set('screenTrack', null);
$(window).unbind('scroll.discourse-on-scroll');
$(document).unbind('touchmove.discourse-on-scroll');
$(window).unbind('resize.discourse-on-scroll');
this.resetExamineDockCache(); this.resetExamineDockCache();
}, },
didInsertElement: function(e) { didInsertElement: function(e) {
var topicView = this; var topicView = this;
var onScroll = Discourse.debounce(function() { return topicView.onScroll(); }, 10); this.bindScrolling({debounce: 0});
$(window).bind('resize.discourse-on-scroll', function() { scrolled(false); });
$(window).bind('scroll.discourse-on-scroll', onScroll);
$(document).bind('touchmove.discourse-on-scroll', onScroll);
$(window).bind('resize.discourse-on-scroll', onScroll);
this.bindScrolling();
var controller = this.get('controller'); var controller = this.get('controller');
controller.subscribe(); controller.subscribe();
@ -151,44 +136,19 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
screenTrack.start(); screenTrack.start();
this.set('screenTrack', screenTrack); this.set('screenTrack', screenTrack);
// Track the user's eyeline
var eyeline = new Discourse.Eyeline('.topic-post');
eyeline.on('saw', function(e) {
topicView.postSeen(e.detail);
});
eyeline.on('sawBottom', function(e) {
topicView.postSeen(e.detail);
topicView.nextPage(e.detail);
});
eyeline.on('sawTop', function(e) {
topicView.postSeen(e.detail);
topicView.prevPage(e.detail);
});
this.set('eyeline', eyeline);
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.onScroll(); this.scrolled();
}, },
// Triggered whenever any posts are rendered, debounced to save over calling // Triggered whenever any posts are rendered, debounced to save over calling
postsRendered: Discourse.debounce(function() { postsRendered: Discourse.debounce(function() {
var $window = $(window); var $window = $(window);
var $lastPost = $('.row:last'); var $lastPost = $('.row:last');
this.scrolled(false);
// we consider stuff at the end of the list as read, right away (if it is visible) }, 50),
if ($window.height() + $window.scrollTop() >= $lastPost.offset().top + $lastPost.height()) {
this.examineRead();
} else {
// last is not in view, so only examine in 2 seconds
var topicView = this;
Em.run.later(function() { topicView.examineRead(); }, 2000);
}
}, 100),
resetRead: function(e) { resetRead: function(e) {
this.get('screenTrack').cancel(); this.get('screenTrack').cancel();
@ -204,18 +164,16 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
gotFocus: function(){ gotFocus: function(){
if (Discourse.get('hasFocus')){ if (Discourse.get('hasFocus')){
this.examineRead(); this.scrolled();
} }
}.observes("Discourse.hasFocus"), }.observes("Discourse.hasFocus"),
// Called for every post seen // Called for every post seen, returns the post number
postSeen: function($post) { postSeen: function($post) {
var post, postView, _ref; var post, postView, _ref;
this.set('postNumberSeen', null);
postView = Ember.View.views[$post.prop('id')]; postView = Ember.View.views[$post.prop('id')];
if (postView) { if (postView) {
post = postView.get('post'); post = postView.get('post');
this.set('postNumberSeen', post.get('post_number'));
if (post.get('post_number') > (this.get('topic.last_read_post_number') || 0)) { if (post.get('post_number') > (this.get('topic.last_read_post_number') || 0)) {
this.set('topic.last_read_post_number', post.get('post_number')); this.set('topic.last_read_post_number', post.get('post_number'));
} }
@ -224,6 +182,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
_ref = this.get('screenTrack'); _ref = this.get('screenTrack');
if (_ref) { _ref.guessedSeen(post.get('post_number')); } if (_ref) { _ref.guessedSeen(post.get('post_number')); }
} }
return post.get('post_number');
} }
}, },
@ -309,22 +268,12 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
}).observes('topic.highest_post_number'), }).observes('topic.highest_post_number'),
loadMore: function(post) { loadMore: function(post) {
if (this.get('controller.loading') || this.get('controller.seenBottom')) return; if (this.get('controller.loading')) { return; }
// Don't load if we know we're at the bottom // Don't load if we know we're at the bottom
if (this.get('topic.highest_post_number') === post.get('post_number')) { if (this.get('topic.highest_post_number') === post.get('post_number')) { return; }
var eyeline = this.get('eyeline');
if (eyeline) {
eyeline.flushRest();
}
// Update our current post to the last number we saw if (this.get('controller.seenBottom')) { return; }
var postNumberSeen = this.get('postNumberSeen');
if (postNumberSeen) {
this.set('controller.currentPost', postNumberSeen);
}
return;
}
// Don't double load ever // Don't double load ever
if (this.topic.posts.last().post_number !== post.post_number) return; if (this.topic.posts.last().post_number !== post.post_number) return;
@ -353,26 +302,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
}); });
}, },
// Examine which posts are on the screen and mark them as read. Also figure out if we
// need to load more posts.
examineRead: function() {
// Track posts time on screen
var postNumberSeen, _ref, _ref1;
if (_ref = this.get('screenTrack')) {
_ref.scrolled();
}
// Update what we can see
if (_ref1 = this.get('eyeline')) {
_ref1.update();
}
// Update our current post to the last number we saw
if (postNumberSeen = this.get('postNumberSeen')) {
this.set('controller.currentPost', postNumberSeen);
}
},
cancelEdit: function() { cancelEdit: function() {
// close editing mode // close editing mode
this.set('editingTopic', false); this.set('editingTopic', false);
@ -415,63 +344,51 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
this.dockedCounter = false; this.dockedCounter = false;
}, },
detectDockPosition: function() { updateDock: function(postView) {
var current, goingUp, i, increment, offset, post, postView, rows, winHeight, winOffset;
rows = $(".topic-post");
if (rows.length === 0) return;
i = parseInt(rows.length / 2, 10);
increment = parseInt(rows.length / 4, 10);
goingUp = undefined;
winOffset = window.pageYOffset || $('html').scrollTop();
winHeight = window.innerHeight || $(window).height();
while (true) {
if (i === 0 || (i >= rows.length - 1)) {
break;
}
current = $(rows[i]);
offset = current.offset();
if (offset.top - winHeight < winOffset) {
if (offset.top + current.outerHeight() - window.innerHeight > winOffset) {
break;
} else {
i = i + increment;
if (goingUp !== undefined && increment === 1 && !goingUp) {
break;
}
goingUp = true;
}
} else {
i = i - increment;
if (goingUp !== undefined && increment === 1 && goingUp) {
break;
}
goingUp = false;
}
if (increment > 1) {
increment = parseInt(increment / 2, 10);
goingUp = undefined;
}
if (increment === 0) {
increment = 1;
goingUp = undefined;
}
}
postView = Ember.View.views[rows[i].id];
if (!postView) return; if (!postView) return;
post = postView.get('post'); post = postView.get('post');
if (!post) return; if (!post) return;
this.set('progressPosition', post.get('post_number')); this.set('progressPosition', post.get('post_number'));
}, },
ensureDockIsTestedOnChange: (function() { nonUrgentScrolled: Discourse.debounce(function(opts){
// this is subtle, firstPostLoaded will trigger ember to render the view containing #topic-title var screenTrack = this.get('screenTrack');
// onScroll needs do know about it to be able to make a decision about the dock if(opts.track && screenTrack) {
Em.run.next(this, this.onScroll); screenTrack.scrolled();
}).observes('firstPostLoaded'), }
this.set('controller.currentPost', opts.currentPost);
},500),
onScroll: function() { scrolled: function(track) {
var $lastPost, firstLoaded, lastPostOffset, offset, title; var $lastPost, firstLoaded, lastPostOffset, offset,
this.detectDockPosition(); title, info, rows, screenTrack, _this, currentPost;
_this = this;
rows = $('.topic-post');
info = Discourse.Eyeline.analyze(rows);
// top on screen
if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
this.prevPage($(rows[0]));
}
// bottom of screen
if(info.bottom === rows.length-1) {
currentPost = _this.postSeen($(rows[info.bottom]));
this.nextPage($(rows[info.bottom]));
}
// update dock
this.updateDock(Ember.View.views[rows[info.bottom].id]);
// mark everything on screen read
$.each(info.onScreen,function(){
var seen = _this.postSeen($(rows[this]));
currentPost = currentPost || seen;
});
this.nonUrgentScrolled({track: track!==false, currentPost: currentPost});
offset = window.pageYOffset || $('html').scrollTop(); offset = window.pageYOffset || $('html').scrollTop();
firstLoaded = this.get('firstPostLoaded'); firstLoaded = this.get('firstPostLoaded');
if (!this.docAt) { if (!this.docAt) {
@ -518,12 +435,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (Em.String.i18n("topic.browse_all_categories")) + "</a>"; opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (Em.String.i18n("topic.browse_all_categories")) + "</a>";
return Ember.String.i18n("topic.read_more", opts); return Ember.String.i18n("topic.read_more", opts);
} }
}).property(), }).property()
// The window has been scrolled
scrolled: function(e) {
return this.examineRead();
}
}); });
Discourse.TopicView.reopenClass({ Discourse.TopicView.reopenClass({