From cc5ca90ac6234483bf3ebf0522df4455cdb044fe Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 24 Feb 2016 17:15:25 -0500
Subject: [PATCH] Cloak posts as they scroll off the screen for memory reasons

---
 .../components/scrolling-post-stream.js.es6   | 26 +++++++++++++----
 .../discourse/widgets/post-stream.js.es6      | 28 +++++++++++++++++--
 2 files changed, 47 insertions(+), 7 deletions(-)

diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
index b18152e8c..032a08c45 100644
--- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
@@ -1,6 +1,7 @@
 import DiscourseURL from 'discourse/lib/url';
 import { keyDirty } from 'discourse/widgets/widget';
 import MountWidget from 'discourse/components/mount-widget';
+import { cloak, uncloak } from 'discourse/widgets/post-stream';
 
 function findTopView($posts, viewportTop, min, max) {
   if (max < min) { return min; }
@@ -45,7 +46,7 @@ export default MountWidget.extend({
 
     let windowTop = $w.scrollTop();
 
-    const $posts = this.$('.onscreen-post');
+    const $posts = this.$('.onscreen-post, .cloaked-post');
     const viewportTop = windowTop - slack;
     const topView = findTopView($posts, viewportTop, 0, $posts.length-1);
 
@@ -64,7 +65,7 @@ export default MountWidget.extend({
       if (!$post) { break; }
 
       const viewTop = $post.offset().top;
-      const viewBottom = viewTop + $post.height();
+      const viewBottom = viewTop + $post.height() + 100;
 
       if (viewTop > viewportBottom) { break; }
 
@@ -76,9 +77,8 @@ export default MountWidget.extend({
     }
 
     const posts = this.posts;
+    const refresh = cb => this.queueRerender(cb);
     if (onscreen.length) {
-
-      const refresh = cb => this.queueRerender(cb);
       const first = posts.objectAt(onscreen[0]);
       if (this._topVisible !== first) {
         this._topVisible = first;
@@ -113,7 +113,21 @@ export default MountWidget.extend({
       this._bottomVisible = null;
     }
 
-    const onscreenPostNumbers = onscreen.map(idx => posts.objectAt(idx).post_number);
+    const onscreenPostNumbers = [];
+    const prev = this._previouslyOnscreen;
+    const newPrev = {};
+    onscreen.forEach(idx => {
+      const post = posts.objectAt(idx);
+      const postNumber = post.post_number;
+      delete prev[postNumber];
+      onscreenPostNumbers.push(postNumber);
+      newPrev[postNumber] = post;
+      uncloak(post, this);
+    });
+
+    Object.keys(prev).forEach(pn => cloak(prev[pn], this));
+
+    this._previouslyOnscreen = newPrev;
     this.screenTrack.setOnscreen(onscreenPostNumbers);
   },
 
@@ -125,6 +139,8 @@ export default MountWidget.extend({
     this._super();
     const debouncedScroll = () => Ember.run.debounce(this, this._scrollTriggered, 10);
 
+    this._previouslyOnscreen = {};
+
     this.appEvents.on('post-stream:refresh', debouncedScroll);
     $(document).bind('touchmove.post-stream', debouncedScroll);
     $(window).bind('scroll.post-stream', debouncedScroll);
diff --git a/app/assets/javascripts/discourse/widgets/post-stream.js.es6 b/app/assets/javascripts/discourse/widgets/post-stream.js.es6
index f7fb75e14..9eddc3b5d 100644
--- a/app/assets/javascripts/discourse/widgets/post-stream.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-stream.js.es6
@@ -1,9 +1,30 @@
 import { createWidget } from 'discourse/widgets/widget';
 import transformPost from 'discourse/lib/transform-post';
 import { Placeholder } from 'discourse/lib/posts-with-placeholders';
+import { h } from 'virtual-dom';
+import { addWidgetCleanCallback } from 'discourse/components/mount-widget';
 
+const CLOAKING_ENABLED = true;
 const DAY = 1000 * 60 * 60 * 24;
 
+let _cloaked = {};
+
+export function cloak(post, component) {
+  if (!CLOAKING_ENABLED || _cloaked[post.id]) { return; }
+
+  const $post = $(`#post_${post.post_number}`);
+  _cloaked[post.id] = $post.height();
+  Ember.run.debounce(component, 'queueRerender', 1000);
+}
+
+export function uncloak(post, component) {
+  if (!CLOAKING_ENABLED || !_cloaked[post.id]) { return; }
+  _cloaked[post.id] = null;
+  component.queueRerender();
+}
+
+addWidgetCleanCallback('post-stream', () => _cloaked = {});
+
 export default createWidget('post-stream', {
   tagName: 'div.post-stream',
 
@@ -63,8 +84,11 @@ export default createWidget('post-stream', {
       }
       prevDate = curTime;
 
-      // actual post contents
-      if (transformed.isSmallAction) {
+      const height = _cloaked[post.id];
+      if (height) {
+        result.push(h('div.cloaked-post', { id: `post_${post.post_number}`,
+                                            attributes: { style: `height: ${height}px` } }));
+      } else if (transformed.isSmallAction) {
         result.push(this.attach('post-small-action', transformed, { model: post }));
       } else {
         result.push(this.attach('post', transformed, { model: post }));