FIX: Quoting a quote preserves the original post information (#8746)

Let's say post #2 quotes post number #1. If a user decides to quote the
quote in post #2, it should keep the information of post #1
("user_1, post: 1, topic: X"), instead of replacing with current post
info ("user_2, post: 2, topic: X").
This commit is contained in:
Bianca Nenciu
2020-01-22 16:10:23 +02:00
committed by GitHub
parent 8a89b7e108
commit 7b7e1717f2
11 changed files with 86 additions and 32 deletions

View File

@ -1,7 +1,7 @@
import { scheduleOnce } from "@ember/runloop"; import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseDebounce from "discourse/lib/debounce"; import discourseDebounce from "discourse/lib/debounce";
import { selectedText } from "discourse/lib/utilities"; import { selectedText, selectedElement } from "discourse/lib/utilities";
export default Component.extend({ export default Component.extend({
classNames: ["quote-button"], classNames: ["quote-button"],
@ -48,8 +48,28 @@ export default Component.extend({
} }
} }
let opts;
for (
let element = selectedElement();
element && element.tagName !== "ARTICLE";
element = element.parentElement
) {
if (element.tagName === "ASIDE" && element.classList.contains("quote")) {
opts = {
username:
element.dataset.username ||
element
.querySelector(".title")
.textContent.trim()
.replace(/:$/, ""),
post: element.dataset.post,
topic: element.dataset.topic
};
}
}
const _selectedText = selectedText(); const _selectedText = selectedText();
quoteState.selected(postId, _selectedText); quoteState.selected(postId, _selectedText, opts);
this.set("visible", quoteState.buffer.length > 0); this.set("visible", quoteState.buffer.length > 0);
// avoid hard loops in quote selection unconditionally // avoid hard loops in quote selection unconditionally
@ -165,8 +185,8 @@ export default Component.extend({
}, },
click() { click() {
const { postId, buffer } = this.quoteState; const { postId, buffer, opts } = this.quoteState;
this.attrs.selectText(postId, buffer).then(() => this._hideButton()); this.attrs.selectText(postId, buffer, opts).then(() => this._hideButton());
return false; return false;
} }
}); });

View File

