diff --git a/patches/quill+1.3.7.patch b/patches/quill+1.3.7.patch
index 7205d7352..b21d23edd 100644
--- a/patches/quill+1.3.7.patch
+++ b/patches/quill+1.3.7.patch
@@ -1,5 +1,5 @@
diff --git a/node_modules/quill/dist/quill.js b/node_modules/quill/dist/quill.js
-index 811b3d0..1082f2a 100644
+index 811b3d0..135dfb2 100644
--- a/node_modules/quill/dist/quill.js
+++ b/node_modules/quill/dist/quill.js
@@ -8896,7 +8896,8 @@ var debug = (0, _logger2.default)('quill:clipboard');
@@ -27,7 +27,7 @@ index 811b3d0..1082f2a 100644
_this.matchers = [];
CLIPBOARD_CONFIG.concat(_this.options.matchers).forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
-@@ -8941,15 +8942,18 @@ var Clipboard = function (_Module) {
+@@ -8941,28 +8942,33 @@ var Clipboard = function (_Module) {
key: 'convert',
value: function convert(html) {
if (typeof html === 'string') {
@@ -52,7 +52,14 @@ index 811b3d0..1082f2a 100644
var _prepareMatching = this.prepareMatching(),
_prepareMatching2 = _slicedToArray(_prepareMatching, 2),
-@@ -8962,7 +8966,8 @@ var Clipboard = function (_Module) {
+ elementMatchers = _prepareMatching2[0],
+ textMatchers = _prepareMatching2[1];
+
+- var delta = traverse(this.container, elementMatchers, textMatchers);
++ // var delta = traverse(this.container, elementMatchers, textMatchers);
++ var delta = traverse(this.container, elementMatchers, textMatchers, formats);
+ // Remove trailing newline
+ if (deltaEndsWith(delta, '\n') && delta.ops[delta.ops.length - 1].attributes == null) {
delta = delta.compose(new _quillDelta2.default().retain(delta.length() - 1).delete(1));
}
debug.log('convert', this.container.innerHTML, delta);
@@ -62,7 +69,7 @@ index 811b3d0..1082f2a 100644
return delta;
}
}, {
-@@ -9056,9 +9061,10 @@ function applyFormat(delta, format, value) {
+@@ -9056,9 +9062,10 @@ function applyFormat(delta, format, value) {
}
function computeStyle(node) {
@@ -76,7 +83,7 @@ index 811b3d0..1082f2a 100644
}
function deltaEndsWith(delta, text) {
-@@ -9074,7 +9080,8 @@ function deltaEndsWith(delta, text) {
+@@ -9074,24 +9081,30 @@ function deltaEndsWith(delta, text) {
function isLine(node) {
if (node.childNodes.length === 0) return false; // Exclude embed blocks
var style = computeStyle(node);
@@ -85,8 +92,35 @@ index 811b3d0..1082f2a 100644
+ return ['block', 'list-item'].indexOf(style.display) > -1 || node.nodeName === 'DIV' || node.nodeName === 'P' || node.nodeName === 'TIME';
}
- function traverse(node, elementMatchers, textMatchers) {
-@@ -9177,8 +9184,10 @@ function matchIndent(node, delta) {
+-function traverse(node, elementMatchers, textMatchers) {
++// function traverse(node, elementMatchers, textMatchers) {
++function traverse(node, elementMatchers, textMatchers, attributes) {
+ // Post-order
+ if (node.nodeType === node.TEXT_NODE) {
+ return textMatchers.reduce(function (delta, matcher) {
+- return matcher(node, delta);
++ // return matcher(node, delta);
++ return matcher(node, delta, attributes);
+ }, new _quillDelta2.default());
+ } else if (node.nodeType === node.ELEMENT_NODE) {
+ return [].reduce.call(node.childNodes || [], function (delta, childNode) {
+- var childrenDelta = traverse(childNode, elementMatchers, textMatchers);
++ // var childrenDelta = traverse(childNode, elementMatchers, textMatchers);
++ var childrenDelta = traverse(childNode, elementMatchers, textMatchers, attributes);
+ if (childNode.nodeType === node.ELEMENT_NODE) {
+ childrenDelta = elementMatchers.reduce(function (childrenDelta, matcher) {
+- return matcher(childNode, childrenDelta);
++ // return matcher(childNode, childrenDelta);
++ return matcher(childNode, childrenDelta, attributes);
+ }, childrenDelta);
+ childrenDelta = (childNode[DOM_KEY] || []).reduce(function (childrenDelta, matcher) {
+- return matcher(childNode, childrenDelta);
++ // return matcher(childNode, childrenDelta);
++ return matcher(childNode, childrenDelta, attributes);
+ }, childrenDelta);
+ }
+ return delta.concat(childrenDelta);
+@@ -9177,8 +9190,10 @@ function matchIndent(node, delta) {
}
function matchNewline(node, delta) {
@@ -99,3 +133,22 @@ index 811b3d0..1082f2a 100644
delta.insert('\n');
}
}
+@@ -9214,7 +9229,7 @@ function matchStyles(node, delta) {
+ return delta;
+ }
+
+-function matchText(node, delta) {
++function matchText(node, delta, attributes) {
+ var text = node.data;
+ // Word represents empty line with
+ if (node.parentNode.tagName === 'O:P') {
+@@ -9238,7 +9253,7 @@ function matchText(node, delta) {
+ text = text.replace(/\s+$/, replacer.bind(replacer, false));
+ }
+ }
+- return delta.insert(text);
++ return delta.insert(text, attributes);
+ }
+
+ exports.default = Clipboard;
+\ No newline at end of file
diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx
index c07ebf89c..40caed661 100644
--- a/ts/components/CompositionInput.tsx
+++ b/ts/components/CompositionInput.tsx
@@ -213,7 +213,7 @@ export function CompositionInput(props: Props): React.ReactElement {
}, []);
const nodes = collapseRangeTree({ tree, text });
const opsWithFormattingAndMentions = insertFormattingAndMentionsOps(nodes);
- const opsWithEmojis = insertEmojiOps(opsWithFormattingAndMentions);
+ const opsWithEmojis = insertEmojiOps(opsWithFormattingAndMentions, {});
return new Delta(opsWithEmojis);
};
diff --git a/ts/quill/emoji/matchers.ts b/ts/quill/emoji/matchers.ts
index 39b3af61e..60b454f51 100644
--- a/ts/quill/emoji/matchers.ts
+++ b/ts/quill/emoji/matchers.ts
@@ -2,33 +2,56 @@
// SPDX-License-Identifier: AGPL-3.0-only
import Delta from 'quill-delta';
+import type { Matcher, AttributeMap } from 'quill';
+
import { insertEmojiOps } from '../util';
-export const matchEmojiImage = (node: Element, delta: Delta): Delta => {
+export const matchEmojiImage: Matcher = (
+ node: Element,
+ delta: Delta,
+ attributes: AttributeMap
+): Delta => {
if (
node.classList.contains('emoji') ||
node.classList.contains('module-emoji__image--16px')
) {
const emoji = node.getAttribute('aria-label');
- return new Delta().insert({ emoji });
+ return new Delta().insert({ emoji }, attributes);
}
return delta;
};
-export const matchEmojiBlot = (node: HTMLElement, delta: Delta): Delta => {
+export const matchEmojiBlot: Matcher = (
+ node: HTMLElement,
+ delta: Delta,
+ attributes: AttributeMap
+): Delta => {
if (node.classList.contains('emoji-blot')) {
const { emoji } = node.dataset;
- return new Delta().insert({ emoji });
+ return new Delta().insert({ emoji }, attributes);
}
return delta;
};
-export const matchEmojiText = (node: Text): Delta => {
- if (node.data.replace(/(\n|\r\n)/g, '') === '') {
+export const matchEmojiText: Matcher = (
+ node: HTMLElement,
+ _delta: Delta,
+ attributes: AttributeMap
+): Delta => {
+ if (!('data' in node)) {
return new Delta();
}
- const nodeAsInsert = { insert: node.data };
+ const { data } = node;
+ if (!data || typeof data !== 'string') {
+ return new Delta();
+ }
- return new Delta(insertEmojiOps([nodeAsInsert]));
+ if (data.replace(/(\n|\r\n)/g, '') === '') {
+ return new Delta();
+ }
+
+ const nodeAsInsert = { insert: data, attributes };
+
+ return new Delta(insertEmojiOps([nodeAsInsert], attributes));
};
diff --git a/ts/quill/formatting/matchers.ts b/ts/quill/formatting/matchers.ts
index 4804a4484..5f867441b 100644
--- a/ts/quill/formatting/matchers.ts
+++ b/ts/quill/formatting/matchers.ts
@@ -2,13 +2,20 @@
// SPDX-License-Identifier: AGPL-3.0-only
import Delta from 'quill-delta';
+import type { Matcher, AttributeMap } from 'quill';
+
import { QuillFormattingStyle } from './menu';
-function applyStyleToOps(delta: Delta, style: QuillFormattingStyle): Delta {
+function applyStyleToOps(
+ delta: Delta,
+ style: QuillFormattingStyle,
+ attributes: AttributeMap
+): Delta {
return new Delta(
delta.map(op => ({
...op,
attributes: {
+ ...attributes,
...op.attributes,
[style]: true,
},
@@ -16,31 +23,47 @@ function applyStyleToOps(delta: Delta, style: QuillFormattingStyle): Delta {
);
}
-export const matchBold = (_node: HTMLElement, delta: Delta): Delta => {
+export const matchBold: Matcher = (
+ _node: HTMLElement,
+ delta: Delta,
+ attributes: AttributeMap
+): Delta => {
if (delta.length() > 0) {
- return applyStyleToOps(delta, QuillFormattingStyle.bold);
+ return applyStyleToOps(delta, QuillFormattingStyle.bold, attributes);
}
return delta;
};
-export const matchItalic = (_node: HTMLElement, delta: Delta): Delta => {
+export const matchItalic: Matcher = (
+ _node: HTMLElement,
+ delta: Delta,
+ attributes: AttributeMap
+): Delta => {
if (delta.length() > 0) {
- return applyStyleToOps(delta, QuillFormattingStyle.italic);
+ return applyStyleToOps(delta, QuillFormattingStyle.italic, attributes);
}
return delta;
};
-export const matchStrikethrough = (_node: HTMLElement, delta: Delta): Delta => {
+export const matchStrikethrough: Matcher = (
+ _node: HTMLElement,
+ delta: Delta,
+ attributes: AttributeMap
+): Delta => {
if (delta.length() > 0) {
- return applyStyleToOps(delta, QuillFormattingStyle.strike);
+ return applyStyleToOps(delta, QuillFormattingStyle.strike, attributes);
}
return delta;
};
-export const matchMonospace = (node: HTMLElement, delta: Delta): Delta => {
+export const matchMonospace: Matcher = (
+ node: HTMLElement,
+ delta: Delta,
+ attributes: AttributeMap
+): Delta => {
const classes = [
'MessageTextRenderer__formatting--monospace',
'quill--monospace',
@@ -55,13 +78,17 @@ export const matchMonospace = (node: HTMLElement, delta: Delta): Delta => {
node.classList.contains(classes[1]) ||
node.attributes.getNamedItem('style')?.value?.includes(fontFamily))
) {
- return applyStyleToOps(delta, QuillFormattingStyle.monospace);
+ return applyStyleToOps(delta, QuillFormattingStyle.monospace, attributes);
}
return delta;
};
-export const matchSpoiler = (node: HTMLElement, delta: Delta): Delta => {
+export const matchSpoiler: Matcher = (
+ node: HTMLElement,
+ delta: Delta,
+ attributes: AttributeMap
+): Delta => {
const classes = [
'quill--spoiler',
'MessageTextRenderer__formatting--spoiler',
@@ -74,7 +101,7 @@ export const matchSpoiler = (node: HTMLElement, delta: Delta): Delta => {
node.classList.contains(classes[1]) ||
node.classList.contains(classes[2]))
) {
- return applyStyleToOps(delta, QuillFormattingStyle.spoiler);
+ return applyStyleToOps(delta, QuillFormattingStyle.spoiler, attributes);
}
return delta;
};
diff --git a/ts/quill/mentions/matchers.ts b/ts/quill/mentions/matchers.ts
index 526663816..6bbc8cb9b 100644
--- a/ts/quill/mentions/matchers.ts
+++ b/ts/quill/mentions/matchers.ts
@@ -3,11 +3,15 @@
import Delta from 'quill-delta';
import type { RefObject } from 'react';
+import type { Matcher, AttributeMap } from 'quill';
+
import type { MemberRepository } from '../memberRepository';
-export const matchMention =
+export const matchMention: (
+ memberRepositoryRef: RefObject
+) => Matcher =
(memberRepositoryRef: RefObject) =>
- (node: HTMLElement, delta: Delta): Delta => {
+ (node: HTMLElement, delta: Delta, attributes: AttributeMap): Delta => {
const memberRepository = memberRepositoryRef.current;
if (memberRepository) {
@@ -18,15 +22,18 @@ export const matchMention =
const conversation = memberRepository.getMemberById(id);
if (conversation && conversation.uuid) {
- return new Delta().insert({
- mention: {
- title,
- uuid: conversation.uuid,
+ return new Delta().insert(
+ {
+ mention: {
+ title,
+ uuid: conversation.uuid,
+ },
},
- });
+ attributes
+ );
}
- return new Delta().insert(`@${title}`);
+ return new Delta().insert(`@${title}`, attributes);
}
if (node.classList.contains('mention-blot')) {
@@ -34,15 +41,18 @@ export const matchMention =
const conversation = memberRepository.getMemberByUuid(uuid);
if (conversation && conversation.uuid) {
- return new Delta().insert({
- mention: {
- title: title || conversation.title,
- uuid: conversation.uuid,
+ return new Delta().insert(
+ {
+ mention: {
+ title: title || conversation.title,
+ uuid: conversation.uuid,
+ },
},
- });
+ attributes
+ );
}
- return new Delta().insert(`@${title}`);
+ return new Delta().insert(`@${title}`, attributes);
}
}
diff --git a/ts/quill/signal-clipboard/index.ts b/ts/quill/signal-clipboard/index.ts
index ffd86c907..a8a39b252 100644
--- a/ts/quill/signal-clipboard/index.ts
+++ b/ts/quill/signal-clipboard/index.ts
@@ -4,17 +4,19 @@
import type Quill from 'quill';
import Delta from 'quill-delta';
-const replaceAngleBrackets = (text: string) => {
+const prepareText = (text: string) => {
const entities: Array<[RegExp, string]> = [
[/&/g, '&'],
[//g, '>'],
];
- return entities.reduce(
+ const escapedEntities = entities.reduce(
(acc, [re, replaceValue]) => acc.replace(re, replaceValue),
text
);
+
+ return `${escapedEntities}`;
};
export class SignalClipboard {
@@ -53,7 +55,7 @@ export class SignalClipboard {
const clipboardDelta = signal
? clipboard.convert(signal)
- : clipboard.convert(replaceAngleBrackets(text));
+ : clipboard.convert(prepareText(text));
const { scrollTop } = this.quill.scrollingContainer;
diff --git a/ts/quill/types.d.ts b/ts/quill/types.d.ts
index cb661d307..2e046c085 100644
--- a/ts/quill/types.d.ts
+++ b/ts/quill/types.d.ts
@@ -25,6 +25,16 @@ declare module 'quill' {
shortKey?: boolean;
}
+ export type AttributeMap = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [key: string]: any;
+ };
+ export type Matcher = (
+ node: HTMLElement,
+ delta: UpdatedDelta,
+ attributes: AttributeMap
+ ) => UpdatedDelta;
+
export type UpdatedTextChangeHandler = (
delta: UpdatedDelta,
oldContents: UpdatedDelta,
diff --git a/ts/quill/util.ts b/ts/quill/util.ts
index 2a5a92cb3..0e2263d1a 100644
--- a/ts/quill/util.ts
+++ b/ts/quill/util.ts
@@ -3,7 +3,7 @@
import emojiRegex from 'emoji-regex';
import Delta from 'quill-delta';
-import type { LeafBlot, DeltaOperation } from 'quill';
+import type { LeafBlot, DeltaOperation, AttributeMap } from 'quill';
import type Op from 'quill-delta/dist/Op';
import type {
@@ -387,7 +387,10 @@ export const insertMentionOps = (
return ops;
};
-export const insertEmojiOps = (incomingOps: ReadonlyArray): Array => {
+export const insertEmojiOps = (
+ incomingOps: ReadonlyArray,
+ existingAttributes: AttributeMap
+): Array => {
return incomingOps.reduce((ops, op) => {
if (typeof op.insert === 'string') {
const text = op.insert;
@@ -400,7 +403,10 @@ export const insertEmojiOps = (incomingOps: ReadonlyArray): Array => {
while ((match = re.exec(text))) {
const [emoji] = match;
ops.push({ insert: text.slice(index, match.index), attributes });
- ops.push({ insert: { emoji }, attributes });
+ ops.push({
+ insert: { emoji },
+ attributes: { ...existingAttributes, ...attributes },
+ });
index = match.index + emoji.length;
}
diff --git a/ts/test-node/quill/mentions/matchers_test.ts b/ts/test-node/quill/mentions/matchers_test.ts
index a1adaf3ab..f5e013957 100644
--- a/ts/test-node/quill/mentions/matchers_test.ts
+++ b/ts/test-node/quill/mentions/matchers_test.ts
@@ -90,25 +90,29 @@ const EMPTY_DELTA = new Delta();
describe('matchMention', () => {
it('handles an AtMentionify from clipboard', () => {
+ const existingAttributes = { italic: true };
const result = matcher(
createMockAtMentionElement({
id: memberMahershala.id,
title: memberMahershala.title,
}),
- EMPTY_DELTA
+ EMPTY_DELTA,
+ existingAttributes
);
const { ops } = result;
assert.isNotEmpty(ops);
const [op] = ops;
- const { insert } = op;
+ const { insert, attributes } = op;
if (isMention(insert)) {
const { title, uuid } = insert.mention;
assert.equal(title, memberMahershala.title);
assert.equal(uuid, memberMahershala.uuid);
+
+ assert.deepEqual(existingAttributes, attributes, 'attributes');
} else {
assert.fail('insert is invalid');
}
@@ -120,7 +124,8 @@ describe('matchMention', () => {
uuid: memberMahershala.uuid || '',
title: memberMahershala.title,
}),
- EMPTY_DELTA
+ EMPTY_DELTA,
+ {}
);
const { ops } = result;
@@ -145,7 +150,8 @@ describe('matchMention', () => {
id: 'florp',
title: 'Nonexistent',
}),
- EMPTY_DELTA
+ EMPTY_DELTA,
+ {}
);
const { ops } = result;
@@ -167,7 +173,8 @@ describe('matchMention', () => {
uuid: 'florp',
title: 'Nonexistent',
}),
- EMPTY_DELTA
+ EMPTY_DELTA,
+ {}
);
const { ops } = result;
@@ -184,7 +191,7 @@ describe('matchMention', () => {
});
it('passes other clipboard elements through', () => {
- const result = matcher(createMockElement('ignore', {}), EMPTY_DELTA);
+ const result = matcher(createMockElement('ignore', {}), EMPTY_DELTA, {});
assert.equal(result, EMPTY_DELTA);
});
});