mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:44:49 +08:00
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"));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Deletes a post
|
||||
|
||||
|
|
|
@ -331,10 +331,6 @@ Discourse = Ember.Application.createWithMixins({
|
|||
Discourse.MessageBus.start();
|
||||
Discourse.KeyValueStore.init("discourse_", Discourse.MessageBus);
|
||||
|
||||
// Don't remove site settings for now. It seems on some browsers the route
|
||||
// tries to use it after it has been removed
|
||||
// PreloadStore.remove('siteSettings');
|
||||
|
||||
// Developer specific functions
|
||||
Discourse.Development.setupProbes();
|
||||
Discourse.Development.observeLiveChanges();
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.URL = {
|
||||
Discourse.URL = Em.Object.createWithMixins({
|
||||
|
||||
// Used for matching a topic
|
||||
TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/,
|
||||
|
@ -23,7 +23,7 @@ Discourse.URL = {
|
|||
**/
|
||||
router: function() {
|
||||
return Discourse.__container__.lookup('router:main');
|
||||
},
|
||||
}.property(),
|
||||
|
||||
/**
|
||||
Browser aware replaceState. Will only be invoked if the browser supports it.
|
||||
|
@ -43,7 +43,8 @@ Discourse.URL = {
|
|||
// while URLs are loading. For example, while a topic loads it sets `currentPost`
|
||||
// which triggers a replaceState even though the topic hasn't fully loaded yet!
|
||||
Em.run.next(function() {
|
||||
Discourse.URL.router().get('location').replaceURL(path);
|
||||
var location = Discourse.URL.get('router.location');
|
||||
if (location.replaceURL) { location.replaceURL(path); }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -85,10 +86,16 @@ Discourse.URL = {
|
|||
if (oldTopicId === newTopicId) {
|
||||
Discourse.URL.replaceState(path);
|
||||
var topicController = Discourse.__container__.lookup('controller:topic');
|
||||
var opts = { trackVisit: false };
|
||||
var opts = { };
|
||||
if (newMatches[3]) opts.nearPost = newMatches[3];
|
||||
topicController.cancelFilter();
|
||||
topicController.loadPosts(opts);
|
||||
|
||||
var postStream = topicController.get('postStream');
|
||||
postStream.refresh(opts).then(function() {
|
||||
topicController.setProperties({
|
||||
currentPost: opts.nearPost || 1,
|
||||
progressPosition: opts.nearPost || 1
|
||||
});
|
||||
});
|
||||
|
||||
// Abort routing, we have replaced our state.
|
||||
return;
|
||||
|
@ -102,11 +109,18 @@ Discourse.URL = {
|
|||
|
||||
// Be wary of looking up the router. In this case, we have links in our
|
||||
// HTML, say form compiled markdown posts, that need to be routed.
|
||||
var router = this.router();
|
||||
var router = this.get('router');
|
||||
router.router.updateURL(path);
|
||||
return router.handleURL(path);
|
||||
},
|
||||
|
||||
/**
|
||||
Replaces the query parameters in the URL. Use no parameters to clear them.
|
||||
|
||||
@method replaceQueryParams
|
||||
**/
|
||||
queryParams: Em.computed.alias('router.location.queryParams'),
|
||||
|
||||
/**
|
||||
@private
|
||||
|
||||
|
@ -131,4 +145,4 @@ Discourse.URL = {
|
|||
window.location = Discourse.getURL(url);
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
|
|
|
@ -10,15 +10,15 @@
|
|||
Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
|
||||
|
||||
setDays: function() {
|
||||
if( this.get('auto_close_at') ) {
|
||||
var closeTime = new Date( this.get('auto_close_at') );
|
||||
if( this.get('details.auto_close_at') ) {
|
||||
var closeTime = new Date( this.get('details.auto_close_at') );
|
||||
if (closeTime > new Date()) {
|
||||
this.set('auto_close_days', closeTime.daysSince());
|
||||
}
|
||||
} else {
|
||||
this.set('auto_close_days', '');
|
||||
this.set('details.auto_close_days', '');
|
||||
}
|
||||
}.observes('auto_close_at'),
|
||||
}.observes('details.auto_close_at'),
|
||||
|
||||
saveAutoClose: function() {
|
||||
this.setAutoClose( parseFloat(this.get('auto_close_days')) );
|
||||
|
@ -36,7 +36,7 @@ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Disco
|
|||
dataType: 'html', // no custom errors, jquery 1.9 enforces json
|
||||
data: { auto_close_days: days > 0 ? days : null }
|
||||
}).then(function(){
|
||||
editTopicAutoCloseController.set('auto_close_at', moment().add('days', days).format());
|
||||
editTopicAutoCloseController.set('details.auto_close_at', moment().add('days', days).format());
|
||||
}, function (error) {
|
||||
bootbox.alert(Em.String.i18n('generic_error'));
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@ Discourse.InvitePrivateController = Discourse.ObjectController.extend(Discourse.
|
|||
invitePrivateController.set('finished', true);
|
||||
|
||||
if(result && result.user) {
|
||||
invitePrivateController.get('content.allowed_users').pushObject(result.user);
|
||||
invitePrivateController.get('content.details.allowed_users').pushObject(result.user);
|
||||
}
|
||||
}, function() {
|
||||
// Failure
|
||||
|
|
|
@ -34,7 +34,7 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({
|
|||
if (!Discourse.User.current()) return;
|
||||
|
||||
// don't display the "quote-reply" button if we can't create a post
|
||||
if (!this.get('controllers.topic.content.can_create_post')) return;
|
||||
if (!this.get('controllers.topic.model.details.can_create_post')) return;
|
||||
|
||||
var selection = window.getSelection();
|
||||
// no selections
|
||||
|
|
|
@ -7,24 +7,28 @@
|
|||
@module Discourse
|
||||
**/
|
||||
Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, {
|
||||
userFilters: new Em.Set(),
|
||||
multiSelect: false,
|
||||
bestOf: false,
|
||||
summaryCollapsed: true,
|
||||
loading: false,
|
||||
loadingBelow: false,
|
||||
loadingAbove: false,
|
||||
needs: ['header', 'modal', 'composer', 'quoteButton'],
|
||||
allPostsSelected: false,
|
||||
selectedPosts: new Em.Set(),
|
||||
editingTopic: false,
|
||||
|
||||
jumpTopDisabled: function() {
|
||||
return this.get('currentPost') === 1;
|
||||
}.property('currentPost'),
|
||||
|
||||
jumpBottomDisabled: function() {
|
||||
return this.get('currentPost') === this.get('highest_post_number');
|
||||
}.property('currentPost'),
|
||||
|
||||
canMergeTopic: function() {
|
||||
if (!this.get('can_move_posts')) return false;
|
||||
if (!this.get('details.can_move_posts')) return false;
|
||||
return (this.get('selectedPostsCount') > 0);
|
||||
}.property('selectedPostsCount'),
|
||||
|
||||
canSplitTopic: function() {
|
||||
if (!this.get('can_move_posts')) return false;
|
||||
if (!this.get('details.can_move_posts')) return false;
|
||||
if (this.get('allPostsSelected')) return false;
|
||||
return (this.get('selectedPostsCount') > 0);
|
||||
}.property('selectedPostsCount'),
|
||||
|
@ -64,11 +68,11 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
}.observes('multiSelect'),
|
||||
|
||||
hideProgress: function() {
|
||||
if (!this.get('content.loaded')) return true;
|
||||
if (!this.get('postStream.loaded')) return true;
|
||||
if (!this.get('currentPost')) return true;
|
||||
if (this.get('content.filtered_posts_count') < 2) return true;
|
||||
if (this.get('postStream.filteredPostsCount') < 2) return true;
|
||||
return false;
|
||||
}.property('content.loaded', 'currentPost', 'content.filtered_posts_count'),
|
||||
}.property('postStream.loaded', 'currentPost', 'postStream.filteredPostsCount'),
|
||||
|
||||
selectPost: function(post) {
|
||||
var selectedPosts = this.get('selectedPosts');
|
||||
|
@ -107,6 +111,58 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
this.toggleProperty('summaryCollapsed');
|
||||
},
|
||||
|
||||
editTopic: function() {
|
||||
if (!this.get('details.can_edit')) return false;
|
||||
|
||||
this.setProperties({
|
||||
editingTopic: true,
|
||||
newTitle: this.get('title'),
|
||||
newCategoryId: this.get('category_id')
|
||||
});
|
||||
return false;
|
||||
},
|
||||
|
||||
// close editing mode
|
||||
cancelEditingTopic: function() {
|
||||
this.set('editingTopic', false);
|
||||
},
|
||||
|
||||
finishedEditingTopic: function() {
|
||||
var topicController = this;
|
||||
if (this.get('editingTopic')) {
|
||||
|
||||
var topic = this.get('model');
|
||||
|
||||
// manually update the titles & category
|
||||
topic.setProperties({
|
||||
title: this.get('newTitle'),
|
||||
category_id: parseInt(this.get('newCategoryId'), 10),
|
||||
fancy_title: this.get('newTitle')
|
||||
});
|
||||
|
||||
// save the modifications
|
||||
topic.save().then(function(result){
|
||||
// update the title if it has been changed (cleaned up) server-side
|
||||
var title = result.basic_topic.fancy_title;
|
||||
topic.setProperties({
|
||||
title: title,
|
||||
fancy_title: title
|
||||
});
|
||||
|
||||
}, function(error) {
|
||||
topicController.set('editingTopic', true);
|
||||
if (error && error.responseText) {
|
||||
bootbox.alert($.parseJSON(error.responseText).errors[0]);
|
||||
} else {
|
||||
bootbox.alert(Em.String.i18n('generic_error'));
|
||||
}
|
||||
});
|
||||
|
||||
// close editing mode
|
||||
topicController.set('editingTopic', false);
|
||||
}
|
||||
},
|
||||
|
||||
deleteSelected: function() {
|
||||
var topicController = this;
|
||||
bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
||||
|
@ -126,25 +182,14 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
},
|
||||
|
||||
jumpTop: function() {
|
||||
if (this.get('bestOf')) {
|
||||
Discourse.TopicView.scrollTo(this.get('id'), this.get('posts')[0].get('post_number'));
|
||||
} else {
|
||||
Discourse.URL.routeTo(this.get('url'));
|
||||
}
|
||||
Discourse.URL.routeTo(this.get('url'));
|
||||
},
|
||||
|
||||
jumpBottom: function() {
|
||||
if (this.get('bestOf')) {
|
||||
Discourse.TopicView.scrollTo(this.get('id'), _.last(this.get('posts')).get('post_number'));
|
||||
} else {
|
||||
Discourse.URL.routeTo(this.get('lastPostUrl'));
|
||||
}
|
||||
Discourse.URL.routeTo(this.get('lastPostUrl'));
|
||||
},
|
||||
|
||||
cancelFilter: function() {
|
||||
this.set('bestOf', false);
|
||||
this.get('userFilters').clear();
|
||||
},
|
||||
|
||||
|
||||
replyAsNewTopic: function(post) {
|
||||
// TODO shut down topic draft cleanly if it exists ...
|
||||
|
@ -182,95 +227,18 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
}
|
||||
},
|
||||
|
||||
toggleParticipant: function(user) {
|
||||
this.set('bestOf', false);
|
||||
var username = Em.get(user, 'username');
|
||||
var userFilters = this.get('userFilters');
|
||||
if (userFilters.contains(username)) {
|
||||
userFilters.remove(username);
|
||||
} else {
|
||||
userFilters.add(username);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Show or hide the bottom bar, depending on our filter options.
|
||||
Toggle a participant for filtering
|
||||
|
||||
@method updateBottomBar
|
||||
@method toggleParticipant
|
||||
**/
|
||||
updateBottomBar: function() {
|
||||
|
||||
var postFilters = this.get('postFilters');
|
||||
|
||||
if (postFilters.bestOf) {
|
||||
this.set('filterDesc', Em.String.i18n("topic.filters.best_of", {
|
||||
n_best_posts: Em.String.i18n("topic.filters.n_best_posts", { count: this.get('filtered_posts_count') }),
|
||||
of_n_posts: Em.String.i18n("topic.filters.of_n_posts", { count: this.get('posts_count') })
|
||||
}));
|
||||
} else if (postFilters.userFilters.length > 0) {
|
||||
this.set('filterDesc', Em.String.i18n("topic.filters.user", {
|
||||
n_posts: Em.String.i18n("topic.filters.n_posts", { count: this.get('filtered_posts_count') }),
|
||||
by_n_users: Em.String.i18n("topic.filters.by_n_users", { count: postFilters.userFilters.length })
|
||||
}));
|
||||
} else {
|
||||
// Hide the bottom bar
|
||||
$('#topic-filter').slideUp();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#topic-filter').slideDown();
|
||||
toggleParticipant: function(user) {
|
||||
this.get('postStream').toggleParticipant(Em.get(user, 'username'));
|
||||
},
|
||||
|
||||
enableBestOf: function(e) {
|
||||
this.set('bestOf', true);
|
||||
this.get('userFilters').clear();
|
||||
},
|
||||
|
||||
postFilters: function() {
|
||||
if (this.get('bestOf') === true) return { bestOf: true };
|
||||
return { userFilters: this.get('userFilters') };
|
||||
}.property('userFilters.[]', 'bestOf'),
|
||||
|
||||
loadPosts: function(opts) {
|
||||
var topicController = this;
|
||||
this.get('content').loadPosts(opts).then(function () {
|
||||
Em.run.scheduleOnce('afterRender', topicController, 'updateBottomBar');
|
||||
});
|
||||
},
|
||||
|
||||
reloadPosts: function() {
|
||||
var topic = this.get('content');
|
||||
if (!topic) return;
|
||||
|
||||
var posts = topic.get('posts');
|
||||
if (!posts) return;
|
||||
|
||||
// Leave the first post -- we keep it above the filter controls
|
||||
posts.removeAt(1, posts.length - 1);
|
||||
|
||||
this.set('loadingBelow', true);
|
||||
|
||||
var topicController = this;
|
||||
var postFilters = this.get('postFilters');
|
||||
return Discourse.Topic.find(this.get('id'), postFilters).then(function(result) {
|
||||
var first = result.posts[0];
|
||||
if (first) {
|
||||
topicController.set('currentPost', first.post_number);
|
||||
}
|
||||
$('#topic-progress .solid').data('progress', false);
|
||||
_.each(result.posts,function(p) {
|
||||
// Skip the first post
|
||||
if (p.post_number === 1) return;
|
||||
posts.pushObject(Discourse.Post.create(p, topic));
|
||||
});
|
||||
|
||||
Em.run.scheduleOnce('afterRender', topicController, 'updateBottomBar');
|
||||
|
||||
topicController.set('filtered_posts_count', result.filtered_posts_count);
|
||||
topicController.set('loadingBelow', false);
|
||||
topicController.set('seenBottom', false);
|
||||
});
|
||||
}.observes('postFilters'),
|
||||
showFavoriteButton: function() {
|
||||
return Discourse.User.current() && !this.get('isPrivateMessage');
|
||||
}.property('isPrivateMessage'),
|
||||
|
||||
deleteTopic: function() {
|
||||
var topicController = this;
|
||||
|
@ -327,23 +295,13 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
bus.subscribe("/topic/" + (this.get('id')), function(data) {
|
||||
var topic = topicController.get('model');
|
||||
if (data.notification_level_change) {
|
||||
topic.set('notification_level', data.notification_level_change);
|
||||
topic.set('notifications_reason_id', data.notifications_reason_id);
|
||||
return;
|
||||
}
|
||||
var posts = topic.get('posts');
|
||||
if (posts.some(function(p) {
|
||||
return p.get('post_number') === data.post_number;
|
||||
})) {
|
||||
topic.set('details.notification_level', data.notification_level_change);
|
||||
topic.set('details.notifications_reason_id', data.notifications_reason_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Robin, TODO when a message comes in we need to figure out if it even goes
|
||||
// in this view ... for now fixed the general case
|
||||
topic.set('filtered_posts_count', topic.get('filtered_posts_count') + 1);
|
||||
topic.set('highest_post_number', data.post_number);
|
||||
topic.set('last_poster', data.user);
|
||||
topic.set('last_posted_at', data.created_at);
|
||||
// Add the new post into the stream
|
||||
topicController.get('postStream').triggerNewPostInStream(data.id);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -424,15 +382,8 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
post.destroy();
|
||||
},
|
||||
|
||||
postRendered: function(post) {
|
||||
var onPostRendered = this.get('onPostRendered');
|
||||
if (onPostRendered) {
|
||||
onPostRendered(post);
|
||||
}
|
||||
},
|
||||
|
||||
removeAllowedUser: function(username) {
|
||||
this.get('model').removeAllowedUser(username);
|
||||
this.get('details').removeAllowedUser(username);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -357,70 +357,40 @@ Discourse.Composer = Discourse.Model.extend({
|
|||
var post = this.get('post'),
|
||||
topic = this.get('topic'),
|
||||
currentUser = Discourse.User.current(),
|
||||
postStream = this.get('topic.postStream'),
|
||||
addedToStream = false;
|
||||
|
||||
// The post number we'll probably get from the server
|
||||
var probablePostNumber = this.get('topic.highest_post_number') + 1;
|
||||
|
||||
// Build the post object
|
||||
var createdPost = Discourse.Post.create({
|
||||
raw: this.get('reply'),
|
||||
title: this.get('title'),
|
||||
category: this.get('categoryName'),
|
||||
topic_id: this.get('topic.id'),
|
||||
reply_to_post_number: post ? post.get('post_number') : null,
|
||||
imageSizes: opts.imageSizes,
|
||||
post_number: probablePostNumber,
|
||||
index: probablePostNumber,
|
||||
cooked: $('#wmd-preview').html(),
|
||||
reply_count: 0,
|
||||
display_username: currentUser.get('name'),
|
||||
username: currentUser.get('username'),
|
||||
user_id: currentUser.get('id'),
|
||||
metaData: this.get('metaData'),
|
||||
archetype: this.get('archetypeId'),
|
||||
post_type: Discourse.Site.instance().get('post_types.regular'),
|
||||
target_usernames: this.get('targetUsernames'),
|
||||
actions_summary: Em.A(),
|
||||
moderator: currentUser.get('moderator'),
|
||||
yours: true,
|
||||
newPost: true,
|
||||
auto_close_days: this.get('auto_close_days')
|
||||
});
|
||||
raw: this.get('reply'),
|
||||
title: this.get('title'),
|
||||
category: this.get('categoryName'),
|
||||
topic_id: this.get('topic.id'),
|
||||
reply_to_post_number: post ? post.get('post_number') : null,
|
||||
imageSizes: opts.imageSizes,
|
||||
cooked: $('#wmd-preview').html(),
|
||||
reply_count: 0,
|
||||
display_username: currentUser.get('name'),
|
||||
username: currentUser.get('username'),
|
||||
user_id: currentUser.get('id'),
|
||||
metaData: this.get('metaData'),
|
||||
archetype: this.get('archetypeId'),
|
||||
post_type: Discourse.Site.instance().get('post_types.regular'),
|
||||
target_usernames: this.get('targetUsernames'),
|
||||
actions_summary: Em.A(),
|
||||
moderator: currentUser.get('moderator'),
|
||||
yours: true,
|
||||
newPost: true,
|
||||
auto_close_days: this.get('auto_close_days')
|
||||
});
|
||||
|
||||
// If we're in a topic, we can append the post instantly.
|
||||
if (topic) {
|
||||
|
||||
// Increase the reply count
|
||||
if (postStream) {
|
||||
// If it's in reply to another post, increase the reply count
|
||||
if (post) {
|
||||
post.set('reply_count', (post.get('reply_count') || 0) + 1);
|
||||
}
|
||||
topic.set('posts_count', topic.get('posts_count') + 1);
|
||||
|
||||
// Update last post
|
||||
topic.set('last_posted_at', new Date());
|
||||
topic.set('highest_post_number', createdPost.get('post_number'));
|
||||
topic.set('last_poster', Discourse.User.current());
|
||||
topic.set('filtered_posts_count', topic.get('filtered_posts_count') + 1);
|
||||
|
||||
// Set the topic view for the new post
|
||||
createdPost.set('topic', topic);
|
||||
createdPost.set('created_at', new Date());
|
||||
|
||||
// If we're near the end of the topic, load new posts
|
||||
var lastPost = topic.posts[topic.posts.length-1];
|
||||
if (lastPost) {
|
||||
var diff = topic.get('highest_post_number') - lastPost.get('post_number');
|
||||
|
||||
// If the new post is within a threshold of the end of the topic,
|
||||
// add it and scroll there instead of adding the link.
|
||||
|
||||
if (diff < 5) {
|
||||
createdPost.set('scrollToAfterInsert', createdPost.get('post_number'));
|
||||
topic.pushPosts([createdPost]);
|
||||
addedToStream = true;
|
||||
}
|
||||
}
|
||||
postStream.stagePost(createdPost, currentUser);
|
||||
}
|
||||
|
||||
// Save callback
|
||||
|
@ -430,11 +400,13 @@ Discourse.Composer = Discourse.Model.extend({
|
|||
var addedPost = false,
|
||||
saving = true;
|
||||
|
||||
createdPost.updateFromSave(result);
|
||||
createdPost.updateFromJson(result);
|
||||
if (topic) {
|
||||
// It's no longer a new post
|
||||
createdPost.set('newPost', false);
|
||||
topic.set('draft_sequence', result.draft_sequence);
|
||||
postStream.commitPost(createdPost);
|
||||
addedToStream = true;
|
||||
} else {
|
||||
// We created a new topic, let's show it.
|
||||
composer.set('composeState', CLOSED);
|
||||
|
@ -448,12 +420,13 @@ Discourse.Composer = Discourse.Model.extend({
|
|||
} else if (saving) {
|
||||
composer.set('composeState', SAVING);
|
||||
}
|
||||
|
||||
|
||||
return promise.resolve({ post: result });
|
||||
}, function(error) {
|
||||
// If an error occurs
|
||||
if (topic) {
|
||||
topic.posts.removeObject(createdPost);
|
||||
topic.set('filtered_posts_count', topic.get('filtered_posts_count') - 1);
|
||||
if (postStream) {
|
||||
postStream.undoPost(createdPost);
|
||||
}
|
||||
promise.reject($.parseJSON(error.responseText).errors[0]);
|
||||
composer.set('composeState', OPEN);
|
||||
|
|
|
@ -15,9 +15,8 @@ Discourse.Post = Discourse.Model.extend({
|
|||
return this.get('url') + (user ? '?u=' + user.get('username_lower') : '');
|
||||
}.property('url'),
|
||||
|
||||
new_user: function() {
|
||||
return this.get('trust_level') === 0;
|
||||
}.property('trust_level'),
|
||||
new_user: Em.computed.equal('trust_level', 0),
|
||||
firstPost: Em.computed.equal('post_number', 1),
|
||||
|
||||
url: function() {
|
||||
return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number'));
|
||||
|
@ -35,14 +34,9 @@ Discourse.Post = Discourse.Model.extend({
|
|||
return this.get('reply_to_user') && (this.get('reply_to_post_number') < (this.get('post_number') - 1));
|
||||
}.property('reply_to_user', 'reply_to_post_number', 'post_number'),
|
||||
|
||||
firstPost: function() {
|
||||
if (this.get('bestOfFirst') === true) return true;
|
||||
return this.get('post_number') === 1;
|
||||
}.property('post_number'),
|
||||
|
||||
byTopicCreator: function() {
|
||||
return this.get('topic.created_by.id') === this.get('user_id');
|
||||
}.property('topic.created_by.id', 'user_id'),
|
||||
return this.get('topic.details.created_by.id') === this.get('user_id');
|
||||
}.property('topic.details.created_by.id', 'user_id'),
|
||||
|
||||
hasHistory: function() {
|
||||
return this.get('version') > 1;
|
||||
|
@ -55,28 +49,23 @@ Discourse.Post = Discourse.Model.extend({
|
|||
// The class for the read icon of the post. It starts with read-icon then adds 'seen' or
|
||||
// 'last-read' if the post has been seen or is the highest post number seen so far respectively.
|
||||
bookmarkClass: function() {
|
||||
var result, topic;
|
||||
result = 'read-icon';
|
||||
var result = 'read-icon';
|
||||
if (this.get('bookmarked')) return result + ' bookmarked';
|
||||
topic = this.get('topic');
|
||||
|
||||
var topic = this.get('topic');
|
||||
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
|
||||
result += ' last-read';
|
||||
} else {
|
||||
if (this.get('read')) {
|
||||
result += ' seen';
|
||||
} else {
|
||||
result += ' unseen';
|
||||
}
|
||||
return result + ' last-read';
|
||||
}
|
||||
return result;
|
||||
|
||||
return result + (this.get('read') ? ' seen' : ' unseen');
|
||||
}.property('read', 'topic.last_read_post_number', 'bookmarked'),
|
||||
|
||||
// Custom tooltips for the bookmark icons
|
||||
bookmarkTooltip: function() {
|
||||
var topic;
|
||||
if (this.get('bookmarked')) return Em.String.i18n('bookmarks.created');
|
||||
if (!this.get('read')) return "";
|
||||
topic = this.get('topic');
|
||||
|
||||
var topic = this.get('topic');
|
||||
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
|
||||
return Em.String.i18n('bookmarks.last_read');
|
||||
}
|
||||
|
@ -123,9 +112,9 @@ Discourse.Post = Discourse.Model.extend({
|
|||
}.property('updated_at'),
|
||||
|
||||
flagsAvailable: function() {
|
||||
var _this = this;
|
||||
var flags = Discourse.Site.instance().get('flagTypes').filter(function(item) {
|
||||
return _this.get("actionByName." + (item.get('name_key')) + ".can_act");
|
||||
var post = this,
|
||||
flags = Discourse.Site.instance().get('flagTypes').filter(function(item) {
|
||||
return post.get("actionByName." + (item.get('name_key')) + ".can_act");
|
||||
});
|
||||
return flags;
|
||||
}.property('actions_summary.@each.can_act'),
|
||||
|
@ -142,7 +131,6 @@ Discourse.Post = Discourse.Model.extend({
|
|||
|
||||
// Save a post and call the callback when done.
|
||||
save: function(complete, error) {
|
||||
var data, metaData;
|
||||
if (!this.get('newPost')) {
|
||||
// We're updating a post
|
||||
return Discourse.ajax("/posts/" + (this.get('id')), {
|
||||
|
@ -163,7 +151,7 @@ Discourse.Post = Discourse.Model.extend({
|
|||
} else {
|
||||
|
||||
// We're saving a post
|
||||
data = {
|
||||
var data = {
|
||||
raw: this.get('raw'),
|
||||
topic_id: this.get('topic_id'),
|
||||
reply_to_post_number: this.get('reply_to_post_number'),
|
||||
|
@ -175,11 +163,13 @@ Discourse.Post = Discourse.Model.extend({
|
|||
auto_close_days: this.get('auto_close_days')
|
||||
};
|
||||
|
||||
var metaData = this.get('metaData');
|
||||
// Put the metaData into the request
|
||||
if (metaData = this.get('metaData')) {
|
||||
if (metaData) {
|
||||
data.meta_data = {};
|
||||
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
|
||||
}
|
||||
|
||||
return Discourse.ajax("/posts", {
|
||||
type: 'POST',
|
||||
data: data
|
||||
|
@ -201,14 +191,35 @@ Discourse.Post = Discourse.Model.extend({
|
|||
return Discourse.ajax("/posts/" + (this.get('id')), { type: 'DELETE' });
|
||||
},
|
||||
|
||||
// Update the properties of this post from an obj, ignoring cooked as we should already
|
||||
// have that rendered.
|
||||
updateFromSave: function(obj) {
|
||||
/**
|
||||
Updates a post from another's attributes. This will normally happen when a post is loading but
|
||||
is already found in an identity map.
|
||||
|
||||
@method updateFromPost
|
||||
@param {Discourse.Post} otherPost The post we're updating from
|
||||
**/
|
||||
updateFromPost: function(otherPost) {
|
||||
var post = this;
|
||||
Object.keys(otherPost).forEach(function (key) {
|
||||
var value = otherPost[key];
|
||||
if (typeof value !== "function") {
|
||||
post.set(key, value);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Updates a post from a JSON packet. This is normally done after the post is saved to refresh any
|
||||
attributes.
|
||||
|
||||
@method updateFromJson
|
||||
@param {Object} obj The Json data to update with
|
||||
**/
|
||||
updateFromJson: function(obj) {
|
||||
if (!obj) return;
|
||||
|
||||
// Update all the properties
|
||||
if (!obj) return;
|
||||
var post = this;
|
||||
_.each(obj, function(val,key) {
|
||||
if (key !== 'actions_summary'){
|
||||
if (val) {
|
||||
|
@ -255,7 +266,7 @@ Discourse.Post = Discourse.Model.extend({
|
|||
},
|
||||
|
||||
// Whether to show replies directly below
|
||||
showRepliesBelow: (function() {
|
||||
showRepliesBelow: function() {
|
||||
var reply_count, _ref;
|
||||
reply_count = this.get('reply_count');
|
||||
|
||||
|
@ -272,15 +283,15 @@ Discourse.Post = Discourse.Model.extend({
|
|||
if ((_ref = this.get('topic')) ? _ref.isReplyDirectlyBelow(this) : void 0) return false;
|
||||
|
||||
return true;
|
||||
}).property('reply_count')
|
||||
}.property('reply_count')
|
||||
|
||||
});
|
||||
|
||||
Discourse.Post.reopenClass({
|
||||
|
||||
createActionSummary: function(result) {
|
||||
var lookup;
|
||||
if (result.actions_summary) {
|
||||
lookup = Em.Object.create();
|
||||
var lookup = Em.Object.create();
|
||||
result.actions_summary = result.actions_summary.map(function(a) {
|
||||
a.post = result;
|
||||
a.actionType = Discourse.Site.instance().postActionTypeById(a.id);
|
||||
|
@ -288,17 +299,16 @@ Discourse.Post.reopenClass({
|
|||
lookup.set(a.actionType.get('name_key'), actionSummary);
|
||||
return actionSummary;
|
||||
});
|
||||
return result.set('actionByName', lookup);
|
||||
result.set('actionByName', lookup);
|
||||
}
|
||||
},
|
||||
|
||||
create: function(obj, topic) {
|
||||
create: function(obj) {
|
||||
var result = this._super(obj);
|
||||
this.createActionSummary(result);
|
||||
if (obj && obj.reply_to_user) {
|
||||
result.set('reply_to_user', Discourse.User.create(obj.reply_to_user));
|
||||
}
|
||||
result.set('topic', topic);
|
||||
return result;
|
||||
},
|
||||
|
||||
|
|
633
app/assets/javascripts/discourse/models/post_stream.js
Normal file
633
app/assets/javascripts/discourse/models/post_stream.js
Normal file
|
@ -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({
|
||||
|
||||
fewParticipants: function() {
|
||||
if (!this.present('participants')) return null;
|
||||
return this.get('participants').slice(0, 3);
|
||||
}.property('participants'),
|
||||
postStream: function() {
|
||||
return Discourse.PostStream.create({topic: this});
|
||||
}.property(),
|
||||
|
||||
details: function() {
|
||||
return Discourse.TopicDetails.create({topic: this});
|
||||
}.property(),
|
||||
|
||||
canConvertToRegular: function() {
|
||||
var a = this.get('archetype');
|
||||
|
@ -34,8 +37,17 @@ Discourse.Topic = Discourse.Model.extend({
|
|||
}.property('id'),
|
||||
|
||||
category: function() {
|
||||
return Discourse.Category.list().findProperty('name', this.get('categoryName'));
|
||||
}.property('categoryName'),
|
||||
var categoryId = this.get('category_id');
|
||||
if (categoryId) {
|
||||
return Discourse.Category.list().findProperty('id', categoryId);
|
||||
}
|
||||
|
||||
var categoryName = this.get('categoryName');
|
||||
if (categoryName) {
|
||||
return Discourse.Category.list().findProperty('name', categoryName);
|
||||
}
|
||||
return null;
|
||||
}.property('category_id', 'categoryName'),
|
||||
|
||||
shareUrl: function(){
|
||||
var user = Discourse.User.current();
|
||||
|
@ -150,17 +162,6 @@ Discourse.Topic = Discourse.Model.extend({
|
|||
});
|
||||
},
|
||||
|
||||
removeAllowedUser: function(username) {
|
||||
var allowedUsers = this.get('allowed_users');
|
||||
|
||||
return Discourse.ajax("/t/" + this.get('id') + "/remove-allowed-user", {
|
||||
type: 'PUT',
|
||||
data: { username: username }
|
||||
}).then(function(){
|
||||
allowedUsers.removeObject(allowedUsers.find(function(item){ return item.username === username; }));
|
||||
});
|
||||
},
|
||||
|
||||
favoriteTooltipKey: (function() {
|
||||
return this.get('starred') ? 'favorite.help.unstar' : 'favorite.help.star';
|
||||
}).property('starred'),
|
||||
|
@ -190,7 +191,7 @@ Discourse.Topic = Discourse.Model.extend({
|
|||
// Save any changes we've made to the model
|
||||
save: function() {
|
||||
// Don't save unless we can
|
||||
if (!this.get('can_edit')) return;
|
||||
if (!this.get('details.can_edit')) return;
|
||||
|
||||
return Discourse.ajax(this.get('url'), {
|
||||
type: 'PUT',
|
||||
|
@ -218,138 +219,19 @@ Discourse.Topic = Discourse.Model.extend({
|
|||
return Discourse.ajax("/t/" + (this.get('id')), { type: 'DELETE' });
|
||||
},
|
||||
|
||||
// Load the posts for this topic
|
||||
loadPosts: function(opts) {
|
||||
// Update our attributes from a JSON result
|
||||
updateFromJson: function(json) {
|
||||
this.get('details').updateFromJson(json.details);
|
||||
|
||||
var keys = Object.keys(json);
|
||||
keys.removeObject('details');
|
||||
keys.removeObject('post_stream');
|
||||
|
||||
var topic = this;
|
||||
|
||||
if (!opts) opts = {};
|
||||
|
||||
// Load the first post by default
|
||||
if ((!opts.bestOf) && (!opts.nearPost)) opts.nearPost = 1;
|
||||
|
||||
// If we already have that post in the DOM, jump to it. Return a promise
|
||||
// that's already complete.
|
||||
if (Discourse.TopicView.scrollTo(this.get('id'), opts.nearPost)) {
|
||||
return Ember.Deferred.promise(function(promise) { promise.resolve(); });
|
||||
}
|
||||
|
||||
// If loading the topic succeeded...
|
||||
var afterTopicLoaded = function(result) {
|
||||
|
||||
var closestPostNumber, lastPost, postDiff;
|
||||
|
||||
// Update the slug if different
|
||||
if (result.slug) topic.set('slug', result.slug);
|
||||
|
||||
// If we want to scroll to a post that doesn't exist, just pop them to the closest
|
||||
// one instead. This is likely happening due to a deleted post.
|
||||
opts.nearPost = parseInt(opts.nearPost, 10);
|
||||
closestPostNumber = 0;
|
||||
postDiff = Number.MAX_VALUE;
|
||||
_.each(result.posts,function(p) {
|
||||
var diff = Math.abs(p.post_number - opts.nearPost);
|
||||
if (diff < postDiff) {
|
||||
postDiff = diff;
|
||||
closestPostNumber = p.post_number;
|
||||
if (diff === 0) return false;
|
||||
}
|
||||
});
|
||||
|
||||
opts.nearPost = closestPostNumber;
|
||||
if (topic.get('participants')) {
|
||||
topic.get('participants').clear();
|
||||
}
|
||||
if (result.suggested_topics) {
|
||||
topic.set('suggested_topics', Em.A());
|
||||
}
|
||||
|
||||
topic.mergeAttributes(result, { suggested_topics: Discourse.Topic });
|
||||
topic.set('posts', Em.A());
|
||||
if (opts.trackVisit && result.draft && result.draft.length > 0) {
|
||||
Discourse.openComposer({
|
||||
draft: Discourse.Draft.getLocal(result.draft_key, result.draft),
|
||||
draftKey: result.draft_key,
|
||||
draftSequence: result.draft_sequence,
|
||||
topic: topic,
|
||||
ignoreIfChanged: true
|
||||
});
|
||||
}
|
||||
|
||||
// Okay this is weird, but let's store the length of the next post when there
|
||||
lastPost = null;
|
||||
_.each(result.posts,function(p) {
|
||||
p.scrollToAfterInsert = opts.nearPost;
|
||||
var post = Discourse.Post.create(p);
|
||||
post.set('topic', topic);
|
||||
topic.get('posts').pushObject(post);
|
||||
lastPost = post;
|
||||
});
|
||||
|
||||
topic.set('allowed_users', Em.A(result.allowed_users));
|
||||
topic.set('loaded', true);
|
||||
};
|
||||
|
||||
var errorLoadingTopic = function(result) {
|
||||
|
||||
topic.set('errorLoading', true);
|
||||
|
||||
// If the result was 404 the post is not found
|
||||
if (result.status === 404) {
|
||||
topic.set('errorTitle', Em.String.i18n('topic.not_found.title'));
|
||||
topic.set('message', Em.String.i18n('topic.not_found.description'));
|
||||
return;
|
||||
}
|
||||
|
||||
// If the result is 403 it means invalid access
|
||||
if (result.status === 403) {
|
||||
topic.set('errorTitle', Em.String.i18n('topic.invalid_access.title'));
|
||||
topic.set('message', Em.String.i18n('topic.invalid_access.description'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise supply a generic error message
|
||||
topic.set('errorTitle', Em.String.i18n('topic.server_error.title'));
|
||||
topic.set('message', Em.String.i18n('topic.server_error.description'));
|
||||
};
|
||||
|
||||
// Finally, call our find method
|
||||
return Discourse.Topic.find(this.get('id'), {
|
||||
nearPost: opts.nearPost,
|
||||
bestOf: opts.bestOf,
|
||||
trackVisit: opts.trackVisit
|
||||
}).then(afterTopicLoaded, errorLoadingTopic);
|
||||
},
|
||||
|
||||
notificationReasonText: function() {
|
||||
var locale_string = "topic.notifications.reasons." + this.get('notification_level');
|
||||
if (typeof this.get('notifications_reason_id') === 'number') {
|
||||
locale_string += "_" + this.get('notifications_reason_id');
|
||||
}
|
||||
return Em.String.i18n(locale_string, { username: Discourse.User.current('username_lower') });
|
||||
}.property('notification_level', 'notifications_reason_id'),
|
||||
|
||||
updateNotifications: function(v) {
|
||||
this.set('notification_level', v);
|
||||
this.set('notifications_reason_id', null);
|
||||
return Discourse.ajax("/t/" + (this.get('id')) + "/notifications", {
|
||||
type: 'POST',
|
||||
data: { notification_level: v }
|
||||
keys.forEach(function (key) {
|
||||
topic.set(key, json[key]);
|
||||
});
|
||||
},
|
||||
|
||||
// use to add post to topics protecting from dupes
|
||||
pushPosts: function(newPosts) {
|
||||
var map, posts;
|
||||
map = {};
|
||||
posts = this.get('posts');
|
||||
_.each(posts,function(post) {
|
||||
map["" + post.post_number] = true;
|
||||
});
|
||||
_.each(newPosts,function(post) {
|
||||
if (!map[post.get('post_number')]) {
|
||||
posts.pushObject(post);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -422,8 +304,6 @@ Discourse.Topic.reopenClass({
|
|||
},
|
||||
|
||||
// Load a topic, but accepts a set of filters
|
||||
// options:
|
||||
// onLoad - the callback after the topic is loaded
|
||||
find: function(topicId, opts) {
|
||||
var data, promise, url;
|
||||
url = Discourse.getURL("/t/") + topicId;
|
||||
|
@ -457,15 +337,7 @@ Discourse.Topic.reopenClass({
|
|||
}
|
||||
|
||||
// Check the preload store. If not, load it via JSON
|
||||
return PreloadStore.getAndRemove("topic_" + topicId, function() {
|
||||
return Discourse.ajax(url + ".json", {data: data});
|
||||
}).then(function(result) {
|
||||
var first = result.posts[0];
|
||||
if (first && opts && opts.bestOf) {
|
||||
first.bestOfFirst = true;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
return Discourse.ajax(url + ".json", {data: data});
|
||||
},
|
||||
|
||||
mergeTopic: function(topicId, destinationTopicId) {
|
||||
|
@ -488,24 +360,6 @@ Discourse.Topic.reopenClass({
|
|||
promise.reject();
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
|
||||
create: function(obj, topicView) {
|
||||
var result = this._super(obj);
|
||||
|
||||
if (result.participants) {
|
||||
result.participants = _.map(result.participants,function(u) {
|
||||
return Discourse.User.create(u);
|
||||
});
|
||||
result.fewParticipants = Em.A();
|
||||
_.each(result.participants,function(p) {
|
||||
// TODO should not be hardcoded
|
||||
if (result.fewParticipants.length >= 8) return false;
|
||||
result.fewParticipants.pushObject(p);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
});
|
||||
|
|
56
app/assets/javascripts/discourse/models/topic_details.js
Normal file
56
app/assets/javascripts/discourse/models/topic_details.js
Normal file
|
@ -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.route('fromParams', { path: '/' });
|
||||
this.route('fromParams', { path: '/:nearPost' });
|
||||
this.route('bestOf', { path: '/best_of' });
|
||||
});
|
||||
|
||||
// Generate static page routes
|
||||
|
|
|
@ -7,6 +7,35 @@
|
|||
var get = Ember.get, set = Ember.set;
|
||||
var popstateReady = false;
|
||||
|
||||
// Thanks: https://gist.github.com/kares/956897
|
||||
var re = /([^&=]+)=?([^&]*)/g;
|
||||
var decode = function(str) {
|
||||
return decodeURIComponent(str.replace(/\+/g, ' '));
|
||||
};
|
||||
$.parseParams = function(query) {
|
||||
var params = {}, e;
|
||||
if (query) {
|
||||
if (query.substr(0, 1) === '?') {
|
||||
query = query.substr(1);
|
||||
}
|
||||
|
||||
while (e = re.exec(query)) {
|
||||
var k = decode(e[1]);
|
||||
var v = decode(e[2]);
|
||||
if (params[k] !== undefined) {
|
||||
if (!$.isArray(params[k])) {
|
||||
params[k] = [params[k]];
|
||||
}
|
||||
params[k].push(v);
|
||||
} else {
|
||||
params[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
`Ember.DiscourseLocation` implements the location API using the browser's
|
||||
`history.pushState` API.
|
||||
|
@ -16,6 +45,7 @@ var popstateReady = false;
|
|||
@extends Ember.Object
|
||||
*/
|
||||
Ember.DiscourseLocation = Ember.Object.extend({
|
||||
|
||||
init: function() {
|
||||
set(this, 'location', get(this, 'location') || window.location);
|
||||
if ( $.inArray('state', $.event.props) < 0 ) {
|
||||
|
@ -32,6 +62,12 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
|||
@method initState
|
||||
*/
|
||||
initState: function() {
|
||||
|
||||
var location = this.get('location');
|
||||
if (location && location.search) {
|
||||
this.set('queryParams', $.parseParams(location.search));
|
||||
}
|
||||
|
||||
this.replaceState(this.formatURL(this.getURL()));
|
||||
set(this, 'history', window.history);
|
||||
},
|
||||
|
@ -62,6 +98,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
|||
@param path {String}
|
||||
*/
|
||||
setURL: function(path) {
|
||||
|
||||
path = this.formatURL(path);
|
||||
if (this.getState() && this.getState().path !== path) {
|
||||
popstateReady = true;
|
||||
|
@ -79,6 +116,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
|||
@param path {String}
|
||||
*/
|
||||
replaceURL: function(path) {
|
||||
|
||||
path = this.formatURL(path);
|
||||
|
||||
if (this.getState() && this.getState().path !== path) {
|
||||
|
@ -129,6 +167,21 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
|||
window.history.replaceState({ path: path }, null, path);
|
||||
},
|
||||
|
||||
|
||||
queryParamsString: function() {
|
||||
var params = this.get('queryParams');
|
||||
if (Em.isEmpty(params) || Em.isEmpty(Object.keys(params))) {
|
||||
return "";
|
||||
} else {
|
||||
return "?" + $.param(params).replace(/%5B/g, "[").replace(/%5D/g, "]");
|
||||
}
|
||||
}.property('queryParams'),
|
||||
|
||||
// When our query params change, update the URL
|
||||
queryParamsStringChanged: function() {
|
||||
this.replaceState(this.formatURL(this.getURL()));
|
||||
}.observes('queryParamsString'),
|
||||
|
||||
/**
|
||||
@private
|
||||
|
||||
|
@ -182,7 +235,7 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
|||
url = url.substring(rootURL.length);
|
||||
}
|
||||
|
||||
return rootURL + url;
|
||||
return rootURL + url + this.get('queryParamsString');
|
||||
},
|
||||
|
||||
willDestroy: function() {
|
||||
|
|
|
@ -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) {
|
||||
params = params || {};
|
||||
params.trackVisit = true;
|
||||
params.track_visit = true;
|
||||
|
||||
var topic = this.modelFor('topic');
|
||||
var postStream = topic.get('postStream');
|
||||
|
||||
var queryParams = Discourse.URL.get('queryParams');
|
||||
if (queryParams) {
|
||||
// Set bestOf on the postStream if present
|
||||
postStream.set('bestOf', Em.get(queryParams, 'filter') === 'best_of');
|
||||
|
||||
// Set any username filters on the postStream
|
||||
var userFilters = Em.get(queryParams, 'username_filters[]');
|
||||
if (userFilters) {
|
||||
if (typeof userFilters === "string") { userFilters = [userFilters]; }
|
||||
userFilters.forEach(function (username) {
|
||||
postStream.get('userFilters').add(username);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var topicController = this.controllerFor('topic');
|
||||
topicController.cancelFilter();
|
||||
topicController.loadPosts(params);
|
||||
postStream.refresh(params).then(function () {
|
||||
topicController.setProperties({
|
||||
currentPost: params.nearPost || 1,
|
||||
progressPosition: params.nearPost || 1
|
||||
});
|
||||
|
||||
if (topic.present('draft')) {
|
||||
Discourse.openComposer({
|
||||
draft: Discourse.Draft.getLocal(topic.get('draft_key'), topic.get('draft')),
|
||||
draftKey: topic.get('draft_key'),
|
||||
draftSequence: topic.get('draft_sequence'),
|
||||
topic: topic,
|
||||
ignoreIfChanged: true
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -60,11 +60,9 @@ Discourse.TopicRoute = Discourse.Route.extend({
|
|||
},
|
||||
|
||||
model: function(params) {
|
||||
var currentModel, _ref;
|
||||
if (currentModel = (_ref = this.controllerFor('topic')) ? _ref.get('content') : void 0) {
|
||||
if (currentModel.get('id') === parseInt(params.id, 10)) {
|
||||
return currentModel;
|
||||
}
|
||||
var currentModel = this.modelFor('topic');
|
||||
if (currentModel && (currentModel.get('id') === parseInt(params.id, 10))) {
|
||||
return currentModel;
|
||||
}
|
||||
return Discourse.Topic.create(params);
|
||||
},
|
||||
|
@ -85,23 +83,28 @@ Discourse.TopicRoute = Discourse.Route.extend({
|
|||
// Clear the search context
|
||||
this.controllerFor('search').set('searchContext', null);
|
||||
|
||||
var headerController, topicController;
|
||||
topicController = this.controllerFor('topic');
|
||||
topicController.cancelFilter();
|
||||
topicController.unsubscribe();
|
||||
var topicController = this.controllerFor('topic');
|
||||
var postStream = topicController.get('postStream');
|
||||
postStream.cancelFilter();
|
||||
|
||||
topicController.set('multiSelect', false);
|
||||
topicController.unsubscribe();
|
||||
this.controllerFor('composer').set('topic', null);
|
||||
Discourse.ScreenTrack.instance().stop();
|
||||
|
||||
var headerController;
|
||||
if (headerController = this.controllerFor('header')) {
|
||||
headerController.set('topic', null);
|
||||
headerController.set('showExtraInfo', false);
|
||||
}
|
||||
|
||||
// Clear any filters when we leave the route
|
||||
Discourse.URL.set('queryParams', null);
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
controller.set('model', model);
|
||||
|
||||
this.controllerFor('header').setProperties({
|
||||
topic: model,
|
||||
showExtraInfo: false
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<a {{bindAttr class=":star topic.starred:starred"}} {{action toggleStar}} href='#' {{bindAttr title="topic.favoriteTooltip"}}></a>
|
||||
{{/if}}
|
||||
<h1>
|
||||
{{#if topic.fancy_title}}
|
||||
{{#if topic.details.loaded}}
|
||||
{{topicStatus topic=topic}}
|
||||
<a class='topic-link' href='{{unbound topic.url}}'>{{{topic.fancy_title}}}</a>
|
||||
{{else}}
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
|
||||
<td class='num likes'>
|
||||
{{#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}}
|
||||
</td>
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
|
||||
<div class='span5 gutter'>
|
||||
{{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>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#with view.content}}
|
||||
{{#group}}
|
||||
<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}}
|
||||
<a href="{{unbound lastReadUrl}}" class='badge unread badge-notification' title='{{i18n topic.unread_posts count="unread"}}'>{{unbound unread}}</a>
|
||||
{{/if}}
|
||||
|
@ -19,7 +19,7 @@
|
|||
|
||||
<td class='num'>
|
||||
{{#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}}
|
||||
</td>
|
||||
|
||||
|
|
|
@ -1,140 +1,133 @@
|
|||
{{#if content}}
|
||||
{{#if loaded}}
|
||||
{{#if postStream.loaded}}
|
||||
|
||||
{{#if view.firstPostLoaded}}
|
||||
{{#if postStream.firstPostLoaded}}
|
||||
<div id='topic-title'>
|
||||
<div class='container'>
|
||||
<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 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-small' {{action cancelEdit target="view"}}><i class='icon-remove'></i></button>
|
||||
<button class='btn btn-primary btn-small' {{action finishedEditingTopic}}><i class='icon-ok'></i></button>
|
||||
<button class='btn btn-small' {{action cancelEditingTopic}}><i class='icon-remove'></i></button>
|
||||
{{else}}
|
||||
<h1>
|
||||
{{#if view.topic.fancy_title}}
|
||||
{{topicStatus topic=view.topic}}
|
||||
<a href='{{unbound view.topic.url}}'>{{{view.topic.fancy_title}}}</a>
|
||||
{{else}}
|
||||
{{#if view.topic.errorLoading}}
|
||||
{{view.topic.errorTitle}}
|
||||
{{else}}
|
||||
{{i18n topic.loading}}
|
||||
{{/if}}
|
||||
{{#if details.loaded}}
|
||||
{{topicStatus topic=model}}
|
||||
<a href='{{unbound url}}'>{{{fancy_title}}}</a>
|
||||
{{/if}}
|
||||
{{categoryLink category}}
|
||||
|
||||
{{#if view.topic.can_edit}}
|
||||
<a href='#' {{action editTopic target="view"}} class='edit-topic' title='{{i18n edit}}'><i class="icon-pencil"></i></a>
|
||||
{{#if details.can_edit}}
|
||||
<a href='#' {{action editTopic}} class='edit-topic' title='{{i18n edit}}'><i class="icon-pencil"></i></a>
|
||||
{{/if}}
|
||||
</h1>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div class="container posts">
|
||||
<div class="container posts">
|
||||
|
||||
{{view Discourse.SelectedPostsView}}
|
||||
<div class="row">
|
||||
<section class="topic-area" id='topic' data-topic-id='{{unbound id}}'>
|
||||
<div class='posts-wrapper'>
|
||||
<div id='topic-progress-wrapper'>
|
||||
<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>
|
||||
<div class='nums'>
|
||||
<h4 title="{{i18n topic.progress.current}}">{{view.progressPosition}}</h4> <span>{{i18n of_value}}</span> <h4>{{filtered_posts_count}}</h4>
|
||||
</div>
|
||||
<button id='jump-bottom' title="{{i18n topic.progress.jump_bottom}}" {{action jumpBottom}}><i class="icon-circle-arrow-down"></i></button>
|
||||
<div class='bg'> </div>
|
||||
</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}}
|
||||
{{view Discourse.SelectedPostsView}}
|
||||
<div class="row">
|
||||
<section class="topic-area" id='topic' data-topic-id='{{unbound id}}'>
|
||||
<div class='posts-wrapper'>
|
||||
<div id='topic-progress-wrapper'>
|
||||
<nav id='topic-progress' title="{{i18n topic.progress.title}}" {{bindAttr class="hideProgress:hidden"}}>
|
||||
<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'>
|
||||
<h4 title="{{i18n topic.progress.current}}">{{progressPosition}}</h4> <span>{{i18n of_value}}</span> <h4>{{postStream.filteredPostsCount}}</h4>
|
||||
</div>
|
||||
<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>
|
||||
</nav>
|
||||
</div>
|
||||
<div id='topic-bottom'></div>
|
||||
|
||||
{{#if loading}}
|
||||
{{#unless loadingBelow}}
|
||||
<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 postStream.loadingAbove}}
|
||||
<div class='spinner'>{{i18n loading}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless postStream.loadingFilter}}
|
||||
{{collection itemViewClass="Discourse.PostView" contentBinding="postStream.posts" topicViewBinding="view"}}
|
||||
{{/unless}}
|
||||
|
||||
</section>
|
||||
</div>
|
||||
{{#if postStream.loadingBelow}}
|
||||
<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>
|
||||
|
||||
{{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}}
|
||||
{{#if message}}
|
||||
<div class='container'>
|
||||
<div class='message'>
|
||||
|
||||
<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}}
|
||||
<div class='container'>
|
||||
<div class='spinner'>{{i18n loading}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div id='topic-filter' style='display: none'>
|
||||
{{filterDesc}}
|
||||
<a href='#' {{action cancelFilter}}>{{i18n topic.filters.cancel}}</a>
|
||||
|
||||
<div id='topic-filter' {{bindAttr class="postStream.hasNoFilters:hidden"}}>
|
||||
{{postStream.filterDesc}}
|
||||
<a href='#' {{action cancelFilter target="postStream"}}>{{i18n topic.filters.cancel}}</a>
|
||||
</div>
|
||||
|
||||
{{render share}}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<button {{action toggleMultiSelect}} class='btn btn-admin'><i class='icon-tasks'></i> {{i18n topic.actions.multi_select}}</button>
|
||||
</li>
|
||||
|
||||
{{#if can_delete}}
|
||||
{{#if details.can_delete}}
|
||||
<li>
|
||||
<button {{action deleteTopic}} class='btn btn-admin btn-danger'><i class='icon-trash'></i> {{i18n topic.actions.delete}}</button>
|
||||
</li>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<h3><i class='icon icon-bullhorn'></i> {{i18n best_of.title}}</h3>
|
||||
{{#if bestOf}}
|
||||
{{#if postStream.bestOf}}
|
||||
<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}}
|
||||
<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}}
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
<li>
|
||||
<a {{bindAttr href="url"}}>
|
||||
<h4>{{i18n created}}</h4>
|
||||
{{avatar created_by imageSize="tiny"}}
|
||||
{{avatar details.created_by imageSize="tiny"}}
|
||||
{{date created_at}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a {{bindAttr href="lastPostUrl"}}>
|
||||
<h4>{{i18n last_post}}</h4>
|
||||
{{avatar last_poster imageSize="tiny"}}
|
||||
{{avatar details.last_poster imageSize="tiny"}}
|
||||
{{date last_posted_at}}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -38,11 +38,11 @@
|
|||
</li>
|
||||
<li>
|
||||
<h4>{{i18n links}}</h4>
|
||||
{{number links.length}}
|
||||
{{number details.links.length}}
|
||||
</li>
|
||||
{{#if fewParticipants}}
|
||||
{{#if details.fewParticipants}}
|
||||
<li class='avatars'>
|
||||
{{#each fewParticipants}}{{participant participant=this}}{{/each}}
|
||||
{{#each details.fewParticipants}}{{participant participant=this}}{{/each}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
|
@ -53,12 +53,12 @@
|
|||
<ul class="clearfix">
|
||||
<li>
|
||||
<h4>{{i18n created}}</h4>
|
||||
{{avatar created_by imageSize="tiny"}}
|
||||
{{avatar details.created_by imageSize="tiny"}}
|
||||
<a {{bindAttr href="url"}}>{{date created_at}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -72,9 +72,9 @@
|
|||
</ul>
|
||||
</section>
|
||||
|
||||
{{#if participants}}
|
||||
{{#if details.participants}}
|
||||
<section class='avatars clearfix'>
|
||||
{{#each participants}}{{participant participant=this}}{{/each}}
|
||||
{{#each details.participants}}{{participant participant=this}}{{/each}}
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
|
@ -92,7 +92,7 @@
|
|||
|
||||
{{#if view.parentView.showAllLinksControls}}
|
||||
<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>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<h3><i class='icon icon-envelope-alt'></i> {{i18n private_message_info.title}}</h3>
|
||||
<div class='participants clearfix'>
|
||||
{{#each allowed_groups}}
|
||||
{{#each details.allowed_groups}}
|
||||
<div class='user group'>
|
||||
#{{unbound name}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#each allowed_users}}
|
||||
{{#each details.allowed_users}}
|
||||
<div class='user'>
|
||||
<a href='/users/{{lower username}}'>
|
||||
{{avatar this imageSize="small"}}
|
||||
|
@ -13,13 +13,13 @@
|
|||
<a href='/users/{{lower username}}'>
|
||||
{{unbound username}}
|
||||
</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>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if can_invite_to}}
|
||||
{{#if details.can_invite_to}}
|
||||
<div class='controls'>
|
||||
<button class='btn' {{action showPrivateInvite}}>{{i18n private_message_info.invite}}</button>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
**/
|
||||
Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
|
||||
title: Em.String.i18n('topic.notifications.title'),
|
||||
longDescriptionBinding: 'topic.notificationReasonText',
|
||||
longDescriptionBinding: 'topic.details.notificationReasonText',
|
||||
|
||||
dropDownContent: [
|
||||
[Discourse.Topic.NotificationLevel.WATCHING, 'topic.notifications.watching'],
|
||||
|
@ -19,7 +19,7 @@ Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
|
|||
|
||||
text: function() {
|
||||
var key = (function() {
|
||||
switch (this.get('topic.notification_level')) {
|
||||
switch (this.get('topic.details.notification_level')) {
|
||||
case Discourse.Topic.NotificationLevel.WATCHING: return 'watching';
|
||||
case Discourse.Topic.NotificationLevel.TRACKING: return 'tracking';
|
||||
case Discourse.Topic.NotificationLevel.REGULAR: return 'regular';
|
||||
|
@ -36,10 +36,10 @@ Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
|
|||
}
|
||||
})();
|
||||
return icon + (Ember.String.i18n("topic.notifications." + key + ".title")) + "<span class='caret'></span>";
|
||||
}.property('topic.notification_level'),
|
||||
}.property('topic.details.notification_level'),
|
||||
|
||||
clicked: function(id) {
|
||||
return this.get('topic').updateNotifications(id);
|
||||
return this.get('topic.details').updateNotifications(id);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ Discourse.ReplyButton = Discourse.ButtonView.extend({
|
|||
classNames: ['btn', 'btn-primary', 'create'],
|
||||
attributeBindings: ['disabled'],
|
||||
helpKey: 'topic.reply.help',
|
||||
disabled: Em.computed.not('controller.content.can_create_post'),
|
||||
disabled: Em.computed.not('controller.model.details.can_create_post'),
|
||||
|
||||
text: function() {
|
||||
var archetypeCapitalized = this.get('controller.content.archetype').capitalize();
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
Discourse.ParticipantView = Discourse.View.extend({
|
||||
templateName: 'participant',
|
||||
|
||||
toggled: (function() {
|
||||
return this.get('controller.userFilters').contains(this.get('participant.username'));
|
||||
}).property('controller.userFilters.[]')
|
||||
toggled: function() {
|
||||
return this.get('controller.postStream.userFilters').contains(this.get('participant.username'));
|
||||
}.property('controller.postStream.userFilters.[]')
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ Discourse.PostMenuView = Discourse.View.extend({
|
|||
|
||||
// Delete button
|
||||
renderDelete: function(post, buffer) {
|
||||
if (post.get('post_number') === 1 && this.get('controller.content.can_delete')) {
|
||||
if (post.get('post_number') === 1 && this.get('controller.model.details.can_delete')) {
|
||||
buffer.push("<button title=\"" +
|
||||
(Em.String.i18n("topic.actions.delete")) +
|
||||
"\" data-action=\"deleteTopic\" class='delete'><i class=\"icon-trash\"></i></button>");
|
||||
|
@ -138,7 +138,7 @@ Discourse.PostMenuView = Discourse.View.extend({
|
|||
|
||||
// Reply button
|
||||
renderReply: function(post, buffer) {
|
||||
if (!this.get('controller.content.can_create_post')) return;
|
||||
if (!this.get('controller.model.details.can_create_post')) return;
|
||||
buffer.push("<button title=\"" +
|
||||
(Em.String.i18n("post.controls.reply")) +
|
||||
"\" class='create' data-action=\"reply\"><i class='icon-reply'></i>" +
|
||||
|
|
|
@ -98,9 +98,9 @@ Discourse.PostView = Discourse.View.extend({
|
|||
updateQuoteElements: function($aside, desc) {
|
||||
var navLink = "";
|
||||
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
|
||||
var topicId, topic;
|
||||
|
@ -209,21 +209,6 @@ Discourse.PostView = Discourse.View.extend({
|
|||
didInsertElement: function() {
|
||||
var $post = this.$();
|
||||
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();
|
||||
|
||||
// Track this post
|
||||
|
@ -233,21 +218,9 @@ Discourse.PostView = Discourse.View.extend({
|
|||
Discourse.SyntaxHighlighting.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
|
||||
this.insertQuoteControls();
|
||||
|
||||
$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() {
|
||||
this.rerender();
|
||||
}.observes('topic.auto_close_at'),
|
||||
}.observes('topic.details.auto_close_at'),
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ Discourse.TopicFooterButtonsView = Discourse.ContainerView.extend({
|
|||
if (!topic.get('isPrivateMessage')) {
|
||||
|
||||
// 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.FavoriteButton);
|
||||
|
|
|
@ -7,25 +7,24 @@
|
|||
@module Discourse
|
||||
**/
|
||||
Discourse.TopicSummaryView = Discourse.ContainerView.extend({
|
||||
topicBinding: 'controller.content',
|
||||
classNameBindings: ['hidden', ':topic-summary'],
|
||||
LINKS_SHOWN: 5,
|
||||
allLinksShown: false,
|
||||
|
||||
topic: Em.computed.alias('controller.model'),
|
||||
|
||||
showAllLinksControls: function() {
|
||||
if (this.blank('topic.links')) 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;
|
||||
}.property('allLinksShown', 'topic.links'),
|
||||
}.property('allLinksShown', 'topic.details.links'),
|
||||
|
||||
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;
|
||||
return allLinks.slice(0, this.LINKS_SHOWN);
|
||||
}.property('topic.links', 'allLinksShown'),
|
||||
return allLinks.slice(0, Discourse.TopicSummaryView.LINKS_SHOWN);
|
||||
}.property('topic.details.links', 'allLinksShown'),
|
||||
|
||||
newPostCreated: function() {
|
||||
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',
|
||||
userFiltersBinding: 'controller.userFilters',
|
||||
classNameBindings: ['controller.multiSelect:multi-select', 'topic.archetype', 'topic.category.secure:secure_category'],
|
||||
progressPosition: 1,
|
||||
menuVisible: true,
|
||||
SHORT_POST: 1200,
|
||||
|
||||
postStream: Em.computed.alias('controller.postStream'),
|
||||
|
||||
// Update the progress bar using sweet animations
|
||||
updateBar: function() {
|
||||
var $topicProgress, bg, currentWidth, progressWidth, ratio, totalWidth;
|
||||
if (!this.get('topic.loaded')) return;
|
||||
$topicProgress = $('#topic-progress');
|
||||
if (!this.get('postStream.loaded')) return;
|
||||
|
||||
var $topicProgress = $('#topic-progress');
|
||||
if (!$topicProgress.length) return;
|
||||
|
||||
ratio = this.get('progressPosition') / this.get('topic.filtered_posts_count');
|
||||
totalWidth = $topicProgress.width();
|
||||
progressWidth = ratio * totalWidth;
|
||||
bg = $topicProgress.find('.bg');
|
||||
var ratio = this.get('controller.progressPosition') / this.get('postStream.filteredPostsCount');
|
||||
var totalWidth = $topicProgress.width();
|
||||
var progressWidth = ratio * totalWidth;
|
||||
var bg = $topicProgress.find('.bg');
|
||||
bg.stop(true, true);
|
||||
currentWidth = bg.width();
|
||||
var currentWidth = bg.width();
|
||||
|
||||
if (currentWidth === totalWidth) {
|
||||
bg.width(currentWidth - 1);
|
||||
|
@ -40,9 +41,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
bg.css("border-right-width", "1px");
|
||||
}
|
||||
|
||||
// Disable animation for now so it performs better
|
||||
bg.width(progressWidth);
|
||||
}.observes('progressPosition', 'topic.filtered_posts_count', 'topic.loaded'),
|
||||
}.observes('controller.progressPosition', 'postStream.filteredPostsCount', 'topic.loaded'),
|
||||
|
||||
updateTitle: function() {
|
||||
var title = this.get('topic.title');
|
||||
|
@ -60,28 +60,9 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
}
|
||||
|
||||
var postUrl = topic.get('url');
|
||||
if (current > 1) {
|
||||
postUrl += "/" + current;
|
||||
} else {
|
||||
if (this.get('controller.bestOf')) {
|
||||
postUrl += "/best_of";
|
||||
}
|
||||
}
|
||||
if (current > 1) { postUrl += "/" + current; }
|
||||
Discourse.URL.replaceState(postUrl);
|
||||
|
||||
// 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'),
|
||||
}.observes('controller.currentPost', 'highest_post_number'),
|
||||
|
||||
composeChanged: function() {
|
||||
var composerController = Discourse.get('router.composerController');
|
||||
|
@ -98,8 +79,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
// Unbind link tracking
|
||||
this.$().off('mouseup.discourse-redirect', '.cooked a, a.track-link');
|
||||
|
||||
this.get('controller').set('onPostRendered', null);
|
||||
|
||||
this.resetExamineDockCache();
|
||||
|
||||
// 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});
|
||||
|
||||
var topicView = this;
|
||||
$(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(false); });
|
||||
|
||||
var controller = this.get('controller');
|
||||
controller.set('onPostRendered', function(){
|
||||
topicView.postsRendered.apply(topicView);
|
||||
});
|
||||
$(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(); });
|
||||
|
||||
this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) {
|
||||
return Discourse.ClickTrack.trackClick(e);
|
||||
});
|
||||
|
||||
this.updatePosition(true);
|
||||
this.updatePosition();
|
||||
},
|
||||
|
||||
debounceLoadSuggested: Discourse.debounce(function(){
|
||||
if (this.get('isDestroyed') || this.get('isDestroying')) { return; }
|
||||
|
||||
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');
|
||||
|
||||
if(suggested) {
|
||||
|
@ -155,11 +129,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
this.debounceLoadSuggested();
|
||||
}.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) {
|
||||
Discourse.ScreenTrack.instance().reset();
|
||||
this.get('controller').unsubscribe();
|
||||
|
@ -192,8 +161,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
|
||||
if (post) {
|
||||
var postNumber = post.get('post_number');
|
||||
if (postNumber > (this.get('topic.last_read_post_number') || 0)) {
|
||||
this.set('topic.last_read_post_number', postNumber);
|
||||
if (postNumber > (this.get('last_read_post_number') || 0)) {
|
||||
this.set('last_read_post_number', postNumber);
|
||||
}
|
||||
if (!post.get('read')) {
|
||||
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() {
|
||||
this.docAt = null;
|
||||
this.dockedTitle = false;
|
||||
|
@ -380,22 +181,23 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
if (!postView) return;
|
||||
var post = postView.get('post');
|
||||
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();
|
||||
var model = this.get('controller.model');
|
||||
if (model) {
|
||||
this.set('controller.currentPost', opts.currentPost);
|
||||
if (model && this.get('nextPositionUpdate')) {
|
||||
this.set('controller.currentPost', this.get('nextPositionUpdate'));
|
||||
}
|
||||
},500),
|
||||
|
||||
scrolled: function(){
|
||||
this.updatePosition(true);
|
||||
this.updatePosition();
|
||||
},
|
||||
|
||||
updatePosition: function(userActive) {
|
||||
updatePosition: function() {
|
||||
var topic = this.get('controller.model');
|
||||
|
||||
var rows = $('.topic-post.ready');
|
||||
if (!rows || rows.length === 0) { return; }
|
||||
|
@ -404,16 +206,23 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
var info = Discourse.Eyeline.analyze(rows);
|
||||
if(!info) { return; }
|
||||
|
||||
// top on screen
|
||||
// are we scrolling upwards?
|
||||
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;
|
||||
if(info.bottom === rows.length-1) {
|
||||
currentPost = this.postSeen($(rows[info.bottom]));
|
||||
this.nextPage($(rows[info.bottom]));
|
||||
this.get('postStream').appendMore();
|
||||
}
|
||||
|
||||
// update dock
|
||||
|
@ -433,16 +242,14 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
}
|
||||
|
||||
if (currentForPositionUpdate) {
|
||||
this.nonUrgentPositionUpdate({
|
||||
userActive: userActive,
|
||||
currentPost: currentPost || currentForPositionUpdate
|
||||
});
|
||||
this.set('nextPositionUpdate', currentPost || currentForPositionUpdate);
|
||||
this.throttledPositionUpdate();
|
||||
} else {
|
||||
console.error("can't update position ");
|
||||
}
|
||||
|
||||
var offset = window.pageYOffset || $('html').scrollTop();
|
||||
var firstLoaded = this.get('firstPostLoaded');
|
||||
var firstLoaded = topic.get('postStream.firstPostLoaded');
|
||||
if (!this.docAt) {
|
||||
var title = $('#topic-title');
|
||||
if (title && title.length === 1) {
|
||||
|
@ -475,18 +282,17 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
}
|
||||
},
|
||||
|
||||
topicTrackingState: function(){
|
||||
topicTrackingState: function() {
|
||||
return Discourse.TopicTrackingState.current();
|
||||
}.property(),
|
||||
|
||||
browseMoreMessage: function() {
|
||||
var category, opts;
|
||||
|
||||
opts = {
|
||||
var opts = {
|
||||
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) {
|
||||
opts.catLink = Discourse.Utilities.categoryLink(category);
|
||||
} else {
|
||||
|
@ -522,27 +328,32 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
Discourse.TopicView.reopenClass({
|
||||
|
||||
// Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not.
|
||||
scrollTo: function(topicId, postNumber, callback) {
|
||||
// Make sure we're looking at the topic we want to scroll to
|
||||
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();
|
||||
jumpToPost: function(topicId, postNumber) {
|
||||
Em.run.scheduleOnce('afterRender', function() {
|
||||
|
||||
if (expectedOffset < 0) {
|
||||
expectedOffset = 0;
|
||||
// Make sure we're looking at the topic we want to scroll to
|
||||
if (topicId !== parseInt($('#topic').data('topic-id'), 10)) { return false; }
|
||||
|
||||
var $post = $("#post_" + postNumber);
|
||||
if ($post.length) {
|
||||
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]}" }
|
||||
|
||||
def show
|
||||
|
||||
# We'd like to migrate the wordpress feed to another url. This keeps up backwards compatibility with
|
||||
# existing installs.
|
||||
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
|
||||
@topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts)
|
||||
rescue Discourse::NotFound
|
||||
|
@ -67,7 +66,15 @@ class TopicsController < ApplicationController
|
|||
wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false)
|
||||
render_json_dump(wordpress_serializer)
|
||||
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
|
||||
|
||||
def destroy_timings
|
||||
|
|
|
@ -74,7 +74,7 @@ class SiteSetting < ActiveRecord::Base
|
|||
setting(:create_thumbnails, false)
|
||||
client_setting(:category_featured_topics, 6)
|
||||
setting(:topics_per_page, 30)
|
||||
setting(:posts_per_page, 20)
|
||||
client_setting(:posts_per_page, 20)
|
||||
setting(:invite_expiry_days, 14)
|
||||
setting(:active_user_rate_limit_secs, 60)
|
||||
setting(:previous_visit_timeout_hours, 1)
|
||||
|
|
32
app/serializers/post_stream_serializer_mixin.rb
Normal file
32
app/serializers/post_stream_serializer_mixin.rb
Normal file
|
@ -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
|
10
app/serializers/topic_view_posts_serializer.rb
Normal file
10
app/serializers/topic_view_posts_serializer.rb
Normal file
|
@ -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'
|
||||
|
||||
class TopicViewSerializer < ApplicationSerializer
|
||||
include PostStreamSerializerMixin
|
||||
|
||||
# These attributes will be delegated to the topic
|
||||
def self.topic_attributes
|
||||
|
@ -15,76 +16,88 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
:visible,
|
||||
:closed,
|
||||
:archived,
|
||||
:moderator_posts_count,
|
||||
:has_best_of,
|
||||
:archetype,
|
||||
:slug,
|
||||
:auto_close_at]
|
||||
:category_id]
|
||||
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,
|
||||
:draft_key,
|
||||
:draft_sequence,
|
||||
:post_action_visibility,
|
||||
:voted_in_topic,
|
||||
:can_create_post,
|
||||
:can_reply_as_new_topic,
|
||||
:categoryName,
|
||||
:starred,
|
||||
:last_read_post_number,
|
||||
:posted,
|
||||
:notification_level,
|
||||
:notifications_reason_id,
|
||||
:posts,
|
||||
:at_bottom,
|
||||
:highest_post_number,
|
||||
:pinned,
|
||||
:filtered_posts_count
|
||||
|
||||
has_one :created_by, serializer: BasicUserSerializer, embed: :objects
|
||||
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
|
||||
:details,
|
||||
:highest_post_number,
|
||||
:last_read_post_number
|
||||
|
||||
# Define a delegator for each attribute of the topic we want
|
||||
attributes *topic_attributes
|
||||
topic_attributes.each do |ta|
|
||||
class_eval %{def #{ta}
|
||||
object.topic.#{ta}
|
||||
end}
|
||||
end
|
||||
|
||||
# Define the guardian attributes
|
||||
guardian_attributes.each do |ga|
|
||||
class_eval %{
|
||||
def #{ga}
|
||||
true
|
||||
end
|
||||
|
||||
def include_#{ga}?
|
||||
scope.#{ga}?(object.topic)
|
||||
end
|
||||
# TODO: Split off into proper object / serializer
|
||||
def details
|
||||
result = {
|
||||
auto_close_at: object.topic.auto_close_at,
|
||||
created_by: BasicUserSerializer.new(object.topic.user, scope: scope, root: false),
|
||||
last_poster: BasicUserSerializer.new(object.topic.last_poster, scope: scope, root: false)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
def draft
|
||||
object.draft
|
||||
end
|
||||
|
||||
def include_allowed_users?
|
||||
object.topic.private_message?
|
||||
end
|
||||
|
||||
def draft_key
|
||||
object.draft_key
|
||||
end
|
||||
|
@ -93,46 +106,6 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
object.draft_sequence
|
||||
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
|
||||
def has_topic_user?
|
||||
object.topic_user.present?
|
||||
|
@ -143,6 +116,10 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
end
|
||||
alias_method :include_starred?, :has_topic_user?
|
||||
|
||||
def highest_post_number
|
||||
object.highest_post_number
|
||||
end
|
||||
|
||||
def last_read_post_number
|
||||
object.topic_user.last_read_post_number
|
||||
end
|
||||
|
@ -153,90 +130,9 @@ class TopicViewSerializer < ApplicationSerializer
|
|||
end
|
||||
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
|
||||
PinnedCheck.new(object.topic, object.topic_user).pinned?
|
||||
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
|
||||
|
|
|
@ -18,7 +18,7 @@ class TopicViewWordpressSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
def filtered_posts_count
|
||||
object.filtered_posts_count
|
||||
object.filtered_post_ids.size
|
||||
end
|
||||
|
||||
def participants
|
||||
|
|
|
@ -92,6 +92,7 @@ predef:
|
|||
- find
|
||||
- resolvingPromise
|
||||
- sinon
|
||||
- controllerFor
|
||||
|
||||
browser: true # true if the standard browser 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' => 'topics#show', constraints: {topic_id: /\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/invite' => 'topics#invite', 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
|
||||
|
||||
attr_reader :topic, :posts, :index_offset, :index_reverse, :guardian
|
||||
attr_reader :topic, :posts, :guardian, :filtered_posts
|
||||
attr_accessor :draft, :draft_key, :draft_sequence
|
||||
|
||||
def initialize(topic_id, user=nil, options={})
|
||||
|
@ -20,13 +20,14 @@ class TopicView
|
|||
end
|
||||
|
||||
guardian.ensure_can_see!(@topic)
|
||||
|
||||
@post_number, @page = options[:post_number], options[:page]
|
||||
|
||||
@limit = options[:limit] || SiteSetting.posts_per_page;
|
||||
|
||||
@filtered_posts = @topic.posts
|
||||
@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?
|
||||
|
||||
if options[:username_filters].present?
|
||||
|
@ -78,10 +79,6 @@ class TopicView
|
|||
@topic.title
|
||||
end
|
||||
|
||||
def filtered_posts_count
|
||||
@filtered_posts_count ||= @filtered_posts.count
|
||||
end
|
||||
|
||||
def summary
|
||||
return nil if posts.blank?
|
||||
Summarize.new(posts.first.cooked).summary
|
||||
|
@ -94,11 +91,8 @@ class TopicView
|
|||
|
||||
def filter_posts(opts = {})
|
||||
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_after(opts[:posts_after].to_i) if opts[:posts_after].present?
|
||||
if opts[:best].present?
|
||||
return filter_best(opts[:best], opts)
|
||||
end
|
||||
return filter_posts_by_ids(opts[:post_ids]) if opts[:post_ids].present?
|
||||
return filter_best(opts[:best], opts) if opts[:best].present?
|
||||
|
||||
filter_posts_paged(opts[:page].to_i)
|
||||
end
|
||||
|
@ -152,36 +146,8 @@ class TopicView
|
|||
filter_posts_in_range(min, max)
|
||||
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={})
|
||||
@index_offset = 0
|
||||
|
||||
if opts[:min_replies] && @topic.posts_count < opts[:min_replies] + 1
|
||||
@posts = []
|
||||
return
|
||||
|
@ -189,8 +155,10 @@ class TopicView
|
|||
|
||||
@posts = @filtered_posts.order('percent_rank asc, sort_order asc')
|
||||
.where("post_number > 1")
|
||||
|
||||
@posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(max)
|
||||
|
||||
|
||||
min_trust_level = opts[:min_trust_level]
|
||||
if min_trust_level && min_trust_level > 0
|
||||
@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)
|
||||
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
|
||||
@links ||= TopicLink.topic_summary(guardian, @topic.id)
|
||||
end
|
||||
|
@ -315,6 +262,16 @@ class TopicView
|
|||
|
||||
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)
|
||||
post_count = (filtered_post_ids.length - 1)
|
||||
|
||||
|
@ -324,15 +281,7 @@ class TopicView
|
|||
|
||||
min = [[min, max].min, 0].max
|
||||
|
||||
@index_offset = min
|
||||
|
||||
# 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 = filter_posts_by_ids(filtered_post_ids[min..max])
|
||||
@posts
|
||||
end
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ describe TopicView do
|
|||
# should not get the status post
|
||||
best = TopicView.new(topic.id, nil, best: 99)
|
||||
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]
|
||||
|
||||
# should get no results for trust level too low
|
||||
|
@ -145,12 +145,6 @@ describe TopicView do
|
|||
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
|
||||
it 'is unread with no logged in user' do
|
||||
TopicView.new(topic.id).read?(1).should be_false
|
||||
|
@ -216,36 +210,6 @@ describe TopicView do
|
|||
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
|
||||
before { SiteSetting.stubs(:posts_per_page).returns(1) }
|
||||
|
||||
|
@ -257,37 +221,6 @@ describe TopicView do
|
|||
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
|
||||
|
||||
def topic_view_near(post)
|
||||
|
@ -297,30 +230,22 @@ describe TopicView do
|
|||
it "snaps to the lower boundary" do
|
||||
near_view = topic_view_near(p1)
|
||||
near_view.posts.should == [p1, p2, p3]
|
||||
near_view.index_offset.should == 0
|
||||
near_view.index_reverse.should be_false
|
||||
end
|
||||
|
||||
it "snaps to the upper boundary" do
|
||||
near_view = topic_view_near(p5)
|
||||
near_view.posts.should == [p2, p3, p5]
|
||||
near_view.index_offset.should == 1
|
||||
near_view.index_reverse.should be_false
|
||||
end
|
||||
|
||||
it "returns the posts in the middle" do
|
||||
near_view = topic_view_near(p2)
|
||||
near_view.posts.should == [p1, p2, p3]
|
||||
near_view.index_offset.should == 0
|
||||
near_view.index_reverse.should be_false
|
||||
end
|
||||
|
||||
it "returns deleted posts to an admin" do
|
||||
coding_horror.admin = true
|
||||
near_view = topic_view_near(p3)
|
||||
near_view.posts.should == [p2, p3, p4]
|
||||
near_view.index_offset.should == 1
|
||||
near_view.index_reverse.should be_false
|
||||
end
|
||||
|
||||
context "when 'posts per page' exceeds the number of posts" do
|
||||
|
@ -329,8 +254,6 @@ describe TopicView do
|
|||
it 'returns all the posts' do
|
||||
near_view = topic_view_near(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
|
||||
|
|
|
@ -517,17 +517,6 @@ describe TopicsController do
|
|||
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
|
||||
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
|
||||
|
||||
context "when 'login required' site setting has been enabled" do
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
{ "path": "script" },
|
||||
{ "path": "cookbooks" },
|
||||
{ "path": "spec" },
|
||||
{ "path": "test" }
|
||||
{ "path": "test",
|
||||
"folder_exclude_patterns": ["fixtures"]
|
||||
}
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
|
|
35
test/javascripts/controllers/topic_controller_test.js
Normal file
35
test/javascripts/controllers/topic_controller_test.js
Normal file
|
@ -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
|
||||
var resolvingPromise = Ember.Deferred.promise(function (p) {
|
||||
p.resolve();
|
||||
})
|
||||
});
|
||||
|
||||
var resolvingPromiseWith = function(result) {
|
||||
return Ember.Deferred.promise(function (p) { p.resolve(result); });
|
||||
};
|
||||
|
||||
function exists(selector) {
|
||||
return !!count(selector);
|
||||
|
@ -11,22 +15,14 @@ function count(selector) {
|
|||
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) {
|
||||
equal(objBlank(obj), false, text);
|
||||
ok(!Ember.isEmpty(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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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",
|
||||
"_",
|
||||
"console",
|
||||
"alert"],
|
||||
"alert",
|
||||
"controllerFor",
|
||||
"containsInstance",
|
||||
"deepEqual",
|
||||
"resolvingPromiseWith"],
|
||||
"node" : false,
|
||||
"browser" : true,
|
||||
"boss" : true,
|
||||
|
|
324
test/javascripts/models/post_stream_test.js
Normal file
324
test/javascripts/models/post_stream_test.js
Normal file
|
@ -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");
|
||||
});
|
||||
|
32
test/javascripts/models/post_test.js
Normal file
32
test/javascripts/models/post_test.js
Normal file
|
@ -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");
|
||||
});
|
32
test/javascripts/models/topic_details_test.js
Normal file
32
test/javascripts/models/topic_details_test.js
Normal file
|
@ -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);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
39
test/javascripts/models/topic_test.js
Normal file
39
test/javascripts/models/topic_test.js
Normal file
|
@ -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
Block a user