@ -266,7 +266,7 @@ export default Controller.extend(bufferedProperty("model"), {
this.send("showFeatureTopic"); this.send("showFeatureTopic");
}, },
selectText(postId, buffer) { selectText(postId, buffer, opts) {
const loadedPost = this.get("model.postStream").findLoadedPost(postId); const loadedPost = this.get("model.postStream").findLoadedPost(postId);
const promise = loadedPost const promise = loadedPost
? Promise.resolve(loadedPost) ? Promise.resolve(loadedPost)
@ -275,7 +275,7 @@ export default Controller.extend(bufferedProperty("model"), {
return promise.then(post => { return promise.then(post => {
const composer = this.composer; const composer = this.composer;
const viewOpen = composer.get("model.viewOpen"); const viewOpen = composer.get("model.viewOpen");
const quotedText = Quote.build(post, buffer); const quotedText = Quote.build(post, buffer, opts);
// If we can't create a post, delegate to reply as new topic // If we can't create a post, delegate to reply as new topic
if (!viewOpen && !this.get("model.details.can_create_post")) { if (!viewOpen && !this.get("model.details.can_create_post")) {

View File

@ -3,13 +3,15 @@ export default class QuoteState {
this.clear(); this.clear();
} }
selected(postId, buffer) { selected(postId, buffer, opts) {
this.postId = postId; this.postId = postId;
this.buffer = buffer; this.buffer = buffer;
this.opts = opts;
} }
clear() { clear() {
this.buffer = ""; this.buffer = "";
this.postId = null; this.postId = null;
this.opts = null;
} }
} }

View File

@ -8,6 +8,7 @@ export default {
} }
if (!contents) contents = ""; if (!contents) contents = "";
if (!opts) opts = {};
const sansQuotes = contents.replace(this.REGEXP, "").trim(); const sansQuotes = contents.replace(this.REGEXP, "").trim();
if (sansQuotes.length === 0) { if (sansQuotes.length === 0) {
@ -26,9 +27,9 @@ export default {
stripped.replace(/\W/g, "") === contents.replace(/\W/g, ""); stripped.replace(/\W/g, "") === contents.replace(/\W/g, "");
const params = [ const params = [
post.get("username"), opts.username || post.username,
`post:${post.get("post_number")}`, `post:${opts.post || post.post_number}`,
`topic:${post.get("topic_id")}` `topic:${opts.topic || post.topic_id}`
]; ];
opts = opts || {}; opts = opts || {};

View File

@ -143,6 +143,13 @@ export function selectedText() {
return toMarkdown($div.html()); return toMarkdown($div.html());
} }
export function selectedElement() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
return selection.getRangeAt(0).startContainer.parentElement;
}
}
// Determine the row and col of the caret in an element // Determine the row and col of the caret in an element
export function caretRowCol(el) { export function caretRowCol(el) {
var cp = caretPosition(el); var cp = caretPosition(el);

View File

@ -73,6 +73,10 @@ const rule = {
token.attrs.push(["class", "quote no-group"]); token.attrs.push(["class", "quote no-group"]);
} }
if (username) {
token.attrs.push(["data-username", username]);
}
if (postNumber) { if (postNumber) {
token.attrs.push(["data-post", postNumber]); token.attrs.push(["data-post", postNumber]);
} }

View File

@ -164,6 +164,8 @@ module Jobs
username_replaced = false username_replaced = false
aside["data-username"] = @new_username if aside["data-username"] == @old_username
div.children.each do |child| div.children.each do |child|
if child.text? if child.text?
content = child.content content = child.content

View File

@ -33,7 +33,7 @@ describe PrettyText do
topic = Fabricate(:topic, title: "this is a test topic :slight_smile:") topic = Fabricate(:topic, title: "this is a test topic :slight_smile:")
expected = <<~HTML expected = <<~HTML
<aside class="quote no-group" data-post="2" data-topic="#{topic.id}"> <aside class="quote no-group" data-username="EvilTrout" data-post="2" data-topic="#{topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<a href="http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2">This is a test topic <img src="/images/emoji/twitter/slight_smile.png?v=#{Emoji::EMOJI_VERSION}" title="slight_smile" alt="slight_smile" class="emoji"></a> <a href="http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2">This is a test topic <img src="/images/emoji/twitter/slight_smile.png?v=#{Emoji::EMOJI_VERSION}" title="slight_smile" alt="slight_smile" class="emoji"></a>
@ -158,7 +158,7 @@ describe PrettyText do
topic = Fabricate(:topic, title: "this is topic with secret category", category: category) topic = Fabricate(:topic, title: "this is topic with secret category", category: category)
expected = <<~HTML expected = <<~HTML
<aside class="quote no-group" data-post="3" data-topic="#{topic.id}"> <aside class="quote no-group" data-username="maja" data-post="3" data-topic="#{topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<a href="http://test.localhost/t/#{topic.id}/3">#{I18n.t("on_another_topic")}</a> <a href="http://test.localhost/t/#{topic.id}/3">#{I18n.t("on_another_topic")}</a>
@ -181,7 +181,7 @@ describe PrettyText do
[/quote] [/quote]
MD MD
html = <<~HTML html = <<~HTML
<aside class="quote no-group" data-post="123" data-topic="456" data-full="true"> <aside class="quote no-group" data-username="#{user.username}" data-post="123" data-topic="456" data-full="true">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div> <img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div>
@ -203,7 +203,7 @@ describe PrettyText do
[/quote] [/quote]
MD MD
html = <<~HTML html = <<~HTML
<aside class="quote no-group" data-post="123" data-topic="456" data-full="true"> <aside class="quote no-group" data-username="#{user.username}" data-post="123" data-topic="456" data-full="true">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div> <img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div>
@ -224,7 +224,7 @@ describe PrettyText do
MD MD
html = <<~HTML html = <<~HTML
<aside class="quote no-group" data-post="555" data-topic="666"> <aside class="quote no-group" data-username="#{user.username}" data-post="555" data-topic="666">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div> <img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div>
@ -251,7 +251,7 @@ describe PrettyText do
topic = Fabricate(:topic, title: "this is a test topic") topic = Fabricate(:topic, title: "this is a test topic")
expected = <<~HTML expected = <<~HTML
<aside class="quote group-#{group.name}" data-post="2" data-topic="#{topic.id}"> <aside class="quote group-#{group.name}" data-username="#{user.username}" data-post="2" data-topic="#{topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"><a href="http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2">This is a test topic</a> <img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"><a href="http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2">This is a test topic</a>

View File

@ -359,7 +359,7 @@ describe UsernameChanger do
expect(post.cooked).to match_html(<<~HTML) expect(post.cooked).to match_html(<<~HTML)
<p>Lorem ipsum</p> <p>Lorem ipsum</p>
<aside class="quote no-group" data-post="1" data-topic="#{quoted_post.topic.id}"> <aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div> <img alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div>
@ -367,7 +367,7 @@ describe UsernameChanger do
<p>quoted post</p> <p>quoted post</p>
</blockquote> </blockquote>
</aside> </aside>
<aside class="quote no-group"> <aside class="quote no-group" data-username="bar">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div> <img alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div>
@ -375,7 +375,7 @@ describe UsernameChanger do
<p>quoted post</p> <p>quoted post</p>
</blockquote> </blockquote>
</aside> </aside>
<aside class="quote no-group" data-post="1" data-topic="#{quoted_post.topic.id}"> <aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div> <img alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div>
@ -410,7 +410,7 @@ describe UsernameChanger do
let(:expected_cooked) do let(:expected_cooked) do
<<~HTML <<~HTML
<p>Lorem ipsum</p> <p>Lorem ipsum</p>
<aside class="quote no-group" data-post="1" data-topic="#{quoted_post.topic.id}"> <aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
<img alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div> <img alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div>

View File

@ -315,3 +315,21 @@ QUnit.test("View Hidden Replies", async assert => {
assert.equal(find(".gap").length, 0, "it hides gap"); assert.equal(find(".gap").length, 0, "it hides gap");
}); });
QUnit.test("Quoting a quote keeps the original poster name", async assert => {
await visit("/t/internationalization-localization/280");
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents($("#post_5 blockquote")[0]);
selection.removeAllRanges();
selection.addRange(range);
await click(".quote-button");
assert.ok(
find(".d-editor-input")
.val()
.indexOf('quote="codinghorror said, post:3, topic:280"') !== -1
);
});

View File

@ -395,7 +395,7 @@ QUnit.test("Quotes", assert => {
assert.cookedOptions( assert.cookedOptions(
'[quote="eviltrout, post: 1"]\na quote\n\nsecond line\n\nthird line\n[/quote]', '[quote="eviltrout, post: 1"]\na quote\n\nsecond line\n\nthird line\n[/quote]',
{ topicId: 2 }, { topicId: 2 },
`<aside class=\"quote no-group\" data-post=\"1\"> `<aside class=\"quote no-group\" data-username=\"eviltrout\" data-post=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
eviltrout:</div> eviltrout:</div>
@ -411,7 +411,7 @@ QUnit.test("Quotes", assert => {
assert.cookedOptions( assert.cookedOptions(
'[quote="bob, post:1"]\nmy quote\n[/quote]', '[quote="bob, post:1"]\nmy quote\n[/quote]',
{ topicId: 2, lookupAvatar: function() {} }, { topicId: 2, lookupAvatar: function() {} },
`<aside class=\"quote no-group\" data-post=\"1\"> `<aside class=\"quote no-group\" data-username=\"bob\" data-post=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
bob:</div> bob:</div>
@ -440,7 +440,7 @@ QUnit.test("Quotes", assert => {
assert.cookedOptions( assert.cookedOptions(
`[quote="bob, post:1, topic:1"]\ntest quote\n[/quote]`, `[quote="bob, post:1, topic:1"]\ntest quote\n[/quote]`,
{ lookupPrimaryUserGroupByPostNumber: () => "aUserGroup" }, { lookupPrimaryUserGroupByPostNumber: () => "aUserGroup" },
`<aside class="quote group-aUserGroup" data-post="1" data-topic="1"> `<aside class="quote group-aUserGroup" data-username="bob" data-post="1" data-topic="1">
<div class="title"> <div class="title">
<div class="quote-controls"></div> <div class="quote-controls"></div>
bob:</div> bob:</div>
@ -1190,7 +1190,7 @@ QUnit.test("quotes", assert => {
QUnit.test("quote formatting", assert => { QUnit.test("quote formatting", assert => {
assert.cooked( assert.cooked(
'[quote="EvilTrout, post:123, topic:456, full:true"]\n[sam]\n[/quote]', '[quote="EvilTrout, post:123, topic:456, full:true"]\n[sam]\n[/quote]',
`<aside class=\"quote no-group\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"> `<aside class=\"quote no-group\" data-username=\"EvilTrout\" data-post=\"123\" data-topic=\"456\" data-full=\"true\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
EvilTrout:</div> EvilTrout:</div>
@ -1203,7 +1203,7 @@ QUnit.test("quote formatting", assert => {
assert.cooked( assert.cooked(
'[quote="eviltrout, post:1, topic:1"]\nabc\n[/quote]', '[quote="eviltrout, post:1, topic:1"]\nabc\n[/quote]',
`<aside class=\"quote no-group\" data-post=\"1\" data-topic=\"1\"> `<aside class=\"quote no-group\" data-username=\"eviltrout\" data-post=\"1\" data-topic=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
eviltrout:</div> eviltrout:</div>
@ -1216,7 +1216,7 @@ QUnit.test("quote formatting", assert => {
assert.cooked( assert.cooked(
'[quote="eviltrout, post:1, topic:1"]\nabc\n[/quote]\nhello', '[quote="eviltrout, post:1, topic:1"]\nabc\n[/quote]\nhello',
`<aside class=\"quote no-group\" data-post=\"1\" data-topic=\"1\"> `<aside class=\"quote no-group\" data-username=\"eviltrout\" data-post=\"1\" data-topic=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
eviltrout:</div> eviltrout:</div>
@ -1230,12 +1230,12 @@ QUnit.test("quote formatting", assert => {
assert.cooked( assert.cooked(
'[quote="Alice, post:1, topic:1"]\n[quote="Bob, post:2, topic:1"]\n[/quote]\n[/quote]', '[quote="Alice, post:1, topic:1"]\n[quote="Bob, post:2, topic:1"]\n[/quote]\n[/quote]',
`<aside class=\"quote no-group\" data-post=\"1\" data-topic=\"1\"> `<aside class=\"quote no-group\" data-username=\"Alice\" data-post=\"1\" data-topic=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
Alice:</div> Alice:</div>
<blockquote> <blockquote>
<aside class=\"quote no-group\" data-post=\"2\" data-topic=\"1\"> <aside class=\"quote no-group\" data-username=\"Bob\" data-post=\"2\" data-topic=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
Bob:</div> Bob:</div>
@ -1249,7 +1249,7 @@ QUnit.test("quote formatting", assert => {
assert.cooked( assert.cooked(
'[quote="Alice, post:1, topic:1"]\n[quote="Bob, post:2, topic:1"]\n[/quote]', '[quote="Alice, post:1, topic:1"]\n[quote="Bob, post:2, topic:1"]\n[/quote]',
`<p>[quote=&quot;Alice, post:1, topic:1&quot;]</p> `<p>[quote=&quot;Alice, post:1, topic:1&quot;]</p>
<aside class=\"quote no-group\" data-post=\"2\" data-topic=\"1\"> <aside class=\"quote no-group\" data-username=\"Bob\" data-post=\"2\" data-topic=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
Bob:</div> Bob:</div>
@ -1261,7 +1261,7 @@ QUnit.test("quote formatting", assert => {
assert.cooked( assert.cooked(
"[quote=\"Alice, post:1, topic:1\"]\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]", "[quote=\"Alice, post:1, topic:1\"]\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]",
`<aside class=\"quote no-group\" data-post=\"1\" data-topic=\"1\"> `<aside class=\"quote no-group\" data-username=\"Alice\" data-post=\"1\" data-topic=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
Alice:</div> Alice:</div>
@ -1276,7 +1276,7 @@ var bar = 'bar';
assert.cooked( assert.cooked(
"[quote=\"Alice, post:1, topic:1\"]\n\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]", "[quote=\"Alice, post:1, topic:1\"]\n\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]",
`<aside class=\"quote no-group\" data-post=\"1\" data-topic=\"1\"> `<aside class=\"quote no-group\" data-username=\"Alice\" data-post=\"1\" data-topic=\"1\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
Alice:</div> Alice:</div>
@ -1296,7 +1296,7 @@ QUnit.test("quotes with trailing formatting", assert => {
); );
assert.equal( assert.equal(
result, result,
`<aside class=\"quote no-group\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"> `<aside class=\"quote no-group\" data-username=\"EvilTrout\" data-post=\"123\" data-topic=\"456\" data-full=\"true\">
<div class=\"title\"> <div class=\"title\">
<div class=\"quote-controls\"></div> <div class=\"quote-controls\"></div>
EvilTrout:</div> EvilTrout:</div>