I upped the slack ratio for a few reasons 1. We render ucloaked anyway on first render, so cloaking really is not saving much 2. On mobile you don't get JS events so you need a lot more slack to minimize white screens 3. Vast majority of memory is used by object model, if we want to tame it we need to remove posts from stream ember cloaking now supports high slack ratios without going into a tail spin
656 lines
20 KiB
JavaScript
656 lines
20 KiB
JavaScript
/**
|
|
This controller supports all actions related to a topic
|
|
|
|
@class TopicController
|
|
@extends Discourse.ObjectController
|
|
@namespace Discourse
|
|
@module Discourse
|
|
**/
|
|
Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, {
|
|
multiSelect: false,
|
|
needs: ['header', 'modal', 'composer', 'quote-button', 'search'],
|
|
allPostsSelected: false,
|
|
editingTopic: false,
|
|
selectedPosts: null,
|
|
selectedReplies: null,
|
|
queryParams: ['filter', 'username_filters'],
|
|
|
|
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: {
|
|
jumpTop: function() {
|
|
Discourse.URL.routeTo(this.get('firstPostUrl'));
|
|
},
|
|
|
|
jumpBottom: function() {
|
|
Discourse.URL.routeTo(this.get('lastPostUrl'));
|
|
},
|
|
|
|
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() {
|
|
var topicController = this;
|
|
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
|
|
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.title;
|
|
var fancy_title = result.basic_topic.fancy_title;
|
|
topic.setProperties({
|
|
title: title,
|
|
fancy_title: fancy_title
|
|
});
|
|
topicController.set('topicSaving', false);
|
|
}, function(error) {
|
|
topicController.set('editingTopic', true);
|
|
topicController.set('topicSaving', false);
|
|
if (error && error.responseText) {
|
|
bootbox.alert($.parseJSON(error.responseText).errors[0]);
|
|
} else {
|
|
bootbox.alert(I18n.t('generic_error'));
|
|
}
|
|
});
|
|
|
|
// close editing mode
|
|
topicController.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');
|
|
}
|
|
});
|
|
},
|
|
|
|
toggleVisibility: function() {
|
|
this.get('content').toggleStatus('visible');
|
|
},
|
|
|
|
toggleClosed: function() {
|
|
this.get('content').toggleStatus('closed');
|
|
},
|
|
|
|
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');
|
|
},
|
|
|
|
convertToRegular: function() {
|
|
this.get('content').convertArchetype('regular');
|
|
},
|
|
|
|
// 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);
|
|
});
|
|
},
|
|
|
|
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(),
|
|
|
|
jumpTopDisabled: function() {
|
|
return (this.get('progressPosition') < 2);
|
|
}.property('progressPosition'),
|
|
|
|
filteredPostCountChanged: function(){
|
|
if(this.get('postStream.filteredPostsCount') < this.get('progressPosition')){
|
|
this.set('progressPosition', this.get('postStream.filteredPostsCount'));
|
|
}
|
|
}.observes('postStream.filteredPostsCount'),
|
|
|
|
jumpBottomDisabled: function() {
|
|
return this.get('progressPosition') >= this.get('postStream.filteredPostsCount') ||
|
|
this.get('progressPosition') >= this.get('highest_post_number');
|
|
}.property('postStream.filteredPostsCount', 'highest_post_number', 'progressPosition'),
|
|
|
|
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'),
|
|
|
|
streamPercentage: function() {
|
|
if (!this.get('postStream.loaded')) { return 0; }
|
|
if (this.get('postStream.highest_post_number') === 0) { return 0; }
|
|
var perc = this.get('progressPosition') / this.get('postStream.filteredPostsCount');
|
|
return (perc > 1.0) ? 1.0 : perc;
|
|
}.property('postStream.loaded', 'progressPosition', 'postStream.filteredPostsCount'),
|
|
|
|
multiSelectChanged: function() {
|
|
// Deselect all posts when multi select is turned off
|
|
if (!this.get('multiSelect')) {
|
|
this.send('deselectAll');
|
|
}
|
|
}.observes('multiSelect'),
|
|
|
|
hideProgress: function() {
|
|
if (!this.get('postStream.loaded')) return true;
|
|
if (!this.get('currentPost')) return true;
|
|
if (this.get('postStream.filteredPostsCount') < 2) return true;
|
|
return false;
|
|
}.property('postStream.loaded', 'currentPost', 'postStream.filteredPostsCount'),
|
|
|
|
hugeNumberOfPosts: function() {
|
|
return (this.get('postStream.filteredPostsCount') >= Discourse.SiteSettings.short_progress_text_threshold);
|
|
}.property('highest_post_number'),
|
|
|
|
jumpToBottomTitle: function() {
|
|
if (this.get('hugeNumberOfPosts')) {
|
|
return I18n.t('topic.progress.jump_bottom_with_number', {post_number: this.get('highest_post_number')});
|
|
} else {
|
|
return I18n.t('topic.progress.jump_bottom');
|
|
}
|
|
}.property('hugeNumberOfPosts', 'highest_post_number'),
|
|
|
|
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/*');
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// Topic related
|
|
reply: function() {
|
|
this.replyToPost();
|
|
},
|
|
|
|
// Edits a post
|
|
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;
|
|
},
|
|
|
|
recoverPost: function(post) {
|
|
post.recover();
|
|
},
|
|
|
|
deletePost: function(post) {
|
|
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'));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
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'));
|
|
$('html, body').scrollTop($elem.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('progressPosition', index);
|
|
|
|
if (lastLoadedPost && lastLoadedPost === post) {
|
|
postStream.appendMore();
|
|
}
|
|
}
|
|
|
|
});
|