diff --git a/app/assets/javascripts/admin/controllers/admin_flags_controller.js b/app/assets/javascripts/admin/controllers/admin_flags_controller.js
index f1843e93d2a..36cb05c3c89 100644
--- a/app/assets/javascripts/admin/controllers/admin_flags_controller.js
+++ b/app/assets/javascripts/admin/controllers/admin_flags_controller.js
@@ -40,6 +40,7 @@ Discourse.AdminFlagsController = Ember.ArrayController.extend({
bootbox.alert(Em.String.i18n("admin.flags.error"));
});
},
+
/**
Deletes a post
diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js
index c2e8be02abc..fa09ab10204 100644
--- a/app/assets/javascripts/discourse.js
+++ b/app/assets/javascripts/discourse.js
@@ -331,10 +331,6 @@ Discourse = Ember.Application.createWithMixins({
Discourse.MessageBus.start();
Discourse.KeyValueStore.init("discourse_", Discourse.MessageBus);
- // Don't remove site settings for now. It seems on some browsers the route
- // tries to use it after it has been removed
- // PreloadStore.remove('siteSettings');
-
// Developer specific functions
Discourse.Development.setupProbes();
Discourse.Development.observeLiveChanges();
diff --git a/app/assets/javascripts/discourse/components/url.js b/app/assets/javascripts/discourse/components/url.js
index 08869b6770b..0ab91a3264f 100644
--- a/app/assets/javascripts/discourse/components/url.js
+++ b/app/assets/javascripts/discourse/components/url.js
@@ -5,7 +5,7 @@
@namespace Discourse
@module Discourse
**/
-Discourse.URL = {
+Discourse.URL = Em.Object.createWithMixins({
// Used for matching a topic
TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/,
@@ -23,7 +23,7 @@ Discourse.URL = {
**/
router: function() {
return Discourse.__container__.lookup('router:main');
- },
+ }.property(),
/**
Browser aware replaceState. Will only be invoked if the browser supports it.
@@ -43,7 +43,8 @@ Discourse.URL = {
// while URLs are loading. For example, while a topic loads it sets `currentPost`
// which triggers a replaceState even though the topic hasn't fully loaded yet!
Em.run.next(function() {
- Discourse.URL.router().get('location').replaceURL(path);
+ var location = Discourse.URL.get('router.location');
+ if (location.replaceURL) { location.replaceURL(path); }
});
}
},
@@ -85,10 +86,16 @@ Discourse.URL = {
if (oldTopicId === newTopicId) {
Discourse.URL.replaceState(path);
var topicController = Discourse.__container__.lookup('controller:topic');
- var opts = { trackVisit: false };
+ var opts = { };
if (newMatches[3]) opts.nearPost = newMatches[3];
- topicController.cancelFilter();
- topicController.loadPosts(opts);
+
+ var postStream = topicController.get('postStream');
+ postStream.refresh(opts).then(function() {
+ topicController.setProperties({
+ currentPost: opts.nearPost || 1,
+ progressPosition: opts.nearPost || 1
+ });
+ });
// Abort routing, we have replaced our state.
return;
@@ -102,11 +109,18 @@ Discourse.URL = {
// Be wary of looking up the router. In this case, we have links in our
// HTML, say form compiled markdown posts, that need to be routed.
- var router = this.router();
+ var router = this.get('router');
router.router.updateURL(path);
return router.handleURL(path);
},
+ /**
+ Replaces the query parameters in the URL. Use no parameters to clear them.
+
+ @method replaceQueryParams
+ **/
+ queryParams: Em.computed.alias('router.location.queryParams'),
+
/**
@private
@@ -131,4 +145,4 @@ Discourse.URL = {
window.location = Discourse.getURL(url);
}
-};
+});
diff --git a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
index 425a75d62be..87031a5562c 100644
--- a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
+++ b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
@@ -10,15 +10,15 @@
Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
setDays: function() {
- if( this.get('auto_close_at') ) {
- var closeTime = new Date( this.get('auto_close_at') );
+ if( this.get('details.auto_close_at') ) {
+ var closeTime = new Date( this.get('details.auto_close_at') );
if (closeTime > new Date()) {
this.set('auto_close_days', closeTime.daysSince());
}
} else {
- this.set('auto_close_days', '');
+ this.set('details.auto_close_days', '');
}
- }.observes('auto_close_at'),
+ }.observes('details.auto_close_at'),
saveAutoClose: function() {
this.setAutoClose( parseFloat(this.get('auto_close_days')) );
@@ -36,7 +36,7 @@ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Disco
dataType: 'html', // no custom errors, jquery 1.9 enforces json
data: { auto_close_days: days > 0 ? days : null }
}).then(function(){
- editTopicAutoCloseController.set('auto_close_at', moment().add('days', days).format());
+ editTopicAutoCloseController.set('details.auto_close_at', moment().add('days', days).format());
}, function (error) {
bootbox.alert(Em.String.i18n('generic_error'));
});
diff --git a/app/assets/javascripts/discourse/controllers/invite_private_controller.js b/app/assets/javascripts/discourse/controllers/invite_private_controller.js
index 2fbfac27420..a3320da57fe 100644
--- a/app/assets/javascripts/discourse/controllers/invite_private_controller.js
+++ b/app/assets/javascripts/discourse/controllers/invite_private_controller.js
@@ -40,7 +40,7 @@ Discourse.InvitePrivateController = Discourse.ObjectController.extend(Discourse.
invitePrivateController.set('finished', true);
if(result && result.user) {
- invitePrivateController.get('content.allowed_users').pushObject(result.user);
+ invitePrivateController.get('content.details.allowed_users').pushObject(result.user);
}
}, function() {
// Failure
diff --git a/app/assets/javascripts/discourse/controllers/quote_button_controller.js b/app/assets/javascripts/discourse/controllers/quote_button_controller.js
index ebcace7f89d..bfd4e2e38b7 100644
--- a/app/assets/javascripts/discourse/controllers/quote_button_controller.js
+++ b/app/assets/javascripts/discourse/controllers/quote_button_controller.js
@@ -34,7 +34,7 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({
if (!Discourse.User.current()) return;
// don't display the "quote-reply" button if we can't create a post
- if (!this.get('controllers.topic.content.can_create_post')) return;
+ if (!this.get('controllers.topic.model.details.can_create_post')) return;
var selection = window.getSelection();
// no selections
diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js
index 5bcc284a874..8dd5c64acf2 100644
--- a/app/assets/javascripts/discourse/controllers/topic_controller.js
+++ b/app/assets/javascripts/discourse/controllers/topic_controller.js
@@ -7,24 +7,28 @@
@module Discourse
**/
Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, {
- userFilters: new Em.Set(),
multiSelect: false,
- bestOf: false,
summaryCollapsed: true,
- loading: false,
- loadingBelow: false,
- loadingAbove: false,
needs: ['header', 'modal', 'composer', 'quoteButton'],
allPostsSelected: false,
selectedPosts: new Em.Set(),
+ editingTopic: false,
+
+ jumpTopDisabled: function() {
+ return this.get('currentPost') === 1;
+ }.property('currentPost'),
+
+ jumpBottomDisabled: function() {
+ return this.get('currentPost') === this.get('highest_post_number');
+ }.property('currentPost'),
canMergeTopic: function() {
- if (!this.get('can_move_posts')) return false;
+ if (!this.get('details.can_move_posts')) return false;
return (this.get('selectedPostsCount') > 0);
}.property('selectedPostsCount'),
canSplitTopic: function() {
- if (!this.get('can_move_posts')) return false;
+ if (!this.get('details.can_move_posts')) return false;
if (this.get('allPostsSelected')) return false;
return (this.get('selectedPostsCount') > 0);
}.property('selectedPostsCount'),
@@ -64,11 +68,11 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
}.observes('multiSelect'),
hideProgress: function() {
- if (!this.get('content.loaded')) return true;
+ if (!this.get('postStream.loaded')) return true;
if (!this.get('currentPost')) return true;
- if (this.get('content.filtered_posts_count') < 2) return true;
+ if (this.get('postStream.filteredPostsCount') < 2) return true;
return false;
- }.property('content.loaded', 'currentPost', 'content.filtered_posts_count'),
+ }.property('postStream.loaded', 'currentPost', 'postStream.filteredPostsCount'),
selectPost: function(post) {
var selectedPosts = this.get('selectedPosts');
@@ -107,6 +111,58 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
this.toggleProperty('summaryCollapsed');
},
+ editTopic: function() {
+ if (!this.get('details.can_edit')) return false;
+
+ this.setProperties({
+ editingTopic: true,
+ newTitle: this.get('title'),
+ newCategoryId: this.get('category_id')
+ });
+ return false;
+ },
+
+ // close editing mode
+ cancelEditingTopic: function() {
+ this.set('editingTopic', false);
+ },
+
+ finishedEditingTopic: function() {
+ var topicController = this;
+ if (this.get('editingTopic')) {
+
+ var topic = this.get('model');
+
+ // manually update the titles & category
+ topic.setProperties({
+ title: this.get('newTitle'),
+ category_id: parseInt(this.get('newCategoryId'), 10),
+ fancy_title: this.get('newTitle')
+ });
+
+ // save the modifications
+ topic.save().then(function(result){
+ // update the title if it has been changed (cleaned up) server-side
+ var title = result.basic_topic.fancy_title;
+ topic.setProperties({
+ title: title,
+ fancy_title: title
+ });
+
+ }, function(error) {
+ topicController.set('editingTopic', true);
+ if (error && error.responseText) {
+ bootbox.alert($.parseJSON(error.responseText).errors[0]);
+ } else {
+ bootbox.alert(Em.String.i18n('generic_error'));
+ }
+ });
+
+ // close editing mode
+ topicController.set('editingTopic', false);
+ }
+ },
+
deleteSelected: function() {
var topicController = this;
bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
@@ -126,25 +182,14 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
},
jumpTop: function() {
- if (this.get('bestOf')) {
- Discourse.TopicView.scrollTo(this.get('id'), this.get('posts')[0].get('post_number'));
- } else {
- Discourse.URL.routeTo(this.get('url'));
- }
+ Discourse.URL.routeTo(this.get('url'));
},
jumpBottom: function() {
- if (this.get('bestOf')) {
- Discourse.TopicView.scrollTo(this.get('id'), _.last(this.get('posts')).get('post_number'));
- } else {
- Discourse.URL.routeTo(this.get('lastPostUrl'));
- }
+ Discourse.URL.routeTo(this.get('lastPostUrl'));
},
- cancelFilter: function() {
- this.set('bestOf', false);
- this.get('userFilters').clear();
- },
+
replyAsNewTopic: function(post) {
// TODO shut down topic draft cleanly if it exists ...
@@ -182,95 +227,18 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
}
},
- toggleParticipant: function(user) {
- this.set('bestOf', false);
- var username = Em.get(user, 'username');
- var userFilters = this.get('userFilters');
- if (userFilters.contains(username)) {
- userFilters.remove(username);
- } else {
- userFilters.add(username);
- }
- },
-
/**
- Show or hide the bottom bar, depending on our filter options.
+ Toggle a participant for filtering
- @method updateBottomBar
+ @method toggleParticipant
**/
- updateBottomBar: function() {
-
- var postFilters = this.get('postFilters');
-
- if (postFilters.bestOf) {
- this.set('filterDesc', Em.String.i18n("topic.filters.best_of", {
- n_best_posts: Em.String.i18n("topic.filters.n_best_posts", { count: this.get('filtered_posts_count') }),
- of_n_posts: Em.String.i18n("topic.filters.of_n_posts", { count: this.get('posts_count') })
- }));
- } else if (postFilters.userFilters.length > 0) {
- this.set('filterDesc', Em.String.i18n("topic.filters.user", {
- n_posts: Em.String.i18n("topic.filters.n_posts", { count: this.get('filtered_posts_count') }),
- by_n_users: Em.String.i18n("topic.filters.by_n_users", { count: postFilters.userFilters.length })
- }));
- } else {
- // Hide the bottom bar
- $('#topic-filter').slideUp();
- return;
- }
-
- $('#topic-filter').slideDown();
+ toggleParticipant: function(user) {
+ this.get('postStream').toggleParticipant(Em.get(user, 'username'));
},
- enableBestOf: function(e) {
- this.set('bestOf', true);
- this.get('userFilters').clear();
- },
-
- postFilters: function() {
- if (this.get('bestOf') === true) return { bestOf: true };
- return { userFilters: this.get('userFilters') };
- }.property('userFilters.[]', 'bestOf'),
-
- loadPosts: function(opts) {
- var topicController = this;
- this.get('content').loadPosts(opts).then(function () {
- Em.run.scheduleOnce('afterRender', topicController, 'updateBottomBar');
- });
- },
-
- reloadPosts: function() {
- var topic = this.get('content');
- if (!topic) return;
-
- var posts = topic.get('posts');
- if (!posts) return;
-
- // Leave the first post -- we keep it above the filter controls
- posts.removeAt(1, posts.length - 1);
-
- this.set('loadingBelow', true);
-
- var topicController = this;
- var postFilters = this.get('postFilters');
- return Discourse.Topic.find(this.get('id'), postFilters).then(function(result) {
- var first = result.posts[0];
- if (first) {
- topicController.set('currentPost', first.post_number);
- }
- $('#topic-progress .solid').data('progress', false);
- _.each(result.posts,function(p) {
- // Skip the first post
- if (p.post_number === 1) return;
- posts.pushObject(Discourse.Post.create(p, topic));
- });
-
- Em.run.scheduleOnce('afterRender', topicController, 'updateBottomBar');
-
- topicController.set('filtered_posts_count', result.filtered_posts_count);
- topicController.set('loadingBelow', false);
- topicController.set('seenBottom', false);
- });
- }.observes('postFilters'),
+ showFavoriteButton: function() {
+ return Discourse.User.current() && !this.get('isPrivateMessage');
+ }.property('isPrivateMessage'),
deleteTopic: function() {
var topicController = this;
@@ -327,23 +295,13 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
bus.subscribe("/topic/" + (this.get('id')), function(data) {
var topic = topicController.get('model');
if (data.notification_level_change) {
- topic.set('notification_level', data.notification_level_change);
- topic.set('notifications_reason_id', data.notifications_reason_id);
- return;
- }
- var posts = topic.get('posts');
- if (posts.some(function(p) {
- return p.get('post_number') === data.post_number;
- })) {
+ topic.set('details.notification_level', data.notification_level_change);
+ topic.set('details.notifications_reason_id', data.notifications_reason_id);
return;
}
- // Robin, TODO when a message comes in we need to figure out if it even goes
- // in this view ... for now fixed the general case
- topic.set('filtered_posts_count', topic.get('filtered_posts_count') + 1);
- topic.set('highest_post_number', data.post_number);
- topic.set('last_poster', data.user);
- topic.set('last_posted_at', data.created_at);
+ // Add the new post into the stream
+ topicController.get('postStream').triggerNewPostInStream(data.id);
});
},
@@ -424,15 +382,8 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
post.destroy();
},
- postRendered: function(post) {
- var onPostRendered = this.get('onPostRendered');
- if (onPostRendered) {
- onPostRendered(post);
- }
- },
-
removeAllowedUser: function(username) {
- this.get('model').removeAllowedUser(username);
+ this.get('details').removeAllowedUser(username);
}
});
diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js
index 8f6d2d3ca5d..b3dc1732644 100644
--- a/app/assets/javascripts/discourse/models/composer.js
+++ b/app/assets/javascripts/discourse/models/composer.js
@@ -357,70 +357,40 @@ Discourse.Composer = Discourse.Model.extend({
var post = this.get('post'),
topic = this.get('topic'),
currentUser = Discourse.User.current(),
+ postStream = this.get('topic.postStream'),
addedToStream = false;
- // The post number we'll probably get from the server
- var probablePostNumber = this.get('topic.highest_post_number') + 1;
-
// Build the post object
var createdPost = Discourse.Post.create({
- raw: this.get('reply'),
- title: this.get('title'),
- category: this.get('categoryName'),
- topic_id: this.get('topic.id'),
- reply_to_post_number: post ? post.get('post_number') : null,
- imageSizes: opts.imageSizes,
- post_number: probablePostNumber,
- index: probablePostNumber,
- cooked: $('#wmd-preview').html(),
- reply_count: 0,
- display_username: currentUser.get('name'),
- username: currentUser.get('username'),
- user_id: currentUser.get('id'),
- metaData: this.get('metaData'),
- archetype: this.get('archetypeId'),
- post_type: Discourse.Site.instance().get('post_types.regular'),
- target_usernames: this.get('targetUsernames'),
- actions_summary: Em.A(),
- moderator: currentUser.get('moderator'),
- yours: true,
- newPost: true,
- auto_close_days: this.get('auto_close_days')
- });
+ raw: this.get('reply'),
+ title: this.get('title'),
+ category: this.get('categoryName'),
+ topic_id: this.get('topic.id'),
+ reply_to_post_number: post ? post.get('post_number') : null,
+ imageSizes: opts.imageSizes,
+ cooked: $('#wmd-preview').html(),
+ reply_count: 0,
+ display_username: currentUser.get('name'),
+ username: currentUser.get('username'),
+ user_id: currentUser.get('id'),
+ metaData: this.get('metaData'),
+ archetype: this.get('archetypeId'),
+ post_type: Discourse.Site.instance().get('post_types.regular'),
+ target_usernames: this.get('targetUsernames'),
+ actions_summary: Em.A(),
+ moderator: currentUser.get('moderator'),
+ yours: true,
+ newPost: true,
+ auto_close_days: this.get('auto_close_days')
+ });
// If we're in a topic, we can append the post instantly.
- if (topic) {
-
- // Increase the reply count
+ if (postStream) {
+ // If it's in reply to another post, increase the reply count
if (post) {
post.set('reply_count', (post.get('reply_count') || 0) + 1);
}
- topic.set('posts_count', topic.get('posts_count') + 1);
-
- // Update last post
- topic.set('last_posted_at', new Date());
- topic.set('highest_post_number', createdPost.get('post_number'));
- topic.set('last_poster', Discourse.User.current());
- topic.set('filtered_posts_count', topic.get('filtered_posts_count') + 1);
-
- // Set the topic view for the new post
- createdPost.set('topic', topic);
- createdPost.set('created_at', new Date());
-
- // If we're near the end of the topic, load new posts
- var lastPost = topic.posts[topic.posts.length-1];
- if (lastPost) {
- var diff = topic.get('highest_post_number') - lastPost.get('post_number');
-
- // If the new post is within a threshold of the end of the topic,
- // add it and scroll there instead of adding the link.
-
- if (diff < 5) {
- createdPost.set('scrollToAfterInsert', createdPost.get('post_number'));
- topic.pushPosts([createdPost]);
- addedToStream = true;
- }
- }
+ postStream.stagePost(createdPost, currentUser);
}
// Save callback
@@ -430,11 +400,13 @@ Discourse.Composer = Discourse.Model.extend({
var addedPost = false,
saving = true;
- createdPost.updateFromSave(result);
+ createdPost.updateFromJson(result);
if (topic) {
// It's no longer a new post
createdPost.set('newPost', false);
topic.set('draft_sequence', result.draft_sequence);
+ postStream.commitPost(createdPost);
+ addedToStream = true;
} else {
// We created a new topic, let's show it.
composer.set('composeState', CLOSED);
@@ -448,12 +420,13 @@ Discourse.Composer = Discourse.Model.extend({
} else if (saving) {
composer.set('composeState', SAVING);
}
+
+
return promise.resolve({ post: result });
}, function(error) {
// If an error occurs
- if (topic) {
- topic.posts.removeObject(createdPost);
- topic.set('filtered_posts_count', topic.get('filtered_posts_count') - 1);
+ if (postStream) {
+ postStream.undoPost(createdPost);
}
promise.reject($.parseJSON(error.responseText).errors[0]);
composer.set('composeState', OPEN);
diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js
index c395efa1ca0..4a2c734759d 100644
--- a/app/assets/javascripts/discourse/models/post.js
+++ b/app/assets/javascripts/discourse/models/post.js
@@ -15,9 +15,8 @@ Discourse.Post = Discourse.Model.extend({
return this.get('url') + (user ? '?u=' + user.get('username_lower') : '');
}.property('url'),
- new_user: function() {
- return this.get('trust_level') === 0;
- }.property('trust_level'),
+ new_user: Em.computed.equal('trust_level', 0),
+ firstPost: Em.computed.equal('post_number', 1),
url: function() {
return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number'));
@@ -35,14 +34,9 @@ Discourse.Post = Discourse.Model.extend({
return this.get('reply_to_user') && (this.get('reply_to_post_number') < (this.get('post_number') - 1));
}.property('reply_to_user', 'reply_to_post_number', 'post_number'),
- firstPost: function() {
- if (this.get('bestOfFirst') === true) return true;
- return this.get('post_number') === 1;
- }.property('post_number'),
-
byTopicCreator: function() {
- return this.get('topic.created_by.id') === this.get('user_id');
- }.property('topic.created_by.id', 'user_id'),
+ return this.get('topic.details.created_by.id') === this.get('user_id');
+ }.property('topic.details.created_by.id', 'user_id'),
hasHistory: function() {
return this.get('version') > 1;
@@ -55,28 +49,23 @@ Discourse.Post = Discourse.Model.extend({
// The class for the read icon of the post. It starts with read-icon then adds 'seen' or
// 'last-read' if the post has been seen or is the highest post number seen so far respectively.
bookmarkClass: function() {
- var result, topic;
- result = 'read-icon';
+ var result = 'read-icon';
if (this.get('bookmarked')) return result + ' bookmarked';
- topic = this.get('topic');
+
+ var topic = this.get('topic');
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
- result += ' last-read';
- } else {
- if (this.get('read')) {
- result += ' seen';
- } else {
- result += ' unseen';
- }
+ return result + ' last-read';
}
- return result;
+
+ return result + (this.get('read') ? ' seen' : ' unseen');
}.property('read', 'topic.last_read_post_number', 'bookmarked'),
// Custom tooltips for the bookmark icons
bookmarkTooltip: function() {
- var topic;
if (this.get('bookmarked')) return Em.String.i18n('bookmarks.created');
if (!this.get('read')) return "";
- topic = this.get('topic');
+
+ var topic = this.get('topic');
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
return Em.String.i18n('bookmarks.last_read');
}
@@ -123,9 +112,9 @@ Discourse.Post = Discourse.Model.extend({
}.property('updated_at'),
flagsAvailable: function() {
- var _this = this;
- var flags = Discourse.Site.instance().get('flagTypes').filter(function(item) {
- return _this.get("actionByName." + (item.get('name_key')) + ".can_act");
+ var post = this,
+ flags = Discourse.Site.instance().get('flagTypes').filter(function(item) {
+ return post.get("actionByName." + (item.get('name_key')) + ".can_act");
});
return flags;
}.property('actions_summary.@each.can_act'),
@@ -142,7 +131,6 @@ Discourse.Post = Discourse.Model.extend({
// Save a post and call the callback when done.
save: function(complete, error) {
- var data, metaData;
if (!this.get('newPost')) {
// We're updating a post
return Discourse.ajax("/posts/" + (this.get('id')), {
@@ -163,7 +151,7 @@ Discourse.Post = Discourse.Model.extend({
} else {
// We're saving a post
- data = {
+ var data = {
raw: this.get('raw'),
topic_id: this.get('topic_id'),
reply_to_post_number: this.get('reply_to_post_number'),
@@ -175,11 +163,13 @@ Discourse.Post = Discourse.Model.extend({
auto_close_days: this.get('auto_close_days')
};
+ var metaData = this.get('metaData');
// Put the metaData into the request
- if (metaData = this.get('metaData')) {
+ if (metaData) {
data.meta_data = {};
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
}
+
return Discourse.ajax("/posts", {
type: 'POST',
data: data
@@ -201,14 +191,35 @@ Discourse.Post = Discourse.Model.extend({
return Discourse.ajax("/posts/" + (this.get('id')), { type: 'DELETE' });
},
- // Update the properties of this post from an obj, ignoring cooked as we should already
- // have that rendered.
- updateFromSave: function(obj) {
+ /**
+ Updates a post from another's attributes. This will normally happen when a post is loading but
+ is already found in an identity map.
+ @method updateFromPost
+ @param {Discourse.Post} otherPost The post we're updating from
+ **/
+ updateFromPost: function(otherPost) {
var post = this;
+ Object.keys(otherPost).forEach(function (key) {
+ var value = otherPost[key];
+ if (typeof value !== "function") {
+ post.set(key, value);
+ }
+ });
+ },
+
+ /**
+ Updates a post from a JSON packet. This is normally done after the post is saved to refresh any
+ attributes.
+
+ @method updateFromJson
+ @param {Object} obj The Json data to update with
+ **/
+ updateFromJson: function(obj) {
+ if (!obj) return;
// Update all the properties
- if (!obj) return;
+ var post = this;
_.each(obj, function(val,key) {
if (key !== 'actions_summary'){
if (val) {
@@ -255,7 +266,7 @@ Discourse.Post = Discourse.Model.extend({
},
// Whether to show replies directly below
- showRepliesBelow: (function() {
+ showRepliesBelow: function() {
var reply_count, _ref;
reply_count = this.get('reply_count');
@@ -272,15 +283,15 @@ Discourse.Post = Discourse.Model.extend({
if ((_ref = this.get('topic')) ? _ref.isReplyDirectlyBelow(this) : void 0) return false;
return true;
- }).property('reply_count')
+ }.property('reply_count')
+
});
Discourse.Post.reopenClass({
createActionSummary: function(result) {
- var lookup;
if (result.actions_summary) {
- lookup = Em.Object.create();
+ var lookup = Em.Object.create();
result.actions_summary = result.actions_summary.map(function(a) {
a.post = result;
a.actionType = Discourse.Site.instance().postActionTypeById(a.id);
@@ -288,17 +299,16 @@ Discourse.Post.reopenClass({
lookup.set(a.actionType.get('name_key'), actionSummary);
return actionSummary;
});
- return result.set('actionByName', lookup);
+ result.set('actionByName', lookup);
}
},
- create: function(obj, topic) {
+ create: function(obj) {
var result = this._super(obj);
this.createActionSummary(result);
if (obj && obj.reply_to_user) {
result.set('reply_to_user', Discourse.User.create(obj.reply_to_user));
}
- result.set('topic', topic);
return result;
},
diff --git a/app/assets/javascripts/discourse/models/post_stream.js b/app/assets/javascripts/discourse/models/post_stream.js
new file mode 100644
index 00000000000..bb2c9f4bd81
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/post_stream.js
@@ -0,0 +1,633 @@
+/**
+ We use this class to keep on top of streaming and filtering posts within a topic.
+
+ @class PostStream
+ @extends Ember.Object
+ @namespace Discourse
+ @module Discourse
+**/
+Discourse.PostStream = Em.Object.extend({
+
+ /**
+ Are we currently loading posts in any way?
+
+ @property loading
+ **/
+ loading: Em.computed.or('loadingAbove', 'loadingBelow', 'loadingFilter', 'stagingPost'),
+
+ notLoading: Em.computed.not('loading'),
+
+ filteredPostsCount: Em.computed.alias('stream.length'),
+
+ /**
+ Have we loaded any posts?
+
+ @property hasPosts
+ **/
+ hasPosts: function() {
+ return this.get('posts.length') > 0;
+ }.property('posts.length'),
+
+ /**
+ Do we have a stream list of post ids?
+
+ @property hasStream
+ **/
+ hasStream: function() {
+ return this.get('filteredPostsCount') > 0;
+ }.property('filteredPostsCount'),
+
+ /**
+ Can we append more posts to our current stream?
+
+ @property canAppendMore
+ **/
+ canAppendMore: Em.computed.and('notLoading', 'hasPosts', 'lastPostNotLoaded'),
+
+
+ /**
+ Can we prepend more posts to our current stream?
+
+ @property canPrependMore
+ **/
+ canPrependMore: Em.computed.and('notLoading', 'hasPosts', 'firstPostNotLoaded'),
+
+ /**
+ Have we loaded the first post in the stream?
+
+ @property firstPostLoaded
+ **/
+ firstPostLoaded: function() {
+ if (!this.get('hasLoadedData')) { return false; }
+ return !!this.get('posts').findProperty('id', this.get('stream')[0]);
+ }.property('hasLoadedData', 'posts.[]', 'stream.@each'),
+
+ firstPostNotLoaded: Em.computed.not('firstPostLoaded'),
+
+ /**
+ Have we loaded the last post in the stream?
+
+ @property lastPostLoaded
+ **/
+ lastPostLoaded: function() {
+ if (!this.get('hasLoadedData')) { return false; }
+ return !!this.get('posts').findProperty('id', _.last(this.get('stream')));
+ }.property('hasLoadedData', 'posts.[]', 'stream.@each'),
+
+ lastPostNotLoaded: Em.computed.not('lastPostLoaded'),
+
+ /**
+ Returns a JS Object of current stream filter options. It should match the query
+ params for the stream.
+
+ @property streamFilters
+ **/
+ streamFilters: function() {
+ var result = {};
+ if (this.get('bestOf')) { result.filter = "best_of"; }
+
+ var userFilters = this.get('userFilters');
+ if (userFilters) {
+ var userFiltersArray = this.get('userFilters').toArray();
+ if (userFiltersArray.length > 0) { result.username_filters = userFiltersArray; }
+ }
+
+ return result;
+ }.property('userFilters.[]', 'bestOf'),
+
+ /**
+ The text describing the current filters. For display in the pop up at the bottom of the
+ screen.
+
+ @property filterDesc
+ **/
+ filterDesc: function() {
+ var streamFilters = this.get('streamFilters');
+
+ if (streamFilters.filter && streamFilters.filter === "best_of") {
+ return Em.String.i18n("topic.filters.best_of", {
+ n_best_posts: Em.String.i18n("topic.filters.n_best_posts", { count: this.get('filteredPostsCount') }),
+ of_n_posts: Em.String.i18n("topic.filters.of_n_posts", { count: this.get('topic.posts_count') })
+ });
+ } else if (streamFilters.username_filters) {
+ return Em.String.i18n("topic.filters.user", {
+ n_posts: Em.String.i18n("topic.filters.n_posts", { count: this.get('filteredPostsCount') }),
+ by_n_users: Em.String.i18n("topic.filters.by_n_users", { count: streamFilters.username_filters.length })
+ });
+ }
+ return "";
+ }.property('streamFilters.[]', 'topic.posts_count', 'posts.length'),
+
+ hasNoFilters: Em.computed.empty('filterDesc'),
+
+
+ /**
+ Returns the window of posts above the current set in the stream, bound to the top of the stream.
+ This is the collection we'll ask for when scrolling upwards.
+
+ @property previousWindow
+ **/
+ previousWindow: function() {
+ // If we can't find the last post loaded, bail
+ var firstPost = _.first(this.get('posts'));
+ if (!firstPost) { return []; }
+
+ // Find the index of the last post loaded, if not found, bail
+ var stream = this.get('stream');
+ var firstIndex = this.indexOf(firstPost);
+ if (firstIndex === -1) { return []; }
+
+ var startIndex = firstIndex - Discourse.SiteSettings.posts_per_page;
+ if (startIndex < 0) { startIndex = 0; }
+ return stream.slice(startIndex, firstIndex);
+
+ }.property('posts.@each', 'stream.@each'),
+
+ /**
+ 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.
+
+ @property nextWindow
+ **/
+ nextWindow: function() {
+ // If we can't find the last post loaded, bail
+ var lastPost = _.last(this.get('posts'));
+ if (!lastPost) { return []; }
+
+ // Find the index of the last post loaded, if not found, bail
+ var stream = this.get('stream');
+ var lastIndex = this.indexOf(lastPost);
+ 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'),
+
+
+ /**
+ Cancel any active filters on the stream and refresh it.
+
+ @method cancelFilter
+ @returns {Ember.Deferred} a promise that resolves when the filter has been cancelled.
+ **/
+ cancelFilter: function() {
+ this.set('bestOf', false);
+ this.get('userFilters').clear();
+ return this.refresh();
+ },
+
+ /**
+ Toggle best of mode on the stream.
+
+ @method toggleBestOf
+ @returns {Ember.Deferred} a promise that resolves when the best of stream has loaded.
+ **/
+ toggleBestOf: function() {
+ this.toggleProperty('bestOf');
+ this.refresh();
+ },
+
+ /**
+ Filter the stream to a particular user.
+
+ @method toggleParticipant
+ @returns {Ember.Deferred} a promise that resolves when the filtered stream has loaded.
+ **/
+ toggleParticipant: function(username) {
+ var userFilters = this.get('userFilters');
+ if (userFilters.contains(username)) {
+ userFilters.remove(username);
+ } else {
+ userFilters.add(username);
+ }
+ return this.refresh();
+ },
+
+ /**
+ Loads a new set of posts into the stream. If you provide a `nearPost` option and the post
+ is already loaded, it will simply scroll there and load nothing.
+
+ @method refresh
+ @param {Object} opts Options for loading the stream
+ @param {Integer} opts.nearPost The post we want to find other posts near to.
+ @param {Boolean} opts.track_visit Whether or not to track this as a visit to a topic.
+ @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;
+
+ // 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.Deferred.create(function(p) { p.reject(); });
+ }
+
+ // TODO: if we have all the posts in the filter, don't go to the server for them.
+ postStream.set('loadingFilter', true);
+
+ opts = _.merge(opts, postStream.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 });
+
+ if (opts.nearPost) {
+ Discourse.TopicView.jumpToPost(topic.get('id'), opts.nearPost);
+ }
+
+ Discourse.URL.set('queryParams', postStream.get('streamFilters'));
+ }, function(result) {
+ postStream.errorLoading(result.status);
+ });
+ },
+ hasLoadedData: Em.computed.and('hasPosts', 'hasStream'),
+
+ /**
+ Appends the next window of posts to the stream. Call it when scrolling downwards.
+
+ @method appendMore
+ @returns {Ember.Deferred} a promise that's resolved when the posts have been added.
+ **/
+ appendMore: function() {
+ var postStream = this,
+ rejectedPromise = Ember.Deferred.create(function(p) { p.reject(); });
+
+ // Make sure we can append more posts
+ if (!postStream.get('canAppendMore')) { return rejectedPromise; }
+
+ var postIds = postStream.get('nextWindow');
+ if (Ember.isEmpty(postIds)) { return rejectedPromise; }
+
+ postStream.set('loadingBelow', true);
+ return postStream.findPostsByIds(postIds).then(function(posts) {
+ posts.forEach(function(p) {
+ postStream.appendPost(p);
+ });
+ postStream.set('loadingBelow', false);
+ });
+ },
+
+ /**
+ Prepend the previous window of posts to the stream. Call it when scrolling upwards.
+
+ @method appendMore
+ @returns {Ember.Deferred} a promise that's resolved when the posts have been added.
+ **/
+ prependMore: function() {
+ var postStream = this,
+ rejectedPromise = Ember.Deferred.create(function(p) { p.reject(); });
+
+ // Make sure we can append more posts
+ if (!postStream.get('canPrependMore')) { return rejectedPromise; }
+
+ var postIds = postStream.get('previousWindow');
+ if (Ember.isEmpty(postIds)) { return rejectedPromise; }
+
+ postStream.set('loadingAbove', true);
+ return postStream.findPostsByIds(postIds.reverse()).then(function(posts) {
+ posts.forEach(function(p) {
+ postStream.prependPost(p);
+ });
+ postStream.set('loadingAbove', false);
+ });
+ },
+
+ /**
+ Stage a post for insertion in the stream. It should be rendered right away under the
+ assumption that the post will succeed. We can then `commitPost` when it succeeds or
+ `undoPost` when it fails.
+
+ @method stagePost
+ @param {Discourse.Post} the post to stage in the stream
+ @param {Discourse.User} the user creating the post
+ **/
+ stagePost: function(post, user) {
+ var topic = this.get('topic');
+
+ topic.setProperties({
+ posts_count: (topic.get('posts_count') || 0) + 1,
+ last_posted_at: new Date(),
+ 'details.last_poster': user,
+ highest_post_number: (topic.get('highest_post_number') || 0) + 1
+ });
+ this.set('stagingPost', true);
+
+ post.setProperties({
+ post_number: topic.get('highest_post_number'),
+ topic: topic,
+ created_at: new Date()
+ });
+
+ // If we're at the end of the stream, add the post
+ if (this.get('lastPostLoaded')) {
+ this.appendPost(post);
+ }
+ },
+
+ /**
+ Commit the post we staged. Call this after a save succeeds.
+
+ @method commitPost
+ @param {Discourse.Post} the post we saved in the stream.
+ **/
+ commitPost: function(post) {
+ this.appendPost(post);
+ this.get('stream').pushObject(post.get('id'));
+ this.set('stagingPost', false);
+ },
+
+ /**
+ Undo a post we've staged in the stream. Remove it from being rendered and revert the
+ state we changed.
+
+ @method undoPost
+ @param {Discourse.Post} the post to undo from the stream
+ **/
+ undoPost: function(post) {
+ this.posts.removeObject(post);
+
+ var topic = this.get('topic');
+
+ this.set('stagingPost', false);
+
+ topic.setProperties({
+ highest_post_number: (topic.get('highest_post_number') || 0) - 1,
+ posts_count: (topic.get('posts_count') || 0) - 1
+ });
+ },
+
+ /**
+ Prepends a single post to the stream.
+
+ @method prependPost
+ @param {Discourse.Post} post The post we're prepending
+ @returns {Discourse.Post} the post that was inserted.
+ **/
+ prependPost: function(post) {
+ this.get('posts').unshiftObject(this.storePost(post));
+ return post;
+ },
+
+ /**
+ Appends a single post into the stream.
+
+ @method appendPost
+ @param {Discourse.Post} post The post we're appending
+ @returns {Discourse.Post} the post that was inserted.
+ **/
+ appendPost: function(post) {
+ this.get('posts').addObject(this.storePost(post));
+ return post;
+ },
+
+ /**
+ Returns a post from the identity map if it's been inserted.
+
+ @method findLoadedPost
+ @param {Integer} id The post we want from the identity map.
+ @returns {Discourse.Post} the post that was inserted.
+ **/
+ findLoadedPost: function(id) {
+ return this.get('postIdentityMap').get(id);
+ },
+
+ /**
+ Finds and adds a post to the stream by id. Typically this would happen if we receive a message
+ from the message bus indicating there's a new post. We'll only insert it if we currently
+ have no filters.
+
+ @method triggerNewPostInStream
+ @param {Integer} postId The id of the new post to be inserted into the stream
+ **/
+ triggerNewPostInStream: function(postId) {
+ if (!postId) { return; }
+
+ // We only trigger if there are no filters active
+ if (!this.get('hasNoFilters')) { return; }
+
+ var lastPostLoaded = this.get('lastPostLoaded');
+
+ if (this.get('stream').indexOf(postId) === -1) {
+ this.get('stream').pushObject(postId);
+ if (lastPostLoaded) { this.appendMore(); }
+ }
+ },
+
+ /**
+ @private
+
+ Given a JSON packet, update this stream and the posts that exist in it.
+
+ @param {Object} postStreamData The JSON data we want to update from.
+ @method updateFromJson
+ **/
+ updateFromJson: function(postStreamData) {
+ var postStream = this;
+
+ var posts = this.get('posts');
+ posts.clear();
+ if (postStreamData) {
+ // Load posts if present
+ postStreamData.posts.forEach(function(p) {
+ postStream.appendPost(Discourse.Post.create(p));
+ });
+ delete postStreamData.posts;
+
+ // Update our attributes
+ postStream.setProperties(postStreamData);
+ }
+ },
+
+ /**
+ @private
+
+ Stores a post in our identity map, and sets up the references it needs to
+ find associated objects like the topic. It might return a different reference
+ than you supplied if the post has already been loaded.
+
+ @method storePost
+ @param {Discourse.Post} post The post we're storing in the identity map
+ @returns {Discourse.Post} the post from the identity map
+ **/
+ storePost: function(post) {
+ var postId = post.get('id');
+ if (postId) {
+ var postIdentityMap = this.get('postIdentityMap'),
+ existing = postIdentityMap.get(post.get('id'));
+
+ if (existing) {
+ // If the post is in the identity map, update it and return the old reference.
+ existing.updateFromPost(post);
+ return existing;
+ }
+
+ post.set('topic', this.get('topic'));
+ postIdentityMap.set(post.get('id'), post);
+ }
+ return post;
+ },
+
+ /**
+ @private
+
+ Given a list of postIds, returns a list of the posts we don't have in our
+ identity map and need to load.
+
+ @method listUnloadedIds
+ @param {Array} postIds The post Ids we want to load from the server
+ @returns {Array} the array of postIds we don't have loaded.
+ **/
+ listUnloadedIds: function(postIds) {
+ var unloaded = Em.A(),
+ postIdentityMap = this.get('postIdentityMap');
+ postIds.forEach(function(p) {
+ if (!postIdentityMap.has(p)) { unloaded.pushObject(p); }
+ });
+ return unloaded;
+ },
+
+ /**
+ @private
+
+ Returns a list of posts in order requested, by id.
+
+ @method findPostsByIds
+ @param {Array} postIds The post Ids we want to retrieve, in order.
+ @returns {Ember.Deferred} a promise that will resolve to the posts in the order requested.
+ **/
+ findPostsByIds: function(postIds) {
+ var unloaded = this.listUnloadedIds(postIds),
+ postIdentityMap = this.get('postIdentityMap');
+
+ // Load our unloaded posts by id
+ return this.loadIntoIdentityMap(unloaded).then(function() {
+ return postIds.map(function (p) {
+ return postIdentityMap.get(p);
+ });
+ });
+ },
+
+ /**
+ @private
+
+ Loads a list of posts from the server and inserts them into our identity map.
+
+ @method loadIntoIdentityMap
+ @param {Array} postIds The post Ids we want to insert into the identity map.
+ @returns {Ember.Deferred} a promise that will resolve to the posts in the order requested.
+ **/
+ loadIntoIdentityMap: function(postIds) {
+
+ // If we don't want any posts, return a promise that resolves right away
+ if (Em.isEmpty(postIds)) {
+ return Ember.Deferred.promise(function (p) { p.resolve(); });
+ }
+
+ var url = "/t/" + this.get('topic.id') + "/posts.json",
+ data = { post_ids: postIds },
+ postStream = this,
+ result = Em.A();
+
+ return Discourse.ajax(url, {data: data}).then(function(result) {
+ var posts = Em.get(result, "post_stream.posts");
+ if (posts) {
+ posts.forEach(function (p) {
+ postStream.storePost(Discourse.Post.create(p));
+ });
+ }
+ });
+ },
+
+
+ /**
+ @private
+
+ Returns the index of a particular post in the stream
+
+ @method indexOf
+ @param {Discourse.Post} post The post we're looking for
+ **/
+ indexOf: function(post) {
+ return this.get('stream').indexOf(post.get('id'));
+ },
+
+ /**
+ @private
+
+ Handles an error loading a topic based on a HTTP status code. Updates
+ the text to the correct values.
+
+ @method errorLoading
+ @param {Integer} status the HTTP status code
+ @param {Discourse.Topic} topic The topic instance we were trying to load
+ **/
+ errorLoading: function(status) {
+
+ var topic = this.get('topic');
+ topic.set('loadingFilter', false);
+ topic.set('errorLoading', true);
+
+ // If the result was 404 the post is not found
+ if (status === 404) {
+ topic.set('errorTitle', Em.String.i18n('topic.not_found.title'));
+ topic.set('message', Em.String.i18n('topic.not_found.description'));
+ return;
+ }
+
+ // If the result is 403 it means invalid access
+ if (status === 403) {
+ topic.set('errorTitle', Em.String.i18n('topic.invalid_access.title'));
+ topic.set('message', Em.String.i18n('topic.invalid_access.description'));
+ return;
+ }
+
+ // Otherwise supply a generic error message
+ topic.set('errorTitle', Em.String.i18n('topic.server_error.title'));
+ topic.set('message', Em.String.i18n('topic.server_error.description'));
+ }
+
+});
+
+
+Discourse.PostStream.reopenClass({
+
+ create: function(args) {
+ var postStream = this._super(args);
+ postStream.setProperties({
+ posts: Em.A(),
+ stream: Em.A(),
+ userFilters: Em.Set.create(),
+ postIdentityMap: Em.Map.create(),
+ bestOf: false,
+ loaded: false,
+ loadingAbove: false,
+ loadingBelow: false,
+ loadingFilter: false,
+ stagingPost: false
+ });
+ return postStream;
+ },
+
+ loadTopicView: function(topicId, args) {
+ var opts = _.merge({}, args);
+ var url = Discourse.getURL("/t/") + topicId;
+ if (opts.nearPost) {
+ url += "/" + opts.nearPost;
+ }
+ delete opts.nearPost;
+
+ return PreloadStore.getAndRemove("topic_" + topicId, function() {
+ return Discourse.ajax(url + ".json", {data: opts});
+ });
+
+ }
+
+});
diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js
index 68a09a32c22..56fdfb8f101 100644
--- a/app/assets/javascripts/discourse/models/topic.js
+++ b/app/assets/javascripts/discourse/models/topic.js
@@ -8,10 +8,13 @@
**/
Discourse.Topic = Discourse.Model.extend({
- fewParticipants: function() {
- if (!this.present('participants')) return null;
- return this.get('participants').slice(0, 3);
- }.property('participants'),
+ postStream: function() {
+ return Discourse.PostStream.create({topic: this});
+ }.property(),
+
+ details: function() {
+ return Discourse.TopicDetails.create({topic: this});
+ }.property(),
canConvertToRegular: function() {
var a = this.get('archetype');
@@ -34,8 +37,17 @@ Discourse.Topic = Discourse.Model.extend({
}.property('id'),
category: function() {
- return Discourse.Category.list().findProperty('name', this.get('categoryName'));
- }.property('categoryName'),
+ var categoryId = this.get('category_id');
+ if (categoryId) {
+ return Discourse.Category.list().findProperty('id', categoryId);
+ }
+
+ var categoryName = this.get('categoryName');
+ if (categoryName) {
+ return Discourse.Category.list().findProperty('name', categoryName);
+ }
+ return null;
+ }.property('category_id', 'categoryName'),
shareUrl: function(){
var user = Discourse.User.current();
@@ -150,17 +162,6 @@ Discourse.Topic = Discourse.Model.extend({
});
},
- removeAllowedUser: function(username) {
- var allowedUsers = this.get('allowed_users');
-
- return Discourse.ajax("/t/" + this.get('id') + "/remove-allowed-user", {
- type: 'PUT',
- data: { username: username }
- }).then(function(){
- allowedUsers.removeObject(allowedUsers.find(function(item){ return item.username === username; }));
- });
- },
-
favoriteTooltipKey: (function() {
return this.get('starred') ? 'favorite.help.unstar' : 'favorite.help.star';
}).property('starred'),
@@ -190,7 +191,7 @@ Discourse.Topic = Discourse.Model.extend({
// Save any changes we've made to the model
save: function() {
// Don't save unless we can
- if (!this.get('can_edit')) return;
+ if (!this.get('details.can_edit')) return;
return Discourse.ajax(this.get('url'), {
type: 'PUT',
@@ -218,138 +219,19 @@ Discourse.Topic = Discourse.Model.extend({
return Discourse.ajax("/t/" + (this.get('id')), { type: 'DELETE' });
},
- // Load the posts for this topic
- loadPosts: function(opts) {
+ // Update our attributes from a JSON result
+ updateFromJson: function(json) {
+ this.get('details').updateFromJson(json.details);
+
+ var keys = Object.keys(json);
+ keys.removeObject('details');
+ keys.removeObject('post_stream');
+
var topic = this;
-
- if (!opts) opts = {};
-
- // Load the first post by default
- if ((!opts.bestOf) && (!opts.nearPost)) opts.nearPost = 1;
-
- // If we already have that post in the DOM, jump to it. Return a promise
- // that's already complete.
- if (Discourse.TopicView.scrollTo(this.get('id'), opts.nearPost)) {
- return Ember.Deferred.promise(function(promise) { promise.resolve(); });
- }
-
- // If loading the topic succeeded...
- var afterTopicLoaded = function(result) {
-
- var closestPostNumber, lastPost, postDiff;
-
- // Update the slug if different
- if (result.slug) topic.set('slug', result.slug);
-
- // If we want to scroll to a post that doesn't exist, just pop them to the closest
- // one instead. This is likely happening due to a deleted post.
- opts.nearPost = parseInt(opts.nearPost, 10);
- closestPostNumber = 0;
- postDiff = Number.MAX_VALUE;
- _.each(result.posts,function(p) {
- var diff = Math.abs(p.post_number - opts.nearPost);
- if (diff < postDiff) {
- postDiff = diff;
- closestPostNumber = p.post_number;
- if (diff === 0) return false;
- }
- });
-
- opts.nearPost = closestPostNumber;
- if (topic.get('participants')) {
- topic.get('participants').clear();
- }
- if (result.suggested_topics) {
- topic.set('suggested_topics', Em.A());
- }
-
- topic.mergeAttributes(result, { suggested_topics: Discourse.Topic });
- topic.set('posts', Em.A());
- if (opts.trackVisit && result.draft && result.draft.length > 0) {
- Discourse.openComposer({
- draft: Discourse.Draft.getLocal(result.draft_key, result.draft),
- draftKey: result.draft_key,
- draftSequence: result.draft_sequence,
- topic: topic,
- ignoreIfChanged: true
- });
- }
-
- // Okay this is weird, but let's store the length of the next post when there
- lastPost = null;
- _.each(result.posts,function(p) {
- p.scrollToAfterInsert = opts.nearPost;
- var post = Discourse.Post.create(p);
- post.set('topic', topic);
- topic.get('posts').pushObject(post);
- lastPost = post;
- });
-
- topic.set('allowed_users', Em.A(result.allowed_users));
- topic.set('loaded', true);
- };
-
- var errorLoadingTopic = function(result) {
-
- topic.set('errorLoading', true);
-
- // If the result was 404 the post is not found
- if (result.status === 404) {
- topic.set('errorTitle', Em.String.i18n('topic.not_found.title'));
- topic.set('message', Em.String.i18n('topic.not_found.description'));
- return;
- }
-
- // If the result is 403 it means invalid access
- if (result.status === 403) {
- topic.set('errorTitle', Em.String.i18n('topic.invalid_access.title'));
- topic.set('message', Em.String.i18n('topic.invalid_access.description'));
- return;
- }
-
- // Otherwise supply a generic error message
- topic.set('errorTitle', Em.String.i18n('topic.server_error.title'));
- topic.set('message', Em.String.i18n('topic.server_error.description'));
- };
-
- // Finally, call our find method
- return Discourse.Topic.find(this.get('id'), {
- nearPost: opts.nearPost,
- bestOf: opts.bestOf,
- trackVisit: opts.trackVisit
- }).then(afterTopicLoaded, errorLoadingTopic);
- },
-
- notificationReasonText: function() {
- var locale_string = "topic.notifications.reasons." + this.get('notification_level');
- if (typeof this.get('notifications_reason_id') === 'number') {
- locale_string += "_" + this.get('notifications_reason_id');
- }
- return Em.String.i18n(locale_string, { username: Discourse.User.current('username_lower') });
- }.property('notification_level', 'notifications_reason_id'),
-
- updateNotifications: function(v) {
- this.set('notification_level', v);
- this.set('notifications_reason_id', null);
- return Discourse.ajax("/t/" + (this.get('id')) + "/notifications", {
- type: 'POST',
- data: { notification_level: v }
+ keys.forEach(function (key) {
+ topic.set(key, json[key]);
});
- },
- // use to add post to topics protecting from dupes
- pushPosts: function(newPosts) {
- var map, posts;
- map = {};
- posts = this.get('posts');
- _.each(posts,function(post) {
- map["" + post.post_number] = true;
- });
- _.each(newPosts,function(post) {
- if (!map[post.get('post_number')]) {
- posts.pushObject(post);
- }
- });
},
/**
@@ -422,8 +304,6 @@ Discourse.Topic.reopenClass({
},
// Load a topic, but accepts a set of filters
- // options:
- // onLoad - the callback after the topic is loaded
find: function(topicId, opts) {
var data, promise, url;
url = Discourse.getURL("/t/") + topicId;
@@ -457,15 +337,7 @@ Discourse.Topic.reopenClass({
}
// Check the preload store. If not, load it via JSON
- return PreloadStore.getAndRemove("topic_" + topicId, function() {
- return Discourse.ajax(url + ".json", {data: data});
- }).then(function(result) {
- var first = result.posts[0];
- if (first && opts && opts.bestOf) {
- first.bestOfFirst = true;
- }
- return result;
- });
+ return Discourse.ajax(url + ".json", {data: data});
},
mergeTopic: function(topicId, destinationTopicId) {
@@ -488,24 +360,6 @@ Discourse.Topic.reopenClass({
promise.reject();
});
return promise;
- },
-
- create: function(obj, topicView) {
- var result = this._super(obj);
-
- if (result.participants) {
- result.participants = _.map(result.participants,function(u) {
- return Discourse.User.create(u);
- });
- result.fewParticipants = Em.A();
- _.each(result.participants,function(p) {
- // TODO should not be hardcoded
- if (result.fewParticipants.length >= 8) return false;
- result.fewParticipants.pushObject(p);
- });
- }
-
- return result;
}
});
diff --git a/app/assets/javascripts/discourse/models/topic_details.js b/app/assets/javascripts/discourse/models/topic_details.js
new file mode 100644
index 00000000000..383974abcde
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/topic_details.js
@@ -0,0 +1,56 @@
+/**
+ A model representing a Topic's details that aren't always present, such as a list of participants.
+ When showing topics in lists and such this information should not be required.
+
+ @class TopicDetails
+ @extends Discourse.Model
+ @namespace Discourse
+ @module Discourse
+**/
+Discourse.TopicDetails = Discourse.Model.extend({
+ loaded: false,
+
+ updateFromJson: function(details) {
+
+ if (details.allowed_users) {
+ details.allowed_users = details.allowed_users.map(function (u) {
+ return Discourse.User.create(u);
+ });
+ }
+
+ if (details.suggested_topics) {
+ details.suggested_topics = details.suggested_topics.map(function (st) {
+ return Discourse.Topic.create(st);
+ });
+ }
+
+ this.setProperties(details);
+ this.set('loaded', true);
+
+ },
+
+ fewParticipants: function() {
+ if (!this.present('participants')) return null;
+ return this.get('participants').slice(0, 3);
+ }.property('participants'),
+
+
+ notificationReasonText: function() {
+ var locale_string = "topic.notifications.reasons." + this.get('notification_level');
+ if (typeof this.get('notifications_reason_id') === 'number') {
+ locale_string += "_" + this.get('notifications_reason_id');
+ }
+ return Em.String.i18n(locale_string, { username: Discourse.User.current('username_lower') });
+ }.property('notification_level', 'notifications_reason_id'),
+
+
+ updateNotifications: function(v) {
+ this.set('notification_level', v);
+ this.set('notifications_reason_id', null);
+ return Discourse.ajax("/t/" + (this.get('topic.id')) + "/notifications", {
+ type: 'POST',
+ data: { notification_level: v }
+ });
+ }
+
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
index d28b5d4291c..554e4767ef8 100644
--- a/app/assets/javascripts/discourse/routes/application_routes.js
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -11,7 +11,6 @@ Discourse.Route.buildRoutes(function() {
this.resource('topic', { path: '/t/:slug/:id' }, function() {
this.route('fromParams', { path: '/' });
this.route('fromParams', { path: '/:nearPost' });
- this.route('bestOf', { path: '/best_of' });
});
// Generate static page routes
diff --git a/app/assets/javascripts/discourse/routes/discourse_location.js b/app/assets/javascripts/discourse/routes/discourse_location.js
index 6c29043b7b3..5c02aa4120e 100644
--- a/app/assets/javascripts/discourse/routes/discourse_location.js
+++ b/app/assets/javascripts/discourse/routes/discourse_location.js
@@ -7,6 +7,35 @@
var get = Ember.get, set = Ember.set;
var popstateReady = false;
+// Thanks: https://gist.github.com/kares/956897
+var re = /([^&=]+)=?([^&]*)/g;
+var decode = function(str) {
+ return decodeURIComponent(str.replace(/\+/g, ' '));
+};
+$.parseParams = function(query) {
+ var params = {}, e;
+ if (query) {
+ if (query.substr(0, 1) === '?') {
+ query = query.substr(1);
+ }
+
+ while (e = re.exec(query)) {
+ var k = decode(e[1]);
+ var v = decode(e[2]);
+ if (params[k] !== undefined) {
+ if (!$.isArray(params[k])) {
+ params[k] = [params[k]];
+ }
+ params[k].push(v);
+ } else {
+ params[k] = v;
+ }
+ }
+ }
+ return params;
+};
+
+
/**
`Ember.DiscourseLocation` implements the location API using the browser's
`history.pushState` API.
@@ -16,6 +45,7 @@ var popstateReady = false;
@extends Ember.Object
*/
Ember.DiscourseLocation = Ember.Object.extend({
+
init: function() {
set(this, 'location', get(this, 'location') || window.location);
if ( $.inArray('state', $.event.props) < 0 ) {
@@ -32,6 +62,12 @@ Ember.DiscourseLocation = Ember.Object.extend({
@method initState
*/
initState: function() {
+
+ var location = this.get('location');
+ if (location && location.search) {
+ this.set('queryParams', $.parseParams(location.search));
+ }
+
this.replaceState(this.formatURL(this.getURL()));
set(this, 'history', window.history);
},
@@ -62,6 +98,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
@param path {String}
*/
setURL: function(path) {
+
path = this.formatURL(path);
if (this.getState() && this.getState().path !== path) {
popstateReady = true;
@@ -79,6 +116,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
@param path {String}
*/
replaceURL: function(path) {
+
path = this.formatURL(path);
if (this.getState() && this.getState().path !== path) {
@@ -129,6 +167,21 @@ Ember.DiscourseLocation = Ember.Object.extend({
window.history.replaceState({ path: path }, null, path);
},
+
+ queryParamsString: function() {
+ var params = this.get('queryParams');
+ if (Em.isEmpty(params) || Em.isEmpty(Object.keys(params))) {
+ return "";
+ } else {
+ return "?" + $.param(params).replace(/%5B/g, "[").replace(/%5D/g, "]");
+ }
+ }.property('queryParams'),
+
+ // When our query params change, update the URL
+ queryParamsStringChanged: function() {
+ this.replaceState(this.formatURL(this.getURL()));
+ }.observes('queryParamsString'),
+
/**
@private
@@ -182,7 +235,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
url = url.substring(rootURL.length);
}
- return rootURL + url;
+ return rootURL + url + this.get('queryParamsString');
},
willDestroy: function() {
diff --git a/app/assets/javascripts/discourse/routes/topic_best_of_route.js b/app/assets/javascripts/discourse/routes/topic_best_of_route.js
deleted file mode 100644
index a2b0b99bfd6..00000000000
--- a/app/assets/javascripts/discourse/routes/topic_best_of_route.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- This route is used when a topic's "best of" filter is applied
-
- @class TopicBestOfRoute
- @extends Discourse.Route
- @namespace Discourse
- @module Discourse
-**/
-Discourse.TopicBestOfRoute = Discourse.Route.extend({
-
- setupController: function(controller, params) {
- var topicController;
- params = params || {};
- params.trackVisit = true;
- params.bestOf = true;
- topicController = this.controllerFor('topic');
- topicController.cancelFilter();
- topicController.set('bestOf', true);
- topicController.loadPosts(params);
- }
-
-});
-
-
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 82769d57494..38f14bc259c 100644
--- a/app/assets/javascripts/discourse/routes/topic_from_params_route.js
+++ b/app/assets/javascripts/discourse/routes/topic_from_params_route.js
@@ -10,11 +10,46 @@ Discourse.TopicFromParamsRoute = Discourse.Route.extend({
setupController: function(controller, params) {
params = params || {};
- params.trackVisit = true;
+ params.track_visit = true;
+
+ var topic = this.modelFor('topic');
+ var postStream = topic.get('postStream');
+
+ var queryParams = Discourse.URL.get('queryParams');
+ if (queryParams) {
+ // Set bestOf on the postStream if present
+ postStream.set('bestOf', Em.get(queryParams, 'filter') === 'best_of');
+
+ // Set any username filters on the postStream
+ var userFilters = Em.get(queryParams, 'username_filters[]');
+ if (userFilters) {
+ if (typeof userFilters === "string") { userFilters = [userFilters]; }
+ userFilters.forEach(function (username) {
+ postStream.get('userFilters').add(username);
+ });
+ }
+ }
var topicController = this.controllerFor('topic');
- topicController.cancelFilter();
- topicController.loadPosts(params);
+ postStream.refresh(params).then(function () {
+ topicController.setProperties({
+ currentPost: params.nearPost || 1,
+ progressPosition: params.nearPost || 1
+ });
+
+ if (topic.present('draft')) {
+ Discourse.openComposer({
+ draft: Discourse.Draft.getLocal(topic.get('draft_key'), topic.get('draft')),
+ draftKey: topic.get('draft_key'),
+ draftSequence: topic.get('draft_sequence'),
+ topic: topic,
+ ignoreIfChanged: true
+ });
+ }
+
+ });
+
+
}
});
diff --git a/app/assets/javascripts/discourse/routes/topic_route.js b/app/assets/javascripts/discourse/routes/topic_route.js
index 517c06243d3..6b3ac7f0bd3 100644
--- a/app/assets/javascripts/discourse/routes/topic_route.js
+++ b/app/assets/javascripts/discourse/routes/topic_route.js
@@ -60,11 +60,9 @@ Discourse.TopicRoute = Discourse.Route.extend({
},
model: function(params) {
- var currentModel, _ref;
- if (currentModel = (_ref = this.controllerFor('topic')) ? _ref.get('content') : void 0) {
- if (currentModel.get('id') === parseInt(params.id, 10)) {
- return currentModel;
- }
+ var currentModel = this.modelFor('topic');
+ if (currentModel && (currentModel.get('id') === parseInt(params.id, 10))) {
+ return currentModel;
}
return Discourse.Topic.create(params);
},
@@ -85,23 +83,28 @@ Discourse.TopicRoute = Discourse.Route.extend({
// Clear the search context
this.controllerFor('search').set('searchContext', null);
- var headerController, topicController;
- topicController = this.controllerFor('topic');
- topicController.cancelFilter();
- topicController.unsubscribe();
+ var topicController = this.controllerFor('topic');
+ var postStream = topicController.get('postStream');
+ postStream.cancelFilter();
topicController.set('multiSelect', false);
+ topicController.unsubscribe();
this.controllerFor('composer').set('topic', null);
Discourse.ScreenTrack.instance().stop();
+ var headerController;
if (headerController = this.controllerFor('header')) {
headerController.set('topic', null);
headerController.set('showExtraInfo', false);
}
+
+ // Clear any filters when we leave the route
+ Discourse.URL.set('queryParams', null);
},
setupController: function(controller, model) {
controller.set('model', model);
+
this.controllerFor('header').setProperties({
topic: model,
showExtraInfo: false
diff --git a/app/assets/javascripts/discourse/templates/header.js.handlebars b/app/assets/javascripts/discourse/templates/header.js.handlebars
index 88ef66e5229..0a68ca98dd0 100644
--- a/app/assets/javascripts/discourse/templates/header.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/header.js.handlebars
@@ -10,7 +10,7 @@
{{/if}}
- {{#if topic.fancy_title}}
+ {{#if topic.details.loaded}}
{{topicStatus topic=topic}}
{{{topic.fancy_title}}}
{{else}}
diff --git a/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars b/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars
index 9f7a67801f7..0b05f97ceb9 100644
--- a/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars
@@ -55,7 +55,7 @@
{{#if like_count}}
- {{like_count}}
+ {{like_count}}
{{/if}}
|
diff --git a/app/assets/javascripts/discourse/templates/post.js.handlebars b/app/assets/javascripts/discourse/templates/post.js.handlebars
index c41e22f0d1f..ff89d2ee4de 100644
--- a/app/assets/javascripts/discourse/templates/post.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/post.js.handlebars
@@ -60,7 +60,7 @@
{{collection contentBinding="internalLinks" itemViewClass="Discourse.PostLinkView" tagName="ul" classNames="post-links"}}
- {{#if controller.can_reply_as_new_topic}}
+ {{#if controller.details.can_reply_as_new_topic}}
{{i18n post.reply_as_new_topic}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/suggested_topic.js.handlebars b/app/assets/javascripts/discourse/templates/suggested_topic.js.handlebars
index 50138c8850c..c1aa4841202 100644
--- a/app/assets/javascripts/discourse/templates/suggested_topic.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/suggested_topic.js.handlebars
@@ -1,7 +1,7 @@
{{#with view.content}}
{{#group}}
- {{{unbound fancy_title}}}
+ {{{unbound title}}}
{{#if unread}}
{{unbound unread}}
{{/if}}
@@ -19,7 +19,7 @@
|
{{#if like_count}}
- {{like_count}}
+ {{like_count}}
{{/if}}
|
diff --git a/app/assets/javascripts/discourse/templates/topic.js.handlebars b/app/assets/javascripts/discourse/templates/topic.js.handlebars
index 1a09bc300ac..45229c21193 100644
--- a/app/assets/javascripts/discourse/templates/topic.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/topic.js.handlebars
@@ -1,140 +1,133 @@
-{{#if content}}
- {{#if loaded}}
+{{#if postStream.loaded}}
- {{#if view.firstPostLoaded}}
+ {{#if postStream.firstPostLoaded}}
- {{#if view.showFavoriteButton}}
-
+
+ {{#if showFavoriteButton}}
+
{{/if}}
- {{#if view.editingTopic}}
-
- {{categoryChooser valueAttribute="name" source=view.topic.categoryName}}
+ {{#if editingTopic}}
+ {{textField id='edit-title' value=newTitle}}
+ {{categoryChooser valueAttribute="id" value=newCategoryId source=category_id}}
-
-
+
+
{{else}}
- {{#if view.topic.fancy_title}}
- {{topicStatus topic=view.topic}}
- {{{view.topic.fancy_title}}}
- {{else}}
- {{#if view.topic.errorLoading}}
- {{view.topic.errorTitle}}
- {{else}}
- {{i18n topic.loading}}
- {{/if}}
+ {{#if details.loaded}}
+ {{topicStatus topic=model}}
+ {{{fancy_title}}}
{{/if}}
{{categoryLink category}}
- {{#if view.topic.can_edit}}
-
+ {{#if details.can_edit}}
+
{{/if}}
{{/if}}
- {{/if}}
+ {{/if}}
-
+
- {{view Discourse.SelectedPostsView}}
-
-
-
-
-
-
-
- {{#if loadingAbove}}
-
{{i18n loading}}
- {{/if}}
-
- {{collection itemViewClass="Discourse.PostView" contentBinding="posts" topicViewBinding="view"}}
-
- {{#if loadingBelow}}
-
{{i18n loading}}
- {{/if}}
+ {{view Discourse.SelectedPostsView}}
+
+
+
+
+
-
- {{#if loading}}
- {{#unless loadingBelow}}
-
{{i18n loading}}
- {{/unless}}
- {{else}}
- {{#if view.fullyLoaded}}
-
- {{view Discourse.TopicClosingView topicBinding="model"}}
-
- {{view Discourse.TopicFooterButtonsView topicBinding="model"}}
-
- {{#if suggested_topics.length}}
-
-
-
{{i18n suggested_topics.title}}
-
-
-
-
-
- {{i18n topic.title}}
- |
- {{i18n category_title}} |
- {{i18n posts}} |
- {{i18n likes}} |
- {{i18n views}} |
- {{i18n activity}} |
-
-
- {{each suggested_topics itemTagName="tr" itemViewClass="Discourse.SuggestedTopicView"}}
-
-
-
-
{{{view.browseMoreMessage}}}
-
- {{/if}}
- {{/if}}
+ {{#if postStream.loadingAbove}}
+
{{i18n loading}}
{{/if}}
+ {{#unless postStream.loadingFilter}}
+ {{collection itemViewClass="Discourse.PostView" contentBinding="postStream.posts" topicViewBinding="view"}}
+ {{/unless}}
-
-
+ {{#if postStream.loadingBelow}}
+ {{i18n loading}}
+ {{/if}}
+
+
+ {{#if postStream.loadingFilter}}
+
{{i18n loading}}
+ {{else}}
+ {{#if postStream.lastPostLoaded}}
+
+ {{view Discourse.TopicClosingView topicBinding="model"}}
+ {{view Discourse.TopicFooterButtonsView topicBinding="model"}}
+
+ {{#if details.suggested_topics.length}}
+
+
+
{{i18n suggested_topics.title}}
+
+
+
+
+
+ {{i18n topic.title}}
+ |
+ {{i18n category_title}} |
+ {{i18n posts}} |
+ {{i18n likes}} |
+ {{i18n views}} |
+ {{i18n activity}} |
+
+
+ {{each details.suggested_topics itemTagName="tr" itemViewClass="Discourse.SuggestedTopicView"}}
+
+
+
+
{{{view.browseMoreMessage}}}
+
+ {{/if}}
+ {{/if}}
+ {{/if}}
+
+
+
+
+
+{{else}}
+ {{#if message}}
+
+
+
+
{{message}}
+
+
+ {{#linkTo list.latest}}{{i18n topic.back_to_list}}{{/linkTo}}
+
+
{{else}}
- {{#if message}}
-
-
-
-
{{message}}
-
-
- {{#linkTo list.latest}}{{i18n topic.back_to_list}}{{/linkTo}}
-
-
- {{else}}
-
- {{/if}}
+
{{/if}}
{{/if}}
-
- {{filterDesc}}
-
{{i18n topic.filters.cancel}}
+
+
+ {{postStream.filterDesc}}
+
{{i18n topic.filters.cancel}}
{{render share}}
diff --git a/app/assets/javascripts/discourse/templates/topic_admin_menu.js.handlebars b/app/assets/javascripts/discourse/templates/topic_admin_menu.js.handlebars
index 3b9258ef430..3a7fe1bb057 100644
--- a/app/assets/javascripts/discourse/templates/topic_admin_menu.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/topic_admin_menu.js.handlebars
@@ -7,7 +7,7 @@
- {{#if can_delete}}
+ {{#if details.can_delete}}
diff --git a/app/assets/javascripts/discourse/templates/topic_summary/best_of_toggle.js.handlebars b/app/assets/javascripts/discourse/templates/topic_summary/best_of_toggle.js.handlebars
index dc949e79f03..1d693781023 100644
--- a/app/assets/javascripts/discourse/templates/topic_summary/best_of_toggle.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/topic_summary/best_of_toggle.js.handlebars
@@ -1,8 +1,8 @@
{{i18n best_of.title}}
-{{#if bestOf}}
+{{#if postStream.bestOf}}
{{{i18n best_of.enabled_description}}}
-
+
{{else}}
{{{i18n best_of.description count="posts_count"}}}
-
+
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/topic_summary/info.js.handlebars b/app/assets/javascripts/discourse/templates/topic_summary/info.js.handlebars
index f1b5088e611..f6378b96193 100644
--- a/app/assets/javascripts/discourse/templates/topic_summary/info.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/topic_summary/info.js.handlebars
@@ -17,14 +17,14 @@
{{i18n created}}
- {{avatar created_by imageSize="tiny"}}
+ {{avatar details.created_by imageSize="tiny"}}
{{date created_at}}
{{i18n last_post}}
- {{avatar last_poster imageSize="tiny"}}
+ {{avatar details.last_poster imageSize="tiny"}}
{{date last_posted_at}}
@@ -38,11 +38,11 @@
{{i18n links}}
- {{number links.length}}
+ {{number details.links.length}}
- {{#if fewParticipants}}
+ {{#if details.fewParticipants}}
- {{#each fewParticipants}}{{participant participant=this}}{{/each}}
+ {{#each details.fewParticipants}}{{participant participant=this}}{{/each}}
{{/if}}
@@ -53,12 +53,12 @@
-
{{i18n created}}
- {{avatar created_by imageSize="tiny"}}
+ {{avatar details.created_by imageSize="tiny"}}
{{date created_at}}
-
{{i18n last_post}}
- {{avatar last_poster imageSize="tiny"}}
+ {{avatar details.last_poster imageSize="tiny"}}
{{date last_posted_at}}
-
@@ -72,9 +72,9 @@
- {{#if participants}}
+ {{#if details.participants}}
- {{#each participants}}{{participant participant=this}}{{/each}}
+ {{#each details.participants}}{{participant participant=this}}{{/each}}
{{/if}}
@@ -92,7 +92,7 @@
{{#if view.parentView.showAllLinksControls}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/topic_summary/private_message.js.handlebars b/app/assets/javascripts/discourse/templates/topic_summary/private_message.js.handlebars
index a915efcd8db..8de464e3d84 100644
--- a/app/assets/javascripts/discourse/templates/topic_summary/private_message.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/topic_summary/private_message.js.handlebars
@@ -1,11 +1,11 @@
{{i18n private_message_info.title}}
- {{#each allowed_groups}}
+ {{#each details.allowed_groups}}
#{{unbound name}}
{{/each}}
- {{#each allowed_users}}
+ {{#each details.allowed_users}}
{{/each}}
-{{#if can_invite_to}}
+{{#if details.can_invite_to}}
diff --git a/app/assets/javascripts/discourse/views/buttons/notifications_button.js b/app/assets/javascripts/discourse/views/buttons/notifications_button.js
index 97bb546831f..a6ffadd0537 100644
--- a/app/assets/javascripts/discourse/views/buttons/notifications_button.js
+++ b/app/assets/javascripts/discourse/views/buttons/notifications_button.js
@@ -8,7 +8,7 @@
**/
Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
title: Em.String.i18n('topic.notifications.title'),
- longDescriptionBinding: 'topic.notificationReasonText',
+ longDescriptionBinding: 'topic.details.notificationReasonText',
dropDownContent: [
[Discourse.Topic.NotificationLevel.WATCHING, 'topic.notifications.watching'],
@@ -19,7 +19,7 @@ Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
text: function() {
var key = (function() {
- switch (this.get('topic.notification_level')) {
+ switch (this.get('topic.details.notification_level')) {
case Discourse.Topic.NotificationLevel.WATCHING: return 'watching';
case Discourse.Topic.NotificationLevel.TRACKING: return 'tracking';
case Discourse.Topic.NotificationLevel.REGULAR: return 'regular';
@@ -36,10 +36,10 @@ Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
}
})();
return icon + (Ember.String.i18n("topic.notifications." + key + ".title")) + "
";
- }.property('topic.notification_level'),
+ }.property('topic.details.notification_level'),
clicked: function(id) {
- return this.get('topic').updateNotifications(id);
+ return this.get('topic.details').updateNotifications(id);
}
});
diff --git a/app/assets/javascripts/discourse/views/buttons/reply_button.js b/app/assets/javascripts/discourse/views/buttons/reply_button.js
index d394816a73a..09e121aee85 100644
--- a/app/assets/javascripts/discourse/views/buttons/reply_button.js
+++ b/app/assets/javascripts/discourse/views/buttons/reply_button.js
@@ -10,7 +10,7 @@ Discourse.ReplyButton = Discourse.ButtonView.extend({
classNames: ['btn', 'btn-primary', 'create'],
attributeBindings: ['disabled'],
helpKey: 'topic.reply.help',
- disabled: Em.computed.not('controller.content.can_create_post'),
+ disabled: Em.computed.not('controller.model.details.can_create_post'),
text: function() {
var archetypeCapitalized = this.get('controller.content.archetype').capitalize();
diff --git a/app/assets/javascripts/discourse/views/participant_view.js b/app/assets/javascripts/discourse/views/participant_view.js
index 93d9979ce9b..f9251adbef8 100644
--- a/app/assets/javascripts/discourse/views/participant_view.js
+++ b/app/assets/javascripts/discourse/views/participant_view.js
@@ -9,9 +9,9 @@
Discourse.ParticipantView = Discourse.View.extend({
templateName: 'participant',
- toggled: (function() {
- return this.get('controller.userFilters').contains(this.get('participant.username'));
- }).property('controller.userFilters.[]')
+ toggled: function() {
+ return this.get('controller.postStream.userFilters').contains(this.get('participant.username'));
+ }.property('controller.postStream.userFilters.[]')
});
diff --git a/app/assets/javascripts/discourse/views/post_menu_view.js b/app/assets/javascripts/discourse/views/post_menu_view.js
index d9a949e63cd..d327ccfc525 100644
--- a/app/assets/javascripts/discourse/views/post_menu_view.js
+++ b/app/assets/javascripts/discourse/views/post_menu_view.js
@@ -60,7 +60,7 @@ Discourse.PostMenuView = Discourse.View.extend({
// Delete button
renderDelete: function(post, buffer) {
- if (post.get('post_number') === 1 && this.get('controller.content.can_delete')) {
+ if (post.get('post_number') === 1 && this.get('controller.model.details.can_delete')) {
buffer.push("
");
@@ -138,7 +138,7 @@ Discourse.PostMenuView = Discourse.View.extend({
// Reply button
renderReply: function(post, buffer) {
- if (!this.get('controller.content.can_create_post')) return;
+ if (!this.get('controller.model.details.can_create_post')) return;
buffer.push("