Better handling of formatting in pasted text
This commit is contained in:
parent
d012779e87
commit
a31cf5645e
12 changed files with 105 additions and 43 deletions
|
@ -1,5 +1,5 @@
|
||||||
diff --git a/node_modules/@formatjs/intl/src/types.d.ts b/node_modules/@formatjs/intl/src/types.d.ts
|
diff --git a/node_modules/@formatjs/intl/src/types.d.ts b/node_modules/@formatjs/intl/src/types.d.ts
|
||||||
index 1f73905..9abaacd 100644
|
index e36094c..2ee473e 100644
|
||||||
--- a/node_modules/@formatjs/intl/src/types.d.ts
|
--- a/node_modules/@formatjs/intl/src/types.d.ts
|
||||||
+++ b/node_modules/@formatjs/intl/src/types.d.ts
|
+++ b/node_modules/@formatjs/intl/src/types.d.ts
|
||||||
@@ -8,10 +8,8 @@ import { DEFAULT_INTL_CONFIG } from './utils';
|
@@ -8,10 +8,8 @@ import { DEFAULT_INTL_CONFIG } from './utils';
|
|
@ -1,5 +1,5 @@
|
||||||
diff --git a/node_modules/quill/dist/quill.js b/node_modules/quill/dist/quill.js
|
diff --git a/node_modules/quill/dist/quill.js b/node_modules/quill/dist/quill.js
|
||||||
index 811b3d0..313f301 100644
|
index 811b3d0..b31c7fd 100644
|
||||||
--- a/node_modules/quill/dist/quill.js
|
--- a/node_modules/quill/dist/quill.js
|
||||||
+++ b/node_modules/quill/dist/quill.js
|
+++ b/node_modules/quill/dist/quill.js
|
||||||
@@ -8916,10 +8916,10 @@ var Clipboard = function (_Module) {
|
@@ -8916,10 +8916,10 @@ var Clipboard = function (_Module) {
|
||||||
|
@ -72,7 +72,20 @@ index 811b3d0..313f301 100644
|
||||||
var style = computeStyle(node);
|
var style = computeStyle(node);
|
||||||
- return ['block', 'list-item'].indexOf(style.display) > -1;
|
- return ['block', 'list-item'].indexOf(style.display) > -1;
|
||||||
+ // return ['block', 'list-item'].indexOf(style.display) > -1;
|
+ // return ['block', 'list-item'].indexOf(style.display) > -1;
|
||||||
+ return ['block', 'list-item'].indexOf(style.display) > -1 || node.nodeName === 'DIV' || node.nodeName === 'P';
|
+ return ['block', 'list-item'].indexOf(style.display) > -1 || node.nodeName === 'DIV' || node.nodeName === 'P' || node.nodeName === 'TIME';
|
||||||
}
|
}
|
||||||
|
|
||||||
function traverse(node, elementMatchers, textMatchers) {
|
function traverse(node, elementMatchers, textMatchers) {
|
||||||
|
@@ -9177,8 +9183,10 @@ function matchIndent(node, delta) {
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchNewline(node, delta) {
|
||||||
|
- if (!deltaEndsWith(delta, '\n')) {
|
||||||
|
- if (isLine(node) || delta.length() > 0 && node.nextSibling && isLine(node.nextSibling)) {
|
||||||
|
+ // if (!deltaEndsWith(delta, '\n')) {
|
||||||
|
+ if (!deltaEndsWith(delta, '\n\n')) {
|
||||||
|
+ // if (isLine(node) || delta.length() > 0 && node.nextSibling && isLine(node.nextSibling)) {
|
||||||
|
+ if (delta.length() > 0 && isLine(node)) {
|
||||||
|
delta.insert('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,22 +49,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--spoiler--copy-target {
|
|
||||||
// We don't want this thing to affect the layout of the message
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
// We can use left here; this is not visible to the user
|
|
||||||
/* stylelint-disable liberty/use-logical-spec */
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
height: 1px;
|
|
||||||
width: 1px;
|
|
||||||
|
|
||||||
// Hide text
|
|
||||||
color: transparent;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: This is referenced in formatting/matchers.ts, to detect these styles on paste
|
// Note: This is referenced in formatting/matchers.ts, to detect these styles on paste
|
||||||
&--spoiler--noninteractive {
|
&--spoiler--noninteractive {
|
||||||
cursor: inherit;
|
cursor: inherit;
|
||||||
|
|
|
@ -188,6 +188,7 @@ import { setBatchingStrategy } from './util/messageBatcher';
|
||||||
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
|
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
|
||||||
import { makeLookup } from './util/makeLookup';
|
import { makeLookup } from './util/makeLookup';
|
||||||
import { addGlobalKeyboardShortcuts } from './services/addGlobalKeyboardShortcuts';
|
import { addGlobalKeyboardShortcuts } from './services/addGlobalKeyboardShortcuts';
|
||||||
|
import { handleCopyEvent } from './quill/signal-clipboard/util';
|
||||||
|
|
||||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||||
|
@ -550,6 +551,9 @@ export async function startApp(): Promise<void> {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Intercept clipboard copies to add our custom text/signal data
|
||||||
|
document.addEventListener('copy', handleCopyEvent);
|
||||||
|
|
||||||
startInteractionMode();
|
startInteractionMode();
|
||||||
|
|
||||||
// We add this to window here because the default Node context is erased at the end
|
// We add this to window here because the default Node context is erased at the end
|
||||||
|
|
|
@ -15,7 +15,6 @@ import type {
|
||||||
RangeNode,
|
RangeNode,
|
||||||
} from '../../types/BodyRange';
|
} from '../../types/BodyRange';
|
||||||
import {
|
import {
|
||||||
SPOILER_REPLACEMENT,
|
|
||||||
BodyRange,
|
BodyRange,
|
||||||
insertRange,
|
insertRange,
|
||||||
collapseRangeTree,
|
collapseRangeTree,
|
||||||
|
@ -215,12 +214,6 @@ function renderNode({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
className="MessageTextRenderer__formatting--spoiler--copy-target"
|
|
||||||
>
|
|
||||||
{SPOILER_REPLACEMENT}
|
|
||||||
</span>
|
|
||||||
<span aria-hidden>{content}</span>
|
<span aria-hidden>{content}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -29,15 +29,15 @@ export function TimelineDateHeader({
|
||||||
}, [i18n, timestamp]);
|
}, [i18n, timestamp]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Time
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'TimelineDateHeader',
|
'TimelineDateHeader',
|
||||||
`TimelineDateHeader--${floating ? 'floating' : 'inline'}`
|
`TimelineDateHeader--${floating ? 'floating' : 'inline'}`
|
||||||
)}
|
)}
|
||||||
dateOnly
|
|
||||||
timestamp={timestamp}
|
|
||||||
>
|
>
|
||||||
{text}
|
<Time dateOnly timestamp={timestamp}>
|
||||||
</Time>
|
{text}
|
||||||
|
</Time>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,8 @@ import { insertEmojiOps } from '../util';
|
||||||
|
|
||||||
export const matchEmojiImage = (node: Element, delta: Delta): Delta => {
|
export const matchEmojiImage = (node: Element, delta: Delta): Delta => {
|
||||||
if (
|
if (
|
||||||
(node.classList.contains('emoji') ||
|
node.classList.contains('emoji') ||
|
||||||
node.classList.contains('module-emoji__image--16px')) &&
|
node.classList.contains('module-emoji__image--16px')
|
||||||
!node.classList.contains('emoji--invisible')
|
|
||||||
) {
|
) {
|
||||||
const emoji = node.getAttribute('aria-label');
|
const emoji = node.getAttribute('aria-label');
|
||||||
return new Delta().insert({ emoji });
|
return new Delta().insert({ emoji });
|
||||||
|
|
|
@ -64,8 +64,8 @@ export const matchMonospace = (node: HTMLElement, delta: Delta): Delta => {
|
||||||
export const matchSpoiler = (node: HTMLElement, delta: Delta): Delta => {
|
export const matchSpoiler = (node: HTMLElement, delta: Delta): Delta => {
|
||||||
const classes = [
|
const classes = [
|
||||||
'quill--spoiler',
|
'quill--spoiler',
|
||||||
|
'MessageTextRenderer__formatting--spoiler',
|
||||||
'MessageTextRenderer__formatting--spoiler--revealed',
|
'MessageTextRenderer__formatting--spoiler--revealed',
|
||||||
// Note: we don't match on hidden spoilers in message body; we use copy-target text
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -13,10 +13,7 @@ export const matchMention =
|
||||||
if (memberRepository) {
|
if (memberRepository) {
|
||||||
const { title } = node.dataset;
|
const { title } = node.dataset;
|
||||||
|
|
||||||
if (
|
if (node.classList.contains('MessageBody__at-mention')) {
|
||||||
node.classList.contains('MessageBody__at-mention') &&
|
|
||||||
!node.classList.contains('MessageBody__at-mention--invisible')
|
|
||||||
) {
|
|
||||||
const { id } = node.dataset;
|
const { id } = node.dataset;
|
||||||
const conversation = memberRepository.getMemberById(id);
|
const conversation = memberRepository.getMemberById(id);
|
||||||
|
|
||||||
|
|
|
@ -46,17 +46,17 @@ export class SignalClipboard {
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = event.clipboardData.getData('text/plain');
|
const text = event.clipboardData.getData('text/plain');
|
||||||
const html = event.clipboardData.getData('text/html');
|
const signal = event.clipboardData.getData('text/signal');
|
||||||
|
|
||||||
if (!text && !html) {
|
if (!text && !signal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const clipboardDelta = html
|
const clipboardDelta = signal
|
||||||
? clipboard.convert(html)
|
? clipboard.convert(signal)
|
||||||
: clipboard.convert(replaceAngleBrackets(text));
|
: clipboard.convert(replaceAngleBrackets(text));
|
||||||
|
|
||||||
const { scrollTop } = this.quill.scrollingContainer;
|
const { scrollTop } = this.quill.scrollingContainer;
|
||||||
|
|
64
ts/quill/signal-clipboard/util.ts
Normal file
64
ts/quill/signal-clipboard/util.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export function handleCopyEvent(event: ClipboardEvent): void {
|
||||||
|
if (!event.clipboardData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create synthetic html with the full selection we can put into clipboard
|
||||||
|
const container = document.createElement('div');
|
||||||
|
for (let i = 0, max = selection.rangeCount; i < max; i += 1) {
|
||||||
|
const range = selection.getRangeAt(i);
|
||||||
|
container.appendChild(range.cloneContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we can't leave text/plain alone and just add text/signal; if we update
|
||||||
|
// clipboardData at all, all other data is reset.
|
||||||
|
const plaintext = getStringFromNode(container);
|
||||||
|
event.clipboardData?.setData('text/plain', plaintext);
|
||||||
|
|
||||||
|
event.clipboardData?.setData('text/signal', container.innerHTML);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFromNode(node: Node): string {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
return node.textContent || '';
|
||||||
|
}
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = node as Element;
|
||||||
|
if (element.nodeName === 'IMG' && element.classList.contains('emoji')) {
|
||||||
|
return element.ariaLabel || '';
|
||||||
|
}
|
||||||
|
if (element.nodeName === 'BR') {
|
||||||
|
return '\n';
|
||||||
|
}
|
||||||
|
if (element.childNodes.length === 0) {
|
||||||
|
return element.textContent || '';
|
||||||
|
}
|
||||||
|
let result = '';
|
||||||
|
for (const child of element.childNodes) {
|
||||||
|
result += getStringFromNode(child);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
element.nodeName === 'P' ||
|
||||||
|
element.nodeName === 'DIV' ||
|
||||||
|
element.nodeName === 'TIME'
|
||||||
|
) {
|
||||||
|
if (result.length > 0 && !result.endsWith('\n\n')) {
|
||||||
|
result += '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -2552,6 +2552,14 @@
|
||||||
"updated": "2023-04-22T00:07:56.294Z",
|
"updated": "2023-04-22T00:07:56.294Z",
|
||||||
"reasonDetail": "We need a persistent timer to track long-hovers"
|
"reasonDetail": "We need a persistent timer to track long-hovers"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "DOM-innerHTML",
|
||||||
|
"path": "ts/quill/signal-clipboard/util.ts",
|
||||||
|
"line": " event.clipboardData?.setData('text/signal', container.innerHTML);",
|
||||||
|
"reasonCategory": "regexMatchedSafeCode",
|
||||||
|
"updated": "2023-05-22T23:45:02.074Z",
|
||||||
|
"reasonDetail": "Reading from innerHTML, not setting it"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/state/smart/InstallScreen.tsx",
|
"path": "ts/state/smart/InstallScreen.tsx",
|
||||||
|
|
Loading…
Reference in a new issue