FIX: Quoting posts (#9378)

FIX: Quoting posts (#9378)

Fixes to the quote feature. Most important changes listed below:

  • FIX: Correctly attribute quotes when using Reply button
  • FIX: Correctly attribute quotes when using replyAsNewTopic
  • FIX: Allow quoting a quote
  • FIX: Correctly mark quotes as “full”
  • FIX: Don’t try to create a quote if it’s empty
  • DEV: Remove an obsolete method loadQuote It isn’t used in core anymore, the only use in core has been removed over 4 years ago in 3251bcb. It’s not used in any plugins in all-the-plugins and all references to it on GitHub are from outdated forks (https://github.com/search?q=“Post.loadQuote”&type=Code)
diff --git a/app/assets/javascripts/discourse/components/quote-button.js b/app/assets/javascripts/discourse/components/quote-button.js
index 3a9ec42..3a35269 100644
--- a/app/assets/javascripts/discourse/components/quote-button.js
+++ b/app/assets/javascripts/discourse/components/quote-button.js
@@ -1,6 +1,7 @@
 import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import discourseDebounce from "discourse/lib/debounce";
+import toMarkdown from "discourse/lib/to-markdown";
 import { selectedText, selectedElement } from "discourse/lib/utilities";
 import { INPUT_DELAY } from "discourse-common/config/environment";
 
@@ -38,11 +39,12 @@ export default Component.extend({
     let firstRange, postId;
     for (let r = 0; r < selection.rangeCount; r++) {
       const range = selection.getRangeAt(r);
+      const $selectionStart = $(range.startContainer);
+      const $ancestor = $(range.commonAncestorContainer);
 
-      if ($(range.startContainer.parentNode).closest(".cooked").length === 0)
+      if ($selectionStart.closest(".cooked").length === 0) {
         return;
-
-      const $ancestor = $(range.commonAncestorContainer);
+      }
 
       firstRange = firstRange || range;
       postId = postId || $ancestor.closest(".boxed, .reply").data("post-id");
@@ -55,9 +57,21 @@ export default Component.extend({
       }
     }
 
-    let opts = { raw: true };
+    const _selectedElement = selectedElement();
+    const _selectedText = selectedText();
+
+    const $selectedElement = $(_selectedElement);
+    const cooked =
+      $selectedElement.find(".cooked")[0] ||
+      $selectedElement.closest(".cooked")[0];
+    const postBody = toMarkdown(cooked.innerHTML);
+
+    let opts = {
+      full: _selectedText === postBody
+    };
+
     for (
-      let element = selectedElement();
+      let element = _selectedElement;
       element && element.tagName !== "ARTICLE";
       element = element.parentElement
     ) {
@@ -69,7 +83,6 @@ export default Component.extend({
       }
     }
 
-    const _selectedText = selectedText();
     quoteState.selected(postId, _selectedText, opts);
     this.set("visible", quoteState.buffer.length > 0);
 
@@ -186,8 +199,7 @@ export default Component.extend({
   },
 
   click() {
-    const { postId, buffer, opts } = this.quoteState;
-    this.attrs.selectText(postId, buffer, opts).then(() => this._hideButton());
+    this.attrs.selectText().then(() => this._hideButton());
     return false;
   }
 });
diff --git a/app/assets/javascripts/discourse/controllers/composer.js b/app/assets/javascripts/discourse/controllers/composer.js
index c26be5f..1706c82 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js
+++ b/app/assets/javascripts/discourse/controllers/composer.js
@@ -5,7 +5,7 @@ import { inject as service } from "@ember/service";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
 import DiscourseURL from "discourse/lib/url";
-import Quote from "discourse/lib/quote";
+import { buildQuote } from "discourse/lib/quote";
 import Draft from "discourse/models/draft";
 import Composer from "discourse/models/composer";
 import discourseComputed, {
@@ -501,15 +501,14 @@ export default Controller.extend({
 
       if (postId) {
         this.set("model.loading", true);
-        const composer = this;
 
         return this.store.find("post", postId).then(post => {
-          const quote = Quote.build(post, post.raw, {
-            raw: true,
+          const quote = buildQuote(post, post.raw, {
             full: true
           });
+
           toolbarEvent.addText(quote);
-          composer.set("model.loading", false);
+          this.set("model.loading", false);
         });
       }
     },
diff --git a/app/assets/javascripts/discourse/controllers/topic.js b/app/assets/javascripts/discourse/controllers/topic.js
index 01a84f7..c273591 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js
+++ b/app/assets/javascripts/discourse/controllers/topic.js
@@ -8,7 +8,7 @@ import { bufferedProperty } from "discourse/mixins/buffered-content";
 import Composer from "discourse/models/composer";
 import DiscourseURL from "discourse/lib/url";
 import Post from "discourse/models/post";
-import Quote from "discourse/lib/quote";
+import { buildQuote } from "discourse/lib/quote";
 import QuoteState from "discourse/lib/quote-state";
 import Topic from "discourse/models/topic";
 import discourseDebounce from "discourse/lib/debounce";
@@ -265,7 +265,8 @@ export default Controller.extend(bufferedProperty("model"), {
       this.send("showFeatureTopic");
     },
 
-    selectText(postId, buffer, opts) {
+    selectText() {
+      const { postId, buffer, opts } = this.quoteState;
       const loadedPost = this.get("model.postStream").findLoadedPost(postId);
       const promise = loadedPost
         ? Promise.resolve(loadedPost)
@@ -274,11 +275,10 @@ export default Controller.extend(bufferedProperty("model"), {
       return promise.then(post => {
         const composer = this.composer;
         const viewOpen = composer.get("model.viewOpen");
-        const quotedText = Quote.build(post, buffer, opts);
 
         // If we can't create a post, delegate to reply as new topic
         if (!viewOpen && !this.get("model.details.can_create_post")) {
-          this.send("replyAsNewTopic", post, quotedText);
+          this.send("replyAsNewTopic", post);
           return;
         }
 
@@ -300,7 +300,9 @@ export default Controller.extend(bufferedProperty("model"), {
           composerOpts.post = composerPost;
         }
 
+        const quotedText = buildQuote(post, buffer, opts);
         composerOpts.quote = quotedText;
+
         if (composer.get("model.viewOpen")) {
           this.appEvents.trigger("composer:insert-block", quotedText);
         } else if (composer.get("model.viewDraft")) {
@@ -483,7 +485,11 @@ export default Controller.extend(bufferedProperty("model"), {
       }
 
       const quotedPost = postStream.findLoadedPost(quoteState.postId);
-      const quotedText = Quote.build(quotedPost, quoteState.buffer);
+      const quotedText = buildQuote(
+        quotedPost,
+        quoteState.buffer,
+        quoteState.opts
+      );
 
       quoteState.clear();
 
@@ -967,14 +973,14 @@ export default Controller.extend(bufferedProperty("model"), {
       }
     },
 
-    replyAsNewTopic(post, quotedText) {
+    replyAsNewTopic(post) {
       const composerController = this.composer;
-
       const { quoteState } = this;
-      quotedText = quotedText || Quote.build(post, quoteState.buffer);
+      const quotedText = buildQuote(post, quoteState.buffer, quoteState.opts);
+
       quoteState.clear();
 
-      var options;
+      let options;
       if (this.get("model.isPrivateMessage")) {
         let users = this.get("model.details.allowed_users");
         let groups = this.get("model.details.allowed_groups");
@@ -998,25 +1004,16 @@ export default Controller.extend(bufferedProperty("model"), {
         };
       }
 
-      composerController
-        .open(options)
-        .then(() => {
-          return 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 }
-            );
-        });
+      composerController.open(options).then(() => {
+        const title = Handlebars.escapeExpression(this.model.title);
+        const postUrl = `${location.protocol}//${location.host}${post.url}`;
+        const postLink = `[${title}](${postUrl})`;
+        const text = `${I18n.t("post.continue_discussion", {

[... diff too long, it was truncated ...]

GitHub sha: ae1a3913

This commit appears in #9378 which was approved by eviltrout. It was merged by CvX.