diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js index d4dd5ac9ef7..7481e49f2d7 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js @@ -200,13 +200,13 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected }, replyAsNewTopic: function(post) { - var composerController = this.get('controllers.composer'); - var promise = composerController.open({ - action: Discourse.Composer.CREATE_TOPIC, - draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY - }); - var postUrl = "" + location.protocol + "//" + location.host + (post.get('url')); - var postLink = "[" + (this.get('title')) + "](" + postUrl + ")"; + var composerController = this.get('controllers.composer'), + promise = composerController.open({ + action: Discourse.Composer.CREATE_TOPIC, + draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY + }), + postUrl = "" + location.protocol + "//" + location.host + (post.get('url')), + postLink = "[" + (this.get('title')) + "](" + postUrl + ")"; promise.then(function() { Discourse.Post.loadQuote(post.get('id')).then(function(q) { @@ -459,6 +459,65 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected } return true; } + }, + + // If our current post is changed, notify the router + _currentPostChanged: function() { + var currentPost = this.get('currentPost'); + if (currentPost) { + this.send('postChangedRoute', currentPost); + } + }.observes('currentPost'), + + sawObjects: function(posts) { + if (posts) { + var self = this, + lastReadPostNumber = this.get('last_read_post_number'); + + posts.forEach(function(post) { + var postNumber = post.get('post_number'); + if (postNumber > lastReadPostNumber) { + lastReadPostNumber = postNumber; + } + post.set('read', true); + }); + self.set('last_read_post_number', lastReadPostNumber); + + } + }, + + topVisibleChanged: function(post) { + var postStream = this.get('postStream'), + firstLoadedPost = postStream.get('firstLoadedPost'); + + this.set('currentPost', post.get('post_number')); + + if (firstLoadedPost && firstLoadedPost === post) { + // Note: jQuery shouldn't be done in a controller, but how else can we + // trigger a scroll after a promise resolves in a controller? We need + // to do this to preserve upwards infinte scrolling. + var $body = $('body'), + $elem = $('#post-cloak-' + post.get('post_number')), + distToElement = $body.scrollTop() - $elem.position().top; + + postStream.prependMore().then(function() { + Em.run.next(function () { + $elem = $('#post-cloak-' + post.get('post_number')); + $('html, body').scrollTop($elem.position().top + distToElement); + }); + }); + } + }, + + bottomVisibleChanged: function(post) { + this.set('progressPosition', post.get('post_number')); + + var postStream = this.get('postStream'), + lastLoadedPost = postStream.get('lastLoadedPost'); + + if (lastLoadedPost && lastLoadedPost === post) { + postStream.appendMore(); + } } diff --git a/app/assets/javascripts/discourse/lib/eyeline.js b/app/assets/javascripts/discourse/lib/eyeline.js index 1dc3defb382..74ffd125c37 100644 --- a/app/assets/javascripts/discourse/lib/eyeline.js +++ b/app/assets/javascripts/discourse/lib/eyeline.js @@ -1,12 +1,6 @@ /** Track visible elemnts on the screen. - You can register for triggers on: - - `focusChanged` the top element we're focusing on - - `seenElement` if we've seen the element - @class Eyeline @namespace Discourse @module Discourse @@ -16,107 +10,23 @@ Discourse.Eyeline = function Eyeline(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 @method update **/ Discourse.Eyeline.prototype.update = function() { - var $elements, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight, - _this = this; + var docViewTop = $(window).scrollTop(), + windowHeight = $(window).height(), + docViewBottom = docViewTop + windowHeight, + documentHeight = $(document).height(), + $elements = $(this.selector), + atBottom = false, + foundElement = false, + bottomOffset = $elements.last().offset(), + self = this; - docViewTop = $(window).scrollTop(); - windowHeight = $(window).height(); - docViewBottom = docViewTop + windowHeight; - documentHeight = $(document).height(); - $elements = $(this.selector); - atBottom = false; - - if (bottomOffset = $elements.last().offset()) { + if (bottomOffset) { atBottom = (bottomOffset.top <= docViewBottom) && (bottomOffset.top >= docViewTop); } @@ -124,14 +34,12 @@ Discourse.Eyeline.prototype.update = function() { foundElement = false; return $elements.each(function(i, elem) { - var $elem, elemBottom, elemTop, markSeen; + var $elem = $(elem), + elemTop = $elem.offset().top, + elemBottom = elemTop + $elem.height(), + markSeen = false; - $elem = $(elem); - elemTop = $elem.offset().top; - elemBottom = elemTop + $elem.height(); - markSeen = false; // It's seen if... - // ...the element is vertically within the top and botom if ((elemTop <= docViewBottom) && (elemTop >= docViewTop)) markSeen = true; @@ -145,19 +53,17 @@ Discourse.Eyeline.prototype.update = function() { // If you hit the bottom we mark all the elements as seen. Otherwise, just the first one if (!atBottom) { - _this.trigger('saw', { - detail: $elem - }); + self.trigger('saw', { detail: $elem }); if (i === 0) { - _this.trigger('sawTop', { detail: $elem }); + self.trigger('sawTop', { detail: $elem }); } return false; } if (i === 0) { - _this.trigger('sawTop', { detail: $elem }); + self.trigger('sawTop', { detail: $elem }); } if (i === ($elements.length - 1)) { - return _this.trigger('sawBottom', { detail: $elem }); + return self.trigger('sawBottom', { detail: $elem }); } }); }; @@ -169,10 +75,9 @@ Discourse.Eyeline.prototype.update = function() { @method flushRest **/ Discourse.Eyeline.prototype.flushRest = function() { - var eyeline = this; - return $(this.selector).each(function(i, elem) { - var $elem = $(elem); - return eyeline.trigger('saw', { detail: $elem }); + var self = this; + $(this.selector).each(function(i, elem) { + return self.trigger('saw', { detail: $(elem) }); }); }; diff --git a/app/assets/javascripts/discourse/lib/screen_track.js b/app/assets/javascripts/discourse/lib/screen_track.js index c5f7c7947b0..1627092e3db 100644 --- a/app/assets/javascripts/discourse/lib/screen_track.js +++ b/app/assets/javascripts/discourse/lib/screen_track.js @@ -6,10 +6,13 @@ @namespace Discourse @module Discourse **/ + +var PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3, + MAX_TRACKING_TIME = 1000 * 60 * 6; + Discourse.ScreenTrack = Ember.Object.extend({ init: function() { - var screenTrack = this; this.reset(); }, @@ -24,9 +27,9 @@ Discourse.ScreenTrack = Ember.Object.extend({ // Create an interval timer if we don't have one. if (!this.get('interval')) { - var screenTrack = this; + var self = this; this.set('interval', setInterval(function () { - screenTrack.tick(); + self.tick(); }, 1000)); } @@ -57,13 +60,15 @@ Discourse.ScreenTrack = Ember.Object.extend({ // Reset our timers reset: function() { - this.set('lastTick', new Date().getTime()); - this.set('lastScrolled', new Date().getTime()); - this.set('lastFlush', 0); - this.set('cancelled', false); - this.set('timings', {}); - this.set('totalTimings', {}); - this.set('topicTime', 0); + this.setProperties({ + lastTick: new Date().getTime(), + lastScrolled: new Date().getTime(), + lastFlush: 0, + cancelled: false, + timings: {}, + totalTimings: {}, + topicTime: 0 + }); }, scrolled: function() { @@ -76,24 +81,23 @@ Discourse.ScreenTrack = Ember.Object.extend({ // We don't log anything unless we're logged in if (!Discourse.User.current()) return; - var newTimings = {}; - - // Update our total timings - var totalTimings = this.get('totalTimings'); + var newTimings = {}, + totalTimings = this.get('totalTimings'); _.each(this.get('timings'), function(timing,key) { if (!totalTimings[timing.postNumber]) totalTimings[timing.postNumber] = 0; - if (timing.time > 0 && totalTimings[timing.postNumber] < Discourse.ScreenTrack.MAX_TRACKING_TIME) { + if (timing.time > 0 && totalTimings[timing.postNumber] < MAX_TRACKING_TIME) { totalTimings[timing.postNumber] += timing.time; newTimings[timing.postNumber] = timing.time; } timing.time = 0; }); - var topicId = parseInt(this.get('topicId'), 10); - var highestSeen = 0; + var topicId = parseInt(this.get('topicId'), 10), + highestSeen = 0; + _.each(newTimings, function(time,postNumber) { highestSeen = Math.max(highestSeen, parseInt(postNumber, 10)); }); @@ -103,6 +107,7 @@ Discourse.ScreenTrack = Ember.Object.extend({ highestSeenByTopic[topicId] = highestSeen; Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen); } + if (!$.isEmptyObject(newTimings)) { Discourse.ajax('/topics/timings', { data: { @@ -126,7 +131,7 @@ Discourse.ScreenTrack = Ember.Object.extend({ // If the user hasn't scrolled the browser in a long time, stop tracking time read var sinceScrolled = new Date().getTime() - this.get('lastScrolled'); - if (sinceScrolled > Discourse.ScreenTrack.PAUSE_UNLESS_SCROLLED) { + if (sinceScrolled > PAUSE_UNLESS_SCROLLED) { this.reset(); return; } @@ -142,18 +147,16 @@ Discourse.ScreenTrack = Ember.Object.extend({ if (!Discourse.get("hasFocus")) return; this.set('topicTime', this.get('topicTime') + diff); - var docViewTop = $(window).scrollTop() + $('header').height(); - var docViewBottom = docViewTop + $(window).height(); + var 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. - var screenTrack = this; _.each(this.get('timings'),function(timing,id) { - var $element, elemBottom, elemTop; - $element = $(id); + var $element = $(id); if ($element.length === 1) { - elemTop = $element.offset().top; - elemBottom = elemTop + $element.height(); + var 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))) { @@ -165,13 +168,5 @@ Discourse.ScreenTrack = Ember.Object.extend({ }); -Discourse.ScreenTrack.reopenClass(Discourse.Singleton, { - - // 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 - -}); +Discourse.ScreenTrack.reopenClass(Discourse.Singleton); diff --git a/app/assets/javascripts/discourse/lib/url.js b/app/assets/javascripts/discourse/lib/url.js index c5532686a54..6071aace125 100644 --- a/app/assets/javascripts/discourse/lib/url.js +++ b/app/assets/javascripts/discourse/lib/url.js @@ -32,7 +32,7 @@ Discourse.URL = Em.Object.createWithMixins({ // which triggers a replaceState even though the topic hasn't fully loaded yet! Em.run.next(function() { var location = Discourse.URL.get('router.location'); - if (location.replaceURL) { location.replaceURL(path); } + if (location && location.replaceURL) { location.replaceURL(path); } }); } }, @@ -133,15 +133,21 @@ Discourse.URL = Em.Object.createWithMixins({ Discourse.URL.replaceState(path); var topicController = Discourse.__container__.lookup('controller:topic'), - opts = {}; + opts = {}, + postStream = topicController.get('postStream'); if (newMatches[3]) opts.nearPost = newMatches[3]; - var postStream = topicController.get('postStream'); + var closest = opts.nearPost || 1; + postStream.refresh(opts).then(function() { topicController.setProperties({ - currentPost: opts.nearPost || 1, - progressPosition: opts.nearPost || 1 + currentPost: closest, + progressPosition: closest, + highlightOnInsert: closest, + enteredAt: new Date().getTime().toString() }); + }).then(function() { + Discourse.TopicView.jumpToPost(closest); }); // Abort routing, we have replaced our state. diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js b/app/assets/javascripts/discourse/mixins/scrolling.js index b106836ea12..def3220df78 100644 --- a/app/assets/javascripts/discourse/mixins/scrolling.js +++ b/app/assets/javascripts/discourse/mixins/scrolling.js @@ -7,6 +7,7 @@ @namespace Discourse @module Discourse **/ + Discourse.Scrolling = Em.Mixin.create({ /** @@ -18,20 +19,16 @@ Discourse.Scrolling = Em.Mixin.create({ bindScrolling: function(opts) { opts = opts || {debounce: 100}; - var scrollingMixin = this; - var onScrollMethod; + var self = this, + onScrollMethod = function(e) { + return Em.run.scheduleOnce('afterRender', self, 'scrolled'); + }; if (opts.debounce) { - onScrollMethod = Discourse.debounce(function() { - return scrollingMixin.scrolled(); - }, opts.debounce); - } else { - onScrollMethod = function() { - return scrollingMixin.scrolled(); - }; + onScrollMethod = Discourse.debounce(onScrollMethod, opts.debounce); } - Discourse.ScrollingDOMMethods.bindOnScroll(onScrollMethod); + Discourse.ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name); }, /** @@ -39,8 +36,8 @@ Discourse.Scrolling = Em.Mixin.create({ @method unbindScrolling */ - unbindScrolling: function() { - Discourse.ScrollingDOMMethods.unbindOnScroll(); + unbindScrolling: function(name) { + Discourse.ScrollingDOMMethods.unbindOnScroll(name); } }); @@ -56,14 +53,16 @@ Discourse.Scrolling = Em.Mixin.create({ **/ Discourse.ScrollingDOMMethods = { - bindOnScroll: function(onScrollMethod) { - $(document).bind('touchmove.discourse', onScrollMethod); - $(window).bind('scroll.discourse', onScrollMethod); + bindOnScroll: function(onScrollMethod, name) { + name = name || 'default'; + $(document).bind('touchmove.discourse-' + name, onScrollMethod); + $(window).bind('scroll.discourse-' + name, onScrollMethod); }, - unbindOnScroll: function() { - $(window).unbind('scroll.discourse'); - $(document).unbind('touchmove.discourse'); + unbindOnScroll: function(name) { + name = name || 'default'; + $(window).unbind('scroll.discourse-' + name); + $(document).unbind('touchmove.discourse-' + name); } }; \ No newline at end of file diff --git a/app/assets/javascripts/discourse/models/post_stream.js b/app/assets/javascripts/discourse/models/post_stream.js index 53a03b4750c..5209a33c3e3 100644 --- a/app/assets/javascripts/discourse/models/post_stream.js +++ b/app/assets/javascripts/discourse/models/post_stream.js @@ -50,14 +50,32 @@ Discourse.PostStream = Em.Object.extend({ /** Have we loaded the first post in the stream? - @property firstPostLoaded + @property firstPostPresent **/ - firstPostLoaded: function() { + firstPostPresent: function() { if (!this.get('hasLoadedData')) { return false; } return !!this.get('posts').findProperty('id', this.get('firstPostId')); }.property('hasLoadedData', 'posts.[]', 'firstPostId'), - firstPostNotLoaded: Em.computed.not('firstPostLoaded'), + firstPostNotLoaded: Em.computed.not('firstPostPresent'), + + /** + The first post that we have loaded. Useful for checking to see if we should scroll upwards + + @property firstLoadedPost + **/ + firstLoadedPost: function() { + return _.first(this.get('posts')); + }.property('posts.@each'), + + /** + The last post we have loaded. Useful for checking to see if we should load more + + @property lastLoadedPost + **/ + lastLoadedPost: function() { + return _.last(this.get('posts')); + }.property('posts.@each'), /** Returns the id of the first post in the set @@ -80,14 +98,14 @@ Discourse.PostStream = Em.Object.extend({ /** Have we loaded the last post in the stream? - @property lastPostLoaded + @property loadedAllPosts **/ - lastPostLoaded: function() { + loadedAllPosts: function() { if (!this.get('hasLoadedData')) { return false; } return !!this.get('posts').findProperty('id', this.get('lastPostId')); }.property('hasLoadedData', 'posts.@each.id', 'lastPostId'), - lastPostNotLoaded: Em.computed.not('lastPostLoaded'), + lastPostNotLoaded: Em.computed.not('loadedAllPosts'), /** Returns a JS Object of current stream filter options. It should match the query @@ -163,18 +181,18 @@ Discourse.PostStream = Em.Object.extend({ **/ nextWindow: function() { // If we can't find the last post loaded, bail - var lastPost = _.last(this.get('posts')); - if (!lastPost) { return []; } + var lastLoadedPost = this.get('lastLoadedPost'); + if (!lastLoadedPost) { return []; } // Find the index of the last post loaded, if not found, bail var stream = this.get('stream'); - var lastIndex = this.indexOf(lastPost); + var lastIndex = this.indexOf(lastLoadedPost); if (lastIndex === -1) { return []; } if ((lastIndex + 1) >= this.get('filteredPostsCount')) { return []; } // find our window of posts return stream.slice(lastIndex+1, lastIndex+Discourse.SiteSettings.posts_per_page+1); - }.property('posts.@each', 'stream.@each'), + }.property('lastLoadedPost', 'stream.@each'), /** @@ -197,7 +215,7 @@ Discourse.PostStream = Em.Object.extend({ **/ toggleSummary: function() { this.toggleProperty('summary'); - this.refresh(); + return this.refresh(); }, /** @@ -227,39 +245,31 @@ Discourse.PostStream = Em.Object.extend({ @returns {Ember.Deferred} a promise that is resolved when the posts have been inserted into the stream. **/ refresh: function(opts) { + opts = opts || {}; opts.nearPost = parseInt(opts.nearPost, 10); - var topic = this.get('topic'); - var postStream = this; + var topic = this.get('topic'), + self = this; // Do we already have the post in our list of posts? Jump there. var postWeWant = this.get('posts').findProperty('post_number', opts.nearPost); - if (postWeWant) { - Discourse.TopicView.jumpToPost(topic.get('id'), opts.nearPost); - return Ember.RSVP.reject(); - } + if (postWeWant) { return Ember.RSVP.resolve(); } // TODO: if we have all the posts in the filter, don't go to the server for them. - postStream.set('loadingFilter', true); + self.set('loadingFilter', true); - opts = _.merge(opts, postStream.get('streamFilters')); + opts = _.merge(opts, self.get('streamFilters')); // Request a topicView return Discourse.PostStream.loadTopicView(topic.get('id'), opts).then(function (json) { topic.updateFromJson(json); - postStream.updateFromJson(json.post_stream); - postStream.setProperties({ loadingFilter: false, loaded: true }); + self.updateFromJson(json.post_stream); + self.setProperties({ loadingFilter: false, loaded: true }); - if (opts.nearPost) { - Discourse.TopicView.jumpToPost(topic.get('id'), opts.nearPost); - } else { - Discourse.TopicView.jumpToPost(topic.get('id'), 1); - } - - Discourse.URL.set('queryParams', postStream.get('streamFilters')); - }, function(result) { - postStream.errorLoading(result); + Discourse.URL.set('queryParams', self.get('streamFilters')); + }).fail(function(result) { + self.errorLoading(result); }); }, hasLoadedData: Em.computed.and('hasPosts', 'hasStream'), @@ -271,23 +281,23 @@ Discourse.PostStream = Em.Object.extend({ @returns {Ember.Deferred} a promise that's resolved when the posts have been added. **/ appendMore: function() { - var postStream = this; + var self = this; // Make sure we can append more posts - if (!postStream.get('canAppendMore')) { return Ember.RSVP.reject(); } + if (!self.get('canAppendMore')) { return Ember.RSVP.reject(); } - var postIds = postStream.get('nextWindow'); + var postIds = self.get('nextWindow'); if (Ember.isEmpty(postIds)) { return Ember.RSVP.reject(); } - postStream.set('loadingBelow', true); + self.set('loadingBelow', true); var stopLoading = function() { - postStream.set('loadingBelow', false); + self.set('loadingBelow', false); }; - return postStream.findPostsByIds(postIds).then(function(posts) { + return self.findPostsByIds(postIds).then(function(posts) { posts.forEach(function(p) { - postStream.appendPost(p); + self.appendPost(p); }); stopLoading(); }, stopLoading); @@ -349,7 +359,7 @@ Discourse.PostStream = Em.Object.extend({ }); // If we're at the end of the stream, add the post - if (this.get('lastPostLoaded')) { + if (this.get('loadedAllPosts')) { this.appendPost(post); } @@ -452,11 +462,11 @@ Discourse.PostStream = Em.Object.extend({ // We only trigger if there are no filters active if (!this.get('hasNoFilters')) { return; } - var lastPostLoaded = this.get('lastPostLoaded'); + var loadedAllPosts = this.get('loadedAllPosts'); if (this.get('stream').indexOf(postId) === -1) { this.get('stream').addObject(postId); - if (lastPostLoaded) { this.appendMore(); } + if (loadedAllPosts) { this.appendMore(); } } }, @@ -702,8 +712,8 @@ Discourse.PostStream.reopenClass({ }, loadTopicView: function(topicId, args) { - var opts = _.merge({}, args); - var url = Discourse.getURL("/t/") + topicId; + var opts = _.merge({}, args), + url = Discourse.getURL("/t/") + topicId; if (opts.nearPost) { url += "/" + opts.nearPost; } diff --git a/app/assets/javascripts/discourse/routes/topic_from_params_route.js b/app/assets/javascripts/discourse/routes/topic_from_params_route.js index cbded81cb9e..b940df2603b 100644 --- a/app/assets/javascripts/discourse/routes/topic_from_params_route.js +++ b/app/assets/javascripts/discourse/routes/topic_from_params_route.js @@ -7,16 +7,16 @@ @module Discourse **/ Discourse.TopicFromParamsRoute = Discourse.Route.extend({ + abc: 'asdfasdf', setupController: function(controller, params) { - params = params || {}; params.track_visit = true; - var topic = this.modelFor('topic'); - var postStream = topic.get('postStream'); + var topic = this.modelFor('topic'), + postStream = topic.get('postStream'), + queryParams = Discourse.URL.get('queryParams'); - var queryParams = Discourse.URL.get('queryParams'); if (queryParams) { // Set summary on the postStream if present postStream.set('summary', Em.get(queryParams, 'filter') === 'summary'); @@ -41,9 +41,12 @@ Discourse.TopicFromParamsRoute = Discourse.Route.extend({ topicController.setProperties({ currentPost: closest, progressPosition: closest, - enteredAt: new Date().getTime() + enteredAt: new Date().getTime().toString(), + highlightOnInsert: closest }); + Discourse.TopicView.jumpToPost(closest); + if (topic.present('draft')) { composerController.open({ draft: Discourse.Draft.getLocal(topic.get('draft_key'), topic.get('draft')), diff --git a/app/assets/javascripts/discourse/routes/topic_route.js b/app/assets/javascripts/discourse/routes/topic_route.js index c4031d9d63f..b0804416332 100644 --- a/app/assets/javascripts/discourse/routes/topic_route.js +++ b/app/assets/javascripts/discourse/routes/topic_route.js @@ -7,12 +7,12 @@ @module Discourse **/ Discourse.TopicRoute = Discourse.Route.extend({ + abc: 'def', redirect: function() { Discourse.redirectIfLoginRequired(this); }, actions: { // Modals that can pop up within a topic - showPosterExpansion: function(post) { this.controllerFor('posterExpansion').show(post); }, @@ -61,7 +61,17 @@ Discourse.TopicRoute = Discourse.Route.extend({ splitTopic: function() { Discourse.Route.showModal(this, 'splitTopic', this.modelFor('topic')); - } + }, + + // Use replaceState to update the URL once it changes + postChangedRoute: Discourse.debounce(function(currentPost) { + var topic = this.modelFor('topic'); + if (topic && currentPost) { + var postUrl = topic.get('url'); + if (currentPost > 1) { postUrl += "/" + currentPost; } + Discourse.URL.replaceState(postUrl); + } + }, 1000) }, diff --git a/app/assets/javascripts/discourse/templates/topic.js.handlebars b/app/assets/javascripts/discourse/templates/topic.js.handlebars index e9fd76a1a31..45464c27c95 100644 --- a/app/assets/javascripts/discourse/templates/topic.js.handlebars +++ b/app/assets/javascripts/discourse/templates/topic.js.handlebars @@ -2,7 +2,7 @@ {{#if postStream.loaded}} - {{#if postStream.firstPostLoaded}} + {{#if postStream.firstPostPresent}}
@@ -64,7 +64,7 @@ {{/if}} {{#unless postStream.loadingFilter}} - {{collection itemViewClass="Discourse.PostView" contentBinding="postStream.posts" topicViewBinding="view"}} + {{cloaked-collection cloakView="post" idProperty="post_number" defaultHeight="200" content=postStream.posts}} {{/unless}} {{#if postStream.loadingBelow}} @@ -76,7 +76,7 @@ {{#if postStream.loadingFilter}}
{{i18n loading}}
{{else}} - {{#if postStream.lastPostLoaded}} + {{#if postStream.loadedAllPosts}} {{view Discourse.TopicClosingView topicBinding="model"}} {{view Discourse.TopicFooterButtonsView topicBinding="model"}} diff --git a/app/assets/javascripts/discourse/views/cloaked_collection_view.js b/app/assets/javascripts/discourse/views/cloaked_collection_view.js new file mode 100644 index 00000000000..e03b0a28f51 --- /dev/null +++ b/app/assets/javascripts/discourse/views/cloaked_collection_view.js @@ -0,0 +1,154 @@ +/** + Display a list of cloaked items + + @class CloakedContainerView + @extends Discourse.View + @namespace Discourse + @module Discourse +**/ + +var SLACK_RATIO = 0.75; + +Discourse.CloakedCollectionView = Ember.CollectionView.extend(Discourse.Scrolling, { + topVisible: null, + bottomVisible: null, + + init: function() { + var cloakView = this.get('cloakView'), + idProperty = this.get('idProperty') || 'id'; + + this.set('itemViewClass', Discourse.CloakedView.extend({ + classNames: [cloakView + '-cloak'], + cloaks: Em.String.classify(cloakView) + 'View', + defaultHeight: this.get('defaultHeight') || 100, + + init: function() { + this._super(); + this.set('elementId', cloakView + '-cloak-' + this.get('content.' + idProperty)); + } + })); + + this._super(); + Ember.run.next(this, 'scrolled'); + }, + + /** + If the topmost visible view changed, we will notify the controller if it has an appropriate hook. + + @method _topVisibleChanged + @observes topVisible + **/ + _topVisibleChanged: function() { + var controller = this.get('controller'); + if (controller.topVisibleChanged) { controller.topVisibleChanged(this.get('topVisible')); } + }.observes('topVisible'), + + /** + If the bottommost visible view changed, we will notify the controller if it has an appropriate hook. + + @method _bottomVisible + @observes bottomVisible + **/ + _bottomVisible: function() { + var controller = this.get('controller'); + if (controller.bottomVisibleChanged) { controller.bottomVisibleChanged(this.get('bottomVisible')); } + }.observes('bottomVisible'), + + /** + Binary search for finding the topmost view on screen. + + @method findTopView + @param {Array} childViews the childViews to search through + @param {Number} windowTop The top of the viewport to search against + @param {Number} min The minimum index to search through of the child views + @param {Number} max The max index to search through of the child views + @returns {Number} the index into childViews of the topmost view + **/ + findTopView: function(childViews, viewportTop, min, max) { + if (max < min) { return min; } + + var mid = Math.floor((min + max) / 2), + $view = childViews[mid].$(), + viewBottom = $view.offset().top + $view.height(); + + if (viewBottom > viewportTop) { + return this.findTopView(childViews, viewportTop, min, mid-1); + } else { + return this.findTopView(childViews, viewportTop, mid+1, max); + } + }, + + /** + Determine what views are onscreen and cloak/uncloak them as necessary. + + @method scrolled + **/ + scrolled: function() { + var childViews = this.get('childViews'), + toUncloak = [], + $w = $(window), + windowHeight = $w.height(), + windowTop = $w.scrollTop(), + slack = Math.round(windowHeight * SLACK_RATIO), + viewportTop = windowTop - slack, + windowBottom = windowTop + windowHeight, + viewportBottom = windowBottom + slack, + topView = this.findTopView(childViews, viewportTop, 0, childViews.length-1), + bodyHeight = $('body').height(), + bottomView = topView, + onscreen = []; + + if (windowBottom > bodyHeight) { windowBottom = bodyHeight; } + if (viewportBottom > bodyHeight) { viewportBottom = bodyHeight; } + + // Find the bottom view and what's onscreen + while (bottomView < childViews.length) { + var view = childViews[bottomView], + $view = view.$(), + viewTop = $view.offset().top, + viewBottom = viewTop + $view.height(); + + if (viewTop > viewportBottom) { break; } + toUncloak.push(view); + + if (viewBottom > windowTop && viewTop <= windowBottom) { + onscreen.push(view.get('content')); + } + + bottomView++; + } + if (bottomView >= childViews.length) { bottomView = childViews.length - 1; } + + // If our controller has a `sawObjects` method, pass the on screen objects to it. + var controller = this.get('controller'); + if (onscreen.length) { + this.setProperties({topVisible: onscreen[0], bottomVisible: onscreen[onscreen.length-1]}); + if (controller && controller.sawObjects) { + Em.run.schedule('afterRender', function() { + controller.sawObjects(onscreen); + }); + } + } else { + this.setProperties({topVisible: null, bottomVisible: null}); + } + + var toCloak = childViews.slice(0, topView).concat(childViews.slice(bottomView+1)); + Em.run.schedule('afterRender', function() { + toUncloak.forEach(function (v) { v.uncloak(); }); + toCloak.forEach(function (v) { v.cloak(); }); + }); + + }, + + didInsertElement: function() { + this.bindScrolling({debounce: 10}); + }, + + willDestroyElement: function() { + this.unbindScrolling(); + } + +}); + + +Discourse.View.registerHelper('cloaked-collection', Discourse.CloakedCollectionView); \ No newline at end of file diff --git a/app/assets/javascripts/discourse/views/cloaked_view.js b/app/assets/javascripts/discourse/views/cloaked_view.js new file mode 100644 index 00000000000..e32c4501755 --- /dev/null +++ b/app/assets/javascripts/discourse/views/cloaked_view.js @@ -0,0 +1,75 @@ +/** + A cloaked view is one that removes its content when scrolled off the screen + + @class CloakedView + @extends Discourse.View + @namespace Discourse + @module Discourse +**/ +Discourse.CloakedView = Discourse.View.extend({ + attributeBindings: ['style'], + + init: function() { + this._super(); + this.set('style', 'height: ' + this.get('defaultHeight') + 'px'); + }, + + /** + Triggers the set up for rendering a view that is cloaked. + + @method uncloak + */ + uncloak: function() { + var containedView = this.get('containedView'); + if (!containedView) { + this.setProperties({ + style: null, + containedView: this.createChildView(Discourse[this.get('cloaks')], { content: this.get('content') }) + }); + + this.rerender(); + } + }, + + /** + Removes the view from the DOM and tears down all observers. + + @method cloak + */ + cloak: function() { + var containedView = this.get('containedView'), + self = this; + + if (containedView && this.get('state') === 'inDOM') { + var style = 'height: ' + this.$().height() + 'px;'; + this.set('style', style); + this.$().prop('style', style); + + // We need to remove the container after the height of the element has taken + // effect. + Ember.run.schedule('afterRender', function() { + self.set('containedView', null); + containedView.willDestroyElement(); + containedView.remove(); + }); + } + }, + + + /** + Render the cloaked view if applicable. + + @method render + */ + render: function(buffer) { + var containedView = this.get('containedView'); + if (containedView && containedView.get('state') !== 'inDOM') { + containedView.renderToBuffer(buffer); + containedView.transitionTo('inDOM'); + Em.run.schedule('afterRender', function() { + containedView.didInsertElement(); + }); + } + } + +}); diff --git a/app/assets/javascripts/discourse/views/discourse_text_field.js b/app/assets/javascripts/discourse/views/discourse_text_field.js index 87d562d4d04..098324ed859 100644 --- a/app/assets/javascripts/discourse/views/discourse_text_field.js +++ b/app/assets/javascripts/discourse/views/discourse_text_field.js @@ -10,8 +10,7 @@ Discourse.TextField = Ember.TextField.extend({ attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus'], placeholder: function() { - - if( this.get('placeholderKey') ) { + if (this.get('placeholderKey')) { return I18n.t(this.get('placeholderKey')); } else { return ''; diff --git a/app/assets/javascripts/discourse/views/post_view.js b/app/assets/javascripts/discourse/views/post_view.js index 592574be893..0d8a677e347 100644 --- a/app/assets/javascripts/discourse/views/post_view.js +++ b/app/assets/javascripts/discourse/views/post_view.js @@ -172,18 +172,17 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, { // Add the quote controls to a post insertQuoteControls: function() { - var postView = this; - + var self = this; return this.$('aside.quote').each(function(i, e) { var $aside = $(e); - postView.updateQuoteElements($aside, 'chevron-down'); + self.updateQuoteElements($aside, 'chevron-down'); var $title = $('.title', $aside); // Unless it's a full quote, allow click to expand if (!($aside.data('full') || $title.data('has-quote-controls'))) { $title.on('click', function(e) { if ($(e.target).is('a')) return true; - postView.toggleQuote($aside); + self.toggleQuote($aside); }); $title.data('has-quote-controls', true); } @@ -191,17 +190,34 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, { }, willDestroyElement: function() { - Discourse.ScreenTrack.current().stopTracking(this.$().prop('id')); + Discourse.ScreenTrack.current().stopTracking(this.get('elementId')); }, didInsertElement: function() { var $post = this.$(), - post = this.get('post'); + post = this.get('post'), + postNumber = post.get('post_number'), + highlightNumber = this.get('controller.highlightOnInsert'); + + // If we're meant to highlight a post + if ((highlightNumber > 1) && (highlightNumber === postNumber)) { + this.set('controller.highlightOnInsert', null); + var $contents = $('.topic-body .contents', $post), + origColor = $contents.data('orig-color') || $contents.css('backgroundColor'); + + $contents.data("orig-color", origColor); + $contents + .addClass('highlighted') + .stop() + .animate({ backgroundColor: origColor }, 2500, 'swing', function(){ + $contents.removeClass('highlighted'); + }); + } this.showLinkCounts(); // Track this post - Discourse.ScreenTrack.current().track(this.$().prop('id'), this.get('post.post_number')); + Discourse.ScreenTrack.current().track(this.$().prop('id'), postNumber); // Add syntax highlighting Discourse.SyntaxHighlighting.apply($post); @@ -211,7 +227,5 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, { // Find all the quotes this.insertQuoteControls(); - - $post.addClass('ready'); } }); diff --git a/app/assets/javascripts/discourse/views/topic_view.js b/app/assets/javascripts/discourse/views/topic_view.js index 106994d3956..feffb23c081 100644 --- a/app/assets/javascripts/discourse/views/topic_view.js +++ b/app/assets/javascripts/discourse/views/topic_view.js @@ -1,3 +1,5 @@ +/*global LockOn:true*/ + /** This view is for rendering an icon representing the status of a topic @@ -21,10 +23,10 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { postStream: Em.computed.alias('controller.postStream'), updateBar: function() { - Em.run.scheduleOnce('afterRender', this, 'updateProgressBar'); + Em.run.scheduleOnce('afterRender', this, '_updateProgressBar'); }.observes('controller.streamPercentage'), - updateProgressBar: function() { + _updateProgressBar: function() { var $topicProgress = this._topicProgress; // cache lookup @@ -45,66 +47,36 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { .width(progressWidth); }, - updateTitle: function() { + _updateTitle: function() { var title = this.get('topic.title'); if (title) return Discourse.set('title', title); }.observes('topic.loaded', 'topic.title'), - currentPostChanged: function() { - var current = this.get('controller.currentPost'); - - var topic = this.get('topic'); - if (!(current && topic)) return; - - if (current > (this.get('maxPost') || 0)) { - this.set('maxPost', current); - } - - var postUrl = topic.get('url'); - if (current > 1) { postUrl += "/" + current; } - // TODO: @Robin, this should all be integrated into the router, - // the view should not be performing routing work - // - // This workaround ensures the router is aware the route changed, - // without it, the up button was broken on long topics. - // To repro, go to a topic with 50 posts, go to first post, - // scroll to end, click up button ... nothing happens - var handler =_.first( - _.where(Discourse.URL.get("router.router.currentHandlerInfos"), - function(o) { - return o.name === "topic.fromParams"; - }) - ); - if(handler){ - handler.context = {nearPost: current}; - } - Discourse.URL.replaceState(postUrl); - }.observes('controller.currentPost', 'highest_post_number'), - - composeChanged: function() { + _composeChanged: function() { var composerController = Discourse.get('router.composerController'); composerController.clearState(); composerController.set('topic', this.get('topic')); }.observes('composer'), - enteredTopic: function() { + _enteredTopic: function() { this._topicProgress = undefined; - if (this.present('controller.enteredAt')) { - var topicView = this; - Em.run.schedule('afterRender', function() { - topicView.updatePosition(); - }); + + // Ember is supposed to only call observers when values change but something + // in our view set up is firing this observer with the same value. This check + // prevents scrolled from being called twice. + var enteredAt = this.get('controller.enteredAt'); + if (enteredAt && (this.get('lastEnteredAt') !== enteredAt)) { + this.scrolled(); + this.set('lastEnteredAt', enteredAt); } }.observes('controller.enteredAt'), didInsertElement: function(e) { - this.bindScrolling({debounce: 0}); + this.bindScrolling({name: 'topic-view'}); var topicView = this; - Em.run.schedule('afterRender', function () { - $(window).resize('resize.discourse-on-scroll', function() { - topicView.updatePosition(); - }); + $(window).resize('resize.discourse-on-scroll', function() { + topicView.scrolled(); }); // This get seems counter intuitive, but it's to trigger the observer on @@ -120,8 +92,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { // This view is being removed. Shut down operations willDestroyElement: function() { - - this.unbindScrolling(); + this.unbindScrolling('topic-view'); $(window).unbind('resize.discourse-on-scroll'); // Unbind link tracking @@ -170,128 +141,16 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { } }.observes("Discourse.hasFocus"), - getPost: function($post){ - var post, postView; - postView = Ember.View.views[$post.prop('id')]; - if (postView) { - return postView.get('post'); - } - return null; - }, - - // Called for every post seen, returns the post number - postSeen: function($post) { - var post = this.getPost($post); - - if (post) { - var postNumber = post.get('post_number'); - if (postNumber > (this.get('controller.last_read_post_number') || 0)) { - this.set('controller.last_read_post_number', postNumber); - } - if (!post.get('read')) { - post.set('read', true); - } - return post.get('post_number'); - } - }, - resetExamineDockCache: function() { this.set('docAt', false); }, - updateDock: function(postView) { - if (!postView) return; - var post = postView.get('post'); - if (!post) return; - - this.set('controller.progressPosition', this.get('postStream').indexOf(post) + 1); - }, - - throttledPositionUpdate: Discourse.debounce(function() { - Discourse.ScreenTrack.current().scrolled(); - var model = this.get('controller.model'); - if (model && this.get('nextPositionUpdate')) { - this.set('controller.currentPost', this.get('nextPositionUpdate')); - } - },500), - - scrolled: function(){ - this.updatePosition(); - }, - - - /** - Process the posts the current user has seen in the topic. - - @private - @method processSeenPosts - **/ - processSeenPosts: function() { - var rows = $('.topic-post.ready'); - if (!rows || rows.length === 0) { return; } - - // if we have no rows - var info = Discourse.Eyeline.analyze(rows); - if(!info) { return; } - - // We disable scrolling of the topic while performing initial positioning - // This code needs to be refactored, the pipline for positioning posts is wack - // Be sure to test on safari as well when playing with this - if(!Discourse.TopicView.disableScroll) { - - // are we scrolling upwards? - if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) { - var $body = $('body'), - $elem = $(rows[0]), - distToElement = $body.scrollTop() - $elem.position().top; - this.get('postStream').prependMore().then(function() { - Em.run.next(function () { - $('html, body').scrollTop($elem.position().top + distToElement); - }); - }); - } - } - - - // are we scrolling down? - var currentPost; - if(info.bottom === rows.length-1) { - currentPost = this.postSeen($(rows[info.bottom])); - this.get('postStream').appendMore(); - } - - - // update dock - this.updateDock(Ember.View.views[rows[info.bottom].id]); - - // mark everything on screen read - var topicView = this; - _.each(info.onScreen,function(item){ - var seen = topicView.postSeen($(rows[item])); - currentPost = currentPost || seen; - }); - - var currentForPositionUpdate = currentPost; - if (!currentForPositionUpdate) { - var postView = this.getPost($(rows[info.bottom])); - if (postView) { currentForPositionUpdate = postView.get('post_number'); } - } - - if (currentForPositionUpdate) { - this.set('nextPositionUpdate', currentPost || currentForPositionUpdate); - this.throttledPositionUpdate(); - } else { - console.error("can't update position "); - } - }, - /** The user has scrolled the window, or it is finished rendering and ready for processing. - @method updatePosition + @method scrolled **/ - updatePosition: function() { - this.processSeenPosts(); + scrolled: function(){ var offset = window.pageYOffset || $('html').scrollTop(); if (!this.get('docAt')) { @@ -324,11 +183,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { }.property(), browseMoreMessage: function() { - var opts = { - latestLink: "" + (I18n.t("topic.view_latest_topics")) + "" - }; - - var category = this.get('controller.content.category'); + var opts = { latestLink: "" + (I18n.t("topic.view_latest_topics")) + "" }, + category = this.get('controller.content.category'); if(Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) { category = null; @@ -340,10 +196,9 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { opts.catLink = "" + (I18n.t("topic.browse_all_categories")) + ""; } - var tracking = this.get('topicTrackingState'); - - var unreadTopics = tracking.countUnread(); - var newTopics = tracking.countNew(); + var tracking = this.get('topicTrackingState'), + unreadTopics = tracking.countUnread(), + newTopics = tracking.countNew(); if (newTopics + unreadTopics > 0) { var hasBoth = unreadTopics > 0 && newTopics > 0; @@ -368,88 +223,23 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { Discourse.TopicView.reopenClass({ - // Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not. - jumpToPost: function(topicId, postNumber, avoidScrollIfPossible) { - this.disableScroll = true; - Em.run.scheduleOnce('afterRender', function() { - var rows = $('.topic-post.ready'); + jumpToPost: function(postNumber) { + var holderId = '#post-cloak-' + postNumber; - // Make sure we're looking at the topic we want to scroll to - if (topicId !== parseInt($('#topic').data('topic-id'), 10)) { return false; } - - var $post = $("#post_" + postNumber); - if ($post.length) { - - var postTop = $post.offset().top; - var highlight = true; - - var header = $('header'); - var title = $('#topic-title'); - var expectedOffset = title.height() - header.find('.contents').height(); - - if (expectedOffset < 0) { - expectedOffset = 0; - } - - var offset = (header.outerHeight(true) + expectedOffset); - var windowScrollTop = $('html, body').scrollTop(); - - if (avoidScrollIfPossible && postTop > windowScrollTop + offset && postTop < windowScrollTop + $(window).height() + 100) { - // in view - } else { - // not in view ... bring into view - if (postNumber === 1) { - $(window).scrollTop(0); - highlight = false; - } else { - var desired = $post.offset().top - offset; - $(window).scrollTop(desired); - - // TODO @Robin, I am seeing multiple events in chrome issued after - // jumpToPost if I refresh a page, sometimes I see 2, sometimes 3 - // - // 1. Where are they coming from? - // 2. On refresh we should only issue a single scrollTop - // 3. If you are scrolled down in BoingBoing desired sometimes is wrong - // due to vanishing header, we should not be rendering it imho until after - // we render the posts - - var first = true; - var t = new Date(); - // console.log("DESIRED:" + desired); - var enforceDesired = function(){ - if($(window).scrollTop() !== desired) { - console.log("GOT EVENT " + $(window).scrollTop()); - console.log("Time " + (new Date() - t)); - console.trace(); - if(first) { - $(window).scrollTop(desired); - first = false; - } - // $(document).unbind("scroll", enforceDesired); - } - }; - - // uncomment this line to help debug this issue. - // $(document).scroll(enforceDesired); - } - } - - if(highlight) { - var $contents = $('.topic-body .contents', $post); - var origColor = $contents.data('orig-color') || $contents.css('backgroundColor'); - - $contents.data("orig-color", origColor); - $contents - .addClass('highlighted') - .stop() - .animate({ backgroundColor: origColor }, 2500, 'swing', function(){ - $contents.removeClass('highlighted'); - }); - } - - setTimeout(function(){Discourse.TopicView.disableScroll = false;}, 500); + Em.run.schedule('afterRender', function() { + if (postNumber === 1) { + $(window).scrollTop(0); + return; } + + new LockOn(holderId, {offsetCalculator: function() { + var $header = $('header'), + $title = $('#topic-title'), + expectedOffset = $title.height() - $header.find('.contents').height(); + + return $header.outerHeight(true) + ((expectedOffset < 0) ? 0 : expectedOffset); + }}).lock(); }); } + }); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 108ea4a3ace..4bf7b692391 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -31,6 +31,7 @@ //= require mousetrap.js //= require rsvp.js //= require show-html.js +//= require lock-on.js //= require ./discourse/helpers/i18n_helpers //= require ./discourse/mixins/ajax diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index cbfc78b79a8..2a028a902ef 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -322,7 +322,7 @@ body { color: inherit; text-rendering: optimizelegibility; } - + // this removes the unwanted top margin on a paragraph under a heading h1+p, h2+p, h3+p, h4+p, h5+p, h6+p { diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 5fca5cd19f1..6ac9de03160 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -17,7 +17,7 @@ h1 .topic-statuses .topic-status i {margin-right: 5px;} .logo-small {margin-right: 8px;} -.topic-post { +.post-cloak { padding: 0; &:first-of-type { diff --git a/test/javascripts/integration/view_topic_test.js b/test/javascripts/integration/view_topic_test.js index 5dd092eeb53..1bd4cca6f87 100644 --- a/test/javascripts/integration/view_topic_test.js +++ b/test/javascripts/integration/view_topic_test.js @@ -5,6 +5,6 @@ test("Enter a Topic", function() { visit("/t/internationalization-localization/280").then(function() { ok(exists("#topic"), "The was rendered"); - ok(exists("#topic .topic-post"), "The topic has posts"); + ok(exists("#topic .post-cloak"), "The topic has cloaked posts"); }); }); diff --git a/test/javascripts/models/post_stream_test.js b/test/javascripts/models/post_stream_test.js index 0344b9d6245..6e4e74faa77 100644 --- a/test/javascripts/models/post_stream_test.js +++ b/test/javascripts/models/post_stream_test.js @@ -29,17 +29,17 @@ test('appending posts', function() { equal(postStream.get('lastPostId'), 4, "the last post id is 4"); ok(!postStream.get('hasPosts'), "there are no posts by default"); - ok(!postStream.get('firstPostLoaded'), "the first post is not loaded"); - ok(!postStream.get('lastPostLoaded'), "the last post is not loaded"); + ok(!postStream.get('firstPostPresent'), "the first post is not loaded"); + ok(!postStream.get('loadedAllPosts'), "the last post is not loaded"); equal(postStream.get('posts.length'), 0, "it has no posts initially"); postStream.appendPost(Discourse.Post.create({id: 2, post_number: 2})); - ok(!postStream.get('firstPostLoaded'), "the first post is still not loaded"); + ok(!postStream.get('firstPostPresent'), "the first post is still not loaded"); equal(postStream.get('posts.length'), 1, "it has one post in the stream"); postStream.appendPost(Discourse.Post.create({id: 4, post_number: 4})); - ok(!postStream.get('firstPostLoaded'), "the first post is still loaded"); - ok(postStream.get('lastPostLoaded'), "the last post is now loaded"); + ok(!postStream.get('firstPostPresent'), "the first post is still loaded"); + ok(postStream.get('loadedAllPosts'), "the last post is now loaded"); equal(postStream.get('posts.length'), 2, "it has two posts in the stream"); postStream.appendPost(Discourse.Post.create({id: 4, post_number: 4})); @@ -54,8 +54,8 @@ test('appending posts', function() { // change the stream postStream.set('stream', [1, 2, 4]); - ok(!postStream.get('firstPostLoaded'), "the first post no longer loaded since the stream changed."); - ok(postStream.get('lastPostLoaded'), "the last post is still the last post in the new stream"); + ok(!postStream.get('firstPostPresent'), "the first post no longer loaded since the stream changed."); + ok(postStream.get('loadedAllPosts'), "the last post is still the last post in the new stream"); }); test('closestPostNumberFor', function() { @@ -383,18 +383,18 @@ test('triggerNewPostInStream', function() { }); -test("lastPostLoaded when the id changes", function() { +test("loadedAllPosts when the id changes", function() { // This can happen in a race condition between staging a post and it coming through on the - // message bus. If the id of a post changes we should reconsider the lastPostLoaded property. + // message bus. If the id of a post changes we should reconsider the loadedAllPosts property. var postStream = buildStream(10101, [1, 2]); var postWithoutId = Discourse.Post.create({ raw: 'hello world this is my new post' }); postStream.appendPost(Discourse.Post.create({id: 1, post_number: 1})); postStream.appendPost(postWithoutId); - ok(!postStream.get('lastPostLoaded'), 'the last post is not loaded'); + ok(!postStream.get('loadedAllPosts'), 'the last post is not loaded'); postWithoutId.set('id', 2); - ok(postStream.get('lastPostLoaded'), 'the last post is loaded now that the post has an id'); + ok(postStream.get('loadedAllPosts'), 'the last post is loaded now that the post has an id'); }); test("comitting and triggerNewPostInStream race condition", function() { diff --git a/vendor/assets/javascripts/development/ember.js b/vendor/assets/javascripts/development/ember.js index ad2f514558e..5977575f57b 100755 --- a/vendor/assets/javascripts/development/ember.js +++ b/vendor/assets/javascripts/development/ember.js @@ -2258,7 +2258,7 @@ function suspendListener(obj, eventName, target, method, callback) { Suspends multiple listeners during a callback. - + @method suspendListeners @for Ember @param obj @@ -2326,7 +2326,7 @@ function watchedEvents(obj) { is skipped, and once listeners are removed. A listener without a target is executed on the passed object. If an array of actions is not passed, the actions stored on the passed object are invoked. - + @method sendEvent @for Ember @param obj @@ -3099,14 +3099,14 @@ Map.create = function() { Map.prototype = { /** This property will change as the number of objects in the map changes. - + @property length @type number @default 0 */ length: 0, - - + + /** Retrieve the value associated with a given key. @@ -12589,7 +12589,7 @@ Ember.computed.sort = function (itemsKey, sortDefinition) { (function() { /** Expose RSVP implementation - + Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md @class RSVP @@ -14838,7 +14838,7 @@ function installPromise(proxy, promise) { controller.get('lastName') //=> 'Penner' ``` - If the controller is backing a template, the attributes are + If the controller is backing a template, the attributes are bindable from within that template ```handlebars @@ -16338,7 +16338,7 @@ function classToString() { if (this[NAME_KEY]) { ret = this[NAME_KEY]; } else if (this._toString) { - ret = this._toString; + ret = this._toString; } else { var str = superClassString(this); if (str) { @@ -17935,8 +17935,8 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; ```javascript songsController.get('content').get('firstObject'); // Returns the unsorted original content songsController.get('firstObject'); // Returns the sorted content. - ``` - + ``` + Although the sorted content can also be accessed through the arrangedContent property, it is preferable to use the proxied class and not the arrangedContent array directly. @@ -18025,7 +18025,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. Also sets up observers for each sortProperty on each item in the content Array. - + @property arrangedContent */ @@ -22075,7 +22075,7 @@ Ember.merge(inBuffer, { // when a view is rendered in a buffer, rerendering it simply // replaces the existing buffer with a new one rerender: function(view) { - throw new Ember.Error("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM."); + throw new Ember.Error("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM." + view.get('content.id')); }, // when a view is rendered in a buffer, appending a child @@ -23581,7 +23581,7 @@ define("metamorph", /** * @public - * + * * Remove this object (including starting and ending * placeholders). * @@ -27990,7 +27990,7 @@ helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { - + var buffer = '', hashTypes, hashContexts; data.buffer.push("