mirror of
https://github.com/discourse/discourse.git
synced 2025-02-14 10:42:45 +08:00
559 lines
16 KiB
JavaScript
559 lines
16 KiB
JavaScript
export default Ember.ObjectController.extend({
|
|
needs: ['modal', 'topic', 'composer-messages', 'application'],
|
|
|
|
replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY),
|
|
checkedMessages: false,
|
|
|
|
showEditReason: false,
|
|
editReason: null,
|
|
maxTitleLength: Discourse.computed.setting('max_topic_title_length'),
|
|
scopedCategoryId: null,
|
|
similarTopics: null,
|
|
similarTopicsMessage: null,
|
|
lastSimilaritySearch: null,
|
|
|
|
topic: null,
|
|
|
|
// TODO: Remove this, very bad
|
|
view: null,
|
|
|
|
_initializeSimilar: function() {
|
|
this.set('similarTopics', []);
|
|
}.on('init'),
|
|
|
|
showWarning: function() {
|
|
if (!Discourse.User.currentProp('staff')) { return false; }
|
|
|
|
var usernames = this.get('model.targetUsernames');
|
|
|
|
// We need exactly one user to issue a warning
|
|
if (Ember.isEmpty(usernames) || usernames.split(',').length !== 1) {
|
|
return false;
|
|
}
|
|
return this.get('model.creatingPrivateMessage');
|
|
}.property('model.creatingPrivateMessage', 'model.targetUsernames'),
|
|
|
|
actions: {
|
|
// Toggle the reply view
|
|
toggle() {
|
|
this.toggle();
|
|
},
|
|
|
|
togglePreview() {
|
|
this.get('model').togglePreview();
|
|
},
|
|
|
|
// Import a quote from the post
|
|
importQuote() {
|
|
const postStream = this.get('topic.postStream');
|
|
let postId = this.get('model.post.id');
|
|
|
|
// If there is no current post, use the first post id from the stream
|
|
if (!postId && postStream) {
|
|
postId = postStream.get('firstPostId');
|
|
}
|
|
|
|
// If we're editing a post, fetch the reply when importing a quote
|
|
if (this.get('model.editingPost')) {
|
|
const replyToPostNumber = this.get('model.post.reply_to_post_number');
|
|
if (replyToPostNumber) {
|
|
const replyPost = postStream.get('posts').findBy('post_number', replyToPostNumber);
|
|
if (replyPost) {
|
|
postId = replyPost.get('id');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (postId) {
|
|
this.set('model.loading', true);
|
|
const composer = this;
|
|
|
|
return this.store.find('post', postId).then(function(post) {
|
|
const quote = Discourse.Quote.build(post, post.get("raw"), {raw: true, full: true});
|
|
composer.appendBlockAtCursor(quote);
|
|
composer.set('model.loading', false);
|
|
});
|
|
}
|
|
},
|
|
|
|
cancel() {
|
|
this.cancelComposer();
|
|
},
|
|
|
|
save() {
|
|
this.save();
|
|
},
|
|
|
|
displayEditReason() {
|
|
this.set("showEditReason", true);
|
|
},
|
|
|
|
hitEsc() {
|
|
|
|
const messages = this.get('controllers.composer-messages.model');
|
|
if (messages.length) {
|
|
messages.popObject();
|
|
return;
|
|
}
|
|
|
|
if (this.get('model.viewOpen')) {
|
|
this.shrink();
|
|
}
|
|
},
|
|
|
|
openIfDraft() {
|
|
if (this.get('model.viewDraft')) {
|
|
this.set('model.composeState', Discourse.Composer.OPEN);
|
|
}
|
|
},
|
|
|
|
},
|
|
|
|
appendText(text, opts) {
|
|
const c = this.get('model');
|
|
if (c) {
|
|
opts = opts || {};
|
|
const wmd = $('#wmd-input'),
|
|
val = wmd.val() || '',
|
|
position = opts.position === "cursor" ? wmd.caret() : val.length,
|
|
caret = c.appendText(text, position, opts);
|
|
|
|
if (wmd[0]) {
|
|
Em.run.next(() => Discourse.Utilities.setCaretPosition(wmd[0], caret));
|
|
}
|
|
}
|
|
},
|
|
|
|
appendTextAtCursor(text, opts) {
|
|
opts = opts || {};
|
|
opts.position = "cursor";
|
|
this.appendText(text, opts);
|
|
},
|
|
|
|
appendBlockAtCursor(text, opts) {
|
|
opts = opts || {};
|
|
opts.position = "cursor";
|
|
opts.block = true;
|
|
this.appendText(text, opts);
|
|
},
|
|
|
|
categories: function() {
|
|
return Discourse.Category.list();
|
|
}.property(),
|
|
|
|
|
|
toggle() {
|
|
this.closeAutocomplete();
|
|
switch (this.get('model.composeState')) {
|
|
case Discourse.Composer.OPEN:
|
|
if (this.blank('model.reply') && this.blank('model.title')) {
|
|
this.close();
|
|
} else {
|
|
this.shrink();
|
|
}
|
|
break;
|
|
case Discourse.Composer.DRAFT:
|
|
this.set('model.composeState', Discourse.Composer.OPEN);
|
|
break;
|
|
case Discourse.Composer.SAVING:
|
|
this.close();
|
|
}
|
|
return false;
|
|
},
|
|
|
|
disableSubmit: function() {
|
|
return this.get('model.loading');
|
|
}.property('model.loading'),
|
|
|
|
save(force) {
|
|
const composer = this.get('model'),
|
|
self = this;
|
|
|
|
// Clear the warning state if we're not showing the checkbox anymore
|
|
if (!this.get('showWarning')) {
|
|
this.set('model.isWarning', false);
|
|
}
|
|
|
|
if(composer.get('cantSubmitPost')) {
|
|
const now = Date.now();
|
|
this.setProperties({
|
|
'view.showTitleTip': now,
|
|
'view.showCategoryTip': now,
|
|
'view.showReplyTip': now
|
|
});
|
|
return;
|
|
}
|
|
|
|
composer.set('disableDrafts', true);
|
|
|
|
// for now handle a very narrow use case
|
|
// if we are replying to a topic AND not on the topic pop the window up
|
|
if (!force && composer.get('replyingToTopic')) {
|
|
const topic = this.get('model.topic');
|
|
if (!topic || topic.get('id') !== composer.get('topic.id'))
|
|
{
|
|
const message = I18n.t("composer.posting_not_on_topic");
|
|
|
|
let buttons = [{
|
|
"label": I18n.t("composer.cancel"),
|
|
"class": "cancel",
|
|
"link": true
|
|
}];
|
|
|
|
if (topic) {
|
|
buttons.push({
|
|
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + Handlebars.Utils.escapeExpression(topic.get('title')) + "</div>",
|
|
"class": "btn btn-reply-here",
|
|
"callback": function() {
|
|
composer.set('topic', topic);
|
|
composer.set('post', null);
|
|
self.save(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
buttons.push({
|
|
"label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + Handlebars.Utils.escapeExpression(this.get('model.topic.title')) + "</div>",
|
|
"class": "btn-primary btn-reply-on-original",
|
|
"callback": function() {
|
|
self.save(true);
|
|
}
|
|
});
|
|
|
|
bootbox.dialog(message, buttons, { "classes": "reply-where-modal" });
|
|
return;
|
|
}
|
|
}
|
|
|
|
var staged = false;
|
|
const disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
|
|
|
|
const promise = composer.save({
|
|
imageSizes: this.get('view').imageSizes(),
|
|
editReason: this.get("editReason")
|
|
}).then(function(result) {
|
|
|
|
if (result.responseJson.action === "enqueued") {
|
|
self.send('postWasEnqueued', result.responseJson);
|
|
self.destroyDraft();
|
|
self.close();
|
|
return result;
|
|
}
|
|
|
|
// If we replied as a new topic successfully, remove the draft.
|
|
if (self.get('replyAsNewTopicDraft')) {
|
|
self.destroyDraft();
|
|
}
|
|
|
|
self.close();
|
|
|
|
const currentUser = Discourse.User.current();
|
|
if (composer.get('creatingTopic')) {
|
|
currentUser.set('topic_count', currentUser.get('topic_count') + 1);
|
|
} else {
|
|
currentUser.set('reply_count', currentUser.get('reply_count') + 1);
|
|
}
|
|
|
|
// TODO disableJumpReply is super crude, it needs to provide some sort
|
|
// of notification to the end user
|
|
if (!composer.get('replyingToTopic') || !disableJumpReply) {
|
|
const post = result.target;
|
|
if (post && !staged) {
|
|
Discourse.URL.routeTo(post.get('url'));
|
|
}
|
|
}
|
|
}).catch(function(error) {
|
|
composer.set('disableDrafts', false);
|
|
self.appEvents.one('composer:opened', () => bootbox.alert(error));
|
|
});
|
|
|
|
if (this.get('controllers.application.currentRouteName').split('.')[0] === 'topic' &&
|
|
composer.get('topic.id') === this.get('controllers.topic.model.id')) {
|
|
staged = composer.get('stagedPost');
|
|
}
|
|
|
|
Em.run.schedule('afterRender', function() {
|
|
if (staged && !disableJumpReply) {
|
|
const postNumber = staged.get('post_number');
|
|
Discourse.URL.jumpToPost(postNumber, { skipIfOnScreen: true });
|
|
self.appEvents.trigger('post:highlight', postNumber);
|
|
}
|
|
});
|
|
|
|
this.messageBus.pause();
|
|
promise.finally(function(){
|
|
self.messageBus.resume();
|
|
});
|
|
|
|
return promise;
|
|
},
|
|
|
|
// Checks to see if a reply has been typed.
|
|
// This is signaled by a keyUp event in a view.
|
|
checkReplyLength() {
|
|
if (!Ember.isEmpty('model.reply')) {
|
|
// Notify the composer messages controller that a reply has been typed. Some
|
|
// messages only appear after typing.
|
|
this.get('controllers.composer-messages').typedReply();
|
|
}
|
|
},
|
|
|
|
// Fired after a user stops typing.
|
|
// Considers whether to check for similar topics based on the current composer state.
|
|
findSimilarTopics() {
|
|
// We don't care about similar topics unless creating a topic
|
|
if (!this.get('model.creatingTopic')) { return; }
|
|
|
|
let body = this.get('model.reply');
|
|
const title = this.get('model.title');
|
|
|
|
// Ensure the fields are of the minimum length
|
|
if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; }
|
|
if (title.length < Discourse.SiteSettings.min_title_similar_length) { return; }
|
|
|
|
// TODO pass the 200 in from somewhere
|
|
body = body.substr(0, 200);
|
|
|
|
// Done search over and over
|
|
if ((title + body) === this.get('lastSimilaritySearch')) { return; }
|
|
this.set('lastSimilaritySearch', title + body);
|
|
|
|
const messageController = this.get('controllers.composer-messages'),
|
|
similarTopics = this.get('similarTopics');
|
|
|
|
let message = this.get('similarTopicsMessage');
|
|
if (!message) {
|
|
message = Discourse.ComposerMessage.create({
|
|
templateName: 'composer/similar_topics',
|
|
extraClass: 'similar-topics'
|
|
});
|
|
this.set('similarTopicsMessage', message);
|
|
}
|
|
|
|
Discourse.Topic.findSimilarTo(title, body).then(function (newTopics) {
|
|
similarTopics.clear();
|
|
similarTopics.pushObjects(newTopics);
|
|
|
|
if (similarTopics.get('length') > 0) {
|
|
message.set('similarTopics', similarTopics);
|
|
messageController.send("popup", message);
|
|
} else if (message) {
|
|
messageController.send("hideMessage", message);
|
|
}
|
|
});
|
|
|
|
},
|
|
|
|
saveDraft() {
|
|
const model = this.get('model');
|
|
if (model) { model.saveDraft(); }
|
|
},
|
|
|
|
/**
|
|
Open the composer view
|
|
|
|
@method open
|
|
@param {Object} opts Options for creating a post
|
|
@param {String} opts.action The action we're performing: edit, reply or createTopic
|
|
@param {Discourse.Post} [opts.post] The post we're replying to
|
|
@param {Discourse.Topic} [opts.topic] The topic we're replying to
|
|
@param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
|
|
**/
|
|
open(opts) {
|
|
opts = opts || {};
|
|
|
|
if (!opts.draftKey) {
|
|
alert("composer was opened without a draft key");
|
|
throw "composer opened without a proper draft key";
|
|
}
|
|
|
|
// If we show the subcategory list, scope the categories drop down to
|
|
// the category we opened the composer with.
|
|
if (this.siteSettings.show_subcategory_list && opts.draftKey !== 'reply_as_new_topic') {
|
|
this.set('scopedCategoryId', opts.categoryId);
|
|
}
|
|
|
|
const composerMessages = this.get('controllers.composer-messages'),
|
|
self = this;
|
|
|
|
let composerModel = this.get('model');
|
|
|
|
this.setProperties({ showEditReason: false, editReason: null });
|
|
composerMessages.reset();
|
|
|
|
// If we want a different draft than the current composer, close it and clear our model.
|
|
if (composerModel &&
|
|
opts.draftKey !== composerModel.draftKey &&
|
|
composerModel.composeState === Discourse.Composer.DRAFT) {
|
|
this.close();
|
|
composerModel = null;
|
|
}
|
|
|
|
return new Ember.RSVP.Promise(function(resolve, reject) {
|
|
if (composerModel && composerModel.get('replyDirty')) {
|
|
|
|
// If we're already open, we don't have to do anything
|
|
if (composerModel.get('composeState') === Discourse.Composer.OPEN &&
|
|
composerModel.get('draftKey') === opts.draftKey) {
|
|
return resolve();
|
|
}
|
|
|
|
// If it's the same draft, just open it up again.
|
|
if (composerModel.get('composeState') === Discourse.Composer.DRAFT &&
|
|
composerModel.get('draftKey') === opts.draftKey) {
|
|
composerModel.set('composeState', Discourse.Composer.OPEN);
|
|
return resolve();
|
|
}
|
|
|
|
// If it's a different draft, cancel it and try opening again.
|
|
return self.cancelComposer().then(function() {
|
|
return self.open(opts);
|
|
}).then(resolve, reject);
|
|
}
|
|
|
|
// we need a draft sequence for the composer to work
|
|
if (opts.draftSequence === void 0) {
|
|
return Discourse.Draft.get(opts.draftKey).then(function(data) {
|
|
opts.draftSequence = data.draft_sequence;
|
|
opts.draft = data.draft;
|
|
self._setModel(composerModel, opts);
|
|
}).then(resolve, reject);
|
|
}
|
|
|
|
self._setModel(composerModel, opts);
|
|
resolve();
|
|
});
|
|
},
|
|
|
|
// Given a potential instance and options, set the model for this composer.
|
|
_setModel(composerModel, opts) {
|
|
if (opts.draft) {
|
|
composerModel = Discourse.Composer.loadDraft(opts);
|
|
if (composerModel) {
|
|
composerModel.set('topic', opts.topic);
|
|
}
|
|
} else {
|
|
composerModel = composerModel || this.store.createRecord('composer');
|
|
composerModel.open(opts);
|
|
}
|
|
|
|
this.set('model', composerModel);
|
|
composerModel.set('composeState', Discourse.Composer.OPEN);
|
|
composerModel.set('isWarning', false);
|
|
|
|
if (opts.topicTitle && opts.topicTitle.length <= this.get('maxTitleLength')) {
|
|
this.set('model.title', opts.topicTitle);
|
|
}
|
|
|
|
if (opts.topicCategoryId) {
|
|
this.set('model.categoryId', opts.topicCategoryId);
|
|
} else if (opts.topicCategory) {
|
|
const splitCategory = opts.topicCategory.split("/");
|
|
let category;
|
|
|
|
if (!splitCategory[1]) {
|
|
category = this.site.get('categories').findProperty('nameLower', splitCategory[0].toLowerCase());
|
|
} else {
|
|
const categories = Discourse.Category.list();
|
|
const mainCategory = categories.findProperty('nameLower', splitCategory[0].toLowerCase());
|
|
category = categories.find(function(item) {
|
|
return item && item.get('nameLower') === splitCategory[1].toLowerCase() && item.get('parent_category_id') === mainCategory.id;
|
|
});
|
|
}
|
|
|
|
if (category) {
|
|
this.set('model.categoryId', category.get('id'));
|
|
}
|
|
}
|
|
|
|
if (opts.topicBody) {
|
|
this.set('model.reply', opts.topicBody);
|
|
}
|
|
|
|
this.get('controllers.composer-messages').queryFor(composerModel);
|
|
},
|
|
|
|
// View a new reply we've made
|
|
viewNewReply() {
|
|
Discourse.URL.routeTo(this.get('model.createdPost.url'));
|
|
this.close();
|
|
return false;
|
|
},
|
|
|
|
destroyDraft() {
|
|
const key = this.get('model.draftKey');
|
|
if (key) {
|
|
Discourse.Draft.clear(key, this.get('model.draftSequence'));
|
|
}
|
|
},
|
|
|
|
cancelComposer() {
|
|
const self = this;
|
|
|
|
return new Ember.RSVP.Promise(function (resolve) {
|
|
if (self.get('model.hasMetaData') || self.get('model.replyDirty')) {
|
|
bootbox.confirm(I18n.t("post.abandon.confirm"), I18n.t("post.abandon.no_value"),
|
|
I18n.t("post.abandon.yes_value"), function(result) {
|
|
if (result) {
|
|
self.destroyDraft();
|
|
self.get('model').clearState();
|
|
self.close();
|
|
resolve();
|
|
}
|
|
});
|
|
} else {
|
|
// it is possible there is some sort of crazy draft with no body ... just give up on it
|
|
self.destroyDraft();
|
|
self.get('model').clearState();
|
|
self.close();
|
|
resolve();
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
shrink() {
|
|
if (this.get('model.replyDirty')) {
|
|
this.collapse();
|
|
} else {
|
|
this.close();
|
|
}
|
|
},
|
|
|
|
collapse() {
|
|
this.saveDraft();
|
|
this.set('model.composeState', Discourse.Composer.DRAFT);
|
|
},
|
|
|
|
close() {
|
|
this.setProperties({
|
|
model: null,
|
|
'view.showTitleTip': false,
|
|
'view.showCategoryTip': false,
|
|
'view.showReplyTip': false
|
|
});
|
|
},
|
|
|
|
closeAutocomplete() {
|
|
$('#wmd-input').autocomplete({ cancel: true });
|
|
},
|
|
|
|
showOptions() {
|
|
var _ref;
|
|
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.ArchetypeOptionsModalView.create({
|
|
archetype: this.get('model.archetype'),
|
|
metaData: this.get('model.metaData')
|
|
})) : void 0;
|
|
},
|
|
|
|
canEdit: function() {
|
|
return this.get("model.action") === "edit" && Discourse.User.current().get("can_edit");
|
|
}.property("model.action"),
|
|
|
|
visible: function() {
|
|
var state = this.get('model.composeState');
|
|
return state && state !== 'closed';
|
|
}.property('model.composeState')
|
|
|
|
});
|