Refactor: Move Topic Details into better objects, identity map, tests, query string filters
This commit is contained in:
parent
d051e35000
commit
5770879472
|
@ -40,6 +40,7 @@ Discourse.AdminFlagsController = Ember.ArrayController.extend({
|
||||||
bootbox.alert(Em.String.i18n("admin.flags.error"));
|
bootbox.alert(Em.String.i18n("admin.flags.error"));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Deletes a post
|
Deletes a post
|
||||||
|
|
||||||
|
|
|
@ -331,10 +331,6 @@ Discourse = Ember.Application.createWithMixins({
|
||||||
Discourse.MessageBus.start();
|
Discourse.MessageBus.start();
|
||||||
Discourse.KeyValueStore.init("discourse_", Discourse.MessageBus);
|
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
|
// Developer specific functions
|
||||||
Discourse.Development.setupProbes();
|
Discourse.Development.setupProbes();
|
||||||
Discourse.Development.observeLiveChanges();
|
Discourse.Development.observeLiveChanges();
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
@namespace Discourse
|
@namespace Discourse
|
||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
Discourse.URL = {
|
Discourse.URL = Em.Object.createWithMixins({
|
||||||
|
|
||||||
// Used for matching a topic
|
// Used for matching a topic
|
||||||
TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/,
|
TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/,
|
||||||
|
@ -23,7 +23,7 @@ Discourse.URL = {
|
||||||
**/
|
**/
|
||||||
router: function() {
|
router: function() {
|
||||||
return Discourse.__container__.lookup('router:main');
|
return Discourse.__container__.lookup('router:main');
|
||||||
},
|
}.property(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Browser aware replaceState. Will only be invoked if the browser supports it.
|
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`
|
// 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!
|
// which triggers a replaceState even though the topic hasn't fully loaded yet!
|
||||||
Em.run.next(function() {
|
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) {
|
if (oldTopicId === newTopicId) {
|
||||||
Discourse.URL.replaceState(path);
|
Discourse.URL.replaceState(path);
|
||||||
var topicController = Discourse.__container__.lookup('controller:topic');
|
var topicController = Discourse.__container__.lookup('controller:topic');
|
||||||
var opts = { trackVisit: false };
|
var opts = { };
|
||||||
if (newMatches[3]) opts.nearPost = newMatches[3];
|
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.
|
// Abort routing, we have replaced our state.
|
||||||
return;
|
return;
|
||||||
|
@ -102,11 +109,18 @@ Discourse.URL = {
|
||||||
|
|
||||||
// Be wary of looking up the router. In this case, we have links in our
|
// 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.
|
// HTML, say form compiled markdown posts, that need to be routed.
|
||||||
var router = this.router();
|
var router = this.get('router');
|
||||||
router.router.updateURL(path);
|
router.router.updateURL(path);
|
||||||
return router.handleURL(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
|
@private
|
||||||
|
|
||||||
|
@ -131,4 +145,4 @@ Discourse.URL = {
|
||||||
window.location = Discourse.getURL(url);
|
window.location = Discourse.getURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
});
|
||||||
|
|
|
@ -10,15 +10,15 @@
|
||||||
Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
|
Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
|
||||||
|
|
||||||
setDays: function() {
|
setDays: function() {
|
||||||
if( this.get('auto_close_at') ) {
|
if( this.get('details.auto_close_at') ) {
|
||||||
var closeTime = new Date( this.get('auto_close_at') );
|
var closeTime = new Date( this.get('details.auto_close_at') );
|
||||||
if (closeTime > new Date()) {
|
if (closeTime > new Date()) {
|
||||||
this.set('auto_close_days', closeTime.daysSince());
|
this.set('auto_close_days', closeTime.daysSince());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.set('auto_close_days', '');
|
this.set('details.auto_close_days', '');
|
||||||
}
|
}
|
||||||
}.observes('auto_close_at'),
|
}.observes('details.auto_close_at'),
|
||||||
|
|
||||||
saveAutoClose: function() {
|
saveAutoClose: function() {
|
||||||
this.setAutoClose( parseFloat(this.get('auto_close_days')) );
|
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
|
dataType: 'html', // no custom errors, jquery 1.9 enforces json
|
||||||
data: { auto_close_days: days > 0 ? days : null }
|
data: { auto_close_days: days > 0 ? days : null }
|
||||||
}).then(function(){
|
}).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) {
|
}, function (error) {
|
||||||
bootbox.alert(Em.String.i18n('generic_error'));
|
bootbox.alert(Em.String.i18n('generic_error'));
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,7 +40,7 @@ Discourse.InvitePrivateController = Discourse.ObjectController.extend(Discourse.
|
||||||
invitePrivateController.set('finished', true);
|
invitePrivateController.set('finished', true);
|
||||||
|
|
||||||
if(result && result.user) {
|
if(result && result.user) {
|
||||||
invitePrivateController.get('content.allowed_users').pushObject(result.user);
|
invitePrivateController.get('content.details.allowed_users').pushObject(result.user);
|
||||||
}
|
}
|
||||||
}, function() {
|
}, function() {
|
||||||
// Failure
|
// Failure
|
||||||
|
|
|
@ -34,7 +34,7 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({
|
||||||
if (!Discourse.User.current()) return;
|
if (!Discourse.User.current()) return;
|
||||||
|
|
||||||
// don't display the "quote-reply" button if we can't create a post
|
// 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();
|
var selection = window.getSelection();
|
||||||
// no selections
|
// no selections
|
||||||
|
|
|
@ -7,24 +7,28 @@
|
||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, {
|
Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, {
|
||||||
userFilters: new Em.Set(),
|
|
||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
bestOf: false,
|
|
||||||
summaryCollapsed: true,
|
summaryCollapsed: true,
|
||||||
loading: false,
|
|
||||||
loadingBelow: false,
|
|
||||||
loadingAbove: false,
|
|
||||||
needs: ['header', 'modal', 'composer', 'quoteButton'],
|
needs: ['header', 'modal', 'composer', 'quoteButton'],
|
||||||
allPostsSelected: false,
|
allPostsSelected: false,
|
||||||
selectedPosts: new Em.Set(),
|
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() {
|
canMergeTopic: function() {
|
||||||
if (!this.get('can_move_posts')) return false;
|
if (!this.get('details.can_move_posts')) return false;
|
||||||
return (this.get('selectedPostsCount') > 0);
|
return (this.get('selectedPostsCount') > 0);
|
||||||
}.property('selectedPostsCount'),
|
}.property('selectedPostsCount'),
|
||||||
|
|
||||||
canSplitTopic: function() {
|
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;
|
if (this.get('allPostsSelected')) return false;
|
||||||
return (this.get('selectedPostsCount') > 0);
|
return (this.get('selectedPostsCount') > 0);
|
||||||
}.property('selectedPostsCount'),
|
}.property('selectedPostsCount'),
|
||||||
|
@ -64,11 +68,11 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
}.observes('multiSelect'),
|
}.observes('multiSelect'),
|
||||||
|
|
||||||
hideProgress: function() {
|
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('currentPost')) return true;
|
||||||
if (this.get('content.filtered_posts_count') < 2) return true;
|
if (this.get('postStream.filteredPostsCount') < 2) return true;
|
||||||
return false;
|
return false;
|
||||||
}.property('content.loaded', 'currentPost', 'content.filtered_posts_count'),
|
}.property('postStream.loaded', 'currentPost', 'postStream.filteredPostsCount'),
|
||||||
|
|
||||||
selectPost: function(post) {
|
selectPost: function(post) {
|
||||||
var selectedPosts = this.get('selectedPosts');
|
var selectedPosts = this.get('selectedPosts');
|
||||||
|
@ -107,6 +111,58 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
this.toggleProperty('summaryCollapsed');
|
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() {
|
deleteSelected: function() {
|
||||||
var topicController = this;
|
var topicController = this;
|
||||||
bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
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() {
|
jumpTop: function() {
|
||||||
if (this.get('bestOf')) {
|
Discourse.URL.routeTo(this.get('url'));
|
||||||
Discourse.TopicView.scrollTo(this.get('id'), this.get('posts')[0].get('post_number'));
|
|
||||||
} else {
|
|
||||||
Discourse.URL.routeTo(this.get('url'));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
jumpBottom: function() {
|
jumpBottom: function() {
|
||||||
if (this.get('bestOf')) {
|
Discourse.URL.routeTo(this.get('lastPostUrl'));
|
||||||
Discourse.TopicView.scrollTo(this.get('id'), _.last(this.get('posts')).get('post_number'));
|
|
||||||
} else {
|
|
||||||
Discourse.URL.routeTo(this.get('lastPostUrl'));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelFilter: function() {
|
|
||||||
this.set('bestOf', false);
|
|
||||||
this.get('userFilters').clear();
|
|
||||||
},
|
|
||||||
|
|
||||||
replyAsNewTopic: function(post) {
|
replyAsNewTopic: function(post) {
|
||||||
// TODO shut down topic draft cleanly if it exists ...
|
// 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() {
|
toggleParticipant: function(user) {
|
||||||
|
this.get('postStream').toggleParticipant(Em.get(user, 'username'));
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
enableBestOf: function(e) {
|
showFavoriteButton: function() {
|
||||||
this.set('bestOf', true);
|
return Discourse.User.current() && !this.get('isPrivateMessage');
|
||||||
this.get('userFilters').clear();
|
}.property('isPrivateMessage'),
|
||||||
},
|
|
||||||
|
|
||||||
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'),
|
|
||||||
|
|
||||||
deleteTopic: function() {
|
deleteTopic: function() {
|
||||||
var topicController = this;
|
var topicController = this;
|
||||||
|
@ -327,23 +295,13 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
bus.subscribe("/topic/" + (this.get('id')), function(data) {
|
bus.subscribe("/topic/" + (this.get('id')), function(data) {
|
||||||
var topic = topicController.get('model');
|
var topic = topicController.get('model');
|
||||||
if (data.notification_level_change) {
|
if (data.notification_level_change) {
|
||||||
topic.set('notification_level', data.notification_level_change);
|
topic.set('details.notification_level', data.notification_level_change);
|
||||||
topic.set('notifications_reason_id', data.notifications_reason_id);
|
topic.set('details.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;
|
|
||||||
})) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Robin, TODO when a message comes in we need to figure out if it even goes
|
// Add the new post into the stream
|
||||||
// in this view ... for now fixed the general case
|
topicController.get('postStream').triggerNewPostInStream(data.id);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -424,15 +382,8 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
post.destroy();
|
post.destroy();
|
||||||
},
|
},
|
||||||
|
|
||||||
postRendered: function(post) {
|
|
||||||
var onPostRendered = this.get('onPostRendered');
|
|
||||||
if (onPostRendered) {
|
|
||||||
onPostRendered(post);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeAllowedUser: function(username) {
|
removeAllowedUser: function(username) {
|
||||||
this.get('model').removeAllowedUser(username);
|
this.get('details').removeAllowedUser(username);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -357,70 +357,40 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
var post = this.get('post'),
|
var post = this.get('post'),
|
||||||
topic = this.get('topic'),
|
topic = this.get('topic'),
|
||||||
currentUser = Discourse.User.current(),
|
currentUser = Discourse.User.current(),
|
||||||
|
postStream = this.get('topic.postStream'),
|
||||||
addedToStream = false;
|
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
|
// Build the post object
|
||||||
var createdPost = Discourse.Post.create({
|
var createdPost = Discourse.Post.create({
|
||||||
raw: this.get('reply'),
|
raw: this.get('reply'),
|
||||||
title: this.get('title'),
|
title: this.get('title'),
|
||||||
category: this.get('categoryName'),
|
category: this.get('categoryName'),
|
||||||
topic_id: this.get('topic.id'),
|
topic_id: this.get('topic.id'),
|
||||||
reply_to_post_number: post ? post.get('post_number') : null,
|
reply_to_post_number: post ? post.get('post_number') : null,
|
||||||
imageSizes: opts.imageSizes,
|
imageSizes: opts.imageSizes,
|
||||||
post_number: probablePostNumber,
|
cooked: $('#wmd-preview').html(),
|
||||||
index: probablePostNumber,
|
reply_count: 0,
|
||||||
cooked: $('#wmd-preview').html(),
|
display_username: currentUser.get('name'),
|
||||||
reply_count: 0,
|
username: currentUser.get('username'),
|
||||||
display_username: currentUser.get('name'),
|
user_id: currentUser.get('id'),
|
||||||
username: currentUser.get('username'),
|
metaData: this.get('metaData'),
|
||||||
user_id: currentUser.get('id'),
|
archetype: this.get('archetypeId'),
|
||||||
metaData: this.get('metaData'),
|
post_type: Discourse.Site.instance().get('post_types.regular'),
|
||||||
archetype: this.get('archetypeId'),
|
target_usernames: this.get('targetUsernames'),
|
||||||
post_type: Discourse.Site.instance().get('post_types.regular'),
|
actions_summary: Em.A(),
|
||||||
target_usernames: this.get('targetUsernames'),
|
moderator: currentUser.get('moderator'),
|
||||||
actions_summary: Em.A(),
|
yours: true,
|
||||||
moderator: currentUser.get('moderator'),
|
newPost: true,
|
||||||
yours: true,
|
auto_close_days: this.get('auto_close_days')
|
||||||
newPost: true,
|
});
|
||||||
auto_close_days: this.get('auto_close_days')
|
|
||||||
});
|
|
||||||
|
|
||||||
// If we're in a topic, we can append the post instantly.
|
// If we're in a topic, we can append the post instantly.
|
||||||
if (topic) {
|
if (postStream) {
|
||||||
|
// If it's in reply to another post, increase the reply count
|
||||||
// Increase the reply count
|
|
||||||
if (post) {
|
if (post) {
|
||||||
post.set('reply_count', (post.get('reply_count') || 0) + 1);
|
post.set('reply_count', (post.get('reply_count') || 0) + 1);
|
||||||
}
|
}
|
||||||
topic.set('posts_count', topic.get('posts_count') + 1);
|
postStream.stagePost(createdPost, currentUser);
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save callback
|
// Save callback
|
||||||
|
@ -430,11 +400,13 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
var addedPost = false,
|
var addedPost = false,
|
||||||
saving = true;
|
saving = true;
|
||||||
|
|
||||||
createdPost.updateFromSave(result);
|
createdPost.updateFromJson(result);
|
||||||
if (topic) {
|
if (topic) {
|
||||||
// It's no longer a new post
|
// It's no longer a new post
|
||||||
createdPost.set('newPost', false);
|
createdPost.set('newPost', false);
|
||||||
topic.set('draft_sequence', result.draft_sequence);
|
topic.set('draft_sequence', result.draft_sequence);
|
||||||
|
postStream.commitPost(createdPost);
|
||||||
|
addedToStream = true;
|
||||||
} else {
|
} else {
|
||||||
// We created a new topic, let's show it.
|
// We created a new topic, let's show it.
|
||||||
composer.set('composeState', CLOSED);
|
composer.set('composeState', CLOSED);
|
||||||
|
@ -448,12 +420,13 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
} else if (saving) {
|
} else if (saving) {
|
||||||
composer.set('composeState', SAVING);
|
composer.set('composeState', SAVING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return promise.resolve({ post: result });
|
return promise.resolve({ post: result });
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
// If an error occurs
|
// If an error occurs
|
||||||
if (topic) {
|
if (postStream) {
|
||||||
topic.posts.removeObject(createdPost);
|
postStream.undoPost(createdPost);
|
||||||
topic.set('filtered_posts_count', topic.get('filtered_posts_count') - 1);
|
|
||||||
}
|
}
|
||||||
promise.reject($.parseJSON(error.responseText).errors[0]);
|
promise.reject($.parseJSON(error.responseText).errors[0]);
|
||||||
composer.set('composeState', OPEN);
|
composer.set('composeState', OPEN);
|
||||||
|
|
|
@ -15,9 +15,8 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
return this.get('url') + (user ? '?u=' + user.get('username_lower') : '');
|
return this.get('url') + (user ? '?u=' + user.get('username_lower') : '');
|
||||||
}.property('url'),
|
}.property('url'),
|
||||||
|
|
||||||
new_user: function() {
|
new_user: Em.computed.equal('trust_level', 0),
|
||||||
return this.get('trust_level') === 0;
|
firstPost: Em.computed.equal('post_number', 1),
|
||||||
}.property('trust_level'),
|
|
||||||
|
|
||||||
url: function() {
|
url: function() {
|
||||||
return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number'));
|
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));
|
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'),
|
}.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() {
|
byTopicCreator: function() {
|
||||||
return this.get('topic.created_by.id') === this.get('user_id');
|
return this.get('topic.details.created_by.id') === this.get('user_id');
|
||||||
}.property('topic.created_by.id', 'user_id'),
|
}.property('topic.details.created_by.id', 'user_id'),
|
||||||
|
|
||||||
hasHistory: function() {
|
hasHistory: function() {
|
||||||
return this.get('version') > 1;
|
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
|
// 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.
|
// 'last-read' if the post has been seen or is the highest post number seen so far respectively.
|
||||||
bookmarkClass: function() {
|
bookmarkClass: function() {
|
||||||
var result, topic;
|
var result = 'read-icon';
|
||||||
result = 'read-icon';
|
|
||||||
if (this.get('bookmarked')) return result + ' bookmarked';
|
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')) {
|
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
|
||||||
result += ' last-read';
|
return result + ' last-read';
|
||||||
} else {
|
|
||||||
if (this.get('read')) {
|
|
||||||
result += ' seen';
|
|
||||||
} else {
|
|
||||||
result += ' unseen';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
|
return result + (this.get('read') ? ' seen' : ' unseen');
|
||||||
}.property('read', 'topic.last_read_post_number', 'bookmarked'),
|
}.property('read', 'topic.last_read_post_number', 'bookmarked'),
|
||||||
|
|
||||||
// Custom tooltips for the bookmark icons
|
// Custom tooltips for the bookmark icons
|
||||||
bookmarkTooltip: function() {
|
bookmarkTooltip: function() {
|
||||||
var topic;
|
|
||||||
if (this.get('bookmarked')) return Em.String.i18n('bookmarks.created');
|
if (this.get('bookmarked')) return Em.String.i18n('bookmarks.created');
|
||||||
if (!this.get('read')) return "";
|
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')) {
|
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
|
||||||
return Em.String.i18n('bookmarks.last_read');
|
return Em.String.i18n('bookmarks.last_read');
|
||||||
}
|
}
|
||||||
|
@ -123,9 +112,9 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
}.property('updated_at'),
|
}.property('updated_at'),
|
||||||
|
|
||||||
flagsAvailable: function() {
|
flagsAvailable: function() {
|
||||||
var _this = this;
|
var post = this,
|
||||||
var flags = Discourse.Site.instance().get('flagTypes').filter(function(item) {
|
flags = Discourse.Site.instance().get('flagTypes').filter(function(item) {
|
||||||
return _this.get("actionByName." + (item.get('name_key')) + ".can_act");
|
return post.get("actionByName." + (item.get('name_key')) + ".can_act");
|
||||||
});
|
});
|
||||||
return flags;
|
return flags;
|
||||||
}.property('actions_summary.@each.can_act'),
|
}.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 a post and call the callback when done.
|
||||||
save: function(complete, error) {
|
save: function(complete, error) {
|
||||||
var data, metaData;
|
|
||||||
if (!this.get('newPost')) {
|
if (!this.get('newPost')) {
|
||||||
// We're updating a post
|
// We're updating a post
|
||||||
return Discourse.ajax("/posts/" + (this.get('id')), {
|
return Discourse.ajax("/posts/" + (this.get('id')), {
|
||||||
|
@ -163,7 +151,7 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// We're saving a post
|
// We're saving a post
|
||||||
data = {
|
var data = {
|
||||||
raw: this.get('raw'),
|
raw: this.get('raw'),
|
||||||
topic_id: this.get('topic_id'),
|
topic_id: this.get('topic_id'),
|
||||||
reply_to_post_number: this.get('reply_to_post_number'),
|
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')
|
auto_close_days: this.get('auto_close_days')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var metaData = this.get('metaData');
|
||||||
// Put the metaData into the request
|
// Put the metaData into the request
|
||||||
if (metaData = this.get('metaData')) {
|
if (metaData) {
|
||||||
data.meta_data = {};
|
data.meta_data = {};
|
||||||
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
|
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
|
||||||
}
|
}
|
||||||
|
|
||||||
return Discourse.ajax("/posts", {
|
return Discourse.ajax("/posts", {
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: data
|
data: data
|
||||||
|
@ -201,14 +191,35 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
return Discourse.ajax("/posts/" + (this.get('id')), { type: 'DELETE' });
|
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.
|
Updates a post from another's attributes. This will normally happen when a post is loading but
|
||||||
updateFromSave: function(obj) {
|
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;
|
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
|
// Update all the properties
|
||||||
if (!obj) return;
|
var post = this;
|
||||||
_.each(obj, function(val,key) {
|
_.each(obj, function(val,key) {
|
||||||
if (key !== 'actions_summary'){
|
if (key !== 'actions_summary'){
|
||||||
if (val) {
|
if (val) {
|
||||||
|
@ -255,7 +266,7 @@ Discourse.Post = Discourse.Model.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Whether to show replies directly below
|
// Whether to show replies directly below
|
||||||
showRepliesBelow: (function() {
|
showRepliesBelow: function() {
|
||||||
var reply_count, _ref;
|
var reply_count, _ref;
|
||||||
reply_count = this.get('reply_count');
|
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;
|
if ((_ref = this.get('topic')) ? _ref.isReplyDirectlyBelow(this) : void 0) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}).property('reply_count')
|
}.property('reply_count')
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Discourse.Post.reopenClass({
|
Discourse.Post.reopenClass({
|
||||||
|
|
||||||
createActionSummary: function(result) {
|
createActionSummary: function(result) {
|
||||||
var lookup;
|
|
||||||
if (result.actions_summary) {
|
if (result.actions_summary) {
|
||||||
lookup = Em.Object.create();
|
var lookup = Em.Object.create();
|
||||||
result.actions_summary = result.actions_summary.map(function(a) {
|
result.actions_summary = result.actions_summary.map(function(a) {
|
||||||
a.post = result;
|
a.post = result;
|
||||||
a.actionType = Discourse.Site.instance().postActionTypeById(a.id);
|
a.actionType = Discourse.Site.instance().postActionTypeById(a.id);
|
||||||
|
@ -288,17 +299,16 @@ Discourse.Post.reopenClass({
|
||||||
lookup.set(a.actionType.get('name_key'), actionSummary);
|
lookup.set(a.actionType.get('name_key'), actionSummary);
|
||||||
return actionSummary;
|
return actionSummary;
|
||||||
});
|
});
|
||||||
return result.set('actionByName', lookup);
|
result.set('actionByName', lookup);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
create: function(obj, topic) {
|
create: function(obj) {
|
||||||
var result = this._super(obj);
|
var result = this._super(obj);
|
||||||
this.createActionSummary(result);
|
this.createActionSummary(result);
|
||||||
if (obj && obj.reply_to_user) {
|
if (obj && obj.reply_to_user) {
|
||||||
result.set('reply_to_user', Discourse.User.create(obj.reply_to_user));
|
result.set('reply_to_user', Discourse.User.create(obj.reply_to_user));
|
||||||
}
|
}
|
||||||
result.set('topic', topic);
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -8,10 +8,13 @@
|
||||||
**/
|
**/
|
||||||
Discourse.Topic = Discourse.Model.extend({
|
Discourse.Topic = Discourse.Model.extend({
|
||||||
|
|
||||||
fewParticipants: function() {
|
postStream: function() {
|
||||||
if (!this.present('participants')) return null;
|
return Discourse.PostStream.create({topic: this});
|
||||||
return this.get('participants').slice(0, 3);
|
}.property(),
|
||||||
}.property('participants'),
|
|
||||||
|
details: function() {
|
||||||
|
return Discourse.TopicDetails.create({topic: this});
|
||||||
|
}.property(),
|
||||||
|
|
||||||
canConvertToRegular: function() {
|
canConvertToRegular: function() {
|
||||||
var a = this.get('archetype');
|
var a = this.get('archetype');
|
||||||
|
@ -34,8 +37,17 @@ Discourse.Topic = Discourse.Model.extend({
|
||||||
}.property('id'),
|
}.property('id'),
|
||||||
|
|
||||||
category: function() {
|
category: function() {
|
||||||
return Discourse.Category.list().findProperty('name', this.get('categoryName'));
|
var categoryId = this.get('category_id');
|
||||||
}.property('categoryName'),
|
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(){
|
shareUrl: function(){
|
||||||
var user = Discourse.User.current();
|
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() {
|
favoriteTooltipKey: (function() {
|
||||||
return this.get('starred') ? 'favorite.help.unstar' : 'favorite.help.star';
|
return this.get('starred') ? 'favorite.help.unstar' : 'favorite.help.star';
|
||||||
}).property('starred'),
|
}).property('starred'),
|
||||||
|
@ -190,7 +191,7 @@ Discourse.Topic = Discourse.Model.extend({
|
||||||
// Save any changes we've made to the model
|
// Save any changes we've made to the model
|
||||||
save: function() {
|
save: function() {
|
||||||
// Don't save unless we can
|
// 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'), {
|
return Discourse.ajax(this.get('url'), {
|
||||||
type: 'PUT',
|
type: 'PUT',
|
||||||
|
@ -218,138 +219,19 @@ Discourse.Topic = Discourse.Model.extend({
|
||||||
return Discourse.ajax("/t/" + (this.get('id')), { type: 'DELETE' });
|
return Discourse.ajax("/t/" + (this.get('id')), { type: 'DELETE' });
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load the posts for this topic
|
// Update our attributes from a JSON result
|
||||||
loadPosts: function(opts) {
|
updateFromJson: function(json) {
|
||||||
|
this.get('details').updateFromJson(json.details);
|
||||||
|
|
||||||
|
var keys = Object.keys(json);
|
||||||
|
keys.removeObject('details');
|
||||||
|
keys.removeObject('post_stream');
|
||||||
|
|
||||||
var topic = this;
|
var topic = this;
|
||||||
|
keys.forEach(function (key) {
|
||||||
if (!opts) opts = {};
|
topic.set(key, json[key]);
|
||||||
|
|
||||||
// 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 }
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
// 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
|
// Load a topic, but accepts a set of filters
|
||||||
// options:
|
|
||||||
// onLoad - the callback after the topic is loaded
|
|
||||||
find: function(topicId, opts) {
|
find: function(topicId, opts) {
|
||||||
var data, promise, url;
|
var data, promise, url;
|
||||||
url = Discourse.getURL("/t/") + topicId;
|
url = Discourse.getURL("/t/") + topicId;
|
||||||
|
@ -457,15 +337,7 @@ Discourse.Topic.reopenClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the preload store. If not, load it via JSON
|
// Check the preload store. If not, load it via JSON
|
||||||
return PreloadStore.getAndRemove("topic_" + topicId, function() {
|
return Discourse.ajax(url + ".json", {data: data});
|
||||||
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;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mergeTopic: function(topicId, destinationTopicId) {
|
mergeTopic: function(topicId, destinationTopicId) {
|
||||||
|
@ -488,24 +360,6 @@ Discourse.Topic.reopenClass({
|
||||||
promise.reject();
|
promise.reject();
|
||||||
});
|
});
|
||||||
return promise;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -11,7 +11,6 @@ Discourse.Route.buildRoutes(function() {
|
||||||
this.resource('topic', { path: '/t/:slug/:id' }, function() {
|
this.resource('topic', { path: '/t/:slug/:id' }, function() {
|
||||||
this.route('fromParams', { path: '/' });
|
this.route('fromParams', { path: '/' });
|
||||||
this.route('fromParams', { path: '/:nearPost' });
|
this.route('fromParams', { path: '/:nearPost' });
|
||||||
this.route('bestOf', { path: '/best_of' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate static page routes
|
// Generate static page routes
|
||||||
|
|
|
@ -7,6 +7,35 @@
|
||||||
var get = Ember.get, set = Ember.set;
|
var get = Ember.get, set = Ember.set;
|
||||||
var popstateReady = false;
|
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
|
`Ember.DiscourseLocation` implements the location API using the browser's
|
||||||
`history.pushState` API.
|
`history.pushState` API.
|
||||||
|
@ -16,6 +45,7 @@ var popstateReady = false;
|
||||||
@extends Ember.Object
|
@extends Ember.Object
|
||||||
*/
|
*/
|
||||||
Ember.DiscourseLocation = Ember.Object.extend({
|
Ember.DiscourseLocation = Ember.Object.extend({
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
set(this, 'location', get(this, 'location') || window.location);
|
set(this, 'location', get(this, 'location') || window.location);
|
||||||
if ( $.inArray('state', $.event.props) < 0 ) {
|
if ( $.inArray('state', $.event.props) < 0 ) {
|
||||||
|
@ -32,6 +62,12 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
||||||
@method initState
|
@method initState
|
||||||
*/
|
*/
|
||||||
initState: function() {
|
initState: function() {
|
||||||
|
|
||||||
|
var location = this.get('location');
|
||||||
|
if (location && location.search) {
|
||||||
|
this.set('queryParams', $.parseParams(location.search));
|
||||||
|
}
|
||||||
|
|
||||||
this.replaceState(this.formatURL(this.getURL()));
|
this.replaceState(this.formatURL(this.getURL()));
|
||||||
set(this, 'history', window.history);
|
set(this, 'history', window.history);
|
||||||
},
|
},
|
||||||
|
@ -62,6 +98,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
||||||
@param path {String}
|
@param path {String}
|
||||||
*/
|
*/
|
||||||
setURL: function(path) {
|
setURL: function(path) {
|
||||||
|
|
||||||
path = this.formatURL(path);
|
path = this.formatURL(path);
|
||||||
if (this.getState() && this.getState().path !== path) {
|
if (this.getState() && this.getState().path !== path) {
|
||||||
popstateReady = true;
|
popstateReady = true;
|
||||||
|
@ -79,6 +116,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
||||||
@param path {String}
|
@param path {String}
|
||||||
*/
|
*/
|
||||||
replaceURL: function(path) {
|
replaceURL: function(path) {
|
||||||
|
|
||||||
path = this.formatURL(path);
|
path = this.formatURL(path);
|
||||||
|
|
||||||
if (this.getState() && this.getState().path !== path) {
|
if (this.getState() && this.getState().path !== path) {
|
||||||
|
@ -129,6 +167,21 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
||||||
window.history.replaceState({ path: path }, null, path);
|
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
|
@private
|
||||||
|
|
||||||
|
@ -182,7 +235,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
||||||
url = url.substring(rootURL.length);
|
url = url.substring(rootURL.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rootURL + url;
|
return rootURL + url + this.get('queryParamsString');
|
||||||
},
|
},
|
||||||
|
|
||||||
willDestroy: function() {
|
willDestroy: function() {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,46 @@ Discourse.TopicFromParamsRoute = Discourse.Route.extend({
|
||||||
|
|
||||||
setupController: function(controller, params) {
|
setupController: function(controller, params) {
|
||||||
params = 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');
|
var topicController = this.controllerFor('topic');
|
||||||
topicController.cancelFilter();
|
postStream.refresh(params).then(function () {
|
||||||
topicController.loadPosts(params);
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,11 +60,9 @@ Discourse.TopicRoute = Discourse.Route.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
model: function(params) {
|
model: function(params) {
|
||||||
var currentModel, _ref;
|
var currentModel = this.modelFor('topic');
|
||||||
if (currentModel = (_ref = this.controllerFor('topic')) ? _ref.get('content') : void 0) {
|
if (currentModel && (currentModel.get('id') === parseInt(params.id, 10))) {
|
||||||
if (currentModel.get('id') === parseInt(params.id, 10)) {
|
return currentModel;
|
||||||
return currentModel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Discourse.Topic.create(params);
|
return Discourse.Topic.create(params);
|
||||||
},
|
},
|
||||||
|
@ -85,23 +83,28 @@ Discourse.TopicRoute = Discourse.Route.extend({
|
||||||
// Clear the search context
|
// Clear the search context
|
||||||
this.controllerFor('search').set('searchContext', null);
|
this.controllerFor('search').set('searchContext', null);
|
||||||
|
|
||||||
var headerController, topicController;
|
var topicController = this.controllerFor('topic');
|
||||||
topicController = this.controllerFor('topic');
|
var postStream = topicController.get('postStream');
|
||||||
topicController.cancelFilter();
|
postStream.cancelFilter();
|
||||||
topicController.unsubscribe();
|
|
||||||
|
|
||||||
topicController.set('multiSelect', false);
|
topicController.set('multiSelect', false);
|
||||||
|
topicController.unsubscribe();
|
||||||
this.controllerFor('composer').set('topic', null);
|
this.controllerFor('composer').set('topic', null);
|
||||||
Discourse.ScreenTrack.instance().stop();
|
Discourse.ScreenTrack.instance().stop();
|
||||||
|
|
||||||
|
var headerController;
|
||||||
if (headerController = this.controllerFor('header')) {
|
if (headerController = this.controllerFor('header')) {
|
||||||
headerController.set('topic', null);
|
headerController.set('topic', null);
|
||||||
headerController.set('showExtraInfo', false);
|
headerController.set('showExtraInfo', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any filters when we leave the route
|
||||||
|
Discourse.URL.set('queryParams', null);
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController: function(controller, model) {
|
setupController: function(controller, model) {
|
||||||
controller.set('model', model);
|
controller.set('model', model);
|
||||||
|
|
||||||
this.controllerFor('header').setProperties({
|
this.controllerFor('header').setProperties({
|
||||||
topic: model,
|
topic: model,
|
||||||
showExtraInfo: false
|
showExtraInfo: false
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<a {{bindAttr class=":star topic.starred:starred"}} {{action toggleStar}} href='#' {{bindAttr title="topic.favoriteTooltip"}}></a>
|
<a {{bindAttr class=":star topic.starred:starred"}} {{action toggleStar}} href='#' {{bindAttr title="topic.favoriteTooltip"}}></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<h1>
|
<h1>
|
||||||
{{#if topic.fancy_title}}
|
{{#if topic.details.loaded}}
|
||||||
{{topicStatus topic=topic}}
|
{{topicStatus topic=topic}}
|
||||||
<a class='topic-link' href='{{unbound topic.url}}'>{{{topic.fancy_title}}}</a>
|
<a class='topic-link' href='{{unbound topic.url}}'>{{{topic.fancy_title}}}</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
|
|
||||||
<td class='num likes'>
|
<td class='num likes'>
|
||||||
{{#if like_count}}
|
{{#if like_count}}
|
||||||
<a href='{{url}}{{#if has_best_of}}/best_of{{/if}}' title='{{i18n topic.likes count="like_count"}}'>{{like_count}} <i class='icon-heart'></i></a>
|
<a href='{{url}}{{#if has_best_of}}?filter=best_of{{/if}}' title='{{i18n topic.likes count="like_count"}}'>{{like_count}} <i class='icon-heart'></i></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<div class='span5 gutter'>
|
<div class='span5 gutter'>
|
||||||
{{collection contentBinding="internalLinks" itemViewClass="Discourse.PostLinkView" tagName="ul" classNames="post-links"}}
|
{{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}}
|
||||||
<a href='#' class='reply-new' {{action replyAsNewTopic this}}><i class='icon icon-plus'></i>{{i18n post.reply_as_new_topic}}</a>
|
<a href='#' class='reply-new' {{action replyAsNewTopic this}}><i class='icon icon-plus'></i>{{i18n post.reply_as_new_topic}}</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{{#with view.content}}
|
{{#with view.content}}
|
||||||
{{#group}}
|
{{#group}}
|
||||||
<td class='main-link'>
|
<td class='main-link'>
|
||||||
<a class='title' href="{{unbound lastReadUrl}}">{{{unbound fancy_title}}}</a>
|
<a class='title' href="{{unbound lastReadUrl}}">{{{unbound title}}}</a>
|
||||||
{{#if unread}}
|
{{#if unread}}
|
||||||
<a href="{{unbound lastReadUrl}}" class='badge unread badge-notification' title='{{i18n topic.unread_posts count="unread"}}'>{{unbound unread}}</a>
|
<a href="{{unbound lastReadUrl}}" class='badge unread badge-notification' title='{{i18n topic.unread_posts count="unread"}}'>{{unbound unread}}</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<td class='num'>
|
<td class='num'>
|
||||||
{{#if like_count}}
|
{{#if like_count}}
|
||||||
<a href='{{url}}{{#if has_best_of}}/best_of{{/if}}'>{{like_count}} <i class='icon-heart'></i></a>
|
<a href='{{url}}{{#if has_best_of}}?filter=best_of{{/if}}'>{{like_count}} <i class='icon-heart'></i></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
|
@ -1,140 +1,133 @@
|
||||||
{{#if content}}
|
{{#if postStream.loaded}}
|
||||||
{{#if loaded}}
|
|
||||||
|
|
||||||
{{#if view.firstPostLoaded}}
|
{{#if postStream.firstPostLoaded}}
|
||||||
<div id='topic-title'>
|
<div id='topic-title'>
|
||||||
<div class='container'>
|
<div class='container'>
|
||||||
<div class='inner'>
|
<div class='inner'>
|
||||||
{{#if view.showFavoriteButton}}
|
|
||||||
<a {{bindAttr class=":star view.topic.starred:starred"}} {{action toggleStar}} href='#' {{bindAttr title="favoriteTooltip"}}></a>
|
{{#if showFavoriteButton}}
|
||||||
|
<a {{bindAttr class=":star starred:starred"}} {{action toggleStar}} href='#' {{bindAttr title="favoriteTooltip"}}></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if view.editingTopic}}
|
|
||||||
<input id='edit-title' type='text' {{bindAttr value="view.topic.title"}} autofocus>
|
|
||||||
|
|
||||||
{{categoryChooser valueAttribute="name" source=view.topic.categoryName}}
|
{{#if editingTopic}}
|
||||||
|
{{textField id='edit-title' value=newTitle}}
|
||||||
|
{{categoryChooser valueAttribute="id" value=newCategoryId source=category_id}}
|
||||||
|
|
||||||
<button class='btn btn-primary btn-small' {{action finishedEdit target="view"}}><i class='icon-ok'></i></button>
|
<button class='btn btn-primary btn-small' {{action finishedEditingTopic}}><i class='icon-ok'></i></button>
|
||||||
<button class='btn btn-small' {{action cancelEdit target="view"}}><i class='icon-remove'></i></button>
|
<button class='btn btn-small' {{action cancelEditingTopic}}><i class='icon-remove'></i></button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<h1>
|
<h1>
|
||||||
{{#if view.topic.fancy_title}}
|
{{#if details.loaded}}
|
||||||
{{topicStatus topic=view.topic}}
|
{{topicStatus topic=model}}
|
||||||
<a href='{{unbound view.topic.url}}'>{{{view.topic.fancy_title}}}</a>
|
<a href='{{unbound url}}'>{{{fancy_title}}}</a>
|
||||||
{{else}}
|
|
||||||
{{#if view.topic.errorLoading}}
|
|
||||||
{{view.topic.errorTitle}}
|
|
||||||
{{else}}
|
|
||||||
{{i18n topic.loading}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{categoryLink category}}
|
{{categoryLink category}}
|
||||||
|
|
||||||
{{#if view.topic.can_edit}}
|
{{#if details.can_edit}}
|
||||||
<a href='#' {{action editTopic target="view"}} class='edit-topic' title='{{i18n edit}}'><i class="icon-pencil"></i></a>
|
<a href='#' {{action editTopic}} class='edit-topic' title='{{i18n edit}}'><i class="icon-pencil"></i></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</h1>
|
</h1>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="container posts">
|
<div class="container posts">
|
||||||
|
|
||||||
{{view Discourse.SelectedPostsView}}
|
{{view Discourse.SelectedPostsView}}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<section class="topic-area" id='topic' data-topic-id='{{unbound id}}'>
|
<section class="topic-area" id='topic' data-topic-id='{{unbound id}}'>
|
||||||
<div class='posts-wrapper'>
|
<div class='posts-wrapper'>
|
||||||
<div id='topic-progress-wrapper'>
|
<div id='topic-progress-wrapper'>
|
||||||
<nav id='topic-progress' title="{{i18n topic.progress.title}}" {{bindAttr class="hideProgress:hidden"}}>
|
<nav id='topic-progress' title="{{i18n topic.progress.title}}" {{bindAttr class="hideProgress:hidden"}}>
|
||||||
<button id='jump-top' title="{{i18n topic.progress.jump_top}}" {{action jumpTop}}><i class="icon-circle-arrow-up"></i></button>
|
<button id='jump-top' title="{{i18n topic.progress.jump_top}}" {{bindAttr disabled="jumpTopDisabled"}} {{action jumpTop}}><i class="icon-circle-arrow-up"></i></button>
|
||||||
<div class='nums'>
|
<div class='nums'>
|
||||||
<h4 title="{{i18n topic.progress.current}}">{{view.progressPosition}}</h4> <span>{{i18n of_value}}</span> <h4>{{filtered_posts_count}}</h4>
|
<h4 title="{{i18n topic.progress.current}}">{{progressPosition}}</h4> <span>{{i18n of_value}}</span> <h4>{{postStream.filteredPostsCount}}</h4>
|
||||||
</div>
|
</div>
|
||||||
<button id='jump-bottom' title="{{i18n topic.progress.jump_bottom}}" {{action jumpBottom}}><i class="icon-circle-arrow-down"></i></button>
|
<button id='jump-bottom' title="{{i18n topic.progress.jump_bottom}}" {{bindAttr disabled="jumpBottomDisabled"}} {{action jumpBottom}}><i class="icon-circle-arrow-down"></i></button>
|
||||||
<div class='bg'> </div>
|
<div class='bg'> </div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if loadingAbove}}
|
|
||||||
<div class='spinner'>{{i18n loading}}</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{collection itemViewClass="Discourse.PostView" contentBinding="posts" topicViewBinding="view"}}
|
|
||||||
|
|
||||||
{{#if loadingBelow}}
|
|
||||||
<div class='spinner'>{{i18n loading}}</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
<div id='topic-bottom'></div>
|
|
||||||
|
|
||||||
{{#if loading}}
|
{{#if postStream.loadingAbove}}
|
||||||
{{#unless loadingBelow}}
|
<div class='spinner'>{{i18n loading}}</div>
|
||||||
<div class='spinner small'>{{i18n loading}}</div>
|
|
||||||
{{/unless}}
|
|
||||||
{{else}}
|
|
||||||
{{#if view.fullyLoaded}}
|
|
||||||
|
|
||||||
{{view Discourse.TopicClosingView topicBinding="model"}}
|
|
||||||
|
|
||||||
{{view Discourse.TopicFooterButtonsView topicBinding="model"}}
|
|
||||||
|
|
||||||
{{#if suggested_topics.length}}
|
|
||||||
<div id='suggested-topics'>
|
|
||||||
|
|
||||||
<h3>{{i18n suggested_topics.title}}</h3>
|
|
||||||
|
|
||||||
<div class='topics'>
|
|
||||||
<table id="topic-list">
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
{{i18n topic.title}}
|
|
||||||
</th>
|
|
||||||
<th>{{i18n category_title}}</th>
|
|
||||||
<th class='num'>{{i18n posts}}</th>
|
|
||||||
<th class='num'>{{i18n likes}}</th>
|
|
||||||
<th class='num'>{{i18n views}}</th>
|
|
||||||
<th class='num activity' colspan='2'>{{i18n activity}}</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{{each suggested_topics itemTagName="tr" itemViewClass="Discourse.SuggestedTopicView"}}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<br/>
|
|
||||||
<h3>{{{view.browseMoreMessage}}}</h3>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#unless postStream.loadingFilter}}
|
||||||
|
{{collection itemViewClass="Discourse.PostView" contentBinding="postStream.posts" topicViewBinding="view"}}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
</section>
|
{{#if postStream.loadingBelow}}
|
||||||
</div>
|
<div class='spinner'>{{i18n loading}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div id='topic-bottom'></div>
|
||||||
|
|
||||||
|
{{#if postStream.loadingFilter}}
|
||||||
|
<div class='spinner small'>{{i18n loading}}</div>
|
||||||
|
{{else}}
|
||||||
|
{{#if postStream.lastPostLoaded}}
|
||||||
|
|
||||||
|
{{view Discourse.TopicClosingView topicBinding="model"}}
|
||||||
|
{{view Discourse.TopicFooterButtonsView topicBinding="model"}}
|
||||||
|
|
||||||
|
{{#if details.suggested_topics.length}}
|
||||||
|
<div id='suggested-topics'>
|
||||||
|
|
||||||
|
<h3>{{i18n suggested_topics.title}}</h3>
|
||||||
|
|
||||||
|
<div class='topics'>
|
||||||
|
<table id="topic-list">
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{{i18n topic.title}}
|
||||||
|
</th>
|
||||||
|
<th>{{i18n category_title}}</th>
|
||||||
|
<th class='num'>{{i18n posts}}</th>
|
||||||
|
<th class='num'>{{i18n likes}}</th>
|
||||||
|
<th class='num'>{{i18n views}}</th>
|
||||||
|
<th class='num activity' colspan='2'>{{i18n activity}}</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{{each details.suggested_topics itemTagName="tr" itemViewClass="Discourse.SuggestedTopicView"}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<h3>{{{view.browseMoreMessage}}}</h3>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
{{#if message}}
|
||||||
|
<div class='container'>
|
||||||
|
<div class='message'>
|
||||||
|
|
||||||
|
<h2>{{message}}</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{#linkTo list.latest}}{{i18n topic.back_to_list}}{{/linkTo}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if message}}
|
<div class='container'>
|
||||||
<div class='container'>
|
<div class='spinner'>{{i18n loading}}</div>
|
||||||
<div class='message'>
|
</div>
|
||||||
|
|
||||||
<h2>{{message}}</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#linkTo list.latest}}{{i18n topic.back_to_list}}{{/linkTo}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class='container'>
|
|
||||||
<div class='spinner'>{{i18n loading}}</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div id='topic-filter' style='display: none'>
|
|
||||||
{{filterDesc}}
|
<div id='topic-filter' {{bindAttr class="postStream.hasNoFilters:hidden"}}>
|
||||||
<a href='#' {{action cancelFilter}}>{{i18n topic.filters.cancel}}</a>
|
{{postStream.filterDesc}}
|
||||||
|
<a href='#' {{action cancelFilter target="postStream"}}>{{i18n topic.filters.cancel}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{render share}}
|
{{render share}}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<button {{action toggleMultiSelect}} class='btn btn-admin'><i class='icon-tasks'></i> {{i18n topic.actions.multi_select}}</button>
|
<button {{action toggleMultiSelect}} class='btn btn-admin'><i class='icon-tasks'></i> {{i18n topic.actions.multi_select}}</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{{#if can_delete}}
|
{{#if details.can_delete}}
|
||||||
<li>
|
<li>
|
||||||
<button {{action deleteTopic}} class='btn btn-admin btn-danger'><i class='icon-trash'></i> {{i18n topic.actions.delete}}</button>
|
<button {{action deleteTopic}} class='btn btn-admin btn-danger'><i class='icon-trash'></i> {{i18n topic.actions.delete}}</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<h3><i class='icon icon-bullhorn'></i> {{i18n best_of.title}}</h3>
|
<h3><i class='icon icon-bullhorn'></i> {{i18n best_of.title}}</h3>
|
||||||
{{#if bestOf}}
|
{{#if postStream.bestOf}}
|
||||||
<p>{{{i18n best_of.enabled_description}}}</p>
|
<p>{{{i18n best_of.enabled_description}}}</p>
|
||||||
<button class='btn' {{action cancelFilter}}>{{i18n best_of.disable}}</button>
|
<button class='btn' {{action toggleBestOf target="postStream"}}>{{i18n best_of.disable}}</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>{{{i18n best_of.description count="posts_count"}}}</p>
|
<p>{{{i18n best_of.description count="posts_count"}}}</p>
|
||||||
<button class='btn' {{action enableBestOf}}>{{i18n best_of.enable}}</button>
|
<button class='btn' {{action toggleBestOf target="postStream"}}>{{i18n best_of.enable}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -17,14 +17,14 @@
|
||||||
<li>
|
<li>
|
||||||
<a {{bindAttr href="url"}}>
|
<a {{bindAttr href="url"}}>
|
||||||
<h4>{{i18n created}}</h4>
|
<h4>{{i18n created}}</h4>
|
||||||
{{avatar created_by imageSize="tiny"}}
|
{{avatar details.created_by imageSize="tiny"}}
|
||||||
{{date created_at}}
|
{{date created_at}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a {{bindAttr href="lastPostUrl"}}>
|
<a {{bindAttr href="lastPostUrl"}}>
|
||||||
<h4>{{i18n last_post}}</h4>
|
<h4>{{i18n last_post}}</h4>
|
||||||
{{avatar last_poster imageSize="tiny"}}
|
{{avatar details.last_poster imageSize="tiny"}}
|
||||||
{{date last_posted_at}}
|
{{date last_posted_at}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -38,11 +38,11 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h4>{{i18n links}}</h4>
|
<h4>{{i18n links}}</h4>
|
||||||
{{number links.length}}
|
{{number details.links.length}}
|
||||||
</li>
|
</li>
|
||||||
{{#if fewParticipants}}
|
{{#if details.fewParticipants}}
|
||||||
<li class='avatars'>
|
<li class='avatars'>
|
||||||
{{#each fewParticipants}}{{participant participant=this}}{{/each}}
|
{{#each details.fewParticipants}}{{participant participant=this}}{{/each}}
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -53,12 +53,12 @@
|
||||||
<ul class="clearfix">
|
<ul class="clearfix">
|
||||||
<li>
|
<li>
|
||||||
<h4>{{i18n created}}</h4>
|
<h4>{{i18n created}}</h4>
|
||||||
{{avatar created_by imageSize="tiny"}}
|
{{avatar details.created_by imageSize="tiny"}}
|
||||||
<a {{bindAttr href="url"}}>{{date created_at}}</a>
|
<a {{bindAttr href="url"}}>{{date created_at}}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h4>{{i18n last_post}}</h4>
|
<h4>{{i18n last_post}}</h4>
|
||||||
{{avatar last_poster imageSize="tiny"}}
|
{{avatar details.last_poster imageSize="tiny"}}
|
||||||
<a {{bindAttr href="lastPostUrl"}}>{{date last_posted_at}}</a>
|
<a {{bindAttr href="lastPostUrl"}}>{{date last_posted_at}}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -72,9 +72,9 @@
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{#if participants}}
|
{{#if details.participants}}
|
||||||
<section class='avatars clearfix'>
|
<section class='avatars clearfix'>
|
||||||
{{#each participants}}{{participant participant=this}}{{/each}}
|
{{#each details.participants}}{{participant participant=this}}{{/each}}
|
||||||
</section>
|
</section>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
|
|
||||||
{{#if view.parentView.showAllLinksControls}}
|
{{#if view.parentView.showAllLinksControls}}
|
||||||
<div class='link-summary'>
|
<div class='link-summary'>
|
||||||
<a href='#' {{action showAllLinks target="view.parentView"}}>{{i18n topic_summary.links_shown totalLinks="links.length"}}</a>
|
<a href='#' {{action showAllLinks target="view.parentView"}}>{{i18n topic_summary.links_shown totalLinks="details.links.length"}}</a>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<h3><i class='icon icon-envelope-alt'></i> {{i18n private_message_info.title}}</h3>
|
<h3><i class='icon icon-envelope-alt'></i> {{i18n private_message_info.title}}</h3>
|
||||||
<div class='participants clearfix'>
|
<div class='participants clearfix'>
|
||||||
{{#each allowed_groups}}
|
{{#each details.allowed_groups}}
|
||||||
<div class='user group'>
|
<div class='user group'>
|
||||||
#{{unbound name}}
|
#{{unbound name}}
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#each allowed_users}}
|
{{#each details.allowed_users}}
|
||||||
<div class='user'>
|
<div class='user'>
|
||||||
<a href='/users/{{lower username}}'>
|
<a href='/users/{{lower username}}'>
|
||||||
{{avatar this imageSize="small"}}
|
{{avatar this imageSize="small"}}
|
||||||
|
@ -13,13 +13,13 @@
|
||||||
<a href='/users/{{lower username}}'>
|
<a href='/users/{{lower username}}'>
|
||||||
{{unbound username}}
|
{{unbound username}}
|
||||||
</a>
|
</a>
|
||||||
{{#if controller.model.can_remove_allowed_users}}
|
{{#if controller.model.details.can_remove_allowed_users}}
|
||||||
<a class='remove-invited' {{action removeAllowedUser username}}><i class="icon-remove"></i></a>
|
<a class='remove-invited' {{action removeAllowedUser username}}><i class="icon-remove"></i></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{#if can_invite_to}}
|
{{#if details.can_invite_to}}
|
||||||
<div class='controls'>
|
<div class='controls'>
|
||||||
<button class='btn' {{action showPrivateInvite}}>{{i18n private_message_info.invite}}</button>
|
<button class='btn' {{action showPrivateInvite}}>{{i18n private_message_info.invite}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
**/
|
**/
|
||||||
Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
|
Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
|
||||||
title: Em.String.i18n('topic.notifications.title'),
|
title: Em.String.i18n('topic.notifications.title'),
|
||||||
longDescriptionBinding: 'topic.notificationReasonText',
|
longDescriptionBinding: 'topic.details.notificationReasonText',
|
||||||
|
|
||||||
dropDownContent: [
|
dropDownContent: [
|
||||||
[Discourse.Topic.NotificationLevel.WATCHING, 'topic.notifications.watching'],
|
[Discourse.Topic.NotificationLevel.WATCHING, 'topic.notifications.watching'],
|
||||||
|
@ -19,7 +19,7 @@ Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
|
||||||
|
|
||||||
text: function() {
|
text: function() {
|
||||||
var key = (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.WATCHING: return 'watching';
|
||||||
case Discourse.Topic.NotificationLevel.TRACKING: return 'tracking';
|
case Discourse.Topic.NotificationLevel.TRACKING: return 'tracking';
|
||||||
case Discourse.Topic.NotificationLevel.REGULAR: return 'regular';
|
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")) + "<span class='caret'></span>";
|
return icon + (Ember.String.i18n("topic.notifications." + key + ".title")) + "<span class='caret'></span>";
|
||||||
}.property('topic.notification_level'),
|
}.property('topic.details.notification_level'),
|
||||||
|
|
||||||
clicked: function(id) {
|
clicked: function(id) {
|
||||||
return this.get('topic').updateNotifications(id);
|
return this.get('topic.details').updateNotifications(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@ Discourse.ReplyButton = Discourse.ButtonView.extend({
|
||||||
classNames: ['btn', 'btn-primary', 'create'],
|
classNames: ['btn', 'btn-primary', 'create'],
|
||||||
attributeBindings: ['disabled'],
|
attributeBindings: ['disabled'],
|
||||||
helpKey: 'topic.reply.help',
|
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() {
|
text: function() {
|
||||||
var archetypeCapitalized = this.get('controller.content.archetype').capitalize();
|
var archetypeCapitalized = this.get('controller.content.archetype').capitalize();
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
Discourse.ParticipantView = Discourse.View.extend({
|
Discourse.ParticipantView = Discourse.View.extend({
|
||||||
templateName: 'participant',
|
templateName: 'participant',
|
||||||
|
|
||||||
toggled: (function() {
|
toggled: function() {
|
||||||
return this.get('controller.userFilters').contains(this.get('participant.username'));
|
return this.get('controller.postStream.userFilters').contains(this.get('participant.username'));
|
||||||
}).property('controller.userFilters.[]')
|
}.property('controller.postStream.userFilters.[]')
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ Discourse.PostMenuView = Discourse.View.extend({
|
||||||
|
|
||||||
// Delete button
|
// Delete button
|
||||||
renderDelete: function(post, buffer) {
|
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("<button title=\"" +
|
buffer.push("<button title=\"" +
|
||||||
(Em.String.i18n("topic.actions.delete")) +
|
(Em.String.i18n("topic.actions.delete")) +
|
||||||
"\" data-action=\"deleteTopic\" class='delete'><i class=\"icon-trash\"></i></button>");
|
"\" data-action=\"deleteTopic\" class='delete'><i class=\"icon-trash\"></i></button>");
|
||||||
|
@ -138,7 +138,7 @@ Discourse.PostMenuView = Discourse.View.extend({
|
||||||
|
|
||||||
// Reply button
|
// Reply button
|
||||||
renderReply: function(post, buffer) {
|
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("<button title=\"" +
|
buffer.push("<button title=\"" +
|
||||||
(Em.String.i18n("post.controls.reply")) +
|
(Em.String.i18n("post.controls.reply")) +
|
||||||
"\" class='create' data-action=\"reply\"><i class='icon-reply'></i>" +
|
"\" class='create' data-action=\"reply\"><i class='icon-reply'></i>" +
|
||||||
|
|
|
@ -98,9 +98,9 @@ Discourse.PostView = Discourse.View.extend({
|
||||||
updateQuoteElements: function($aside, desc) {
|
updateQuoteElements: function($aside, desc) {
|
||||||
var navLink = "";
|
var navLink = "";
|
||||||
var quoteTitle = Em.String.i18n("post.follow_quote");
|
var quoteTitle = Em.String.i18n("post.follow_quote");
|
||||||
var postNumber;
|
var postNumber = $aside.data('post');
|
||||||
|
|
||||||
if (postNumber = $aside.data('post')) {
|
if (postNumber) {
|
||||||
|
|
||||||
// If we have a topic reference
|
// If we have a topic reference
|
||||||
var topicId, topic;
|
var topicId, topic;
|
||||||
|
@ -209,21 +209,6 @@ Discourse.PostView = Discourse.View.extend({
|
||||||
didInsertElement: function() {
|
didInsertElement: function() {
|
||||||
var $post = this.$();
|
var $post = this.$();
|
||||||
var post = this.get('post');
|
var post = this.get('post');
|
||||||
var postNumber = post.get('scrollToAfterInsert');
|
|
||||||
|
|
||||||
// Do we want to scroll to this post now that we've inserted it?
|
|
||||||
if (postNumber) {
|
|
||||||
Discourse.TopicView.scrollTo(this.get('post.topic_id'), postNumber);
|
|
||||||
if (postNumber === post.get('post_number')) {
|
|
||||||
var $contents = $('.topic-body .contents', $post);
|
|
||||||
var originalCol = $contents.css('backgroundColor');
|
|
||||||
$contents.css({
|
|
||||||
backgroundColor: "#ffffcc"
|
|
||||||
}).animate({
|
|
||||||
backgroundColor: originalCol
|
|
||||||
}, 2500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.showLinkCounts();
|
this.showLinkCounts();
|
||||||
|
|
||||||
// Track this post
|
// Track this post
|
||||||
|
@ -233,21 +218,9 @@ Discourse.PostView = Discourse.View.extend({
|
||||||
Discourse.SyntaxHighlighting.apply($post);
|
Discourse.SyntaxHighlighting.apply($post);
|
||||||
Discourse.Lightbox.apply($post);
|
Discourse.Lightbox.apply($post);
|
||||||
|
|
||||||
// If we're scrolling upwards, adjust the scroll position accordingly
|
|
||||||
var scrollTo = this.get('post.scrollTo');
|
|
||||||
if (scrollTo) {
|
|
||||||
$('body').scrollTop(($(document).height() - scrollTo.height) + scrollTo.top);
|
|
||||||
$('section.divider').addClass('fade');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all the quotes
|
// Find all the quotes
|
||||||
this.insertQuoteControls();
|
this.insertQuoteControls();
|
||||||
|
|
||||||
$post.addClass('ready');
|
$post.addClass('ready');
|
||||||
// be sure that eyeline tracked it
|
|
||||||
var controller = this.get('controller');
|
|
||||||
if (controller && controller.postRendered) {
|
|
||||||
controller.postRendered(post);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,12 +13,12 @@ Discourse.TopicClosingView = Discourse.View.extend({
|
||||||
|
|
||||||
contentChanged: function() {
|
contentChanged: function() {
|
||||||
this.rerender();
|
this.rerender();
|
||||||
}.observes('topic.auto_close_at'),
|
}.observes('topic.details.auto_close_at'),
|
||||||
|
|
||||||
render: function(buffer) {
|
render: function(buffer) {
|
||||||
if (!this.present('topic.auto_close_at')) return;
|
if (!this.present('topic.details.auto_close_at')) return;
|
||||||
|
|
||||||
var autoCloseAt = moment(this.get('topic.auto_close_at'));
|
var autoCloseAt = moment(this.get('topic.details.auto_close_at'));
|
||||||
|
|
||||||
if (autoCloseAt < new Date()) return;
|
if (autoCloseAt < new Date()) return;
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ Discourse.TopicFooterButtonsView = Discourse.ContainerView.extend({
|
||||||
if (!topic.get('isPrivateMessage')) {
|
if (!topic.get('isPrivateMessage')) {
|
||||||
|
|
||||||
// We hide some controls from private messages
|
// We hide some controls from private messages
|
||||||
if (this.get('topic.can_invite_to')) {
|
if (this.get('topic.details.can_invite_to')) {
|
||||||
this.attachViewClass(Discourse.InviteReplyButton);
|
this.attachViewClass(Discourse.InviteReplyButton);
|
||||||
}
|
}
|
||||||
this.attachViewClass(Discourse.FavoriteButton);
|
this.attachViewClass(Discourse.FavoriteButton);
|
||||||
|
|
|
@ -7,25 +7,24 @@
|
||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
Discourse.TopicSummaryView = Discourse.ContainerView.extend({
|
Discourse.TopicSummaryView = Discourse.ContainerView.extend({
|
||||||
topicBinding: 'controller.content',
|
|
||||||
classNameBindings: ['hidden', ':topic-summary'],
|
classNameBindings: ['hidden', ':topic-summary'],
|
||||||
LINKS_SHOWN: 5,
|
|
||||||
allLinksShown: false,
|
allLinksShown: false,
|
||||||
|
|
||||||
|
topic: Em.computed.alias('controller.model'),
|
||||||
|
|
||||||
showAllLinksControls: function() {
|
showAllLinksControls: function() {
|
||||||
if (this.blank('topic.links')) return false;
|
|
||||||
if (this.get('allLinksShown')) return false;
|
if (this.get('allLinksShown')) return false;
|
||||||
if (this.get('topic.links.length') <= this.LINKS_SHOWN) return false;
|
if ((this.get('topic.details.links.length') || 0) <= Discourse.TopicSummaryView.LINKS_SHOWN) return false;
|
||||||
return true;
|
return true;
|
||||||
}.property('allLinksShown', 'topic.links'),
|
}.property('allLinksShown', 'topic.details.links'),
|
||||||
|
|
||||||
infoLinks: function() {
|
infoLinks: function() {
|
||||||
if (this.blank('topic.links')) return [];
|
if (this.blank('topic.details.links')) return [];
|
||||||
|
|
||||||
var allLinks = this.get('topic.links');
|
var allLinks = this.get('topic.details.links');
|
||||||
if (this.get('allLinksShown')) return allLinks;
|
if (this.get('allLinksShown')) return allLinks;
|
||||||
return allLinks.slice(0, this.LINKS_SHOWN);
|
return allLinks.slice(0, Discourse.TopicSummaryView.LINKS_SHOWN);
|
||||||
}.property('topic.links', 'allLinksShown'),
|
}.property('topic.details.links', 'allLinksShown'),
|
||||||
|
|
||||||
newPostCreated: function() {
|
newPostCreated: function() {
|
||||||
this.rerender();
|
this.rerender();
|
||||||
|
@ -77,4 +76,6 @@ Discourse.TopicSummaryView = Discourse.ContainerView.extend({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Discourse.TopicSummaryView.reopenClass({
|
||||||
|
LINKS_SHOWN: 5
|
||||||
|
});
|
||||||
|
|
|
@ -12,23 +12,24 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
topicBinding: 'controller.content',
|
topicBinding: 'controller.content',
|
||||||
userFiltersBinding: 'controller.userFilters',
|
userFiltersBinding: 'controller.userFilters',
|
||||||
classNameBindings: ['controller.multiSelect:multi-select', 'topic.archetype', 'topic.category.secure:secure_category'],
|
classNameBindings: ['controller.multiSelect:multi-select', 'topic.archetype', 'topic.category.secure:secure_category'],
|
||||||
progressPosition: 1,
|
|
||||||
menuVisible: true,
|
menuVisible: true,
|
||||||
SHORT_POST: 1200,
|
SHORT_POST: 1200,
|
||||||
|
|
||||||
|
postStream: Em.computed.alias('controller.postStream'),
|
||||||
|
|
||||||
// Update the progress bar using sweet animations
|
// Update the progress bar using sweet animations
|
||||||
updateBar: function() {
|
updateBar: function() {
|
||||||
var $topicProgress, bg, currentWidth, progressWidth, ratio, totalWidth;
|
if (!this.get('postStream.loaded')) return;
|
||||||
if (!this.get('topic.loaded')) return;
|
|
||||||
$topicProgress = $('#topic-progress');
|
var $topicProgress = $('#topic-progress');
|
||||||
if (!$topicProgress.length) return;
|
if (!$topicProgress.length) return;
|
||||||
|
|
||||||
ratio = this.get('progressPosition') / this.get('topic.filtered_posts_count');
|
var ratio = this.get('controller.progressPosition') / this.get('postStream.filteredPostsCount');
|
||||||
totalWidth = $topicProgress.width();
|
var totalWidth = $topicProgress.width();
|
||||||
progressWidth = ratio * totalWidth;
|
var progressWidth = ratio * totalWidth;
|
||||||
bg = $topicProgress.find('.bg');
|
var bg = $topicProgress.find('.bg');
|
||||||
bg.stop(true, true);
|
bg.stop(true, true);
|
||||||
currentWidth = bg.width();
|
var currentWidth = bg.width();
|
||||||
|
|
||||||
if (currentWidth === totalWidth) {
|
if (currentWidth === totalWidth) {
|
||||||
bg.width(currentWidth - 1);
|
bg.width(currentWidth - 1);
|
||||||
|
@ -40,9 +41,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
bg.css("border-right-width", "1px");
|
bg.css("border-right-width", "1px");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable animation for now so it performs better
|
|
||||||
bg.width(progressWidth);
|
bg.width(progressWidth);
|
||||||
}.observes('progressPosition', 'topic.filtered_posts_count', 'topic.loaded'),
|
}.observes('controller.progressPosition', 'postStream.filteredPostsCount', 'topic.loaded'),
|
||||||
|
|
||||||
updateTitle: function() {
|
updateTitle: function() {
|
||||||
var title = this.get('topic.title');
|
var title = this.get('topic.title');
|
||||||
|
@ -60,28 +60,9 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
}
|
}
|
||||||
|
|
||||||
var postUrl = topic.get('url');
|
var postUrl = topic.get('url');
|
||||||
if (current > 1) {
|
if (current > 1) { postUrl += "/" + current; }
|
||||||
postUrl += "/" + current;
|
|
||||||
} else {
|
|
||||||
if (this.get('controller.bestOf')) {
|
|
||||||
postUrl += "/best_of";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Discourse.URL.replaceState(postUrl);
|
Discourse.URL.replaceState(postUrl);
|
||||||
|
}.observes('controller.currentPost', 'highest_post_number'),
|
||||||
// Show appropriate jump tools
|
|
||||||
if (current === 1) {
|
|
||||||
$('#jump-top').attr('disabled', true);
|
|
||||||
} else {
|
|
||||||
$('#jump-top').attr('disabled', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current === this.get('topic.highest_post_number')) {
|
|
||||||
$('#jump-bottom').attr('disabled', true);
|
|
||||||
} else {
|
|
||||||
$('#jump-bottom').attr('disabled', false);
|
|
||||||
}
|
|
||||||
}.observes('controller.currentPost', 'controller.bestOf', 'topic.highest_post_number'),
|
|
||||||
|
|
||||||
composeChanged: function() {
|
composeChanged: function() {
|
||||||
var composerController = Discourse.get('router.composerController');
|
var composerController = Discourse.get('router.composerController');
|
||||||
|
@ -98,8 +79,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
// Unbind link tracking
|
// Unbind link tracking
|
||||||
this.$().off('mouseup.discourse-redirect', '.cooked a, a.track-link');
|
this.$().off('mouseup.discourse-redirect', '.cooked a, a.track-link');
|
||||||
|
|
||||||
this.get('controller').set('onPostRendered', null);
|
|
||||||
|
|
||||||
this.resetExamineDockCache();
|
this.resetExamineDockCache();
|
||||||
|
|
||||||
// this happens after route exit, stuff could have trickled in
|
// this happens after route exit, stuff could have trickled in
|
||||||
|
@ -110,25 +89,20 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
this.bindScrolling({debounce: 0});
|
this.bindScrolling({debounce: 0});
|
||||||
|
|
||||||
var topicView = this;
|
var topicView = this;
|
||||||
$(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(false); });
|
$(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(); });
|
||||||
|
|
||||||
var controller = this.get('controller');
|
|
||||||
controller.set('onPostRendered', function(){
|
|
||||||
topicView.postsRendered.apply(topicView);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
|
this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
|
||||||
return Discourse.ClickTrack.trackClick(e);
|
return Discourse.ClickTrack.trackClick(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updatePosition(true);
|
this.updatePosition();
|
||||||
},
|
},
|
||||||
|
|
||||||
debounceLoadSuggested: Discourse.debounce(function(){
|
debounceLoadSuggested: Discourse.debounce(function(){
|
||||||
if (this.get('isDestroyed') || this.get('isDestroying')) { return; }
|
if (this.get('isDestroyed') || this.get('isDestroying')) { return; }
|
||||||
|
|
||||||
var incoming = this.get('topicTrackingState.newIncoming');
|
var incoming = this.get('topicTrackingState.newIncoming');
|
||||||
var suggested = this.get('topic.suggested_topics');
|
var suggested = this.get('topic.details.suggested_topics');
|
||||||
var topicId = this.get('topic.id');
|
var topicId = this.get('topic.id');
|
||||||
|
|
||||||
if(suggested) {
|
if(suggested) {
|
||||||
|
@ -155,11 +129,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
this.debounceLoadSuggested();
|
this.debounceLoadSuggested();
|
||||||
}.observes('topicTrackingState.incomingCount'),
|
}.observes('topicTrackingState.incomingCount'),
|
||||||
|
|
||||||
// Triggered whenever any posts are rendered, debounced to save over calling
|
|
||||||
postsRendered: Discourse.debounce(function() {
|
|
||||||
this.updatePosition(false);
|
|
||||||
}, 50),
|
|
||||||
|
|
||||||
resetRead: function(e) {
|
resetRead: function(e) {
|
||||||
Discourse.ScreenTrack.instance().reset();
|
Discourse.ScreenTrack.instance().reset();
|
||||||
this.get('controller').unsubscribe();
|
this.get('controller').unsubscribe();
|
||||||
|
@ -192,8 +161,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
|
|
||||||
if (post) {
|
if (post) {
|
||||||
var postNumber = post.get('post_number');
|
var postNumber = post.get('post_number');
|
||||||
if (postNumber > (this.get('topic.last_read_post_number') || 0)) {
|
if (postNumber > (this.get('last_read_post_number') || 0)) {
|
||||||
this.set('topic.last_read_post_number', postNumber);
|
this.set('last_read_post_number', postNumber);
|
||||||
}
|
}
|
||||||
if (!post.get('read')) {
|
if (!post.get('read')) {
|
||||||
post.set('read', true);
|
post.set('read', true);
|
||||||
|
@ -202,174 +171,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
observeFirstPostLoaded: (function() {
|
|
||||||
var loaded, old, posts;
|
|
||||||
posts = this.get('topic.posts');
|
|
||||||
// TODO topic.posts stores non ember objects in it for a period of time, this is bad
|
|
||||||
loaded = posts && posts[0] && posts[0].post_number === 1;
|
|
||||||
|
|
||||||
// I avoided a computed property cause I did not want to set it, over and over again
|
|
||||||
old = this.get('firstPostLoaded');
|
|
||||||
if (loaded) {
|
|
||||||
if (old !== true) {
|
|
||||||
this.set('firstPostLoaded', true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (old !== false) {
|
|
||||||
this.set('firstPostLoaded', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).observes('topic.posts.@each'),
|
|
||||||
|
|
||||||
// Load previous posts if there are some
|
|
||||||
prevPage: function($post) {
|
|
||||||
var postView = Ember.View.views[$post.prop('id')];
|
|
||||||
if (!postView) return;
|
|
||||||
|
|
||||||
var post = postView.get('post');
|
|
||||||
if (!post) return;
|
|
||||||
|
|
||||||
// We don't load upwards from the first page
|
|
||||||
if (post.post_number === 1) return;
|
|
||||||
|
|
||||||
// double check
|
|
||||||
if (this.topic && this.topic.posts && this.topic.posts.length > 0 && this.topic.posts[0].post_number !== post.post_number) return;
|
|
||||||
|
|
||||||
// half mutex
|
|
||||||
if (this.get('controller.loading')) return;
|
|
||||||
this.set('controller.loading', true);
|
|
||||||
this.set('controller.loadingAbove', true);
|
|
||||||
var opts = $.extend({ postsBefore: post.get('post_number') }, this.get('controller.postFilters'));
|
|
||||||
|
|
||||||
var topicView = this;
|
|
||||||
return Discourse.Topic.find(this.get('topic.id'), opts).then(function(result) {
|
|
||||||
var lastPostNum, posts;
|
|
||||||
posts = topicView.get('topic.posts');
|
|
||||||
|
|
||||||
// Add a scrollTo record to the last post inserted to the DOM
|
|
||||||
lastPostNum = result.posts[0].post_number;
|
|
||||||
_.each(result.posts,function(post) {
|
|
||||||
var newPost;
|
|
||||||
newPost = Discourse.Post.create(post, topicView.get('topic'));
|
|
||||||
if (post.post_number === lastPostNum) {
|
|
||||||
newPost.set('scrollTo', {
|
|
||||||
top: $(window).scrollTop(),
|
|
||||||
height: $(document).height()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return posts.unshiftObject(newPost);
|
|
||||||
});
|
|
||||||
topicView.set('controller.loading', false);
|
|
||||||
return topicView.set('controller.loadingAbove', false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fullyLoaded: (function() {
|
|
||||||
return this.get('controller.seenBottom') || this.get('topic.at_bottom');
|
|
||||||
}).property('topic.at_bottom', 'controller.seenBottom'),
|
|
||||||
|
|
||||||
// Load new posts if there are some
|
|
||||||
nextPage: function($post) {
|
|
||||||
if (this.get('controller.loading') || this.get('controller.seenBottom')) return;
|
|
||||||
return this.loadMore(this.getPost($post));
|
|
||||||
},
|
|
||||||
|
|
||||||
postCountChanged: function() {
|
|
||||||
this.set('controller.seenBottom', false);
|
|
||||||
}.observes('topic.highest_post_number'),
|
|
||||||
|
|
||||||
loadMore: function(post) {
|
|
||||||
if (!post) return;
|
|
||||||
if (this.get('controller.loading')) return;
|
|
||||||
|
|
||||||
// Don't load if we know we're at the bottom
|
|
||||||
if (this.get('topic.highest_post_number') === post.get('post_number')) return;
|
|
||||||
|
|
||||||
if (this.get('controller.seenBottom')) return;
|
|
||||||
|
|
||||||
// Don't double load ever
|
|
||||||
if (this.topic.posts[this.topic.posts.length-1].post_number !== post.post_number) return;
|
|
||||||
this.set('controller.loadingBelow', true);
|
|
||||||
this.set('controller.loading', true);
|
|
||||||
var opts = $.extend({ postsAfter: post.get('post_number') }, this.get('controller.postFilters'));
|
|
||||||
|
|
||||||
var topicView = this;
|
|
||||||
var topic = this.get('controller.content');
|
|
||||||
return Discourse.Topic.find(topic.get('id'), opts).then(function(result) {
|
|
||||||
if (result.at_bottom || result.posts.length === 0) {
|
|
||||||
topicView.set('controller.seenBottom', 'true');
|
|
||||||
}
|
|
||||||
topic.pushPosts(_.map(result.posts,function(p) {
|
|
||||||
return Discourse.Post.create(p, topic);
|
|
||||||
}));
|
|
||||||
if (result.suggested_topics) {
|
|
||||||
var suggested = Em.A();
|
|
||||||
_.each(result.suggested_topics,function(topic) {
|
|
||||||
suggested.pushObject(Discourse.Topic.create(topic));
|
|
||||||
});
|
|
||||||
topicView.set('topic.suggested_topics', suggested);
|
|
||||||
}
|
|
||||||
topicView.set('controller.loadingBelow', false);
|
|
||||||
return topicView.set('controller.loading', false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelEdit: function() {
|
|
||||||
// close editing mode
|
|
||||||
this.set('editingTopic', false);
|
|
||||||
},
|
|
||||||
|
|
||||||
finishedEdit: function() {
|
|
||||||
|
|
||||||
// TODO: This should be in a controller and use proper text fields
|
|
||||||
|
|
||||||
var topicView = this;
|
|
||||||
|
|
||||||
if (this.get('editingTopic')) {
|
|
||||||
var topic = this.get('topic');
|
|
||||||
// retrieve the title from the text field
|
|
||||||
var newTitle = $('#edit-title').val();
|
|
||||||
// retrieve the category from the combox box
|
|
||||||
var newCategoryName = $('#topic-title select option:selected').val();
|
|
||||||
// manually update the titles & category
|
|
||||||
topic.setProperties({
|
|
||||||
title: newTitle,
|
|
||||||
fancy_title: newTitle,
|
|
||||||
categoryName: newCategoryName
|
|
||||||
});
|
|
||||||
// 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) {
|
|
||||||
topicView.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
|
|
||||||
topicView.set('editingTopic', false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
editTopic: function() {
|
|
||||||
if (!this.get('topic.can_edit')) return false;
|
|
||||||
// enable editing mode
|
|
||||||
this.set('editingTopic', true);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
showFavoriteButton: function() {
|
|
||||||
return Discourse.User.current() && !this.get('topic.isPrivateMessage');
|
|
||||||
}.property('topic.isPrivateMessage'),
|
|
||||||
|
|
||||||
resetExamineDockCache: function() {
|
resetExamineDockCache: function() {
|
||||||
this.docAt = null;
|
this.docAt = null;
|
||||||
this.dockedTitle = false;
|
this.dockedTitle = false;
|
||||||
|
@ -380,22 +181,23 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
if (!postView) return;
|
if (!postView) return;
|
||||||
var post = postView.get('post');
|
var post = postView.get('post');
|
||||||
if (!post) return;
|
if (!post) return;
|
||||||
this.set('progressPosition', post.get('index'));
|
this.set('controller.progressPosition', this.get('postStream').indexOf(post) + 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
nonUrgentPositionUpdate: Discourse.debounce(function(opts) {
|
throttledPositionUpdate: Discourse.debounce(function() {
|
||||||
Discourse.ScreenTrack.instance().scrolled();
|
Discourse.ScreenTrack.instance().scrolled();
|
||||||
var model = this.get('controller.model');
|
var model = this.get('controller.model');
|
||||||
if (model) {
|
if (model && this.get('nextPositionUpdate')) {
|
||||||
this.set('controller.currentPost', opts.currentPost);
|
this.set('controller.currentPost', this.get('nextPositionUpdate'));
|
||||||
}
|
}
|
||||||
},500),
|
},500),
|
||||||
|
|
||||||
scrolled: function(){
|
scrolled: function(){
|
||||||
this.updatePosition(true);
|
this.updatePosition();
|
||||||
},
|
},
|
||||||
|
|
||||||
updatePosition: function(userActive) {
|
updatePosition: function() {
|
||||||
|
var topic = this.get('controller.model');
|
||||||
|
|
||||||
var rows = $('.topic-post.ready');
|
var rows = $('.topic-post.ready');
|
||||||
if (!rows || rows.length === 0) { return; }
|
if (!rows || rows.length === 0) { return; }
|
||||||
|
@ -404,16 +206,23 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
var info = Discourse.Eyeline.analyze(rows);
|
var info = Discourse.Eyeline.analyze(rows);
|
||||||
if(!info) { return; }
|
if(!info) { return; }
|
||||||
|
|
||||||
// top on screen
|
// are we scrolling upwards?
|
||||||
if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
|
if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
|
||||||
this.prevPage($(rows[0]));
|
var $body = $('body');
|
||||||
|
var $elem = $(rows[0]);
|
||||||
|
var distToElement = $body.scrollTop() - $elem.position().top;
|
||||||
|
this.get('postStream').prependMore().then(function() {
|
||||||
|
Em.run.next(function () {
|
||||||
|
$('html, body').scrollTop($elem.position().top + distToElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// bottom of screen
|
// are we scrolling down?
|
||||||
var currentPost;
|
var currentPost;
|
||||||
if(info.bottom === rows.length-1) {
|
if(info.bottom === rows.length-1) {
|
||||||
currentPost = this.postSeen($(rows[info.bottom]));
|
currentPost = this.postSeen($(rows[info.bottom]));
|
||||||
this.nextPage($(rows[info.bottom]));
|
this.get('postStream').appendMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// update dock
|
// update dock
|
||||||
|
@ -433,16 +242,14 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentForPositionUpdate) {
|
if (currentForPositionUpdate) {
|
||||||
this.nonUrgentPositionUpdate({
|
this.set('nextPositionUpdate', currentPost || currentForPositionUpdate);
|
||||||
userActive: userActive,
|
this.throttledPositionUpdate();
|
||||||
currentPost: currentPost || currentForPositionUpdate
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.error("can't update position ");
|
console.error("can't update position ");
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = window.pageYOffset || $('html').scrollTop();
|
var offset = window.pageYOffset || $('html').scrollTop();
|
||||||
var firstLoaded = this.get('firstPostLoaded');
|
var firstLoaded = topic.get('postStream.firstPostLoaded');
|
||||||
if (!this.docAt) {
|
if (!this.docAt) {
|
||||||
var title = $('#topic-title');
|
var title = $('#topic-title');
|
||||||
if (title && title.length === 1) {
|
if (title && title.length === 1) {
|
||||||
|
@ -475,18 +282,17 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
topicTrackingState: function(){
|
topicTrackingState: function() {
|
||||||
return Discourse.TopicTrackingState.current();
|
return Discourse.TopicTrackingState.current();
|
||||||
}.property(),
|
}.property(),
|
||||||
|
|
||||||
browseMoreMessage: function() {
|
browseMoreMessage: function() {
|
||||||
var category, opts;
|
var opts = {
|
||||||
|
|
||||||
opts = {
|
|
||||||
latestLink: "<a href=\"/\">" + (Em.String.i18n("topic.view_latest_topics")) + "</a>"
|
latestLink: "<a href=\"/\">" + (Em.String.i18n("topic.view_latest_topics")) + "</a>"
|
||||||
};
|
};
|
||||||
|
|
||||||
category = this.get('controller.content.category');
|
|
||||||
|
var category = this.get('controller.content.category');
|
||||||
if (category) {
|
if (category) {
|
||||||
opts.catLink = Discourse.Utilities.categoryLink(category);
|
opts.catLink = Discourse.Utilities.categoryLink(category);
|
||||||
} else {
|
} else {
|
||||||
|
@ -522,27 +328,32 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
||||||
Discourse.TopicView.reopenClass({
|
Discourse.TopicView.reopenClass({
|
||||||
|
|
||||||
// Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not.
|
// Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not.
|
||||||
scrollTo: function(topicId, postNumber, callback) {
|
jumpToPost: function(topicId, postNumber) {
|
||||||
// Make sure we're looking at the topic we want to scroll to
|
Em.run.scheduleOnce('afterRender', function() {
|
||||||
var existing, header, title, expectedOffset;
|
|
||||||
if (parseInt(topicId, 10) !== parseInt($('#topic').data('topic-id'), 10)) return false;
|
|
||||||
existing = $("#post_" + postNumber);
|
|
||||||
if (existing.length) {
|
|
||||||
if (postNumber === 1) {
|
|
||||||
$('html, body').scrollTop(0);
|
|
||||||
} else {
|
|
||||||
header = $('header');
|
|
||||||
title = $('#topic-title');
|
|
||||||
expectedOffset = title.height() - header.find('.contents').height();
|
|
||||||
|
|
||||||
if (expectedOffset < 0) {
|
// Make sure we're looking at the topic we want to scroll to
|
||||||
expectedOffset = 0;
|
if (topicId !== parseInt($('#topic').data('topic-id'), 10)) { return false; }
|
||||||
|
|
||||||
|
var $post = $("#post_" + postNumber);
|
||||||
|
if ($post.length) {
|
||||||
|
if (postNumber === 1) {
|
||||||
|
$('html, body').scrollTop(0);
|
||||||
|
} else {
|
||||||
|
var header = $('header');
|
||||||
|
var title = $('#topic-title');
|
||||||
|
var expectedOffset = title.height() - header.find('.contents').height();
|
||||||
|
|
||||||
|
if (expectedOffset < 0) {
|
||||||
|
expectedOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('html, body').scrollTop($post.offset().top - (header.outerHeight(true) + expectedOffset));
|
||||||
|
|
||||||
|
var $contents = $('.topic-body .contents', $post);
|
||||||
|
var originalCol = $contents.css('backgroundColor');
|
||||||
|
$contents.css({ backgroundColor: "#ffffcc" }).animate({ backgroundColor: originalCol }, 2500);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('html, body').scrollTop(existing.offset().top - (header.outerHeight(true) + expectedOffset));
|
|
||||||
}
|
}
|
||||||
return true;
|
});
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,12 +25,11 @@ class TopicsController < ApplicationController
|
||||||
caches_action :avatar, cache_path: Proc.new {|c| "#{c.params[:post_number]}-#{c.params[:topic_id]}" }
|
caches_action :avatar, cache_path: Proc.new {|c| "#{c.params[:post_number]}-#{c.params[:topic_id]}" }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
||||||
# We'd like to migrate the wordpress feed to another url. This keeps up backwards compatibility with
|
# We'd like to migrate the wordpress feed to another url. This keeps up backwards compatibility with
|
||||||
# existing installs.
|
# existing installs.
|
||||||
return wordpress if params[:best].present?
|
return wordpress if params[:best].present?
|
||||||
|
|
||||||
opts = params.slice(:username_filters, :best_of, :page, :post_number, :posts_before, :posts_after)
|
opts = params.slice(:username_filters, :filter, :page, :post_number)
|
||||||
begin
|
begin
|
||||||
@topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts)
|
@topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts)
|
||||||
rescue Discourse::NotFound
|
rescue Discourse::NotFound
|
||||||
|
@ -67,7 +66,15 @@ class TopicsController < ApplicationController
|
||||||
wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false)
|
wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false)
|
||||||
render_json_dump(wordpress_serializer)
|
render_json_dump(wordpress_serializer)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def posts
|
||||||
|
params.require(:topic_id)
|
||||||
|
params.require(:post_ids)
|
||||||
|
|
||||||
|
@topic_view = TopicView.new(params[:topic_id], current_user, post_ids: params[:post_ids])
|
||||||
|
render_json_dump(TopicViewPostsSerializer.new(@topic_view, scope: guardian, root: false))
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy_timings
|
def destroy_timings
|
||||||
|
|
|
@ -74,7 +74,7 @@ class SiteSetting < ActiveRecord::Base
|
||||||
setting(:create_thumbnails, false)
|
setting(:create_thumbnails, false)
|
||||||
client_setting(:category_featured_topics, 6)
|
client_setting(:category_featured_topics, 6)
|
||||||
setting(:topics_per_page, 30)
|
setting(:topics_per_page, 30)
|
||||||
setting(:posts_per_page, 20)
|
client_setting(:posts_per_page, 20)
|
||||||
setting(:invite_expiry_days, 14)
|
setting(:invite_expiry_days, 14)
|
||||||
setting(:active_user_rate_limit_secs, 60)
|
setting(:active_user_rate_limit_secs, 60)
|
||||||
setting(:previous_visit_timeout_hours, 1)
|
setting(:previous_visit_timeout_hours, 1)
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
module PostStreamSerializerMixin
|
||||||
|
|
||||||
|
def self.included(klass)
|
||||||
|
klass.attributes :post_stream
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_stream
|
||||||
|
{ posts: posts,
|
||||||
|
stream: object.filtered_post_ids }
|
||||||
|
end
|
||||||
|
|
||||||
|
def posts
|
||||||
|
return @posts if @posts.present?
|
||||||
|
@posts = []
|
||||||
|
@highest_number_in_posts = 0
|
||||||
|
if object.posts.present?
|
||||||
|
object.posts.each_with_index do |p, idx|
|
||||||
|
if p.user
|
||||||
|
@highest_number_in_posts = p.post_number if p.post_number > @highest_number_in_posts
|
||||||
|
ps = PostSerializer.new(p, scope: scope, root: false)
|
||||||
|
ps.topic_slug = object.topic.slug
|
||||||
|
ps.topic_view = object
|
||||||
|
p.topic = object.topic
|
||||||
|
|
||||||
|
@posts << ps.as_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@posts
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,10 @@
|
||||||
|
class TopicViewPostsSerializer < ApplicationSerializer
|
||||||
|
include PostStreamSerializerMixin
|
||||||
|
|
||||||
|
attributes :id
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.topic.id
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
require_dependency 'pinned_check'
|
require_dependency 'pinned_check'
|
||||||
|
|
||||||
class TopicViewSerializer < ApplicationSerializer
|
class TopicViewSerializer < ApplicationSerializer
|
||||||
|
include PostStreamSerializerMixin
|
||||||
|
|
||||||
# These attributes will be delegated to the topic
|
# These attributes will be delegated to the topic
|
||||||
def self.topic_attributes
|
def self.topic_attributes
|
||||||
|
@ -15,76 +16,88 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
:visible,
|
:visible,
|
||||||
:closed,
|
:closed,
|
||||||
:archived,
|
:archived,
|
||||||
:moderator_posts_count,
|
|
||||||
:has_best_of,
|
:has_best_of,
|
||||||
:archetype,
|
:archetype,
|
||||||
:slug,
|
:slug,
|
||||||
:auto_close_at]
|
:category_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.guardian_attributes
|
|
||||||
[:can_moderate, :can_edit, :can_delete, :can_invite_to, :can_move_posts, :can_remove_allowed_users]
|
|
||||||
end
|
|
||||||
|
|
||||||
attributes *topic_attributes
|
|
||||||
attributes *guardian_attributes
|
|
||||||
|
|
||||||
attributes :draft,
|
attributes :draft,
|
||||||
:draft_key,
|
:draft_key,
|
||||||
:draft_sequence,
|
:draft_sequence,
|
||||||
:post_action_visibility,
|
|
||||||
:voted_in_topic,
|
|
||||||
:can_create_post,
|
|
||||||
:can_reply_as_new_topic,
|
|
||||||
:categoryName,
|
|
||||||
:starred,
|
:starred,
|
||||||
:last_read_post_number,
|
|
||||||
:posted,
|
:posted,
|
||||||
:notification_level,
|
|
||||||
:notifications_reason_id,
|
|
||||||
:posts,
|
|
||||||
:at_bottom,
|
|
||||||
:highest_post_number,
|
|
||||||
:pinned,
|
:pinned,
|
||||||
:filtered_posts_count
|
:details,
|
||||||
|
:highest_post_number,
|
||||||
has_one :created_by, serializer: BasicUserSerializer, embed: :objects
|
:last_read_post_number
|
||||||
has_one :last_poster, serializer: BasicUserSerializer, embed: :objects
|
|
||||||
has_many :allowed_users, serializer: BasicUserSerializer, embed: :objects
|
|
||||||
has_many :allowed_groups, serializer: BasicGroupSerializer, embed: :objects
|
|
||||||
|
|
||||||
has_many :links, serializer: TopicLinkSerializer, embed: :objects
|
|
||||||
has_many :participants, serializer: TopicPostCountSerializer, embed: :objects
|
|
||||||
has_many :suggested_topics, serializer: SuggestedTopicSerializer, embed: :objects
|
|
||||||
|
|
||||||
# Define a delegator for each attribute of the topic we want
|
# Define a delegator for each attribute of the topic we want
|
||||||
|
attributes *topic_attributes
|
||||||
topic_attributes.each do |ta|
|
topic_attributes.each do |ta|
|
||||||
class_eval %{def #{ta}
|
class_eval %{def #{ta}
|
||||||
object.topic.#{ta}
|
object.topic.#{ta}
|
||||||
end}
|
end}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Define the guardian attributes
|
# TODO: Split off into proper object / serializer
|
||||||
guardian_attributes.each do |ga|
|
def details
|
||||||
class_eval %{
|
result = {
|
||||||
def #{ga}
|
auto_close_at: object.topic.auto_close_at,
|
||||||
true
|
created_by: BasicUserSerializer.new(object.topic.user, scope: scope, root: false),
|
||||||
end
|
last_poster: BasicUserSerializer.new(object.topic.last_poster, scope: scope, root: false)
|
||||||
|
|
||||||
def include_#{ga}?
|
|
||||||
scope.#{ga}?(object.topic)
|
|
||||||
end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if object.topic.allowed_users.present?
|
||||||
|
result[:allowed_users] = object.topic.allowed_users.map do |user|
|
||||||
|
BasicUserSerializer.new(user, scope: scope, root: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if object.topic.allowed_groups.present?
|
||||||
|
result[:allowed_groups] = object.topic.allowed_groups.map do |ag|
|
||||||
|
BasicGroupSerializer.new(ag, scope: scope, root: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if object.post_counts_by_user.present?
|
||||||
|
result[:participants] = object.post_counts_by_user.map do |pc|
|
||||||
|
TopicPostCountSerializer.new({user: object.participants[pc[0]], post_count: pc[1]}, scope: scope, root: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
if object.suggested_topics.try(:topics).present?
|
||||||
|
result[:suggested_topics] = object.suggested_topics.topics.map do |user|
|
||||||
|
SuggestedTopicSerializer.new(user, scope: scope, root: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if object.links.present?
|
||||||
|
result[:links] = object.links.map do |user|
|
||||||
|
TopicLinkSerializer.new(user, scope: scope, root: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if has_topic_user?
|
||||||
|
result[:notification_level] = object.topic_user.notification_level
|
||||||
|
result[:notifications_reason_id] = object.topic_user.notifications_reason_id
|
||||||
|
end
|
||||||
|
|
||||||
|
result[:can_move_posts] = true if scope.can_move_posts?(object.topic)
|
||||||
|
result[:can_edit] = true if scope.can_edit?(object.topic)
|
||||||
|
result[:can_delete] = true if scope.can_delete?(object.topic)
|
||||||
|
result[:can_remove_allowed_users] = true if scope.can_remove_allowed_users?(object.topic)
|
||||||
|
result[:can_invite_to] = true if scope.can_invite_to?(object.topic)
|
||||||
|
result[:can_create_post] = true if scope.can_create?(Post, object.topic)
|
||||||
|
result[:can_reply_as_new_topic] = true if scope.can_reply_as_new_topic?(object.topic)
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def draft
|
def draft
|
||||||
object.draft
|
object.draft
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_allowed_users?
|
|
||||||
object.topic.private_message?
|
|
||||||
end
|
|
||||||
|
|
||||||
def draft_key
|
def draft_key
|
||||||
object.draft_key
|
object.draft_key
|
||||||
end
|
end
|
||||||
|
@ -93,46 +106,6 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
object.draft_sequence
|
object.draft_sequence
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_action_visibility
|
|
||||||
object.post_action_visibility
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_post_action_visibility?
|
|
||||||
object.post_action_visibility.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_posts_count
|
|
||||||
object.filtered_posts_count
|
|
||||||
end
|
|
||||||
|
|
||||||
def voted_in_topic
|
|
||||||
object.voted_in_topic?
|
|
||||||
end
|
|
||||||
|
|
||||||
def can_reply_as_new_topic
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_can_reply_as_new_topic?
|
|
||||||
scope.can_reply_as_new_topic?(object.topic)
|
|
||||||
end
|
|
||||||
|
|
||||||
def can_create_post
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_can_create_post?
|
|
||||||
scope.can_create?(Post, object.topic)
|
|
||||||
end
|
|
||||||
|
|
||||||
def categoryName
|
|
||||||
object.topic.category.name
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_categoryName?
|
|
||||||
object.topic.category.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Topic user stuff
|
# Topic user stuff
|
||||||
def has_topic_user?
|
def has_topic_user?
|
||||||
object.topic_user.present?
|
object.topic_user.present?
|
||||||
|
@ -143,6 +116,10 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
end
|
end
|
||||||
alias_method :include_starred?, :has_topic_user?
|
alias_method :include_starred?, :has_topic_user?
|
||||||
|
|
||||||
|
def highest_post_number
|
||||||
|
object.highest_post_number
|
||||||
|
end
|
||||||
|
|
||||||
def last_read_post_number
|
def last_read_post_number
|
||||||
object.topic_user.last_read_post_number
|
object.topic_user.last_read_post_number
|
||||||
end
|
end
|
||||||
|
@ -153,90 +130,9 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
end
|
end
|
||||||
alias_method :include_posted?, :has_topic_user?
|
alias_method :include_posted?, :has_topic_user?
|
||||||
|
|
||||||
def notification_level
|
|
||||||
object.topic_user.notification_level
|
|
||||||
end
|
|
||||||
alias_method :include_notification_level?, :has_topic_user?
|
|
||||||
|
|
||||||
def notifications_reason_id
|
|
||||||
object.topic_user.notifications_reason_id
|
|
||||||
end
|
|
||||||
alias_method :include_notifications_reason_id?, :has_topic_user?
|
|
||||||
|
|
||||||
def created_by
|
|
||||||
object.topic.user
|
|
||||||
end
|
|
||||||
|
|
||||||
def last_poster
|
|
||||||
object.topic.last_poster
|
|
||||||
end
|
|
||||||
|
|
||||||
def allowed_users
|
|
||||||
object.topic.allowed_users
|
|
||||||
end
|
|
||||||
|
|
||||||
def allowed_groups
|
|
||||||
object.topic.allowed_groups
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_links?
|
|
||||||
object.links.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def participants
|
|
||||||
object.post_counts_by_user.collect {|tuple| {user: object.participants[tuple.first], post_count: tuple[1]}}
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_participants?
|
|
||||||
object.initial_load? && object.post_counts_by_user.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def suggested_topics
|
|
||||||
object.suggested_topics.topics
|
|
||||||
end
|
|
||||||
def include_suggested_topics?
|
|
||||||
at_bottom && object.suggested_topics.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Whether we're at the bottom of a topic (last page)
|
|
||||||
def at_bottom
|
|
||||||
posts.present? && (@highest_number_in_posts == object.highest_post_number)
|
|
||||||
end
|
|
||||||
|
|
||||||
def highest_post_number
|
|
||||||
object.highest_post_number
|
|
||||||
end
|
|
||||||
|
|
||||||
def pinned
|
def pinned
|
||||||
PinnedCheck.new(object.topic, object.topic_user).pinned?
|
PinnedCheck.new(object.topic, object.topic_user).pinned?
|
||||||
end
|
end
|
||||||
|
|
||||||
def posts
|
|
||||||
return @posts if @posts.present?
|
|
||||||
@posts = []
|
|
||||||
@highest_number_in_posts = 0
|
|
||||||
if object.posts.present?
|
|
||||||
object.posts.each_with_index do |p, idx|
|
|
||||||
if p.user
|
|
||||||
@highest_number_in_posts = p.post_number if p.post_number > @highest_number_in_posts
|
|
||||||
ps = PostSerializer.new(p, scope: scope, root: false)
|
|
||||||
ps.topic_slug = object.topic.slug
|
|
||||||
ps.topic_view = object
|
|
||||||
p.topic = object.topic
|
|
||||||
|
|
||||||
post_json = ps.as_json
|
|
||||||
|
|
||||||
if object.index_reverse
|
|
||||||
post_json[:index] = object.index_offset - idx
|
|
||||||
else
|
|
||||||
post_json[:index] = object.index_offset + idx + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
@posts << post_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@posts
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,7 +18,7 @@ class TopicViewWordpressSerializer < ApplicationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_posts_count
|
def filtered_posts_count
|
||||||
object.filtered_posts_count
|
object.filtered_post_ids.size
|
||||||
end
|
end
|
||||||
|
|
||||||
def participants
|
def participants
|
||||||
|
|
|
@ -92,6 +92,7 @@ predef:
|
||||||
- find
|
- find
|
||||||
- resolvingPromise
|
- resolvingPromise
|
||||||
- sinon
|
- sinon
|
||||||
|
- controllerFor
|
||||||
|
|
||||||
browser: true # true if the standard browser globals should be predefined
|
browser: true # true if the standard browser globals should be predefined
|
||||||
rhino: false # true if the Rhino environment globals should be predefined
|
rhino: false # true if the Rhino environment globals should be predefined
|
||||||
|
|
|
@ -216,6 +216,7 @@ Discourse::Application.routes.draw do
|
||||||
get 't/:slug/:topic_id.rss' => 'topics#feed', format: :rss, constraints: {topic_id: /\d+/}
|
get 't/:slug/:topic_id.rss' => 'topics#feed', format: :rss, constraints: {topic_id: /\d+/}
|
||||||
get 't/:slug/:topic_id' => 'topics#show', constraints: {topic_id: /\d+/}
|
get 't/:slug/:topic_id' => 'topics#show', constraints: {topic_id: /\d+/}
|
||||||
get 't/:slug/:topic_id/:post_number' => 'topics#show', constraints: {topic_id: /\d+/, post_number: /\d+/}
|
get 't/:slug/:topic_id/:post_number' => 'topics#show', constraints: {topic_id: /\d+/, post_number: /\d+/}
|
||||||
|
get 't/:topic_id/posts' => 'topics#posts', constraints: {topic_id: /\d+/}
|
||||||
post 't/:topic_id/timings' => 'topics#timings', constraints: {topic_id: /\d+/}
|
post 't/:topic_id/timings' => 'topics#timings', constraints: {topic_id: /\d+/}
|
||||||
post 't/:topic_id/invite' => 'topics#invite', constraints: {topic_id: /\d+/}
|
post 't/:topic_id/invite' => 'topics#invite', constraints: {topic_id: /\d+/}
|
||||||
post 't/:topic_id/move-posts' => 'topics#move_posts', constraints: {topic_id: /\d+/}
|
post 't/:topic_id/move-posts' => 'topics#move_posts', constraints: {topic_id: /\d+/}
|
||||||
|
|
|
@ -4,7 +4,7 @@ require_dependency 'summarize'
|
||||||
|
|
||||||
class TopicView
|
class TopicView
|
||||||
|
|
||||||
attr_reader :topic, :posts, :index_offset, :index_reverse, :guardian
|
attr_reader :topic, :posts, :guardian, :filtered_posts
|
||||||
attr_accessor :draft, :draft_key, :draft_sequence
|
attr_accessor :draft, :draft_key, :draft_sequence
|
||||||
|
|
||||||
def initialize(topic_id, user=nil, options={})
|
def initialize(topic_id, user=nil, options={})
|
||||||
|
@ -20,13 +20,14 @@ class TopicView
|
||||||
end
|
end
|
||||||
|
|
||||||
guardian.ensure_can_see!(@topic)
|
guardian.ensure_can_see!(@topic)
|
||||||
|
|
||||||
@post_number, @page = options[:post_number], options[:page]
|
@post_number, @page = options[:post_number], options[:page]
|
||||||
|
|
||||||
@limit = options[:limit] || SiteSetting.posts_per_page;
|
@limit = options[:limit] || SiteSetting.posts_per_page;
|
||||||
|
|
||||||
@filtered_posts = @topic.posts
|
@filtered_posts = @topic.posts
|
||||||
@filtered_posts = @filtered_posts.with_deleted if user.try(:staff?)
|
@filtered_posts = @filtered_posts.with_deleted if user.try(:staff?)
|
||||||
@filtered_posts = @filtered_posts.best_of if options[:best_of].present?
|
@filtered_posts = @filtered_posts.best_of if options[:filter] == 'best_of'
|
||||||
@filtered_posts = @filtered_posts.where('posts.post_type <> ?', Post.types[:moderator_action]) if options[:best].present?
|
@filtered_posts = @filtered_posts.where('posts.post_type <> ?', Post.types[:moderator_action]) if options[:best].present?
|
||||||
|
|
||||||
if options[:username_filters].present?
|
if options[:username_filters].present?
|
||||||
|
@ -78,10 +79,6 @@ class TopicView
|
||||||
@topic.title
|
@topic.title
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_posts_count
|
|
||||||
@filtered_posts_count ||= @filtered_posts.count
|
|
||||||
end
|
|
||||||
|
|
||||||
def summary
|
def summary
|
||||||
return nil if posts.blank?
|
return nil if posts.blank?
|
||||||
Summarize.new(posts.first.cooked).summary
|
Summarize.new(posts.first.cooked).summary
|
||||||
|
@ -94,11 +91,8 @@ class TopicView
|
||||||
|
|
||||||
def filter_posts(opts = {})
|
def filter_posts(opts = {})
|
||||||
return filter_posts_near(opts[:post_number].to_i) if opts[:post_number].present?
|
return filter_posts_near(opts[:post_number].to_i) if opts[:post_number].present?
|
||||||
return filter_posts_before(opts[:posts_before].to_i) if opts[:posts_before].present?
|
return filter_posts_by_ids(opts[:post_ids]) if opts[:post_ids].present?
|
||||||
return filter_posts_after(opts[:posts_after].to_i) if opts[:posts_after].present?
|
return filter_best(opts[:best], opts) if opts[:best].present?
|
||||||
if opts[:best].present?
|
|
||||||
return filter_best(opts[:best], opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
filter_posts_paged(opts[:page].to_i)
|
filter_posts_paged(opts[:page].to_i)
|
||||||
end
|
end
|
||||||
|
@ -152,36 +146,8 @@ class TopicView
|
||||||
filter_posts_in_range(min, max)
|
filter_posts_in_range(min, max)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Filter to all posts before a particular post number
|
|
||||||
def filter_posts_before(post_number)
|
|
||||||
@initial_load = false
|
|
||||||
|
|
||||||
sort_order = sort_order_for_post_number(post_number)
|
|
||||||
return nil unless sort_order
|
|
||||||
|
|
||||||
# Find posts before the `sort_order`
|
|
||||||
@posts = @filtered_posts.order('sort_order desc').where("sort_order < ?", sort_order)
|
|
||||||
@index_offset = @posts.count
|
|
||||||
@index_reverse = true
|
|
||||||
|
|
||||||
@posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(@limit)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Filter to all posts after a particular post number
|
|
||||||
def filter_posts_after(post_number)
|
|
||||||
@initial_load = false
|
|
||||||
|
|
||||||
sort_order = sort_order_for_post_number(post_number)
|
|
||||||
return nil unless sort_order
|
|
||||||
|
|
||||||
@index_offset = @filtered_posts.where("sort_order <= ?", sort_order).count
|
|
||||||
@posts = @filtered_posts.order('sort_order').where("sort_order > ?", sort_order)
|
|
||||||
@posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(@limit)
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_best(max, opts={})
|
def filter_best(max, opts={})
|
||||||
@index_offset = 0
|
|
||||||
|
|
||||||
if opts[:min_replies] && @topic.posts_count < opts[:min_replies] + 1
|
if opts[:min_replies] && @topic.posts_count < opts[:min_replies] + 1
|
||||||
@posts = []
|
@posts = []
|
||||||
return
|
return
|
||||||
|
@ -189,8 +155,10 @@ class TopicView
|
||||||
|
|
||||||
@posts = @filtered_posts.order('percent_rank asc, sort_order asc')
|
@posts = @filtered_posts.order('percent_rank asc, sort_order asc')
|
||||||
.where("post_number > 1")
|
.where("post_number > 1")
|
||||||
|
|
||||||
@posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(max)
|
@posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(max)
|
||||||
|
|
||||||
|
|
||||||
min_trust_level = opts[:min_trust_level]
|
min_trust_level = opts[:min_trust_level]
|
||||||
if min_trust_level && min_trust_level > 0
|
if min_trust_level && min_trust_level > 0
|
||||||
@posts = @posts.where('COALESCE(users.trust_level,0) >= ?', min_trust_level)
|
@posts = @posts.where('COALESCE(users.trust_level,0) >= ?', min_trust_level)
|
||||||
|
@ -233,27 +201,6 @@ class TopicView
|
||||||
@all_post_actions ||= PostAction.counts_for(posts, @user)
|
@all_post_actions ||= PostAction.counts_for(posts, @user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def voted_in_topic?
|
|
||||||
return false
|
|
||||||
|
|
||||||
# all post_actions is not the way to do this, cut down on the query, roll it up into topic if we need it
|
|
||||||
|
|
||||||
@voted_in_topic ||= begin
|
|
||||||
return false unless all_post_actions.present?
|
|
||||||
all_post_actions.values.flatten.map {|ac| ac.keys}.flatten.include?(PostActionType.types[:vote])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def post_action_visibility
|
|
||||||
@post_action_visibility ||= begin
|
|
||||||
result = []
|
|
||||||
PostActionType.types.each do |k, v|
|
|
||||||
result << v if guardian.can_see_post_actors?(@topic, v)
|
|
||||||
end
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def links
|
def links
|
||||||
@links ||= TopicLink.topic_summary(guardian, @topic.id)
|
@links ||= TopicLink.topic_summary(guardian, @topic.id)
|
||||||
end
|
end
|
||||||
|
@ -315,6 +262,16 @@ class TopicView
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filter_posts_by_ids(post_ids)
|
||||||
|
# TODO: Sort might be off
|
||||||
|
@posts = Post.where(id: post_ids)
|
||||||
|
.includes(:user)
|
||||||
|
.includes(:reply_to_user)
|
||||||
|
.order('sort_order')
|
||||||
|
@posts = @posts.with_deleted if @user.try(:staff?)
|
||||||
|
@posts
|
||||||
|
end
|
||||||
|
|
||||||
def filter_posts_in_range(min, max)
|
def filter_posts_in_range(min, max)
|
||||||
post_count = (filtered_post_ids.length - 1)
|
post_count = (filtered_post_ids.length - 1)
|
||||||
|
|
||||||
|
@ -324,15 +281,7 @@ class TopicView
|
||||||
|
|
||||||
min = [[min, max].min, 0].max
|
min = [[min, max].min, 0].max
|
||||||
|
|
||||||
@index_offset = min
|
@posts = filter_posts_by_ids(filtered_post_ids[min..max])
|
||||||
|
|
||||||
# TODO: Sort might be off
|
|
||||||
@posts = Post.where(id: filtered_post_ids[min..max])
|
|
||||||
.includes(:user)
|
|
||||||
.includes(:reply_to_user)
|
|
||||||
.order('sort_order')
|
|
||||||
@posts = @posts.with_deleted if @user.try(:staff?)
|
|
||||||
|
|
||||||
@posts
|
@posts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ describe TopicView do
|
||||||
# should not get the status post
|
# should not get the status post
|
||||||
best = TopicView.new(topic.id, nil, best: 99)
|
best = TopicView.new(topic.id, nil, best: 99)
|
||||||
best.posts.count.should == 2
|
best.posts.count.should == 2
|
||||||
best.filtered_posts_count.should == 3
|
best.filtered_post_ids.size.should == 3
|
||||||
best.current_post_ids.should =~ [p2.id, p3.id]
|
best.current_post_ids.should =~ [p2.id, p3.id]
|
||||||
|
|
||||||
# should get no results for trust level too low
|
# should get no results for trust level too low
|
||||||
|
@ -145,12 +145,6 @@ describe TopicView do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context '.post_action_visibility' do
|
|
||||||
it "is allows users to see likes" do
|
|
||||||
topic_view.post_action_visibility.include?(PostActionType.types[:like]).should be_true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context '.read?' do
|
context '.read?' do
|
||||||
it 'is unread with no logged in user' do
|
it 'is unread with no logged in user' do
|
||||||
TopicView.new(topic.id).read?(1).should be_false
|
TopicView.new(topic.id).read?(1).should be_false
|
||||||
|
@ -216,36 +210,6 @@ describe TopicView do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "filter_posts_after" do
|
|
||||||
it "returns undeleted posts after a post" do
|
|
||||||
topic_view.filter_posts_after(p1.post_number).map(&:id).should == [p2.id, p3.id, p5.id]
|
|
||||||
topic_view.should_not be_initial_load
|
|
||||||
topic_view.index_offset.should == 1
|
|
||||||
topic_view.index_reverse.should be_false
|
|
||||||
end
|
|
||||||
|
|
||||||
it "clips to the end boundary" do
|
|
||||||
topic_view.filter_posts_after(p2.post_number).should == [p3, p5]
|
|
||||||
topic_view.index_offset.should == 2
|
|
||||||
topic_view.index_reverse.should be_false
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns nothing after the last post" do
|
|
||||||
topic_view.filter_posts_after(p5.post_number).should be_blank
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns nothing after an invalid post number" do
|
|
||||||
topic_view.filter_posts_after(1000).should be_blank
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns deleted posts to an admin" do
|
|
||||||
coding_horror.admin = true
|
|
||||||
topic_view.filter_posts_after(p1.post_number).should == [p2, p3, p4]
|
|
||||||
topic_view.index_offset.should == 1
|
|
||||||
topic_view.index_reverse.should be_false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#filter_posts_paged' do
|
describe '#filter_posts_paged' do
|
||||||
before { SiteSetting.stubs(:posts_per_page).returns(1) }
|
before { SiteSetting.stubs(:posts_per_page).returns(1) }
|
||||||
|
|
||||||
|
@ -257,37 +221,6 @@ describe TopicView do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "filter_posts_before" do
|
|
||||||
it "returns undeleted posts before a post" do
|
|
||||||
topic_view.filter_posts_before(p5.post_number).should == [p3, p2, p1]
|
|
||||||
topic_view.should_not be_initial_load
|
|
||||||
topic_view.index_offset.should == 3
|
|
||||||
topic_view.index_reverse.should be_true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "clips to the beginning boundary" do
|
|
||||||
topic_view.filter_posts_before(p3.post_number).should == [p2, p1]
|
|
||||||
topic_view.index_offset.should == 2
|
|
||||||
topic_view.index_reverse.should be_true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns nothing before the first post" do
|
|
||||||
topic_view.filter_posts_before(p1.post_number).should be_blank
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns nothing before an invalid post number" do
|
|
||||||
topic_view.filter_posts_before(-10).should be_blank
|
|
||||||
topic_view.filter_posts_before(1000).should be_blank
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns deleted posts to an admin" do
|
|
||||||
coding_horror.admin = true
|
|
||||||
topic_view.filter_posts_before(p5.post_number).should == [p4, p3, p2]
|
|
||||||
topic_view.index_offset.should == 4
|
|
||||||
topic_view.index_reverse.should be_true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "filter_posts_near" do
|
describe "filter_posts_near" do
|
||||||
|
|
||||||
def topic_view_near(post)
|
def topic_view_near(post)
|
||||||
|
@ -297,30 +230,22 @@ describe TopicView do
|
||||||
it "snaps to the lower boundary" do
|
it "snaps to the lower boundary" do
|
||||||
near_view = topic_view_near(p1)
|
near_view = topic_view_near(p1)
|
||||||
near_view.posts.should == [p1, p2, p3]
|
near_view.posts.should == [p1, p2, p3]
|
||||||
near_view.index_offset.should == 0
|
|
||||||
near_view.index_reverse.should be_false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "snaps to the upper boundary" do
|
it "snaps to the upper boundary" do
|
||||||
near_view = topic_view_near(p5)
|
near_view = topic_view_near(p5)
|
||||||
near_view.posts.should == [p2, p3, p5]
|
near_view.posts.should == [p2, p3, p5]
|
||||||
near_view.index_offset.should == 1
|
|
||||||
near_view.index_reverse.should be_false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns the posts in the middle" do
|
it "returns the posts in the middle" do
|
||||||
near_view = topic_view_near(p2)
|
near_view = topic_view_near(p2)
|
||||||
near_view.posts.should == [p1, p2, p3]
|
near_view.posts.should == [p1, p2, p3]
|
||||||
near_view.index_offset.should == 0
|
|
||||||
near_view.index_reverse.should be_false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns deleted posts to an admin" do
|
it "returns deleted posts to an admin" do
|
||||||
coding_horror.admin = true
|
coding_horror.admin = true
|
||||||
near_view = topic_view_near(p3)
|
near_view = topic_view_near(p3)
|
||||||
near_view.posts.should == [p2, p3, p4]
|
near_view.posts.should == [p2, p3, p4]
|
||||||
near_view.index_offset.should == 1
|
|
||||||
near_view.index_reverse.should be_false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when 'posts per page' exceeds the number of posts" do
|
context "when 'posts per page' exceeds the number of posts" do
|
||||||
|
@ -329,8 +254,6 @@ describe TopicView do
|
||||||
it 'returns all the posts' do
|
it 'returns all the posts' do
|
||||||
near_view = topic_view_near(p5)
|
near_view = topic_view_near(p5)
|
||||||
near_view.posts.should == [p1, p2, p3, p5]
|
near_view.posts.should == [p1, p2, p3, p5]
|
||||||
near_view.index_offset.should == 0
|
|
||||||
near_view.index_reverse.should be_false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -517,17 +517,6 @@ describe TopicsController do
|
||||||
TopicView.any_instance.expects(:filter_posts_near).with(p2.post_number)
|
TopicView.any_instance.expects(:filter_posts_near).with(p2.post_number)
|
||||||
xhr :get, :show, topic_id: topic.id, slug: topic.slug, post_number: p2.post_number
|
xhr :get, :show, topic_id: topic.id, slug: topic.slug, post_number: p2.post_number
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'delegates a posts_after param to TopicView#filter_posts_after' do
|
|
||||||
TopicView.any_instance.expects(:filter_posts_after).with(p1.post_number)
|
|
||||||
xhr :get, :show, topic_id: topic.id, slug: topic.slug, posts_after: p1.post_number
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'delegates a posts_before param to TopicView#filter_posts_before' do
|
|
||||||
TopicView.any_instance.expects(:filter_posts_before).with(p2.post_number)
|
|
||||||
xhr :get, :show, topic_id: topic.id, slug: topic.slug, posts_before: p2.post_number
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when 'login required' site setting has been enabled" do
|
context "when 'login required' site setting has been enabled" do
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
{ "path": "script" },
|
{ "path": "script" },
|
||||||
{ "path": "cookbooks" },
|
{ "path": "cookbooks" },
|
||||||
{ "path": "spec" },
|
{ "path": "spec" },
|
||||||
{ "path": "test" }
|
{ "path": "test",
|
||||||
|
"folder_exclude_patterns": ["fixtures"]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"settings":
|
"settings":
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
var topic = Discourse.Topic.create({
|
||||||
|
title: "Qunit Test Topic",
|
||||||
|
participants: [
|
||||||
|
{id: 1234,
|
||||||
|
post_count: 4,
|
||||||
|
username: "eviltrout"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module("Discourse.TopicController", {
|
||||||
|
setup: function() {
|
||||||
|
this.topicController = controllerFor('topic', topic);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("editingMode", function() {
|
||||||
|
var topicController = this.topicController;
|
||||||
|
|
||||||
|
ok(!topicController.get('editingTopic'), "we are not editing by default");
|
||||||
|
|
||||||
|
topicController.set('model.details.can_edit', false);
|
||||||
|
topicController.editTopic();
|
||||||
|
ok(!topicController.get('editingTopic'), "calling editTopic doesn't enable editing unless the user can edit");
|
||||||
|
|
||||||
|
topicController.set('model.details.can_edit', true);
|
||||||
|
topicController.editTopic();
|
||||||
|
ok(topicController.get('editingTopic'), "calling editTopic enables editing if the user can edit");
|
||||||
|
equal(topicController.get('newTitle'), topic.get('title'));
|
||||||
|
equal(topicController.get('newCategoryId'), topic.get('category_id'));
|
||||||
|
|
||||||
|
topicController.cancelEditingTopic();
|
||||||
|
ok(!topicController.get('editingTopic'), "cancelling edit mode reverts the property value");
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,11 @@
|
||||||
// Test helpers
|
// Test helpers
|
||||||
var resolvingPromise = Ember.Deferred.promise(function (p) {
|
var resolvingPromise = Ember.Deferred.promise(function (p) {
|
||||||
p.resolve();
|
p.resolve();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
var resolvingPromiseWith = function(result) {
|
||||||
|
return Ember.Deferred.promise(function (p) { p.resolve(result); });
|
||||||
|
};
|
||||||
|
|
||||||
function exists(selector) {
|
function exists(selector) {
|
||||||
return !!count(selector);
|
return !!count(selector);
|
||||||
|
@ -11,22 +15,14 @@ function count(selector) {
|
||||||
return find(selector).length;
|
return find(selector).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function objBlank(obj) {
|
|
||||||
if (obj === undefined) return true;
|
|
||||||
|
|
||||||
switch (typeof obj) {
|
|
||||||
case "string":
|
|
||||||
return obj.trim().length === 0;
|
|
||||||
case "object":
|
|
||||||
return $.isEmptyObject(obj);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function present(obj, text) {
|
function present(obj, text) {
|
||||||
equal(objBlank(obj), false, text);
|
ok(!Ember.isEmpty(obj), text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function blank(obj, text) {
|
function blank(obj, text) {
|
||||||
equal(objBlank(obj), true, text);
|
ok(Ember.isEmpty(obj), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsInstance(collection, klass, text) {
|
||||||
|
ok(klass.detectInstance(_.first(collection)), text);
|
||||||
}
|
}
|
|
@ -12,4 +12,10 @@ function integration(name) {
|
||||||
Discourse.ScrollingDOMMethods.unbindOnScroll.restore();
|
Discourse.ScrollingDOMMethods.unbindOnScroll.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function controllerFor(controller, model) {
|
||||||
|
var controller = Discourse.__container__.lookup('controller:' + controller);
|
||||||
|
if (model) { controller.set('model', model ); }
|
||||||
|
return controller;
|
||||||
}
|
}
|
|
@ -121,7 +121,11 @@ var jsHintOpts = {
|
||||||
"start",
|
"start",
|
||||||
"_",
|
"_",
|
||||||
"console",
|
"console",
|
||||||
"alert"],
|
"alert",
|
||||||
|
"controllerFor",
|
||||||
|
"containsInstance",
|
||||||
|
"deepEqual",
|
||||||
|
"resolvingPromiseWith"],
|
||||||
"node" : false,
|
"node" : false,
|
||||||
"browser" : true,
|
"browser" : true,
|
||||||
"boss" : true,
|
"boss" : true,
|
||||||
|
|
|
@ -0,0 +1,324 @@
|
||||||
|
module("Discourse.PostStream");
|
||||||
|
|
||||||
|
var buildStream = function(id, stream) {
|
||||||
|
var topic = Discourse.Topic.create({id: id});
|
||||||
|
var ps = topic.get('postStream');
|
||||||
|
if (stream) {
|
||||||
|
ps.set('stream', stream);
|
||||||
|
}
|
||||||
|
return ps;
|
||||||
|
};
|
||||||
|
|
||||||
|
var participant = {username: 'eviltrout'};
|
||||||
|
|
||||||
|
test('defaults', function() {
|
||||||
|
var postStream = buildStream(1234);
|
||||||
|
blank(postStream.get('posts'), "there are no posts in a stream by default");
|
||||||
|
ok(!postStream.get('loaded'), "it has never loaded");
|
||||||
|
present(postStream.get('topic'));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('appending posts', function() {
|
||||||
|
var postStream = buildStream(4567, [1, 3, 4]);
|
||||||
|
|
||||||
|
ok(!postStream.get('hasPosts'), "there are no posts by default");
|
||||||
|
ok(!postStream.get('firstPostLoaded'), "the first post is not loaded");
|
||||||
|
ok(!postStream.get('lastPostLoaded'), "the last post is not loaded");
|
||||||
|
equal(postStream.get('posts.length'), 0, "it has no posts initially");
|
||||||
|
|
||||||
|
postStream.appendPost(Discourse.Post.create({id: 2, post_number: 2}));
|
||||||
|
ok(!postStream.get('firstPostLoaded'), "the first post is still not loaded");
|
||||||
|
ok(!postStream.get('lastPostLoaded'), "the last post is still not loaded");
|
||||||
|
equal(postStream.get('posts.length'), 1, "it has one post in the stream");
|
||||||
|
|
||||||
|
postStream.appendPost(Discourse.Post.create({id: 4, post_number: 4}));
|
||||||
|
ok(!postStream.get('firstPostLoaded'), "the first post is still loaded");
|
||||||
|
ok(postStream.get('lastPostLoaded'), "the last post is now loaded");
|
||||||
|
equal(postStream.get('posts.length'), 2, "it has two posts in the stream");
|
||||||
|
|
||||||
|
postStream.appendPost(Discourse.Post.create({id: 4, post_number: 4}));
|
||||||
|
equal(postStream.get('posts.length'), 2, "it will not add the same post with id twice");
|
||||||
|
|
||||||
|
var stagedPost = Discourse.Post.create({raw: 'incomplete post'});
|
||||||
|
postStream.appendPost(stagedPost);
|
||||||
|
equal(postStream.get('posts.length'), 3, "it can handle posts without ids");
|
||||||
|
postStream.appendPost(stagedPost);
|
||||||
|
equal(postStream.get('posts.length'), 3, "it won't add the same post without an id twice");
|
||||||
|
|
||||||
|
|
||||||
|
// change the stream
|
||||||
|
postStream.set('stream', [1, 2, 4]);
|
||||||
|
ok(!postStream.get('firstPostLoaded'), "the first post no longer loaded since the stream changed.");
|
||||||
|
ok(postStream.get('lastPostLoaded'), "the last post is still the last post in the new stream");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('updateFromJson', function() {
|
||||||
|
var postStream = buildStream(1231);
|
||||||
|
|
||||||
|
postStream.updateFromJson({
|
||||||
|
posts: [{id: 1}],
|
||||||
|
stream: [1],
|
||||||
|
extra_property: 12
|
||||||
|
});
|
||||||
|
|
||||||
|
equal(postStream.get('posts.length'), 1, 'it loaded the posts');
|
||||||
|
containsInstance(postStream.get('posts'), Discourse.Post);
|
||||||
|
|
||||||
|
equal(postStream.get('extra_property'), 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cancelFilter", function() {
|
||||||
|
var postStream = buildStream(1235);
|
||||||
|
|
||||||
|
this.stub(postStream, "refresh");
|
||||||
|
|
||||||
|
postStream.set('bestOf', true);
|
||||||
|
postStream.cancelFilter();
|
||||||
|
ok(!postStream.get('bestOf'), "best of is cancelled");
|
||||||
|
|
||||||
|
postStream.toggleParticipant(participant);
|
||||||
|
postStream.cancelFilter();
|
||||||
|
blank(postStream.get('userFilters'), "cancelling the filters clears the userFilters");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("toggleParticipant", function() {
|
||||||
|
var postStream = buildStream(1236);
|
||||||
|
this.stub(postStream, "refresh");
|
||||||
|
|
||||||
|
equal(postStream.get('userFilters.length'), 0, "by default no participants are toggled");
|
||||||
|
|
||||||
|
postStream.toggleParticipant(participant.username);
|
||||||
|
ok(postStream.get('userFilters').contains('eviltrout'), 'eviltrout is in the filters');
|
||||||
|
|
||||||
|
postStream.toggleParticipant(participant.username);
|
||||||
|
blank(postStream.get('userFilters'), "toggling the participant again removes them");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("streamFilters", function() {
|
||||||
|
var postStream = buildStream(1237);
|
||||||
|
this.stub(postStream, "refresh");
|
||||||
|
|
||||||
|
deepEqual(postStream.get('streamFilters'), {}, "there are no postFilters by default");
|
||||||
|
ok(postStream.get('hasNoFilters'), "there are no filters by default");
|
||||||
|
blank(postStream.get("filterDesc"), "there is no description of the filter");
|
||||||
|
|
||||||
|
postStream.set('bestOf', true);
|
||||||
|
deepEqual(postStream.get('streamFilters'), {filter: "best_of"}, "postFilters contains the bestOf flag");
|
||||||
|
ok(!postStream.get('hasNoFilters'), "now there are filters present");
|
||||||
|
present(postStream.get("filterDesc"), "there is a description of the filter");
|
||||||
|
|
||||||
|
postStream.toggleParticipant(participant.username);
|
||||||
|
deepEqual(postStream.get('streamFilters'), {
|
||||||
|
filter: "best_of",
|
||||||
|
username_filters: ['eviltrout']
|
||||||
|
}, "streamFilters contains the username we filtered");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loading", function() {
|
||||||
|
var postStream = buildStream(1234);
|
||||||
|
ok(!postStream.get('loading'), "we're not loading by default");
|
||||||
|
|
||||||
|
postStream.set('loadingAbove', true);
|
||||||
|
ok(postStream.get('loading'), "we're loading if loading above");
|
||||||
|
|
||||||
|
postStream = buildStream(1234);
|
||||||
|
postStream.set('loadingBelow', true);
|
||||||
|
ok(postStream.get('loading'), "we're loading if loading below");
|
||||||
|
|
||||||
|
postStream = buildStream(1234);
|
||||||
|
postStream.set('loadingFilter', true);
|
||||||
|
ok(postStream.get('loading'), "we're loading if loading a filter");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nextWindow", function() {
|
||||||
|
Discourse.SiteSettings.posts_per_page = 5;
|
||||||
|
var postStream = buildStream(1234, [1,2,3,5,8,9,10,11,13,14,15,16]);
|
||||||
|
|
||||||
|
blank(postStream.get('nextWindow'), 'With no posts loaded, the window is blank');
|
||||||
|
|
||||||
|
postStream.updateFromJson({ posts: [{id: 1}, {id: 2}] });
|
||||||
|
deepEqual(postStream.get('nextWindow'), [3,5,8,9,10],
|
||||||
|
"If we've loaded the first 2 posts, the window should be the 5 after that");
|
||||||
|
|
||||||
|
postStream.updateFromJson({ posts: [{id: 13}] });
|
||||||
|
deepEqual(postStream.get('nextWindow'), [14, 15, 16], "Boundary check: stop at the end.");
|
||||||
|
|
||||||
|
postStream.updateFromJson({ posts: [{id: 16}] });
|
||||||
|
blank(postStream.get('nextWindow'), "Once we've seen everything there's nothing to load.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("previousWindow", function() {
|
||||||
|
Discourse.SiteSettings.posts_per_page = 5;
|
||||||
|
var postStream = buildStream(1234, [1,2,3,5,8,9,10,11,13,14,15,16]);
|
||||||
|
|
||||||
|
blank(postStream.get('previousWindow'), 'With no posts loaded, the window is blank');
|
||||||
|
|
||||||
|
postStream.updateFromJson({ posts: [{id: 11}, {id: 13}] });
|
||||||
|
deepEqual(postStream.get('previousWindow'), [3, 5, 8, 9, 10],
|
||||||
|
"If we've loaded in the middle, it's the previous 5 posts");
|
||||||
|
|
||||||
|
postStream.updateFromJson({ posts: [{id: 3}] });
|
||||||
|
deepEqual(postStream.get('previousWindow'), [1, 2], "Boundary check: stop at the beginning.");
|
||||||
|
|
||||||
|
postStream.updateFromJson({ posts: [{id: 1}] });
|
||||||
|
blank(postStream.get('previousWindow'), "Once we've seen everything there's nothing to load.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("storePost", function() {
|
||||||
|
var postStream = buildStream(1234);
|
||||||
|
|
||||||
|
var post = Discourse.Post.create({id: 1, post_number: 1, raw: 'initial value'});
|
||||||
|
var stored = postStream.storePost(post);
|
||||||
|
equal(post, stored, "it returns the post it stored");
|
||||||
|
equal(post.get('topic'), postStream.get('topic'), "it creates the topic reference properly");
|
||||||
|
|
||||||
|
var dupePost = Discourse.Post.create({id: 1, post_number: 1, raw: 'updated value'});
|
||||||
|
var storedDupe = postStream.storePost(dupePost);
|
||||||
|
equal(storedDupe, post, "it returns the previously stored post instead to avoid dupes");
|
||||||
|
equal(storedDupe.get('raw'), 'updated value', 'it updates the previously stored post');
|
||||||
|
|
||||||
|
var postWithoutId = Discourse.Post.create({raw: 'hello world'});
|
||||||
|
stored = postStream.storePost(postWithoutId);
|
||||||
|
equal(stored, postWithoutId, "it returns the same post back");
|
||||||
|
equal(postStream.get('postIdentityMap.length'), 1, "it does not add a new entry into the identity map");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("identity map", function() {
|
||||||
|
var postStream = buildStream(1234);
|
||||||
|
var p1 = postStream.appendPost(Discourse.Post.create({id: 1, post_number: 1}));
|
||||||
|
var p3 = postStream.appendPost(Discourse.Post.create({id: 3, post_number: 4}));
|
||||||
|
|
||||||
|
equal(postStream.findLoadedPost(1), p1, "it can return cached posts by id");
|
||||||
|
blank(postStream.findLoadedPost(4), "it can't find uncached posts");
|
||||||
|
|
||||||
|
deepEqual(postStream.listUnloadedIds([10, 11, 12]), [10, 11, 12], "it returns a list of all unloaded ids");
|
||||||
|
blank(postStream.listUnloadedIds([1, 3]), "if we have loaded all posts it's blank");
|
||||||
|
deepEqual(postStream.listUnloadedIds([1, 2, 3, 4]), [2, 4], "it only returns unloaded posts");
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncTest("loadIntoIdentityMap with no data", function() {
|
||||||
|
var postStream = buildStream(1234);
|
||||||
|
expect(1);
|
||||||
|
|
||||||
|
this.stub(Discourse, "ajax");
|
||||||
|
postStream.loadIntoIdentityMap([]).then(function() {
|
||||||
|
ok(!Discourse.ajax.calledOnce, "an empty array returned a promise yet performed no ajax request");
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncTest("loadIntoIdentityMap with post ids", function() {
|
||||||
|
var postStream = buildStream(1234);
|
||||||
|
expect(1);
|
||||||
|
|
||||||
|
this.stub(Discourse, "ajax").returns(resolvingPromiseWith({
|
||||||
|
post_stream: {
|
||||||
|
posts: [{id: 10, post_number: 10}]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
postStream.loadIntoIdentityMap([10]).then(function() {
|
||||||
|
present(postStream.findLoadedPost(10), "it adds the returned post to the store");
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("staging and undoing a new post", function() {
|
||||||
|
var postStream = buildStream(10101, [1]);
|
||||||
|
postStream.appendPost(Discourse.Post.create({id: 1, post_number: 1}));
|
||||||
|
|
||||||
|
var user = Discourse.User.create({username: 'eviltrout', name: 'eviltrout', id: 321});
|
||||||
|
var stagedPost = Discourse.Post.create({ raw: 'hello world this is my new post' });
|
||||||
|
|
||||||
|
var topic = postStream.get('topic');
|
||||||
|
topic.setProperties({
|
||||||
|
posts_count: 1,
|
||||||
|
highest_post_number: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stage the new post in the stream
|
||||||
|
postStream.stagePost(stagedPost, user);
|
||||||
|
equal(topic.get('highest_post_number'), 2, "it updates the highest_post_number");
|
||||||
|
ok(postStream.get('loading'), "it is loading while the post is being staged");
|
||||||
|
|
||||||
|
equal(topic.get('posts_count'), 2, "it increases the post count");
|
||||||
|
present(topic.get('last_posted_at'), "it updates last_posted_at");
|
||||||
|
equal(topic.get('details.last_poster'), user, "it changes the last poster");
|
||||||
|
|
||||||
|
equal(stagedPost.get('topic'), topic, "it assigns the topic reference");
|
||||||
|
equal(stagedPost.get('post_number'), 2, "it is assigned the probable post_number");
|
||||||
|
equal(postStream.get('filteredPostsCount'), 1, "it retains the filteredPostsCount");
|
||||||
|
present(stagedPost.get('created_at'), "it is assigned a created date");
|
||||||
|
ok(postStream.get('posts').contains(stagedPost), "the post is added to the stream");
|
||||||
|
blank(stagedPost.get('id'), "the post has no id yet");
|
||||||
|
|
||||||
|
// Undoing a created post (there was an error)
|
||||||
|
postStream.undoPost(stagedPost);
|
||||||
|
|
||||||
|
ok(!postStream.get('loading'), "it is no longer loading");
|
||||||
|
equal(topic.get('highest_post_number'), 1, "it reverts the highest_post_number");
|
||||||
|
equal(topic.get('posts_count'), 1, "it reverts the post count");
|
||||||
|
equal(postStream.get('filteredPostsCount'), 1, "it retains the filteredPostsCount");
|
||||||
|
ok(!postStream.get('posts').contains(stagedPost), "the post is removed from the stream");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("staging and committing a post", function() {
|
||||||
|
var postStream = buildStream(10101, [1]);
|
||||||
|
postStream.appendPost(Discourse.Post.create({id: 1, post_number: 1}));
|
||||||
|
var user = Discourse.User.create({username: 'eviltrout', name: 'eviltrout', id: 321});
|
||||||
|
var stagedPost = Discourse.Post.create({ raw: 'hello world this is my new post' });
|
||||||
|
|
||||||
|
var topic = postStream.get('topic');
|
||||||
|
topic.set('posts_count', 1);
|
||||||
|
|
||||||
|
// Stage the new post in the stream
|
||||||
|
postStream.stagePost(stagedPost, user);
|
||||||
|
ok(postStream.get('loading'), "it is loading while the post is being staged");
|
||||||
|
stagedPost.setProperties({ id: 1234, raw: "different raw value" });
|
||||||
|
equal(postStream.get('filteredPostsCount'), 1, "it retains the filteredPostsCount");
|
||||||
|
|
||||||
|
postStream.commitPost(stagedPost);
|
||||||
|
ok(postStream.get('posts').contains(stagedPost), "the post is still in the stream");
|
||||||
|
ok(!postStream.get('loading'), "it is no longer loading");
|
||||||
|
equal(postStream.get('filteredPostsCount'), 2, "it increases the filteredPostsCount");
|
||||||
|
|
||||||
|
var found = postStream.findLoadedPost(stagedPost.get('id'));
|
||||||
|
present(found, "the post is in the identity map");
|
||||||
|
ok(postStream.indexOf(stagedPost) > -1, "the post is in the stream");
|
||||||
|
equal(found.get('raw'), 'different raw value', 'it also updated the value in the stream');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('triggerNewPostInStream', function() {
|
||||||
|
var postStream = buildStream(225566);
|
||||||
|
|
||||||
|
this.stub(postStream, 'appendMore');
|
||||||
|
this.stub(postStream, 'refresh');
|
||||||
|
|
||||||
|
postStream.triggerNewPostInStream(null);
|
||||||
|
ok(!postStream.appendMore.calledOnce, "asking for a null id does nothing");
|
||||||
|
|
||||||
|
postStream.toggleBestOf();
|
||||||
|
postStream.triggerNewPostInStream(1);
|
||||||
|
ok(!postStream.appendMore.calledOnce, "it will not trigger when bestOf is active");
|
||||||
|
|
||||||
|
postStream.cancelFilter();
|
||||||
|
postStream.toggleParticipant('eviltrout');
|
||||||
|
postStream.triggerNewPostInStream(1);
|
||||||
|
ok(!postStream.appendMore.calledOnce, "it will not trigger when a participant filter is active");
|
||||||
|
|
||||||
|
postStream.cancelFilter();
|
||||||
|
postStream.triggerNewPostInStream(1);
|
||||||
|
ok(!postStream.appendMore.calledOnce, "it wont't delegate to appendMore because the last post is not loaded");
|
||||||
|
|
||||||
|
postStream.cancelFilter();
|
||||||
|
postStream.appendPost(Discourse.Post.create({id: 1, post_number: 2}));
|
||||||
|
postStream.triggerNewPostInStream(2);
|
||||||
|
ok(postStream.appendMore.calledOnce, "delegates to appendMore because the last post is loaded");
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
module("Discourse.Post");
|
||||||
|
|
||||||
|
test('new_user', function() {
|
||||||
|
var post = Discourse.Post.create({trust_level: 0});
|
||||||
|
ok(post.get('new_user'), "post is from a new user");
|
||||||
|
|
||||||
|
post.set('trust_level', 1);
|
||||||
|
ok(!post.get('new_user'), "post is no longer from a new user");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('firstPost', function() {
|
||||||
|
var post = Discourse.Post.create({post_number: 1});
|
||||||
|
ok(post.get('firstPost'), "it's the first post");
|
||||||
|
|
||||||
|
post.set('post_number', 10);
|
||||||
|
ok(!post.get('firstPost'), "post is no longer the first post");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('updateFromPost', function() {
|
||||||
|
var post = Discourse.Post.create({
|
||||||
|
post_number: 1,
|
||||||
|
raw: 'hello world'
|
||||||
|
});
|
||||||
|
|
||||||
|
post.updateFromPost(Discourse.Post.create({
|
||||||
|
raw: 'different raw',
|
||||||
|
wat: function() { return 123; }
|
||||||
|
}));
|
||||||
|
|
||||||
|
equal(post.get('raw'), "different raw", "raw field updated");
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
module("Discourse.TopicDetails");
|
||||||
|
|
||||||
|
var buildDetails = function(id) {
|
||||||
|
var topic = Discourse.Topic.create({id: id});
|
||||||
|
return topic.get('details');
|
||||||
|
};
|
||||||
|
|
||||||
|
test('defaults', function() {
|
||||||
|
var details = buildDetails(1234);
|
||||||
|
present(details, "the details are present by default");
|
||||||
|
ok(!details.get('loaded'), "details are not loaded by default");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateFromJson', function() {
|
||||||
|
var details = buildDetails(1234);
|
||||||
|
|
||||||
|
details.updateFromJson({
|
||||||
|
suggested_topics: [{id: 1}, {id: 3}],
|
||||||
|
allowed_users: [{username: 'eviltrout'}]
|
||||||
|
});
|
||||||
|
|
||||||
|
equal(details.get('suggested_topics.length'), 2, 'it loaded the suggested_topics');
|
||||||
|
containsInstance(details.get('suggested_topics'), Discourse.Topic);
|
||||||
|
|
||||||
|
equal(details.get('allowed_users.length'), 1, 'it loaded the allowed users');
|
||||||
|
containsInstance(details.get('allowed_users'), Discourse.User);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
module("Discourse.Topic");
|
||||||
|
|
||||||
|
test('has details', function() {
|
||||||
|
var topic = Discourse.Topic.create({id: 1234});
|
||||||
|
var topicDetails = topic.get('details');
|
||||||
|
present(topicDetails, "a topic has topicDetails after we create it");
|
||||||
|
equal(topicDetails.get('topic'), topic, "the topicDetails has a reference back to the topic");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('has a postStream', function() {
|
||||||
|
var topic = Discourse.Topic.create({id: 1234});
|
||||||
|
var postStream = topic.get('postStream');
|
||||||
|
present(postStream, "a topic has a postStream after we create it");
|
||||||
|
equal(postStream.get('topic'), topic, "the postStream has a reference back to the topic");
|
||||||
|
});
|
||||||
|
|
||||||
|
var category = _.first(Discourse.Category.list());
|
||||||
|
|
||||||
|
test('category relationship', function() {
|
||||||
|
// It finds the category by id
|
||||||
|
var topic = Discourse.Topic.create({id: 1111, category_id: category.get('id') });
|
||||||
|
equal(topic.get('category'), category);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updateFromJson", function() {
|
||||||
|
var topic = Discourse.Topic.create({id: 1234});
|
||||||
|
|
||||||
|
topic.updateFromJson({
|
||||||
|
post_stream: [1,2,3],
|
||||||
|
details: {hello: 'world'},
|
||||||
|
cool: 'property',
|
||||||
|
category_id: category.get('id')
|
||||||
|
});
|
||||||
|
|
||||||
|
blank(topic.get('post_stream'), "it does not update post_stream");
|
||||||
|
equal(topic.get('details.hello'), 'world', 'it updates the details');
|
||||||
|
equal(topic.get('cool'), "property", "it updates other properties");
|
||||||
|
equal(topic.get('category'), category);
|
||||||
|
});
|
Loading…
Reference in New Issue