From 73efaaeb1cde81a1370bd6f1eeed5008dacf8afc Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 6 Dec 2022 13:29:47 +0100 Subject: [PATCH] Message List: fix copying of selected messages with quotes (#2172) --- .../mediaViewer/helpers/ghostAnimation.ts | 4 ++-- src/components/middle/message/Invoice.tsx | 6 +++--- src/components/middle/message/Location.tsx | 6 +++--- src/components/middle/message/Photo.tsx | 6 +++--- .../message/helpers/buildContentClassName.ts | 4 ++-- src/config.ts | 2 ++ src/hooks/useNativeCopySelectedMessages.ts | 2 +- src/util/getMessageIdsForSelectedText.ts | 19 ++++++++++++++++++- 8 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/components/mediaViewer/helpers/ghostAnimation.ts b/src/components/mediaViewer/helpers/ghostAnimation.ts index 616f50d51..ead25680f 100644 --- a/src/components/mediaViewer/helpers/ghostAnimation.ts +++ b/src/components/mediaViewer/helpers/ghostAnimation.ts @@ -2,7 +2,7 @@ import type { ApiMessage, ApiDimensions } from '../../../api/types'; import { MediaViewerOrigin } from '../../../types'; -import { ANIMATION_END_DELAY } from '../../../config'; +import { ANIMATION_END_DELAY, MESSAGE_CONTENT_SELECTOR } from '../../../config'; import { calculateDimensions, getMediaViewerAvailableDimensions, @@ -330,7 +330,7 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) { case MediaViewerOrigin.Inline: default: containerSelector = `.Transition__slide--active > .MessageList #${getMessageHtmlId(message!.id)}`; - mediaSelector = '.message-content .full-media, .message-content .thumbnail'; + mediaSelector = `${MESSAGE_CONTENT_SELECTOR} .full-media, ${MESSAGE_CONTENT_SELECTOR} .thumbnail`; } const container = document.querySelector(containerSelector)!; diff --git a/src/components/middle/message/Invoice.tsx b/src/components/middle/message/Invoice.tsx index 9deb3c6a8..d14a60933 100644 --- a/src/components/middle/message/Invoice.tsx +++ b/src/components/middle/message/Invoice.tsx @@ -4,7 +4,7 @@ import type { FC } from '../../../lib/teact/teact'; import type { ApiMessage } from '../../../api/types'; import type { ISettings } from '../../../types'; -import { CUSTOM_APPENDIX_ATTRIBUTE } from '../../../config'; +import { CUSTOM_APPENDIX_ATTRIBUTE, MESSAGE_CONTENT_SELECTOR } from '../../../config'; import { getMessageInvoice, getWebDocumentHash } from '../../../global/helpers'; import { formatCurrency } from '../../../util/formatCurrency'; import renderText from '../../common/helpers/renderText'; @@ -53,13 +53,13 @@ const Invoice: FC = ({ useLayoutEffectWithPrevDeps(([prevShouldAffectAppendix]) => { if (!shouldAffectAppendix) { if (prevShouldAffectAppendix) { - ref.current!.closest('.message-content')!.removeAttribute(CUSTOM_APPENDIX_ATTRIBUTE); + ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!.removeAttribute(CUSTOM_APPENDIX_ATTRIBUTE); } return; } if (photoUrl) { - const contentEl = ref.current!.closest('.message-content')!; + const contentEl = ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!; getCustomAppendixBg(photoUrl, false, isInSelectMode, isSelected, theme).then((appendixBg) => { contentEl.style.setProperty('--appendix-bg', appendixBg); contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, ''); diff --git a/src/components/middle/message/Location.tsx b/src/components/middle/message/Location.tsx index 4d2543d2c..48b93ba6f 100644 --- a/src/components/middle/message/Location.tsx +++ b/src/components/middle/message/Location.tsx @@ -7,7 +7,7 @@ import type { FC } from '../../../lib/teact/teact'; import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types'; import type { ISettings } from '../../../types'; -import { CUSTOM_APPENDIX_ATTRIBUTE } from '../../../config'; +import { CUSTOM_APPENDIX_ATTRIBUTE, MESSAGE_CONTENT_SELECTOR } from '../../../config'; import { getMessageLocation, buildStaticMapHash, @@ -150,13 +150,13 @@ const Location: FC = ({ useLayoutEffectWithPrevDeps(([prevShouldRenderText]) => { if (shouldRenderText) { if (!prevShouldRenderText) { - ref.current!.closest('.message-content')!.removeAttribute(CUSTOM_APPENDIX_ATTRIBUTE); + ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!.removeAttribute(CUSTOM_APPENDIX_ATTRIBUTE); } return; } if (mapBlobUrl) { - const contentEl = ref.current!.closest('.message-content')!; + const contentEl = ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!; getCustomAppendixBg(mapBlobUrl, isOwn, isInSelectMode, isSelected, theme).then((appendixBg) => { contentEl.style.setProperty('--appendix-bg', appendixBg); contentEl.classList.add('has-appendix-thumb'); diff --git a/src/components/middle/message/Photo.tsx b/src/components/middle/message/Photo.tsx index 2b48ae29f..d3f23a821 100644 --- a/src/components/middle/message/Photo.tsx +++ b/src/components/middle/message/Photo.tsx @@ -8,7 +8,7 @@ import type { ISettings } from '../../../types'; import type { IMediaDimensions } from './helpers/calculateAlbumLayout'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; -import { CUSTOM_APPENDIX_ATTRIBUTE } from '../../../config'; +import { CUSTOM_APPENDIX_ATTRIBUTE, MESSAGE_CONTENT_SELECTOR } from '../../../config'; import { getMessagePhoto, getMessageWebPagePhoto, @@ -132,12 +132,12 @@ const Photo: FC = ({ useLayoutEffectWithPrevDeps(([prevShouldAffectAppendix]) => { if (!shouldAffectAppendix) { if (prevShouldAffectAppendix) { - ref.current!.closest('.message-content')!.removeAttribute(CUSTOM_APPENDIX_ATTRIBUTE); + ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!.removeAttribute(CUSTOM_APPENDIX_ATTRIBUTE); } return; } - const contentEl = ref.current!.closest('.message-content')!; + const contentEl = ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!; if (fullMediaData) { getCustomAppendixBg(fullMediaData, isOwn, isInSelectMode, isSelected, theme).then((appendixBg) => { contentEl.style.setProperty('--appendix-bg', appendixBg); diff --git a/src/components/middle/message/helpers/buildContentClassName.ts b/src/components/middle/message/helpers/buildContentClassName.ts index 0da0c8ff6..95bfdce8e 100644 --- a/src/components/middle/message/helpers/buildContentClassName.ts +++ b/src/components/middle/message/helpers/buildContentClassName.ts @@ -1,6 +1,6 @@ import type { ApiMessage } from '../../../../api/types'; -import { EMOJI_SIZES } from '../../../../config'; +import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config'; import { getMessageContent } from '../../../../global/helpers'; export function buildContentClassName( @@ -35,7 +35,7 @@ export function buildContentClassName( text, photo, video, audio, voice, document, poll, webPage, contact, location, invoice, } = getMessageContent(message); - const classNames = ['message-content']; + const classNames = [MESSAGE_CONTENT_CLASS_NAME]; const isMedia = photo || video || location || invoice?.extendedMedia; const hasText = text || location?.type === 'venue' || isGeoLiveActive; const isMediaWithNoText = isMedia && !hasText; diff --git a/src/config.ts b/src/config.ts index 7be04d87f..23cf40d29 100644 --- a/src/config.ts +++ b/src/config.ts @@ -102,6 +102,8 @@ export const EDITABLE_INPUT_MODAL_ID = 'editable-message-text-modal'; export const EDITABLE_INPUT_CSS_SELECTOR = `.messages-layout .Transition__slide--active #${EDITABLE_INPUT_ID}, .messages-layout .Transition > .to #${EDITABLE_INPUT_ID}`; export const CUSTOM_APPENDIX_ATTRIBUTE = 'data-has-custom-appendix'; +export const MESSAGE_CONTENT_CLASS_NAME = 'message-content'; +export const MESSAGE_CONTENT_SELECTOR = '.message-content'; // Screen width where Pinned Message / Audio Player in the Middle Header can be safely displayed export const SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN = 1440; // px diff --git a/src/hooks/useNativeCopySelectedMessages.ts b/src/hooks/useNativeCopySelectedMessages.ts index 5379989c4..f05544707 100644 --- a/src/hooks/useNativeCopySelectedMessages.ts +++ b/src/hooks/useNativeCopySelectedMessages.ts @@ -5,7 +5,7 @@ const useNativeCopySelectedMessages = (copyMessagesByIds: ({ messageIds }: { mes function handleCopy(e: KeyboardEvent) { const messageIds = getMessageIdsForSelectedText(); - if (messageIds && messageIds.length > 0) { + if (messageIds && messageIds.length > 1) { e.preventDefault(); copyMessagesByIds({ messageIds }); } diff --git a/src/util/getMessageIdsForSelectedText.ts b/src/util/getMessageIdsForSelectedText.ts index aa6464b3e..22f1a588a 100644 --- a/src/util/getMessageIdsForSelectedText.ts +++ b/src/util/getMessageIdsForSelectedText.ts @@ -1,8 +1,13 @@ +import { MESSAGE_CONTENT_CLASS_NAME } from '../config'; + const ELEMENT_NODE = 1; export default function getMessageIdsForSelectedText() { const selection = window.getSelection(); - let selectedFragments = selection && selection.rangeCount ? selection.getRangeAt(0).cloneContents() : undefined; + let selectedFragments = selection?.rangeCount ? selection.getRangeAt(0).cloneContents() : undefined; + + const shouldIncludeLastMessage = selection?.focusNode && selection.focusOffset > 0 + && hasParentWithClassName(selection.focusNode, MESSAGE_CONTENT_CLASS_NAME); if (!selectedFragments || selectedFragments.childElementCount === 0) { return undefined; } @@ -25,5 +30,17 @@ export default function getMessageIdsForSelectedText() { } selectedFragments = undefined; + if (!shouldIncludeLastMessage) { + messageIds.pop(); + } + return messageIds; } + +function hasParentWithClassName(element: Node, className: string): boolean { + if (element.nodeType === ELEMENT_NODE && (element as HTMLElement).classList.contains(className)) { + return true; + } + + return element.parentNode ? hasParentWithClassName(element.parentNode, className) : false; +}