import { setting } from 'discourse/lib/computed'; import DiscourseURL from 'discourse/lib/url'; import Quote from 'discourse/lib/quote'; import Draft from 'discourse/models/draft'; export default Ember.Controller.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: 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 = 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 (Ember.isEmpty(this.get('model.reply')) && Ember.isEmpty(this.get('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'); const 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 currentTopic = this.get('controllers.topic.model'); if (!currentTopic || currentTopic.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 (currentTopic) { buttons.push({ "label": I18n.t("composer.reply_here") + "
" + Handlebars.Utils.escapeExpression(currentTopic.get('title')) + "
", "class": "btn btn-reply-here", "callback": function() { composer.set('topic', currentTopic); composer.set('post', null); self.save(true); } }); } buttons.push({ "label": I18n.t("composer.reply_original") + "
" + Handlebars.Utils.escapeExpression(this.get('model.topic.title')) + "
", "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) { DiscourseURL.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'); DiscourseURL.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); } this.store.find('similar-topic', {title, raw: body}).then(function(newTopics) { similarTopics.clear(); similarTopics.pushObjects(newTopics.get('content')); 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 && self._isComposerReply(composerModel, opts)) { 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); if (self._isComposerReply(composerModel, opts)) 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 === undefined) { return 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(); }); }, _isComposerReply(composerModel, opts) { return (composerModel.get('action') === Discourse.Composer.REPLY && composerModel.get('action') === opts.action); }, // 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() { DiscourseURL.routeTo(this.get('model.createdPost.url')); this.close(); return false; }, destroyDraft() { const key = this.get('model.draftKey'); if (key) { 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') });