浏览代码

Correctly display multi-line nested quotes (#3339)

* Correctly display multi-line nested quotes

* Add previous change to changelog

* Correctly display quotes with empty lines without spaces

* Correctly collapse lines inside quotes
BetaRays 1 年之前
父节点
当前提交
adbea038a8
共有 4 个文件被更改,包括 39 次插入4 次删除
  1. 1 0
      CHANGES.md
  2. 31 0
      src/plugins/chatview/tests/styling.js
  3. 4 1
      src/shared/rich-text.js
  4. 3 3
      src/shared/styling.js

+ 1 - 0
CHANGES.md

@@ -7,6 +7,7 @@
 - #3300: Adding the maxWait option for `debouncedPruneHistory`
 - #3302: debounce MUC sidebar rendering
 - #3307: Fix inconsistency between browsers on textarea outlines
+- #3337: Correctly display multiline nested quotes
 - Add an occupants filter to the MUC sidebar
 - Fix: MUC occupant list does not sort itself on nicknames or roles changes
 - Fix: refresh the MUC sidebar when participants collection is sorted

+ 31 - 0
src/plugins/chatview/tests/styling.js

@@ -397,6 +397,37 @@ describe("An incoming chat Message", function () {
             `<blockquote>What do you think of it <span class="mention" data-uri="romeo@montague.lit">romeo</span>?</blockquote>\n `+
             `Did you see this <span class="mention" data-uri="romeo@montague.lit">romeo</span>?`);
 
+        msg_text = '> > This is a nested quote...\n> > spanning multiple lines!\n> It is.\nYes.';
+        msg = mock.createChatMessage(_converse, contact_jid, msg_text)
+        await _converse.handleMessageStanza(msg);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 14);
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
+        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
+            '<blockquote><blockquote>This is a nested quote...\n\u200B\u200B\u200B\u200Bspanning multiple lines!</blockquote>'
+            + '\n\u200B\u200BIt is.</blockquote>\nYes.'
+        );
+
+        msg_text = '> This next line will be purposefully empty, and include no trailing space.\n>\n> This is the end of this quote.';
+        msg = mock.createChatMessage(_converse, contact_jid, msg_text)
+        await _converse.handleMessageStanza(msg);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 15);
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
+        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
+            '<blockquote>This next line will be purposefully empty, and include no trailing space.'+
+            '\n\u200B\n\u200B\u200BThis is the end of this quote.</blockquote>'
+        );
+
+        msg_text = '> Quotes shouldn’t prevent multiple lines from being collapsed together.\n> \n> \n> \n> Like so.';
+        msg = mock.createChatMessage(_converse, contact_jid, msg_text)
+        await _converse.handleMessageStanza(msg);
+        await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 16);
+        msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
+        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
+            '<blockquote>Quotes shouldn’t prevent multiple lines from being collapsed together.'+
+            '\n\u200B\u200B\u200B\u200B\u200B\u200B\u200B\u200B'+ // block of removed '> ' and newlines
+            '\n\u200B\u200BLike so.</blockquote>'
+        );
+
         expect(true).toBe(true);
     }));
 

+ 4 - 1
src/shared/rich-text.js

@@ -29,7 +29,10 @@ const isString = s => typeof s === 'string';
 
 // We don't render more than two line-breaks, replace extra line-breaks with
 // the zero-width whitespace character
-const collapseLineBreaks = text => text.replace(/\n\n+/g, m => `\n${'\u200B'.repeat(m.length - 2)}\n`);
+// This takes into account other characters that may have been removed by
+// being replaced with a zero-width space, such as '> ' in the case of
+// multi-line quotes.
+const collapseLineBreaks = text => text.replace(/\n(\u200B*\n)+/g, m => `\n${'\u200B'.repeat(m.length - 2)}\n`);
 
 const tplMentionWithNick = o => html`<span class="mention mention--self badge badge-info" data-uri="${o.uri}">${o.mention}</span>`;
 const tplMention = o => html`<span class="mention" data-uri="${o.uri}">${o.mention}</span>`;

+ 3 - 3
src/shared/styling.js

@@ -106,7 +106,7 @@ function getDirectiveLength (d, text, i) {
     const begin = i;
     i += d.length;
     if (isQuoteDirective(d)) {
-        i += text.slice(i).split(/\n[^>]/).shift().length;
+        i += text.slice(i).split(/\n\u200B*[^>\u200B]/).shift().length;
         return i-begin;
     } else if (styling_map[d].type === 'span') {
         const line = text.slice(i).split('\n').shift();
@@ -150,8 +150,8 @@ export function getDirectiveTemplate (d, text, offset, options) {
     if (isQuoteDirective(d)) {
         const newtext = text
             // Don't show the directive itself
-            .replace(/\n>\s/g, '\n\u200B\u200B')
-            .replace(/\n>/g, '\n\u200B')
+            // This big [] corresponds to \s without newlines, to avoid issues when the > is the last character of the line
+            .replace(/\n\u200B*>[ \f\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?/g, m => `\n${'\u200B'.repeat(m.length - 1)}`)
             .replace(/\n$/, ''); // Trim line-break at the end
         return template(newtext, offset, options);
     } else {