diff --git a/patches/@formatjs+intl+2.4.1.patch b/patches/@formatjs+intl+2.6.7.patch similarity index 95% rename from patches/@formatjs+intl+2.4.1.patch rename to patches/@formatjs+intl+2.6.7.patch index 66fbd7b50..00ee45288 100644 --- a/patches/@formatjs+intl+2.4.1.patch +++ b/patches/@formatjs+intl+2.6.7.patch @@ -1,5 +1,5 @@ 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 +++ b/node_modules/@formatjs/intl/src/types.d.ts @@ -8,10 +8,8 @@ import { DEFAULT_INTL_CONFIG } from './utils'; diff --git a/patches/quill+1.3.7.patch b/patches/quill+1.3.7.patch index 0652d68f1..75a0f54f0 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..313f301 100644 +index 811b3d0..b31c7fd 100644 --- a/node_modules/quill/dist/quill.js +++ b/node_modules/quill/dist/quill.js @@ -8916,10 +8916,10 @@ var Clipboard = function (_Module) { @@ -72,7 +72,20 @@ index 811b3d0..313f301 100644 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 || 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) { +@@ -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'); + } + } diff --git a/stylesheets/components/MessageTextRenderer.scss b/stylesheets/components/MessageTextRenderer.scss index bec15faa0..9d5db01fe 100644 --- a/stylesheets/components/MessageTextRenderer.scss +++ b/stylesheets/components/MessageTextRenderer.scss @@ -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 &--spoiler--noninteractive { cursor: inherit; diff --git a/ts/background.ts b/ts/background.ts index e88876e5b..1b738ec2f 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -188,6 +188,7 @@ import { setBatchingStrategy } from './util/messageBatcher'; import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration'; import { makeLookup } from './util/makeLookup'; import { addGlobalKeyboardShortcuts } from './services/addGlobalKeyboardShortcuts'; +import { handleCopyEvent } from './quill/signal-clipboard/util'; export function isOverHourIntoPast(timestamp: number): boolean { return isNumber(timestamp) && isOlderThan(timestamp, HOUR); @@ -550,6 +551,9 @@ export async function startApp(): Promise { false ); + // Intercept clipboard copies to add our custom text/signal data + document.addEventListener('copy', handleCopyEvent); + startInteractionMode(); // We add this to window here because the default Node context is erased at the end diff --git a/ts/components/conversation/MessageTextRenderer.tsx b/ts/components/conversation/MessageTextRenderer.tsx index f1cfc9a33..a4be2200c 100644 --- a/ts/components/conversation/MessageTextRenderer.tsx +++ b/ts/components/conversation/MessageTextRenderer.tsx @@ -15,7 +15,6 @@ import type { RangeNode, } from '../../types/BodyRange'; import { - SPOILER_REPLACEMENT, BodyRange, insertRange, collapseRangeTree, @@ -215,12 +214,6 @@ function renderNode({ } } > - - {SPOILER_REPLACEMENT} - {content} ); diff --git a/ts/components/conversation/TimelineDateHeader.tsx b/ts/components/conversation/TimelineDateHeader.tsx index 17cb8ea64..eee5b9c66 100644 --- a/ts/components/conversation/TimelineDateHeader.tsx +++ b/ts/components/conversation/TimelineDateHeader.tsx @@ -29,15 +29,15 @@ export function TimelineDateHeader({ }, [i18n, timestamp]); return ( - + + ); } diff --git a/ts/quill/emoji/matchers.ts b/ts/quill/emoji/matchers.ts index ea2324227..39b3af61e 100644 --- a/ts/quill/emoji/matchers.ts +++ b/ts/quill/emoji/matchers.ts @@ -6,9 +6,8 @@ import { insertEmojiOps } from '../util'; export const matchEmojiImage = (node: Element, delta: Delta): Delta => { if ( - (node.classList.contains('emoji') || - node.classList.contains('module-emoji__image--16px')) && - !node.classList.contains('emoji--invisible') + node.classList.contains('emoji') || + node.classList.contains('module-emoji__image--16px') ) { const emoji = node.getAttribute('aria-label'); return new Delta().insert({ emoji }); diff --git a/ts/quill/formatting/matchers.ts b/ts/quill/formatting/matchers.ts index 31d4c2bae..4804a4484 100644 --- a/ts/quill/formatting/matchers.ts +++ b/ts/quill/formatting/matchers.ts @@ -64,8 +64,8 @@ export const matchMonospace = (node: HTMLElement, delta: Delta): Delta => { export const matchSpoiler = (node: HTMLElement, delta: Delta): Delta => { const classes = [ 'quill--spoiler', + 'MessageTextRenderer__formatting--spoiler', 'MessageTextRenderer__formatting--spoiler--revealed', - // Note: we don't match on hidden spoilers in message body; we use copy-target text ]; if ( diff --git a/ts/quill/mentions/matchers.ts b/ts/quill/mentions/matchers.ts index 1189e6f1d..526663816 100644 --- a/ts/quill/mentions/matchers.ts +++ b/ts/quill/mentions/matchers.ts @@ -13,10 +13,7 @@ export const matchMention = if (memberRepository) { const { title } = node.dataset; - if ( - node.classList.contains('MessageBody__at-mention') && - !node.classList.contains('MessageBody__at-mention--invisible') - ) { + if (node.classList.contains('MessageBody__at-mention')) { const { id } = node.dataset; const conversation = memberRepository.getMemberById(id); diff --git a/ts/quill/signal-clipboard/index.ts b/ts/quill/signal-clipboard/index.ts index 47f618eb4..7ab2912d1 100644 --- a/ts/quill/signal-clipboard/index.ts +++ b/ts/quill/signal-clipboard/index.ts @@ -46,17 +46,17 @@ export class SignalClipboard { } 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; } event.preventDefault(); event.stopPropagation(); - const clipboardDelta = html - ? clipboard.convert(html) + const clipboardDelta = signal + ? clipboard.convert(signal) : clipboard.convert(replaceAngleBrackets(text)); const { scrollTop } = this.quill.scrollingContainer; diff --git a/ts/quill/signal-clipboard/util.ts b/ts/quill/signal-clipboard/util.ts new file mode 100644 index 000000000..f2da5487d --- /dev/null +++ b/ts/quill/signal-clipboard/util.ts @@ -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; +} diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 49bcb6e00..52da48606 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -2552,6 +2552,14 @@ "updated": "2023-04-22T00:07:56.294Z", "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", "path": "ts/state/smart/InstallScreen.tsx",