diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6
index 822c833010c..1c7384c254b 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/composer.js.es6
@@ -115,7 +115,7 @@ export default Ember.Controller.extend({
// If there is no current post, use the first post id from the stream
if (!postId && postStream) {
- postId = postStream.get('firstPostId');
+ postId = postStream.get('stream.firstObject');
}
// If we're editing a post, fetch the reply when importing a quote
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index 435418ecf00..a6e098ee2d2 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -668,8 +668,8 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
topVisibleChanged(post) {
if (!post) { return; }
- const postStream = this.get('model.postStream'),
- firstLoadedPost = postStream.get('firstLoadedPost');
+ const postStream = this.get('model.postStream');
+ const firstLoadedPost = postStream.get('posts.firstObject');
this.set('model.currentPost', post.get('post_number'));
@@ -680,15 +680,16 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
// trigger a scroll after a promise resolves in a controller? We need
// to do this to preserve upwards infinte scrolling.
const $body = $('body');
- let $elem = $('#post-cloak-' + post.get('post_number'));
+ const elemId = `#post_${post.get('post_number')}`;
+ const $elem = $(elemId).closest('.post-cloak');
const distToElement = $body.scrollTop() - $elem.position().top;
postStream.prependMore().then(function() {
Em.run.next(function () {
- $elem = $('#post-cloak-' + post.get('post_number'));
+ const $refreshedElem = $(elemId).closest('.post-cloak');
// Quickly going back might mean the element is destroyed
- const position = $elem.position();
+ const position = $refreshedElem.position();
if (position && position.top) {
$('html, body').scrollTop(position.top + distToElement);
}
@@ -706,8 +707,8 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
bottomVisibleChanged(post) {
if (!post) { return; }
- const postStream = this.get('model.postStream'),
- lastLoadedPost = postStream.get('lastLoadedPost');
+ const postStream = this.get('model.postStream');
+ const lastLoadedPost = postStream.get('posts.lastObject');
this.set('controllers.topic-progress.progressPosition', postStream.progressIndexOfPost(post));
diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6
index 14e2392744e..7a39b5bdaa0 100644
--- a/app/assets/javascripts/discourse/lib/url.js.es6
+++ b/app/assets/javascripts/discourse/lib/url.js.es6
@@ -15,14 +15,13 @@ const DiscourseURL = Ember.Object.createWithMixins({
Jumps to a particular post in the stream
**/
jumpToPost: function(postNumber, opts) {
- const holderId = '#post-cloak-' + postNumber;
+ const holderId = `#post_${postNumber}`;
+ const offset = function() {
- const offset = function(){
-
- const $header = $('header'),
- $title = $('#topic-title'),
- windowHeight = $(window).height() - $title.height(),
- expectedOffset = $title.height() - $header.find('.contents').height() + (windowHeight / 5);
+ const $header = $('header');
+ const $title = $('#topic-title');
+ const windowHeight = $(window).height() - $title.height();
+ const expectedOffset = $title.height() - $header.find('.contents').height() + (windowHeight / 5);
return $header.outerHeight(true) + ((expectedOffset < 0) ? 0 : expectedOffset);
};
@@ -203,40 +202,40 @@ const DiscourseURL = Ember.Object.createWithMixins({
@param {String} oldPath the previous path we were on
@param {String} path the path we're navigating to
**/
- navigatedToPost: function(oldPath, path) {
- const newMatches = this.TOPIC_REGEXP.exec(path),
- newTopicId = newMatches ? newMatches[2] : null;
+ navigatedToPost(oldPath, path) {
+ const newMatches = this.TOPIC_REGEXP.exec(path);
+ const newTopicId = newMatches ? newMatches[2] : null;
if (newTopicId) {
- const oldMatches = this.TOPIC_REGEXP.exec(oldPath),
- oldTopicId = oldMatches ? oldMatches[2] : null;
+ const oldMatches = this.TOPIC_REGEXP.exec(oldPath);
+ const oldTopicId = oldMatches ? oldMatches[2] : null;
// If the topic_id is the same
if (oldTopicId === newTopicId) {
DiscourseURL.replaceState(path);
- const container = Discourse.__container__,
- topicController = container.lookup('controller:topic'),
- opts = {},
- postStream = topicController.get('model.postStream');
+ const container = Discourse.__container__;
+ const topicController = container.lookup('controller:topic');
+ const opts = {};
+ const postStream = topicController.get('model.postStream');
- if (newMatches[3]) opts.nearPost = newMatches[3];
+ if (newMatches[3]) { opts.nearPost = newMatches[3]; }
if (path.match(/last$/)) { opts.nearPost = topicController.get('model.highest_post_number'); }
const closest = opts.nearPost || 1;
- const self = this;
- postStream.refresh(opts).then(function() {
+ postStream.refresh(opts).then(() => {
topicController.setProperties({
'model.currentPost': closest,
enteredAt: new Date().getTime().toString()
});
- const closestPost = postStream.closestPostForPostNumber(closest),
- progress = postStream.progressIndexOfPost(closestPost),
- progressController = container.lookup('controller:topic-progress');
+
+ const closestPost = postStream.closestPostForPostNumber(closest);
+ const progress = postStream.progressIndexOfPost(closestPost);
+ const progressController = container.lookup('controller:topic-progress');
progressController.set('progressPosition', progress);
- self.appEvents.trigger('post:highlight', closest);
- }).then(function() {
+ this.appEvents.trigger('post:highlight', closest);
+ }).then(() => {
DiscourseURL.jumpToPost(closest, {skipIfOnScreen: true});
});
diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6
index 5178b55ffc3..8ee3893b1fc 100644
--- a/app/assets/javascripts/discourse/models/post-stream.js.es6
+++ b/app/assets/javascripts/discourse/models/post-stream.js.es6
@@ -1,6 +1,7 @@
import DiscourseURL from 'discourse/lib/url';
import RestModel from 'discourse/models/rest';
import { default as computed } from 'ember-addons/ember-computed-decorators';
+import { Placeholder } from 'discourse/views/cloaked';
function calcDayDiff(p1, p2) {
if (!p1) { return; }
@@ -17,7 +18,119 @@ function calcDayDiff(p1, p2) {
}
}
-const PostStream = RestModel.extend({
+export function loadTopicView(topic, args) {
+ const topicId = topic.get('id');
+ const data = _.merge({}, args);
+ const url = Discourse.getURL("/t/") + topicId;
+ const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + '.json';
+
+ delete data.nearPost;
+ delete data.__type;
+ delete data.store;
+
+ return PreloadStore.getAndRemove(`topic_${topicId}`, () => {
+ return Discourse.ajax(jsonUrl, {data});
+ }).then(json => {
+ topic.updateFromJson(json);
+ return json;
+ });
+}
+
+const PostsWithPlaceholders = Ember.Object.extend(Ember.Array, {
+ posts: null,
+ _appendingIds: null,
+
+ init() {
+ this._appendingIds = {};
+ },
+
+ @computed
+ length() {
+ return this.get('posts.length') + Object.keys(this._appendingIds || {}).length;
+ },
+
+ append(cb) {
+ const l = this.get('posts.length');
+ this.arrayContentWillChange(l, 0, 1);
+ cb();
+ this.arrayContentDidChange(l, 0, 1);
+ this.propertyDidChange('length');
+ },
+
+ removePost(cb) {
+ const l = this.get('posts.length') - 1;
+ this.arrayContentWillChange(l, 1, 0);
+ cb();
+ this.arrayContentDidChange(l, 1, 0);
+ this.propertyDidChange('length');
+ },
+
+ appending(postIds) {
+ console.log('appending');
+ const l = this.get('length');
+ this.arrayContentWillChange(l, 0, postIds.length);
+ const appendingIds = this._appendingIds;
+ postIds.forEach(pid => appendingIds[pid] = true);
+ this.arrayContentDidChange(l, 0, postIds.length);
+ this.propertyDidChange('length');
+ },
+
+ finishedAppending(postIds) {
+ const l = this.get('posts.length') - postIds.length;
+ this.arrayContentWillChange(l, postIds.length, postIds.length);
+ const appendingIds = this._appendingIds;
+ postIds.forEach(pid => delete appendingIds[pid]);
+ this.arrayContentDidChange(l, postIds.length, postIds.length);
+ this.propertyDidChange('length');
+ },
+
+ finishedPrepending(postIds) {
+ this.arrayContentDidChange(0, 0, postIds.length);
+ this.propertyDidChange('length');
+ },
+
+ objectAt(index) {
+ const posts = this.get('posts');
+ if (index < posts.length) {
+ return posts[index];
+ } else {
+ return new Placeholder('post-placeholder');
+ }
+ },
+});
+
+export default RestModel.extend({
+ _identityMap: null,
+ posts: null,
+ stream: null,
+ userFilters: null,
+ summary: null,
+ loaded: null,
+ loadingAbove: null,
+ loadingBelow: null,
+ loadingFilter: null,
+ stagingPost: null,
+ postsWithPlaceholders: null,
+
+ init() {
+ this._identityMap = {};
+ const posts = [];
+ const postsWithPlaceholders = PostsWithPlaceholders.create({ posts, store: this.store });
+
+ this.setProperties({
+ posts,
+ postsWithPlaceholders,
+ stream: [],
+ userFilters: [],
+ summary: false,
+ loaded: false,
+ loadingAbove: false,
+ loadingBelow: false,
+ loadingFilter: false,
+ stagingPost: false,
+ });
+ },
+
loading: Ember.computed.or('loadingAbove', 'loadingBelow', 'loadingFilter', 'stagingPost'),
notLoading: Ember.computed.not('loading'),
filteredPostsCount: Ember.computed.alias("stream.length"),
@@ -27,7 +140,11 @@ const PostStream = RestModel.extend({
return this.get('posts.length') > 0;
},
- hasStream: Ember.computed.gt('filteredPostsCount', 0),
+ @computed('hasPosts', 'filteredPostsCount')
+ hasLoadedData(hasPosts, filteredPostsCount) {
+ return hasPosts && filteredPostsCount > 0;
+ },
+
canAppendMore: Ember.computed.and('notLoading', 'hasPosts', 'lastPostNotLoaded'),
canPrependMore: Ember.computed.and('notLoading', 'hasPosts', 'firstPostNotLoaded'),
@@ -38,26 +155,8 @@ const PostStream = RestModel.extend({
},
firstPostNotLoaded: Ember.computed.not('firstPostPresent'),
-
- @computed('posts.@each')
- firstLoadedPost() {
- return _.first(this.get('posts'));
- },
-
- @computed('posts.@each')
- lastLoadedPost() {
- return _.last(this.get('posts'));
- },
-
- @computed('stream.@each')
- firstPostId() {
- return this.get('stream')[0];
- },
-
- @computed('stream.@each')
- lastPostId() {
- return _.last(this.get('stream'));
- },
+ firstPostId: Ember.computed.alias('stream.firstObject'),
+ lastPostId: Ember.computed.alias('stream.lastObject'),
@computed('hasLoadedData', 'lastPostId', 'posts.@each.id')
loadedAllPosts(hasLoadedData, lastPostId) {
@@ -117,7 +216,7 @@ const PostStream = RestModel.extend({
Returns the window of posts below the current set in the stream, bound by the bottom of the
stream. This is the collection we use when scrolling downwards.
**/
- @computed('lastLoadedPost', 'stream.@each')
+ @computed('posts.lastObject', 'stream.@each')
nextWindow(lastLoadedPost) {
// If we can't find the last post loaded, bail
if (!lastLoadedPost) { return []; }
@@ -206,8 +305,7 @@ const PostStream = RestModel.extend({
opts = _.merge(opts, this.get('streamFilters'));
// Request a topicView
- return PostStream.loadTopicView(topic.get('id'), opts).then(json => {
- topic.updateFromJson(json);
+ return loadTopicView(topic, opts).then(json => {
this.updateFromJson(json.post_stream);
this.setProperties({ loadingFilter: false, loaded: true });
}).catch(result => {
@@ -215,7 +313,6 @@ const PostStream = RestModel.extend({
throw result;
});
},
- hasLoadedData: Ember.computed.and('hasPosts', 'hasStream'),
collapsePosts(from, to){
const posts = this.get('posts');
@@ -237,7 +334,6 @@ const PostStream = RestModel.extend({
this.get('stream').enumerableContentDidChange();
},
-
// Fill in a gap of posts before a particular post
fillGapBefore(post, gap) {
const postId = post.get('id'),
@@ -293,12 +389,15 @@ const PostStream = RestModel.extend({
this.set('loadingBelow', true);
- const stopLoading = () => this.set('loadingBelow', false);
-
- return this.findPostsByIds(postIds).then((posts) => {
+ const postsWithPlaceholders = this.get('postsWithPlaceholders');
+ postsWithPlaceholders.appending(postIds);
+ return this.findPostsByIds(postIds).then(posts => {
posts.forEach(p => this.appendPost(p));
- stopLoading();
- }, stopLoading);
+ return posts;
+ }).finally(() => {
+ postsWithPlaceholders.finishedAppending(postIds);
+ this.set('loadingBelow', false);
+ });
},
// Prepend the previous window of posts to the stream. Call it when scrolling upwards.
@@ -312,6 +411,9 @@ const PostStream = RestModel.extend({
this.set('loadingAbove', true);
return this.findPostsByIds(postIds.reverse()).then(posts => {
posts.forEach(p => this.prependPost(p));
+ }).finally(() => {
+ const postsWithPlaceholders = this.get('postsWithPlaceholders');
+ postsWithPlaceholders.finishedPrepending(postIds);
this.set('loadingAbove', false);
});
},
@@ -363,8 +465,7 @@ const PostStream = RestModel.extend({
}
this.get('stream').removeObject(-1);
- this.get('postIdentityMap').set(-1, null);
-
+ this._identityMap[-1] = null;
this.set('stagingPost', false);
},
@@ -374,8 +475,8 @@ const PostStream = RestModel.extend({
**/
undoPost(post) {
this.get('stream').removeObject(-1);
- this.posts.removeObject(post);
- this.get('postIdentityMap').set(-1, null);
+ this.get('postsWithPlaceholders').removePost(() => this.posts.removeObject(post));
+ this._identityMap[-1] = null;
const topic = this.get('topic');
this.set('stagingPost', false);
@@ -405,7 +506,13 @@ const PostStream = RestModel.extend({
const posts = this.get('posts');
calcDayDiff(stored, this.get('lastAppended'));
- posts.addObject(stored);
+ if (!posts.contains(stored)) {
+ if (!this.get('loadingBelow')) {
+ this.get('postsWithPlaceholders').append(() => posts.pushObject(stored));
+ } else {
+ posts.pushObject(stored);
+ }
+ }
if (stored.get('id') !== -1) {
this.set('lastAppended', stored);
@@ -418,16 +525,16 @@ const PostStream = RestModel.extend({
if (Ember.isEmpty(posts)) { return; }
const postIds = posts.map(p => p.get('id'));
- const identityMap = this.get('postIdentityMap');
+ const identityMap = this._identityMap;
this.get('stream').removeObjects(postIds);
this.get('posts').removeObjects(posts);
- postIds.forEach(id => identityMap.delete(id));
+ postIds.forEach(id => delete identityMap[id]);
},
// Returns a post from the identity map if it's been inserted.
findLoadedPost(id) {
- return this.get('postIdentityMap').get(id);
+ return this._identityMap[id];
},
loadPost(postId){
@@ -454,16 +561,13 @@ const PostStream = RestModel.extend({
this.get('stream').addObject(postId);
if (loadedAllPosts) {
this.set('loadingLastPost', true);
- this.appendMore().finally(
- ()=>this.set('loadingLastPost', true)
- );
+ this.appendMore().finally(()=> this.set('loadingLastPost', true));
}
}
},
triggerRecoveredPost(postId) {
- const postIdentityMap = this.get('postIdentityMap');
- const existing = postIdentityMap.get(postId);
+ const existing = this._identityMap[postId];
if (existing) {
this.triggerChangedPost(postId, new Date());
@@ -506,8 +610,7 @@ const PostStream = RestModel.extend({
},
triggerDeletedPost(postId){
- const postIdentityMap = this.get('postIdentityMap');
- const existing = postIdentityMap.get(postId);
+ const existing = this._identityMap[postId];
if (existing) {
const url = "/posts/" + postId;
@@ -524,8 +627,7 @@ const PostStream = RestModel.extend({
triggerChangedPost(postId, updatedAt) {
if (!postId) { return; }
- const postIdentityMap = this.get('postIdentityMap');
- const existing = postIdentityMap.get(postId);
+ const existing = this._identityMap[postId];
if (existing && existing.updated_at !== updatedAt) {
const url = "/posts/" + postId;
const store = this.store;
@@ -625,19 +727,18 @@ const PostStream = RestModel.extend({
},
updateFromJson(postStreamData) {
- const postStream = this,
- posts = this.get('posts');
+ const posts = this.get('posts');
posts.clear();
this.set('gaps', null);
if (postStreamData) {
// Load posts if present
const store = this.store;
- postStreamData.posts.forEach(p => postStream.appendPost(store.createRecord('post', p)));
+ postStreamData.posts.forEach(p => this.appendPost(store.createRecord('post', p)));
delete postStreamData.posts;
// Update our attributes
- postStream.setProperties(postStreamData);
+ this.setProperties(postStreamData);
}
},
@@ -647,13 +748,12 @@ const PostStream = RestModel.extend({
than you supplied if the post has already been loaded.
**/
storePost(post) {
- // Calling `Ember.get(undefined` raises an error
+ // Calling `Ember.get(undefined)` raises an error
if (!post) { return; }
const postId = Ember.get(post, 'id');
if (postId) {
- const postIdentityMap = this.get('postIdentityMap'),
- existing = postIdentityMap.get(post.get('id'));
+ const existing = this._identityMap[post.get('id')];
// Update the `highest_post_number` if this post is higher.
const postNumber = post.get('post_number');
@@ -668,31 +768,18 @@ const PostStream = RestModel.extend({
}
post.set('topic', this.get('topic'));
- postIdentityMap.set(post.get('id'), post);
+ this._identityMap[post.get('id')] = post;
}
return post;
},
- /**
- Given a list of postIds, returns a list of the posts we don't have in our
- identity map and need to load.
- **/
- listUnloadedIds(postIds) {
- const unloaded = [];
- const postIdentityMap = this.get('postIdentityMap');
- postIds.forEach(p => {
- if (!postIdentityMap.has(p)) { unloaded.pushObject(p); }
- });
- return unloaded;
- },
-
findPostsByIds(postIds) {
- const unloaded = this.listUnloadedIds(postIds);
- const postIdentityMap = this.get('postIdentityMap');
+ const identityMap = this._identityMap;
+ const unloaded = postIds.filter(p => !identityMap[p]);
// Load our unloaded posts by id
return this.loadIntoIdentityMap(unloaded).then(() => {
- return postIds.map(p => postIdentityMap.get(p)).compact();
+ return postIds.map(p => identityMap[p]).compact();
});
},
@@ -747,40 +834,3 @@ const PostStream = RestModel.extend({
}
});
-
-
-PostStream.reopenClass({
- create() {
- const postStream = this._super.apply(this, arguments);
- postStream.setProperties({
- posts: [],
- stream: [],
- userFilters: [],
- postIdentityMap: Ember.Map.create(),
- summary: false,
- loaded: false,
- loadingAbove: false,
- loadingBelow: false,
- loadingFilter: false,
- stagingPost: false
- });
- return postStream;
- },
-
- loadTopicView(topicId, args) {
- const opts = _.merge({}, args);
- let url = Discourse.getURL("/t/") + topicId;
- if (opts.nearPost) {
- url += "/" + opts.nearPost;
- }
- delete opts.nearPost;
- delete opts.__type;
- delete opts.store;
-
- return PreloadStore.getAndRemove("topic_" + topicId, () => {
- return Discourse.ajax(url + ".json", {data: opts});
- });
- }
-});
-
-export default PostStream;
diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6
index e3e651aa8d7..889349f1db1 100644
--- a/app/assets/javascripts/discourse/models/topic.js.es6
+++ b/app/assets/javascripts/discourse/models/topic.js.es6
@@ -319,11 +319,7 @@ const Topic = RestModel.extend({
keys.removeObject('details');
keys.removeObject('post_stream');
- const topic = this;
- keys.forEach(function (key) {
- topic.set(key, json[key]);
- });
-
+ keys.forEach(key => this.set(key, json[key]));
},
isPinnedUncategorized: function() {
diff --git a/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6
index 10e77c47b48..26e476af233 100644
--- a/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6
+++ b/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6
@@ -1,12 +1,9 @@
-import PostStream from "discourse/models/post-stream";
+import { loadTopicView } from "discourse/models/post-stream";
export default Discourse.Route.extend({
model(params) {
const topic = this.store.createRecord("topic", { id: params.id });
- return PostStream.loadTopicView(params.id).then(json => {
- topic.updateFromJson(json);
- return topic;
- });
+ return loadTopicView(topic).then(() => topic);
},
afterModel(topic) {
diff --git a/app/assets/javascripts/discourse/templates/post-placeholder.hbs b/app/assets/javascripts/discourse/templates/post-placeholder.hbs
new file mode 100644
index 00000000000..936863f191e
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/post-placeholder.hbs
@@ -0,0 +1,13 @@
+