mirror of
https://github.com/discourse/discourse.git
synced 2025-02-15 05:03:01 +08:00
641 lines
19 KiB
JavaScript
641 lines
19 KiB
JavaScript
Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, {
|
|
multiSelect: false,
|
|
needs: ['header', 'modal', 'composer', 'quote-button', 'search', 'topic-progress'],
|
|
allPostsSelected: false,
|
|
editingTopic: false,
|
|
selectedPosts: null,
|
|
selectedReplies: null,
|
|
queryParams: ['filter', 'username_filters', 'show_deleted'],
|
|
|
|
maxTitleLength: Discourse.computed.setting('max_topic_title_length'),
|
|
|
|
contextChanged: function(){
|
|
this.set('controllers.search.searchContext', this.get('model.searchContext'));
|
|
}.observes('topic'),
|
|
|
|
termChanged: function(){
|
|
var dropdown = this.get('controllers.header.visibleDropdown');
|
|
var term = this.get('controllers.search.term');
|
|
|
|
if(dropdown === 'search-dropdown' && term){
|
|
this.set('searchHighlight', term);
|
|
} else {
|
|
if(this.get('searchHighlight')){
|
|
this.set('searchHighlight', null);
|
|
}
|
|
}
|
|
|
|
}.observes('controllers.search.term', 'controllers.header.visibleDropdown'),
|
|
|
|
filter: function(key, value) {
|
|
if (arguments.length > 1) {
|
|
this.set('postStream.summary', value === "summary");
|
|
}
|
|
return this.get('postStream.summary') ? "summary" : null;
|
|
}.property('postStream.summary'),
|
|
|
|
username_filters: Discourse.computed.queryAlias('postStream.streamFilters.username_filters'),
|
|
|
|
init: function() {
|
|
this._super();
|
|
this.set('selectedPosts', new Em.Set());
|
|
this.set('selectedReplies', new Em.Set());
|
|
},
|
|
|
|
actions: {
|
|
// Post related methods
|
|
replyToPost: function(post) {
|
|
var composerController = this.get('controllers.composer'),
|
|
quoteController = this.get('controllers.quote-button'),
|
|
quotedText = Discourse.Quote.build(quoteController.get('post'), quoteController.get('buffer')),
|
|
topic = post ? post.get('topic') : this.get('model');
|
|
|
|
quoteController.set('buffer', '');
|
|
|
|
if (composerController.get('content.topic.id') === topic.get('id') &&
|
|
composerController.get('content.action') === Discourse.Composer.REPLY) {
|
|
composerController.set('content.post', post);
|
|
composerController.set('content.composeState', Discourse.Composer.OPEN);
|
|
composerController.appendText(quotedText);
|
|
} else {
|
|
|
|
var opts = {
|
|
action: Discourse.Composer.REPLY,
|
|
draftKey: topic.get('draft_key'),
|
|
draftSequence: topic.get('draft_sequence')
|
|
};
|
|
|
|
if(post && post.get("post_number") !== 1){
|
|
opts.post = post;
|
|
} else {
|
|
opts.topic = topic;
|
|
}
|
|
|
|
composerController.open(opts).then(function() {
|
|
composerController.appendText(quotedText);
|
|
});
|
|
}
|
|
return false;
|
|
},
|
|
|
|
toggleLike: function(post) {
|
|
var likeAction = post.get('actionByName.like');
|
|
if (likeAction && likeAction.get('canToggle')) {
|
|
likeAction.toggle();
|
|
}
|
|
},
|
|
|
|
recoverPost: function(post) {
|
|
// Recovering the first post recovers the topic instead
|
|
if (post.get('post_number') === 1) {
|
|
this.recoverTopic();
|
|
return;
|
|
}
|
|
post.recover();
|
|
},
|
|
|
|
deletePost: function(post) {
|
|
|
|
// Deleting the first post deletes the topic
|
|
if (post.get('post_number') === 1) {
|
|
this.deleteTopic();
|
|
return;
|
|
}
|
|
|
|
var user = Discourse.User.current(),
|
|
replyCount = post.get('reply_count'),
|
|
self = this;
|
|
|
|
// If the user is staff and the post has replies, ask if they want to delete replies too.
|
|
if (user.get('staff') && replyCount > 0) {
|
|
bootbox.dialog(I18n.t("post.controls.delete_replies.confirm", {count: replyCount}), [
|
|
{label: I18n.t("cancel"),
|
|
'class': 'btn-danger rightg'},
|
|
{label: I18n.t("post.controls.delete_replies.no_value"),
|
|
callback: function() {
|
|
post.destroy(user);
|
|
}
|
|
},
|
|
{label: I18n.t("post.controls.delete_replies.yes_value"),
|
|
'class': 'btn-primary',
|
|
callback: function() {
|
|
Discourse.Post.deleteMany([post], [post]);
|
|
self.get('postStream.posts').forEach(function (p) {
|
|
if (p === post || p.get('reply_to_post_number') === post.get('post_number')) {
|
|
p.setDeletedState(user);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
]);
|
|
} else {
|
|
post.destroy(user).then(null, function(e) {
|
|
post.undoDeleteState();
|
|
var response = $.parseJSON(e.responseText);
|
|
if (response && response.errors) {
|
|
bootbox.alert(response.errors[0]);
|
|
} else {
|
|
bootbox.alert(I18n.t('generic_error'));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
editPost: function(post) {
|
|
this.get('controllers.composer').open({
|
|
post: post,
|
|
action: Discourse.Composer.EDIT,
|
|
draftKey: post.get('topic.draft_key'),
|
|
draftSequence: post.get('topic.draft_sequence')
|
|
});
|
|
},
|
|
|
|
toggleBookmark: function(post) {
|
|
if (!Discourse.User.current()) {
|
|
alert(I18n.t("bookmarks.not_bookmarked"));
|
|
return;
|
|
}
|
|
post.toggleProperty('bookmarked');
|
|
return false;
|
|
},
|
|
|
|
jumpTop: function() {
|
|
this.get('controllers.topic-progress').send('jumpTop');
|
|
},
|
|
|
|
selectAll: function() {
|
|
var posts = this.get('postStream.posts'),
|
|
selectedPosts = this.get('selectedPosts');
|
|
if (posts) {
|
|
selectedPosts.addObjects(posts);
|
|
}
|
|
this.set('allPostsSelected', true);
|
|
},
|
|
|
|
deselectAll: function() {
|
|
this.get('selectedPosts').clear();
|
|
this.get('selectedReplies').clear();
|
|
this.set('allPostsSelected', false);
|
|
},
|
|
|
|
/**
|
|
Toggle a participant for filtering
|
|
|
|
@method toggleParticipant
|
|
**/
|
|
toggleParticipant: function(user) {
|
|
this.get('postStream').toggleParticipant(Em.get(user, 'username'));
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
toggleMultiSelect: function() {
|
|
this.toggleProperty('multiSelect');
|
|
},
|
|
|
|
finishedEditingTopic: function() {
|
|
if (this.get('editingTopic')) {
|
|
|
|
var topic = this.get('model');
|
|
|
|
// Topic title hasn't been sanitized yet, so the template shouldn't trust it.
|
|
this.set('topicSaving', true);
|
|
|
|
// manually update the titles & category
|
|
var backup = topic.setPropertiesBackup({
|
|
title: this.get('newTitle'),
|
|
category_id: parseInt(this.get('newCategoryId'), 10),
|
|
fancy_title: this.get('newTitle')
|
|
});
|
|
|
|
// save the modifications
|
|
var self = this;
|
|
topic.save().then(function(result){
|
|
// update the title if it has been changed (cleaned up) server-side
|
|
topic.setProperties(Em.getProperties(result.basic_topic, 'title', 'fancy_title'));
|
|
self.set('topicSaving', false);
|
|
}, function(error) {
|
|
self.setProperties({ editingTopic: true, topicSaving: false });
|
|
topic.setProperties(backup);
|
|
if (error && error.responseText) {
|
|
bootbox.alert($.parseJSON(error.responseText).errors[0]);
|
|
} else {
|
|
bootbox.alert(I18n.t('generic_error'));
|
|
}
|
|
});
|
|
|
|
// close editing mode
|
|
self.set('editingTopic', false);
|
|
}
|
|
},
|
|
|
|
toggledSelectedPost: function(post) {
|
|
this.performTogglePost(post);
|
|
},
|
|
|
|
toggledSelectedPostReplies: function(post) {
|
|
var selectedReplies = this.get('selectedReplies');
|
|
if (this.performTogglePost(post)) {
|
|
selectedReplies.addObject(post);
|
|
} else {
|
|
selectedReplies.removeObject(post);
|
|
}
|
|
},
|
|
|
|
deleteSelected: function() {
|
|
var self = this;
|
|
bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
|
if (result) {
|
|
|
|
// If all posts are selected, it's the same thing as deleting the topic
|
|
if (self.get('allPostsSelected')) {
|
|
return self.deleteTopic();
|
|
}
|
|
|
|
var selectedPosts = self.get('selectedPosts'),
|
|
selectedReplies = self.get('selectedReplies'),
|
|
postStream = self.get('postStream'),
|
|
toRemove = new Ember.Set();
|
|
|
|
|
|
Discourse.Post.deleteMany(selectedPosts, selectedReplies);
|
|
postStream.get('posts').forEach(function (p) {
|
|
if (self.postSelected(p)) { toRemove.addObject(p); }
|
|
});
|
|
|
|
postStream.removePosts(toRemove);
|
|
self.send('toggleMultiSelect');
|
|
}
|
|
});
|
|
},
|
|
|
|
expandHidden: function(post) {
|
|
post.expandHidden();
|
|
},
|
|
|
|
toggleVisibility: function() {
|
|
this.get('content').toggleStatus('visible');
|
|
},
|
|
|
|
toggleClosed: function() {
|
|
this.get('content').toggleStatus('closed');
|
|
},
|
|
|
|
makeBanner: function() {
|
|
this.get('content').makeBanner();
|
|
},
|
|
|
|
removeBanner: function() {
|
|
this.get('content').removeBanner();
|
|
},
|
|
|
|
togglePinned: function() {
|
|
// Note that this is different than clearPin
|
|
this.get('content').setStatus('pinned', this.get('pinned_at') ? false : true);
|
|
},
|
|
|
|
togglePinnedGlobally: function() {
|
|
// Note that this is different than clearPin
|
|
this.get('content').setStatus('pinned_globally', this.get('pinned_at') ? false : true);
|
|
},
|
|
|
|
toggleArchived: function() {
|
|
this.get('content').toggleStatus('archived');
|
|
},
|
|
|
|
// Toggle the star on the topic
|
|
toggleStar: function() {
|
|
this.get('content').toggleStar();
|
|
},
|
|
|
|
/**
|
|
Clears the pin from a topic for the currently logged in user
|
|
|
|
@method clearPin
|
|
**/
|
|
clearPin: function() {
|
|
this.get('content').clearPin();
|
|
},
|
|
|
|
resetRead: function() {
|
|
Discourse.ScreenTrack.current().reset();
|
|
this.unsubscribe();
|
|
|
|
var topicController = this;
|
|
this.get('model').resetRead().then(function() {
|
|
topicController.set('message', I18n.t("topic.read_position_reset"));
|
|
topicController.set('postStream.loaded', false);
|
|
});
|
|
},
|
|
|
|
replyAsNewTopic: function(post) {
|
|
var composerController = this.get('controllers.composer'),
|
|
quoteController = this.get('controllers.quote-button'),
|
|
quotedText = Discourse.Quote.build(quoteController.get('post'), quoteController.get('buffer')),
|
|
self = this;
|
|
|
|
quoteController.deselectText();
|
|
|
|
composerController.open({
|
|
action: Discourse.Composer.CREATE_TOPIC,
|
|
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
|
|
}).then(function() {
|
|
return Em.isEmpty(quotedText) ? Discourse.Post.loadQuote(post.get('id')) : quotedText;
|
|
}).then(function(q) {
|
|
var postUrl = "" + location.protocol + "//" + location.host + (post.get('url')),
|
|
postLink = "[" + self.get('title') + "](" + postUrl + ")";
|
|
composerController.appendText(I18n.t("post.continue_discussion", { postLink: postLink }) + "\n\n" + q);
|
|
});
|
|
},
|
|
|
|
expandFirstPost: function(post) {
|
|
var self = this;
|
|
this.set('loadingExpanded', true);
|
|
post.expand().then(function() {
|
|
self.set('firstPostExpanded', true);
|
|
}).catch(function(error) {
|
|
bootbox.alert($.parseJSON(error.responseText).errors);
|
|
}).finally(function() {
|
|
self.set('loadingExpanded', false);
|
|
});
|
|
},
|
|
|
|
retryLoading: function() {
|
|
var self = this;
|
|
self.set('retrying', true);
|
|
this.get('postStream').refresh().then(function() {
|
|
self.set('retrying', false);
|
|
}, function() {
|
|
self.set('retrying', false);
|
|
});
|
|
},
|
|
|
|
toggleWiki: function(post) {
|
|
post.toggleProperty('wiki');
|
|
}
|
|
},
|
|
|
|
showExpandButton: function() {
|
|
var post = this.get('post');
|
|
return post.get('post_number') === 1 && post.get('topic.expandable_first_post');
|
|
}.property(),
|
|
|
|
canMergeTopic: function() {
|
|
if (!this.get('details.can_move_posts')) return false;
|
|
return (this.get('selectedPostsCount') > 0);
|
|
}.property('selectedPostsCount'),
|
|
|
|
canSplitTopic: function() {
|
|
if (!this.get('details.can_move_posts')) return false;
|
|
if (this.get('allPostsSelected')) return false;
|
|
return (this.get('selectedPostsCount') > 0);
|
|
}.property('selectedPostsCount'),
|
|
|
|
canChangeOwner: function() {
|
|
if (!Discourse.User.current() || !Discourse.User.current().admin) return false;
|
|
return !!this.get('selectedPostsUsername');
|
|
}.property('selectedPostsUsername'),
|
|
|
|
categories: function() {
|
|
return Discourse.Category.list();
|
|
}.property(),
|
|
|
|
canSelectAll: Em.computed.not('allPostsSelected'),
|
|
|
|
canDeselectAll: function () {
|
|
if (this.get('selectedPostsCount') > 0) return true;
|
|
if (this.get('allPostsSelected')) return true;
|
|
}.property('selectedPostsCount', 'allPostsSelected'),
|
|
|
|
canDeleteSelected: function() {
|
|
var selectedPosts = this.get('selectedPosts');
|
|
|
|
if (this.get('allPostsSelected')) return true;
|
|
if (this.get('selectedPostsCount') === 0) return false;
|
|
|
|
var canDelete = true;
|
|
selectedPosts.forEach(function(p) {
|
|
if (!p.get('can_delete')) {
|
|
canDelete = false;
|
|
return false;
|
|
}
|
|
});
|
|
return canDelete;
|
|
}.property('selectedPostsCount'),
|
|
|
|
hasError: Ember.computed.or('errorBodyHtml', 'message'),
|
|
|
|
multiSelectChanged: function() {
|
|
// Deselect all posts when multi select is turned off
|
|
if (!this.get('multiSelect')) {
|
|
this.send('deselectAll');
|
|
}
|
|
}.observes('multiSelect'),
|
|
|
|
deselectPost: function(post) {
|
|
this.get('selectedPosts').removeObject(post);
|
|
|
|
var selectedReplies = this.get('selectedReplies');
|
|
selectedReplies.removeObject(post);
|
|
|
|
var selectedReply = selectedReplies.findProperty('post_number', post.get('reply_to_post_number'));
|
|
if (selectedReply) { selectedReplies.removeObject(selectedReply); }
|
|
|
|
this.set('allPostsSelected', false);
|
|
},
|
|
|
|
postSelected: function(post) {
|
|
if (this.get('allPostsSelected')) { return true; }
|
|
if (this.get('selectedPosts').contains(post)) { return true; }
|
|
if (this.get('selectedReplies').findProperty('post_number', post.get('reply_to_post_number'))) { return true; }
|
|
|
|
return false;
|
|
},
|
|
|
|
showStarButton: function() {
|
|
return Discourse.User.current() && !this.get('isPrivateMessage');
|
|
}.property('isPrivateMessage'),
|
|
|
|
loadingHTML: function() {
|
|
return "<div class='spinner'>" + I18n.t('loading') + "</div>";
|
|
}.property(),
|
|
|
|
recoverTopic: function() {
|
|
this.get('content').recover();
|
|
},
|
|
|
|
deleteTopic: function() {
|
|
this.unsubscribe();
|
|
this.get('content').destroy(Discourse.User.current());
|
|
},
|
|
|
|
// Receive notifications for this topic
|
|
subscribe: function() {
|
|
|
|
// Unsubscribe before subscribing again
|
|
this.unsubscribe();
|
|
|
|
var bus = Discourse.MessageBus;
|
|
|
|
var topicController = this;
|
|
bus.subscribe("/topic/" + (this.get('id')), function(data) {
|
|
var topic = topicController.get('model');
|
|
if (data.notification_level_change) {
|
|
topic.set('details.notification_level', data.notification_level_change);
|
|
topic.set('details.notifications_reason_id', data.notifications_reason_id);
|
|
return;
|
|
}
|
|
|
|
var postStream = topicController.get('postStream');
|
|
if (data.type === "revised" || data.type === "acted"){
|
|
// TODO we could update less data for "acted"
|
|
// (only post actions)
|
|
postStream.triggerChangedPost(data.id, data.updated_at);
|
|
return;
|
|
}
|
|
|
|
if (data.type === "deleted"){
|
|
postStream.triggerDeletedPost(data.id, data.post_number);
|
|
return;
|
|
}
|
|
|
|
if (data.type === "recovered"){
|
|
postStream.triggerRecoveredPost(data.id, data.post_number);
|
|
return;
|
|
}
|
|
|
|
// Add the new post into the stream
|
|
postStream.triggerNewPostInStream(data.id);
|
|
});
|
|
},
|
|
|
|
unsubscribe: function() {
|
|
var topicId = this.get('content.id');
|
|
if (!topicId) return;
|
|
|
|
// there is a condition where the view never calls unsubscribe, navigate to a topic from a topic
|
|
Discourse.MessageBus.unsubscribe('/topic/*');
|
|
},
|
|
|
|
// Topic related
|
|
reply: function() {
|
|
this.replyToPost();
|
|
},
|
|
|
|
performTogglePost: function(post) {
|
|
var selectedPosts = this.get('selectedPosts');
|
|
if (this.postSelected(post)) {
|
|
this.deselectPost(post);
|
|
return false;
|
|
} else {
|
|
selectedPosts.addObject(post);
|
|
|
|
// If the user manually selects all posts, all posts are selected
|
|
if (selectedPosts.length === this.get('posts_count')) {
|
|
this.set('allPostsSelected', true);
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
|
|
// If our current post is changed, notify the router
|
|
_currentPostChanged: function() {
|
|
var currentPost = this.get('currentPost');
|
|
if (currentPost) {
|
|
this.send('postChangedRoute', currentPost);
|
|
}
|
|
}.observes('currentPost'),
|
|
|
|
readPosts: function(topicId, postNumbers) {
|
|
var postStream = this.get('postStream');
|
|
|
|
if(this.get('postStream.topic.id') === topicId){
|
|
_.each(postStream.get('posts'), function(post){
|
|
// optimise heavy loop
|
|
// TODO identity map for postNumber
|
|
if(_.include(postNumbers,post.post_number) && !post.read){
|
|
post.set("read", true);
|
|
}
|
|
});
|
|
|
|
var max = _.max(postNumbers);
|
|
if(max > this.get('last_read_post_number')){
|
|
this.set('last_read_post_number', max);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
Called the the topmost visible post on the page changes.
|
|
|
|
@method topVisibleChanged
|
|
@params {Discourse.Post} post that is at the top
|
|
**/
|
|
topVisibleChanged: function(post) {
|
|
if (!post) { return; }
|
|
|
|
var postStream = this.get('postStream'),
|
|
firstLoadedPost = postStream.get('firstLoadedPost');
|
|
|
|
this.set('currentPost', post.get('post_number'));
|
|
|
|
if (post.get('post_number') === 1) { return; }
|
|
|
|
if (firstLoadedPost && firstLoadedPost === post) {
|
|
// Note: jQuery shouldn't be done in a controller, but how else can we
|
|
// trigger a scroll after a promise resolves in a controller? We need
|
|
// to do this to preserve upwards infinte scrolling.
|
|
var $body = $('body'),
|
|
$elem = $('#post-cloak-' + post.get('post_number')),
|
|
distToElement = $body.scrollTop() - $elem.position().top;
|
|
|
|
postStream.prependMore().then(function() {
|
|
Em.run.next(function () {
|
|
$elem = $('#post-cloak-' + post.get('post_number'));
|
|
|
|
// Quickly going back might mean the element is destroyed
|
|
var position = $elem.position();
|
|
if (position && position.top) {
|
|
$('html, body').scrollTop(position.top + distToElement);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
Called the the bottommost visible post on the page changes.
|
|
|
|
@method bottomVisibleChanged
|
|
@params {Discourse.Post} post that is at the bottom
|
|
**/
|
|
bottomVisibleChanged: function(post) {
|
|
if (!post) { return; }
|
|
|
|
var postStream = this.get('postStream'),
|
|
lastLoadedPost = postStream.get('lastLoadedPost'),
|
|
index = postStream.get('stream').indexOf(post.get('id'))+1;
|
|
|
|
this.set('controllers.topic-progress.progressPosition', index);
|
|
|
|
if (lastLoadedPost && lastLoadedPost === post) {
|
|
postStream.appendMore();
|
|
}
|
|
}
|
|
|
|
});
|