discourse/app/assets/javascripts/discourse/controllers/topic.js.es6
2016-09-26 20:46:55 -03:00

909 lines
27 KiB
JavaScript

import BufferedContent from 'discourse/mixins/buffered-content';
import SelectedPostsCount from 'discourse/mixins/selected-posts-count';
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
import Topic from 'discourse/models/topic';
import Quote from 'discourse/lib/quote';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
import Composer from 'discourse/models/composer';
import DiscourseURL from 'discourse/lib/url';
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Post from 'discourse/models/post';
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
needs: ['modal', 'composer', 'quote-button', 'application'],
multiSelect: false,
allPostsSelected: false,
editingTopic: false,
selectedPosts: null,
selectedReplies: null,
queryParams: ['filter', 'username_filters', 'show_deleted'],
loadedAllPosts: Ember.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'),
enteredAt: null,
enteredIndex: null,
retrying: false,
userTriggeredProgress: null,
_progressIndex: null,
topicDelegated: [
'toggleMultiSelect',
'deleteTopic',
'recoverTopic',
'toggleClosed',
'showAutoClose',
'showFeatureTopic',
'showChangeTimestamp',
'toggleArchived',
'toggleVisibility',
'convertToPublicTopic',
'convertToPrivateMessage',
'jumpTop',
'jumpToPost',
'jumpToIndex',
'jumpBottom',
'replyToPost'
],
_titleChanged: function() {
const title = this.get('model.title');
if (!Ember.isEmpty(title)) {
// Note normally you don't have to trigger this, but topic titles can be updated
// and are sometimes lazily loaded.
this.send('refreshTitle');
}
}.observes('model.title', 'category'),
@computed('site.mobileView', 'model.posts_count')
showSelectedPostsAtBottom(mobileView, postsCount) {
return mobileView && (postsCount > 3);
},
@computed('model.postStream.posts')
postsToRender() {
return this.capabilities.isAndroid ? this.get('model.postStream.posts')
: this.get('model.postStream.postsWithPlaceholders');
},
@computed('model.postStream.loadingFilter')
androidLoading(loading) {
return this.capabilities.isAndroid && loading;
},
@computed('model.postStream.summary')
show_deleted: {
set(value) {
const postStream = this.get('model.postStream');
if (!postStream) { return; }
postStream.set('show_deleted', value);
return postStream.get('show_deleted') ? true : undefined;
},
get() {
return this.get('postStream.show_deleted') ? true : undefined;
}
},
@computed('model.postStream.summary')
filter: {
set(value) {
const postStream = this.get('model.postStream');
if (!postStream) { return; }
postStream.set('summary', value === "summary");
return postStream.get('summary') ? "summary" : undefined;
},
get() {
return this.get('postStream.summary') ? "summary" : undefined;
}
},
@computed('model', 'topicTrackingState.messageCount')
browseMoreMessage(model) {
// TODO decide what to show for pms
if (model.get('isPrivateMessage')) { return; }
const opts = { latestLink: `<a href="${Discourse.getURL("/latest")}">${I18n.t("topic.view_latest_topics")}</a>` };
let category = model.get('category');
if (category && Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) {
category = null;
}
if (category) {
opts.catLink = categoryBadgeHTML(category);
} else {
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + I18n.t("topic.browse_all_categories") + "</a>";
}
const unreadTopics = this.topicTrackingState.countUnread();
const newTopics = this.topicTrackingState.countNew();
if (newTopics + unreadTopics > 0) {
const hasBoth = unreadTopics > 0 && newTopics > 0;
return I18n.messageFormat("topic.read_more_MF", {
"BOTH": hasBoth,
"UNREAD": unreadTopics,
"NEW": newTopics,
"CATEGORY": category ? true : false,
latestLink: opts.latestLink,
catLink: opts.catLink
});
} else if (category) {
return I18n.t("topic.read_more_in_category", opts);
} else {
return I18n.t("topic.read_more", opts);
}
},
@computed('model')
pmPath(model) {
return this.currentUser && this.currentUser.pmPath(model);
},
@computed('model')
suggestedTitle(model) {
return model.get('isPrivateMessage') ?
`<a href="${this.get('pmPath')}"><i class='private-message-glyph fa fa-envelope'></i></a> ${I18n.t("suggested_topics.pm_title")}` :
I18n.t("suggested_topics.title");
},
@computed('model.postStream.streamFilters.username_filters')
username_filters: {
set(value) {
const postStream = this.get('model.postStream');
if (!postStream) { return; }
postStream.set('streamFilters.username_filters', value);
return postStream.get('streamFilters.username_filters');
},
get() {
return this.get('postStream.streamFilters.username_filters');
}
},
_clearSelected: function() {
this.set('selectedPosts', []);
this.set('selectedReplies', []);
}.on('init'),
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
gotoInbox(name) {
var url = '/users/' + this.get('currentUser.username_lower') + '/messages';
if (name) {
url = url + '/group/' + name;
}
DiscourseURL.routeTo(url);
},
selectedQuery: function() {
return post => this.postSelected(post);
}.property(),
@computed('model.isPrivateMessage')
canEditTags(isPrivateMessage) {
return !isPrivateMessage && this.site.get('can_tag_topics');
},
actions: {
fillGapBefore(args) {
return this.get('model.postStream').fillGapBefore(args.post, args.gap);
},
fillGapAfter(args) {
return this.get('model.postStream').fillGapAfter(args.post, args.gap);
},
currentPostChanged(event) {
const { post } = event;
if (!post) { return; }
const postNumber = post.get('post_number');
const topic = this.get('model');
topic.set('currentPost', postNumber);
if (postNumber > (topic.get('last_read_post_number') || 0)) {
topic.set('last_read_post_id', post.get('id'));
topic.set('last_read_post_number', postNumber);
}
this.send('postChangedRoute', postNumber);
this._progressIndex = topic.get('postStream').progressIndexOfPost(post);
},
currentPostScrolled(event) {
const total = this.get('model.postStream.filteredPostsCount');
const percent = (parseFloat(this._progressIndex + event.percent - 1) / total);
this.appEvents.trigger('topic:current-post-scrolled', {
postIndex: this._progressIndex,
percent: Math.max(Math.min(percent, 1.0), 0.0)
});
},
// Called the the topmost visible post on the page changes.
topVisibleChanged(event) {
const { post, refresh } = event;
if (!post) { return; }
const postStream = this.get('model.postStream');
const firstLoadedPost = postStream.get('posts.firstObject');
if (post.get('post_number') === 1) { return; }
if (firstLoadedPost && firstLoadedPost === post) {
postStream.prependMore().then(() => refresh());
}
},
// Called the the bottommost visible post on the page changes.
bottomVisibleChanged(event) {
const { post, refresh } = event;
const postStream = this.get('model.postStream');
const lastLoadedPost = postStream.get('posts.lastObject');
if (lastLoadedPost && lastLoadedPost === post && postStream.get('canAppendMore')) {
postStream.appendMore().then(() => refresh());
// show loading stuff
refresh();
}
},
toggleSummary() {
return this.get('model.postStream').toggleSummary();
},
removeAllowedUser(user) {
return this.get('model.details').removeAllowedUser(user);
},
removeAllowedGroup(group) {
return this.get('model.details').removeAllowedGroup(group);
},
deleteTopic() {
this.deleteTopic();
},
archiveMessage() {
const topic = this.get('model');
topic.archiveMessage().then(()=>{
this.gotoInbox(topic.get("inboxGroupName"));
});
},
moveToInbox() {
const topic = this.get('model');
topic.moveToInbox().then(()=>{
this.gotoInbox(topic.get("inboxGroupName"));
});
},
// Post related methods
replyToPost(post) {
const composerController = this.get('controllers.composer'),
quoteController = this.get('controllers.quote-button'),
quotedText = 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') === Composer.REPLY) {
composerController.set('content.post', post);
composerController.set('content.composeState', Composer.OPEN);
this.appEvents.trigger('composer:insert-text', quotedText.trim());
} else {
const opts = {
action: Composer.REPLY,
draftKey: topic.get('draft_key'),
draftSequence: topic.get('draft_sequence')
};
if (quotedText) { opts.quote = quotedText; }
if(post && post.get("post_number") !== 1){
opts.post = post;
} else {
opts.topic = topic;
}
composerController.open(opts);
}
return false;
},
recoverPost(post) {
// Recovering the first post recovers the topic instead
if (post.get('post_number') === 1) {
this.recoverTopic();
return;
}
post.recover();
},
deletePost(post) {
// Deleting the first post deletes the topic
if (post.get('post_number') === 1) {
return this.deleteTopic();
} else if (!post.can_delete) {
// check if current user can delete post
return false;
}
const 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 right'},
{label: I18n.t("post.controls.delete_replies.no_value"),
callback() {
post.destroy(user);
}
},
{label: I18n.t("post.controls.delete_replies.yes_value"),
'class': 'btn-primary',
callback() {
Discourse.Post.deleteMany([post], [post]);
self.get('model.postStream.posts').forEach(function (p) {
if (p === post || p.get('reply_to_post_number') === post.get('post_number')) {
p.setDeletedState(user);
}
});
}
}
]);
} else {
return post.destroy(user).then(() => {
this.appEvents.trigger('post-stream:refresh');
}).catch(error => {
popupAjaxError(error);
post.undoDeleteState();
});
}
},
editPost(post) {
if (!Discourse.User.current()) {
return bootbox.alert(I18n.t('post.controls.edit_anonymous'));
}
// check if current user can edit post
if (!post.can_edit) {
return false;
}
const composer = this.get('controllers.composer'),
composerModel = composer.get('model'),
opts = {
post: post,
action: Composer.EDIT,
draftKey: post.get('topic.draft_key'),
draftSequence: post.get('topic.draft_sequence')
};
// Cancel and reopen the composer for the first post
if (composerModel && (post.get('firstPost') || composerModel.get('editingFirstPost'))) {
composer.cancelComposer().then(() => composer.open(opts));
} else {
composer.open(opts);
}
},
toggleBookmark(post) {
if (!this.currentUser) {
alert(I18n.t("bookmarks.not_bookmarked"));
return;
}
if (post) {
return post.toggleBookmark().catch(popupAjaxError);
} else {
return this.get("model").toggleBookmark().then(changedIds => {
if (!changedIds) { return; }
changedIds.forEach(id => this.appEvents.trigger('post-stream:refresh', { id }));
});
}
},
jumpToIndex(index) {
this._jumpToPostId(this.get('model.postStream.stream')[index-1]);
},
jumpToPost(postNumber) {
this._jumpToPostId(this.get('model.postStream').findPostIdForPostNumber(postNumber));
},
jumpTop() {
DiscourseURL.routeTo(this.get('model.firstPostUrl'), { skipIfOnScreen: false });
},
jumpBottom() {
DiscourseURL.routeTo(this.get('model.lastPostUrl'), { skipIfOnScreen: false });
},
selectAll() {
const posts = this.get('model.postStream.posts');
const selectedPosts = this.get('selectedPosts');
if (posts) {
selectedPosts.addObjects(posts);
}
this.set('allPostsSelected', true);
this.appEvents.trigger('post-stream:refresh', { force: true });
},
deselectAll() {
this.get('selectedPosts').clear();
this.get('selectedReplies').clear();
this.set('allPostsSelected', false);
this.appEvents.trigger('post-stream:refresh', { force: true });
},
toggleParticipant(user) {
this.get('model.postStream').toggleParticipant(Em.get(user, 'username'));
},
editTopic() {
if (!this.get('model.details.can_edit')) return false;
this.set('editingTopic', true);
return false;
},
cancelEditingTopic() {
this.set('editingTopic', false);
this.rollbackBuffer();
},
toggleMultiSelect() {
this.toggleProperty('multiSelect');
this.appEvents.trigger('post-stream:refresh', { force: true });
},
finishedEditingTopic() {
if (!this.get('editingTopic')) { return; }
// save the modifications
const self = this,
props = this.get('buffered.buffer');
Topic.update(this.get('model'), props).then(function() {
// Note we roll back on success here because `update` saves
// the properties to the topic.
self.rollbackBuffer();
self.set('editingTopic', false);
}).catch(popupAjaxError);
},
toggledSelectedPost(post) {
this.performTogglePost(post);
},
toggledSelectedPostReplies(post) {
const selectedReplies = this.get('selectedReplies');
if (this.performTogglePost(post)) {
selectedReplies.addObject(post);
} else {
selectedReplies.removeObject(post);
}
},
deleteSelected() {
bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), result => {
if (result) {
// If all posts are selected, it's the same thing as deleting the topic
if (this.get('allPostsSelected')) {
return this.deleteTopic();
}
const selectedPosts = this.get('selectedPosts');
const selectedReplies = this.get('selectedReplies');
const postStream = this.get('model.postStream');
Discourse.Post.deleteMany(selectedPosts, selectedReplies);
postStream.get('posts').forEach(p => {
if (this.postSelected(p)) {
p.set('deleted_at', new Date());
}
});
this.send('toggleMultiSelect');
}
});
},
mergePosts() {
bootbox.confirm(I18n.t("post.merge.confirm", { count: this.get('selectedPostsCount') }), result => {
if (result) {
const selectedPosts = this.get('selectedPosts');
Post.mergePosts(selectedPosts);
this.send('toggleMultiSelect');
}
});
},
expandHidden(post) {
post.expandHidden();
},
toggleVisibility() {
this.get('content').toggleStatus('visible');
},
toggleClosed() {
this.get('content').toggleStatus('closed');
},
recoverTopic() {
this.get('content').recover();
},
makeBanner() {
this.get('content').makeBanner();
},
removeBanner() {
this.get('content').removeBanner();
},
togglePinned() {
const value = this.get('model.pinned_at') ? false : true,
topic = this.get('content'),
until = this.get('model.pinnedInCategoryUntil');
// optimistic update
topic.setProperties({
pinned_at: value ? moment() : null,
pinned_globally: false,
pinned_until: value ? until : null
});
return topic.saveStatus("pinned", value, until);
},
pinGlobally() {
const topic = this.get('content'),
until = this.get('model.pinnedGloballyUntil');
// optimistic update
topic.setProperties({
pinned_at: moment(),
pinned_globally: true,
pinned_until: until
});
return topic.saveStatus("pinned_globally", true, until);
},
toggleArchived() {
this.get('content').toggleStatus('archived');
},
clearPin() {
this.get('content').clearPin();
},
togglePinnedForUser() {
if (this.get('model.pinned_at')) {
const topic = this.get('content');
if (topic.get('pinned')) {
topic.clearPin();
} else {
topic.rePin();
}
}
},
replyAsNewTopic(post) {
const composerController = this.get('controllers.composer');
const quoteController = this.get('controllers.quote-button');
post = post || quoteController.get('post');
const quotedText = Quote.build(post, quoteController.get('buffer'));
quoteController.deselectText();
composerController.open({
action: Composer.CREATE_TOPIC,
draftKey: Composer.REPLY_AS_NEW_TOPIC_KEY,
categoryId: this.get('model.category.id')
}).then(() => {
return Em.isEmpty(quotedText) ? "" : quotedText;
}).then(q => {
const postUrl = `${location.protocol}//${location.host}${post.get('url')}`;
const postLink = `[${Handlebars.escapeExpression(this.get('model.title'))}](${postUrl})`;
composerController.get('model').prependText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`, {new_line: true});
});
},
retryLoading() {
const self = this;
self.set('retrying', true);
this.get('model.postStream').refresh().then(function() {
self.set('retrying', false);
}, function() {
self.set('retrying', false);
});
},
toggleWiki(post) {
return post.updatePostField('wiki', !post.get('wiki'));
},
togglePostType(post) {
const regular = this.site.get('post_types.regular');
const moderator = this.site.get('post_types.moderator_action');
return post.updatePostField('post_type', post.get('post_type') === moderator ? regular : moderator);
},
rebakePost(post) {
return post.rebake();
},
unhidePost(post) {
return post.unhide();
},
changePostOwner(post) {
this.get('selectedPosts').addObject(post);
this.send('changeOwner');
},
convertToPublicTopic() {
this.get('content').convertTopic("public");
},
convertToPrivateMessage() {
this.get('content').convertTopic("private");
}
},
_jumpToPostId(postId) {
if (!postId) {
Ember.Logger.warn("jump-post code broken - requested an index outside the stream array");
return;
}
const topic = this.get('model');
const postStream = topic.get('postStream');
const post = postStream.findLoadedPost(postId);
if (post) {
DiscourseURL.routeTo(topic.urlForPostNumber(post.get('post_number')));
} else {
// need to load it
postStream.findPostsByIds([postId]).then(arr => {
DiscourseURL.routeTo(topic.urlForPostNumber(arr[0].get('post_number')));
});
}
},
togglePinnedState() {
this.send('togglePinnedForUser');
},
print() {
if (this.siteSettings.max_prints_per_hour_per_user > 0) {
window.open(this.get('model.printUrl'), '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=600,height=315');
}
},
canMergeTopic: function() {
if (!this.get('model.details.can_move_posts')) return false;
return this.get('selectedPostsCount') > 0;
}.property('selectedPostsCount'),
canSplitTopic: function() {
if (!this.get('model.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') !== undefined;
}.property('selectedPostsUsername'),
@computed('selectedPosts', 'selectedPostsCount', 'selectedPostsUsername')
canMergePosts(selectedPosts, selectedPostsCount, selectedPostsUsername) {
if (selectedPostsCount < 2) return false;
if (!selectedPosts.every(p => p.get('can_delete'))) return false;
return selectedPostsUsername !== undefined;
},
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() {
const selectedPosts = this.get('selectedPosts');
if (this.get('allPostsSelected')) return true;
if (this.get('selectedPostsCount') === 0) return false;
let canDelete = true;
selectedPosts.forEach(function(p) {
if (!p.get('can_delete')) {
canDelete = false;
return false;
}
});
return canDelete;
}.property('selectedPostsCount'),
hasError: Ember.computed.or('model.notFoundHtml', 'model.message'),
noErrorYet: Ember.computed.not('hasError'),
multiSelectChanged: function() {
// Deselect all posts when multi select is turned off
if (!this.get('multiSelect')) {
this.send('deselectAll');
}
}.observes('multiSelect'),
deselectPost(post) {
this.get('selectedPosts').removeObject(post);
const selectedReplies = this.get('selectedReplies');
selectedReplies.removeObject(post);
const selectedReply = selectedReplies.findProperty('post_number', post.get('reply_to_post_number'));
if (selectedReply) { selectedReplies.removeObject(selectedReply); }
this.set('allPostsSelected', false);
},
postSelected(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;
},
loadingHTML: function() {
return spinnerHTML;
}.property(),
recoverTopic() {
this.get('content').recover();
},
deleteTopic() {
this.get('content').destroy(Discourse.User.current());
},
// Receive notifications for this topic
subscribe() {
// Unsubscribe before subscribing again
this.unsubscribe();
const refresh = (args) => this.appEvents.trigger('post-stream:refresh', args);
this.messageBus.subscribe("/topic/" + this.get('model.id'), data => {
const topic = this.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;
}
const postStream = this.get('model.postStream');
switch (data.type) {
case "acted":
postStream.triggerChangedPost(data.id, data.updated_at).then(() => refresh({ id: data.id, refreshLikes: true }));
break;
case "revised":
case "rebaked": {
postStream.triggerChangedPost(data.id, data.updated_at).then(() => refresh({ id: data.id }));
break;
}
case "deleted": {
postStream.triggerDeletedPost(data.id, data.post_number).then(() => refresh({ id: data.id }));
break;
}
case "recovered": {
postStream.triggerRecoveredPost(data.id, data.post_number).then(() => refresh({ id: data.id }));
break;
}
case "created": {
postStream.triggerNewPostInStream(data.id).then(() => refresh());
if (this.get('currentUser.id') !== data.user_id) {
Discourse.notifyBackgroundCountIncrement();
}
break;
}
case "move_to_inbox": {
topic.set("message_archived",false);
break;
}
case "archived": {
topic.set("message_archived",true);
break;
}
default: {
Em.Logger.warn("unknown topic bus message type", data);
}
}
if (data.reload_topic) {
topic.reload().then(() => {
this.send('postChangedRoute', topic.get('post_number') || 1);
});
}
});
},
unsubscribe() {
const 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
this.messageBus.unsubscribe('/topic/*');
},
// Topic related
reply() {
this.replyToPost();
},
performTogglePost(post) {
const 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
this.set('allPostsSelected', selectedPosts.length === this.get('model.posts_count'));
return true;
}
},
readPosts(topicId, postNumbers) {
const topic = this.get("model");
const postStream = topic.get("postStream");
if (topic.get('id') === topicId) {
postStream.get('posts').forEach(post => {
if (!post.read && postNumbers.indexOf(post.post_number) !== -1) {
post.set('read', true);
this.appEvents.trigger('post-stream:refresh', { id: post.get('id') });
}
});
if (this.siteSettings.automatically_unpin_topics &&
this.currentUser &&
this.currentUser.automatically_unpin_topics) {
// automatically unpin topics when the user reaches the bottom
const max = _.max(postNumbers);
if (topic.get("pinned") && max >= topic.get("highest_post_number")) {
Em.run.next(() => topic.clearPin());
}
}
}
},
_showFooter: function() {
const showFooter = this.get("model.postStream.loaded") && this.get("model.postStream.loadedAllPosts");
this.set("controllers.application.showFooter", showFooter);
}.observes("model.postStream.{loaded,loadedAllPosts}")
});