From eb619350b3070ec1552837b6b4f341d823fa9368 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:42:49 -0500 Subject: [PATCH] Disable pasting in composer when in background Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> --- ts/components/CompositionArea.tsx | 3 +++ ts/components/CompositionInput.stories.tsx | 1 + ts/components/CompositionInput.tsx | 13 +++++++++++-- ts/components/CompositionTextArea.tsx | 3 +++ ts/components/ForwardMessagesModal.stories.tsx | 1 + ts/components/ForwardMessagesModal.tsx | 1 + ts/components/MediaEditor.tsx | 1 + ts/components/StoryViewsNRepliesModal.tsx | 1 + ts/components/conversation/ConversationView.tsx | 8 +++++++- ts/quill/signal-clipboard/index.ts | 16 +++++++++++++++- ts/quill/types.d.ts | 2 ++ ts/state/smart/CompositionArea.tsx | 8 ++++++++ ts/state/smart/CompositionTextArea.tsx | 2 ++ ts/state/smart/ConversationView.tsx | 1 + 14 files changed, 57 insertions(+), 4 deletions(-) diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index 2edb66e1e1..c4a464dce4 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -111,6 +111,7 @@ export type OwnProps = Readonly<{ isGroupV1AndDisabled: boolean | null; isMissingMandatoryProfileSharing: boolean | null; isSignalConversation: boolean | null; + isActive: boolean; lastEditableMessageId: string | null; recordingState: RecordingState; messageCompositionId: string; @@ -236,6 +237,7 @@ export const CompositionArea = memo(function CompositionArea({ imageToBlurHash, isDisabled, isSignalConversation, + isActive, lastEditableMessageId, messageCompositionId, pushPanelForConversation, @@ -1001,6 +1003,7 @@ export const CompositionArea = memo(function CompositionArea({ i18n={i18n} inputApi={inputApiRef} isFormattingEnabled={isFormattingEnabled} + isActive={isActive} large={large} linkPreviewLoading={linkPreviewLoading} linkPreviewResult={linkPreviewResult} diff --git a/ts/components/CompositionInput.stories.tsx b/ts/components/CompositionInput.stories.tsx index 7c1179fb3e..98ec20fe6b 100644 --- a/ts/components/CompositionInput.stories.tsx +++ b/ts/components/CompositionInput.stories.tsx @@ -33,6 +33,7 @@ const useProps = (overrideProps: Partial = {}): Props => { clearQuotedMessage: action('clearQuotedMessage'), getPreferredBadge: () => undefined, getQuotedMessage: action('getQuotedMessage'), + isActive: true, isFormattingEnabled: overrideProps.isFormattingEnabled === false ? overrideProps.isFormattingEnabled diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index 6bbdb94b5f..07fe5c888f 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -105,6 +105,7 @@ export type Props = Readonly<{ large: boolean | null; inputApi: React.MutableRefObject | null; isFormattingEnabled: boolean; + isActive: boolean; sendCounter: number; skinTone: NonNullable | null; draftText: string | null; @@ -158,6 +159,7 @@ export function CompositionInput(props: Props): React.ReactElement { i18n, inputApi, isFormattingEnabled, + isActive, large, linkPreviewLoading, linkPreviewResult, @@ -409,9 +411,14 @@ export function CompositionInput(props: Props): React.ReactElement { isMouseDown, previousFormattingEnabled, previousIsMouseDown, - quillRef, ]); + React.useEffect(() => { + quillRef.current?.getModule('signalClipboard').updateOptions({ + isDisabled: !isActive, + }); + }, [isActive]); + const onEnter = (): boolean => { const quill = quillRef.current; const emojiCompletion = emojiCompletionRef.current; @@ -702,7 +709,9 @@ export function CompositionInput(props: Props): React.ReactElement { defaultValue={delta} modules={{ toolbar: false, - signalClipboard: true, + signalClipboard: { + isDisabled: !isActive, + }, clipboard: { matchers: [ ['IMG', matchEmojiImage], diff --git a/ts/components/CompositionTextArea.tsx b/ts/components/CompositionTextArea.tsx index 8fa90b8c4a..ccbb0082dc 100644 --- a/ts/components/CompositionTextArea.tsx +++ b/ts/components/CompositionTextArea.tsx @@ -21,6 +21,7 @@ import * as grapheme from '../util/grapheme'; export type CompositionTextAreaProps = { bodyRanges: HydratedBodyRangesType | null; i18n: LocalizerType; + isActive: boolean; isFormattingEnabled: boolean; maxLength?: number; placeholder?: string; @@ -58,6 +59,7 @@ export function CompositionTextArea({ draftText, getPreferredBadge, i18n, + isActive, isFormattingEnabled, maxLength, onChange, @@ -139,6 +141,7 @@ export function CompositionTextArea({ getPreferredBadge={getPreferredBadge} getQuotedMessage={noop} i18n={i18n} + isActive={isActive} isFormattingEnabled={isFormattingEnabled} inputApi={inputApiRef} large diff --git a/ts/components/ForwardMessagesModal.stories.tsx b/ts/components/ForwardMessagesModal.stories.tsx index a0ec491ce8..913096e721 100644 --- a/ts/components/ForwardMessagesModal.stories.tsx +++ b/ts/components/ForwardMessagesModal.stories.tsx @@ -62,6 +62,7 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ {...props} getPreferredBadge={() => undefined} i18n={i18n} + isActive isFormattingEnabled onPickEmoji={action('onPickEmoji')} onSetSkinTone={action('onSetSkinTone')} diff --git a/ts/components/ForwardMessagesModal.tsx b/ts/components/ForwardMessagesModal.tsx index 93f99d3c08..c64b425ad9 100644 --- a/ts/components/ForwardMessagesModal.tsx +++ b/ts/components/ForwardMessagesModal.tsx @@ -492,6 +492,7 @@ function ForwardMessageEditor({ void; processAttachments: (options: { @@ -24,6 +25,7 @@ export type PropsType = { export function ConversationView({ conversationId, hasOpenModal, + hasOpenPanel, isSelectMode, onExitSelectMode, processAttachments, @@ -57,6 +59,10 @@ export function ConversationView({ const onPaste = React.useCallback( (event: React.ClipboardEvent) => { + if (hasOpenModal || hasOpenPanel) { + return; + } + if (!event.clipboardData) { return; } @@ -102,7 +108,7 @@ export function ConversationView({ event.preventDefault(); } }, - [conversationId, processAttachments] + [conversationId, processAttachments, hasOpenModal, hasOpenPanel] ); useEscapeHandling( diff --git a/ts/quill/signal-clipboard/index.ts b/ts/quill/signal-clipboard/index.ts index 6684203098..deab0c6470 100644 --- a/ts/quill/signal-clipboard/index.ts +++ b/ts/quill/signal-clipboard/index.ts @@ -19,16 +19,30 @@ const prepareText = (text: string) => { return `${escapedEntities}`; }; +type ClipboardOptions = Readonly<{ + isDisabled: boolean; +}>; + export class SignalClipboard { quill: Quill; + options: ClipboardOptions; - constructor(quill: Quill) { + constructor(quill: Quill, options: ClipboardOptions) { this.quill = quill; + this.options = options; this.quill.root.addEventListener('paste', e => this.onCapturePaste(e)); } + updateOptions(options: Partial): void { + this.options = { ...this.options, ...options }; + } + onCapturePaste(event: ClipboardEvent): void { + if (this.options.isDisabled) { + return; + } + if (event.clipboardData == null) { event.preventDefault(); event.stopPropagation(); diff --git a/ts/quill/types.d.ts b/ts/quill/types.d.ts index 2e046c0857..70c86708f3 100644 --- a/ts/quill/types.d.ts +++ b/ts/quill/types.d.ts @@ -5,6 +5,7 @@ import type UpdatedDelta from 'quill-delta'; import type { MentionCompletion } from './mentions/completion'; import type { EmojiCompletion } from './emoji/completion'; import type { FormattingMenu } from './formatting/menu'; +import type { SignalClipboard } from './signal-clipboard'; declare module 'react-quill' { // `react-quill` uses a different but compatible version of Delta @@ -88,6 +89,7 @@ declare module 'quill' { getModule(module: 'formattingMenu'): FormattingMenu; getModule(module: 'history'): HistoryStatic; getModule(module: 'mentionCompletion'): MentionCompletion; + getModule(module: 'signalClipboard'): SignalClipboard; getModule(module: string): unknown; selection: SelectionStatic; diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index 4777d43c95..a2a0dccf99 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -64,6 +64,7 @@ import { useEmojisActions } from '../ducks/emojis'; import { useGlobalModalActions } from '../ducks/globalModals'; import { useStickersActions } from '../ducks/stickers'; import { useToastActions } from '../ducks/toast'; +import { isShowingAnyModal } from '../selectors/globalModals'; function renderSmartCompositionRecording( recProps: SmartCompositionRecordingProps @@ -107,6 +108,8 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ const errorDialogAudioRecorderType = useSelector( getErrorDialogAudioRecorderType ); + const hasGlobalModalOpen = useSelector(isShowingAnyModal); + const hasPanelOpen = useSelector(getHasPanelOpen); const getGroupAdmins = useSelector(getGroupAdminsSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector); const composerStateForConversationIdSelector = useSelector( @@ -126,6 +129,10 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ shouldSendHighQualityAttachments, } = composerState; + const isActive = useMemo(() => { + return !hasGlobalModalOpen && !hasPanelOpen; + }, [hasGlobalModalOpen, hasPanelOpen]); + const groupAdmins = useMemo(() => { return getGroupAdmins(id); }, [getGroupAdmins, id]); @@ -244,6 +251,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ i18n={i18n} isDisabled={isDisabled} isFormattingEnabled={isFormattingEnabled} + isActive={isActive} lastEditableMessageId={lastEditableMessageId ?? null} messageCompositionId={messageCompositionId} platform={platform} diff --git a/ts/state/smart/CompositionTextArea.tsx b/ts/state/smart/CompositionTextArea.tsx index 81b7a5e3bd..36fcd25986 100644 --- a/ts/state/smart/CompositionTextArea.tsx +++ b/ts/state/smart/CompositionTextArea.tsx @@ -15,6 +15,7 @@ export type SmartCompositionTextAreaProps = Pick< CompositionTextAreaProps, | 'bodyRanges' | 'draftText' + | 'isActive' | 'placeholder' | 'onChange' | 'onScroll' @@ -43,6 +44,7 @@ export const SmartCompositionTextArea = memo(function SmartCompositionTextArea( {...props} getPreferredBadge={getPreferredBadge} i18n={i18n} + isActive isFormattingEnabled={isFormattingEnabled} onPickEmoji={onPickEmoji} onSetSkinTone={onSetSkinTone} diff --git a/ts/state/smart/ConversationView.tsx b/ts/state/smart/ConversationView.tsx index 6f5fbf3a3e..4109dc6fb4 100644 --- a/ts/state/smart/ConversationView.tsx +++ b/ts/state/smart/ConversationView.tsx @@ -61,6 +61,7 @@ export const SmartConversationView = memo(