Disable pasting in composer when in background

This commit is contained in:
Jamie Kyle 2024-06-13 16:22:07 -07:00 committed by GitHub
parent b315162676
commit 5dcb42f964
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 57 additions and 4 deletions

View file

@ -111,6 +111,7 @@ export type OwnProps = Readonly<{
isGroupV1AndDisabled: boolean | null; isGroupV1AndDisabled: boolean | null;
isMissingMandatoryProfileSharing: boolean | null; isMissingMandatoryProfileSharing: boolean | null;
isSignalConversation: boolean | null; isSignalConversation: boolean | null;
isActive: boolean;
lastEditableMessageId: string | null; lastEditableMessageId: string | null;
recordingState: RecordingState; recordingState: RecordingState;
messageCompositionId: string; messageCompositionId: string;
@ -236,6 +237,7 @@ export const CompositionArea = memo(function CompositionArea({
imageToBlurHash, imageToBlurHash,
isDisabled, isDisabled,
isSignalConversation, isSignalConversation,
isActive,
lastEditableMessageId, lastEditableMessageId,
messageCompositionId, messageCompositionId,
pushPanelForConversation, pushPanelForConversation,
@ -1001,6 +1003,7 @@ export const CompositionArea = memo(function CompositionArea({
i18n={i18n} i18n={i18n}
inputApi={inputApiRef} inputApi={inputApiRef}
isFormattingEnabled={isFormattingEnabled} isFormattingEnabled={isFormattingEnabled}
isActive={isActive}
large={large} large={large}
linkPreviewLoading={linkPreviewLoading} linkPreviewLoading={linkPreviewLoading}
linkPreviewResult={linkPreviewResult} linkPreviewResult={linkPreviewResult}

View file

@ -33,6 +33,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => {
clearQuotedMessage: action('clearQuotedMessage'), clearQuotedMessage: action('clearQuotedMessage'),
getPreferredBadge: () => undefined, getPreferredBadge: () => undefined,
getQuotedMessage: action('getQuotedMessage'), getQuotedMessage: action('getQuotedMessage'),
isActive: true,
isFormattingEnabled: isFormattingEnabled:
overrideProps.isFormattingEnabled === false overrideProps.isFormattingEnabled === false
? overrideProps.isFormattingEnabled ? overrideProps.isFormattingEnabled

View file

@ -105,6 +105,7 @@ export type Props = Readonly<{
large: boolean | null; large: boolean | null;
inputApi: React.MutableRefObject<InputApi | undefined> | null; inputApi: React.MutableRefObject<InputApi | undefined> | null;
isFormattingEnabled: boolean; isFormattingEnabled: boolean;
isActive: boolean;
sendCounter: number; sendCounter: number;
skinTone: NonNullable<EmojiPickDataType['skinTone']> | null; skinTone: NonNullable<EmojiPickDataType['skinTone']> | null;
draftText: string | null; draftText: string | null;
@ -158,6 +159,7 @@ export function CompositionInput(props: Props): React.ReactElement {
i18n, i18n,
inputApi, inputApi,
isFormattingEnabled, isFormattingEnabled,
isActive,
large, large,
linkPreviewLoading, linkPreviewLoading,
linkPreviewResult, linkPreviewResult,
@ -409,9 +411,14 @@ export function CompositionInput(props: Props): React.ReactElement {
isMouseDown, isMouseDown,
previousFormattingEnabled, previousFormattingEnabled,
previousIsMouseDown, previousIsMouseDown,
quillRef,
]); ]);
React.useEffect(() => {
quillRef.current?.getModule('signalClipboard').updateOptions({
isDisabled: !isActive,
});
}, [isActive]);
const onEnter = (): boolean => { const onEnter = (): boolean => {
const quill = quillRef.current; const quill = quillRef.current;
const emojiCompletion = emojiCompletionRef.current; const emojiCompletion = emojiCompletionRef.current;
@ -702,7 +709,9 @@ export function CompositionInput(props: Props): React.ReactElement {
defaultValue={delta} defaultValue={delta}
modules={{ modules={{
toolbar: false, toolbar: false,
signalClipboard: true, signalClipboard: {
isDisabled: !isActive,
},
clipboard: { clipboard: {
matchers: [ matchers: [
['IMG', matchEmojiImage], ['IMG', matchEmojiImage],

View file

@ -21,6 +21,7 @@ import * as grapheme from '../util/grapheme';
export type CompositionTextAreaProps = { export type CompositionTextAreaProps = {
bodyRanges: HydratedBodyRangesType | null; bodyRanges: HydratedBodyRangesType | null;
i18n: LocalizerType; i18n: LocalizerType;
isActive: boolean;
isFormattingEnabled: boolean; isFormattingEnabled: boolean;
maxLength?: number; maxLength?: number;
placeholder?: string; placeholder?: string;
@ -58,6 +59,7 @@ export function CompositionTextArea({
draftText, draftText,
getPreferredBadge, getPreferredBadge,
i18n, i18n,
isActive,
isFormattingEnabled, isFormattingEnabled,
maxLength, maxLength,
onChange, onChange,
@ -139,6 +141,7 @@ export function CompositionTextArea({
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
getQuotedMessage={noop} getQuotedMessage={noop}
i18n={i18n} i18n={i18n}
isActive={isActive}
isFormattingEnabled={isFormattingEnabled} isFormattingEnabled={isFormattingEnabled}
inputApi={inputApiRef} inputApi={inputApiRef}
large large

View file

@ -62,6 +62,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
{...props} {...props}
getPreferredBadge={() => undefined} getPreferredBadge={() => undefined}
i18n={i18n} i18n={i18n}
isActive
isFormattingEnabled isFormattingEnabled
onPickEmoji={action('onPickEmoji')} onPickEmoji={action('onPickEmoji')}
onSetSkinTone={action('onSetSkinTone')} onSetSkinTone={action('onSetSkinTone')}

View file

@ -492,6 +492,7 @@ function ForwardMessageEditor({
<RenderCompositionTextArea <RenderCompositionTextArea
bodyRanges={draft.bodyRanges ?? null} bodyRanges={draft.bodyRanges ?? null}
draftText={draft.messageBody ?? ''} draftText={draft.messageBody ?? ''}
isActive
onChange={onChange} onChange={onChange}
onSubmit={onSubmit} onSubmit={onSubmit}
theme={theme} theme={theme}

View file

@ -1300,6 +1300,7 @@ export function MediaEditor({
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
inputApi={inputApiRef} inputApi={inputApiRef}
isActive
isFormattingEnabled={isFormattingEnabled} isFormattingEnabled={isFormattingEnabled}
moduleClassName="StoryViewsNRepliesModal__input" moduleClassName="StoryViewsNRepliesModal__input"
onCloseLinkPreview={noop} onCloseLinkPreview={noop}

View file

@ -236,6 +236,7 @@ export function StoryViewsNRepliesModal({
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
inputApi={inputApiRef} inputApi={inputApiRef}
isActive
isFormattingEnabled={isFormattingEnabled} isFormattingEnabled={isFormattingEnabled}
moduleClassName="StoryViewsNRepliesModal__input" moduleClassName="StoryViewsNRepliesModal__input"
onCloseLinkPreview={noop} onCloseLinkPreview={noop}

View file

@ -8,6 +8,7 @@ import { useEscapeHandling } from '../../hooks/useEscapeHandling';
export type PropsType = { export type PropsType = {
conversationId: string; conversationId: string;
hasOpenModal: boolean; hasOpenModal: boolean;
hasOpenPanel: boolean;
isSelectMode: boolean; isSelectMode: boolean;
onExitSelectMode: () => void; onExitSelectMode: () => void;
processAttachments: (options: { processAttachments: (options: {
@ -24,6 +25,7 @@ export type PropsType = {
export function ConversationView({ export function ConversationView({
conversationId, conversationId,
hasOpenModal, hasOpenModal,
hasOpenPanel,
isSelectMode, isSelectMode,
onExitSelectMode, onExitSelectMode,
processAttachments, processAttachments,
@ -57,6 +59,10 @@ export function ConversationView({
const onPaste = React.useCallback( const onPaste = React.useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => { (event: React.ClipboardEvent<HTMLDivElement>) => {
if (hasOpenModal || hasOpenPanel) {
return;
}
if (!event.clipboardData) { if (!event.clipboardData) {
return; return;
} }
@ -102,7 +108,7 @@ export function ConversationView({
event.preventDefault(); event.preventDefault();
} }
}, },
[conversationId, processAttachments] [conversationId, processAttachments, hasOpenModal, hasOpenPanel]
); );
useEscapeHandling( useEscapeHandling(

View file

@ -19,16 +19,30 @@ const prepareText = (text: string) => {
return `<span>${escapedEntities}</span>`; return `<span>${escapedEntities}</span>`;
}; };
type ClipboardOptions = Readonly<{
isDisabled: boolean;
}>;
export class SignalClipboard { export class SignalClipboard {
quill: Quill; quill: Quill;
options: ClipboardOptions;
constructor(quill: Quill) { constructor(quill: Quill, options: ClipboardOptions) {
this.quill = quill; this.quill = quill;
this.options = options;
this.quill.root.addEventListener('paste', e => this.onCapturePaste(e)); this.quill.root.addEventListener('paste', e => this.onCapturePaste(e));
} }
updateOptions(options: Partial<ClipboardOptions>): void {
this.options = { ...this.options, ...options };
}
onCapturePaste(event: ClipboardEvent): void { onCapturePaste(event: ClipboardEvent): void {
if (this.options.isDisabled) {
return;
}
if (event.clipboardData == null) { if (event.clipboardData == null) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

2
ts/quill/types.d.ts vendored
View file

@ -5,6 +5,7 @@ import type UpdatedDelta from 'quill-delta';
import type { MentionCompletion } from './mentions/completion'; import type { MentionCompletion } from './mentions/completion';
import type { EmojiCompletion } from './emoji/completion'; import type { EmojiCompletion } from './emoji/completion';
import type { FormattingMenu } from './formatting/menu'; import type { FormattingMenu } from './formatting/menu';
import type { SignalClipboard } from './signal-clipboard';
declare module 'react-quill' { declare module 'react-quill' {
// `react-quill` uses a different but compatible version of Delta // `react-quill` uses a different but compatible version of Delta
@ -88,6 +89,7 @@ declare module 'quill' {
getModule(module: 'formattingMenu'): FormattingMenu; getModule(module: 'formattingMenu'): FormattingMenu;
getModule(module: 'history'): HistoryStatic; getModule(module: 'history'): HistoryStatic;
getModule(module: 'mentionCompletion'): MentionCompletion; getModule(module: 'mentionCompletion'): MentionCompletion;
getModule(module: 'signalClipboard'): SignalClipboard;
getModule(module: string): unknown; getModule(module: string): unknown;
selection: SelectionStatic; selection: SelectionStatic;

View file

@ -64,6 +64,7 @@ import { useEmojisActions } from '../ducks/emojis';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { useStickersActions } from '../ducks/stickers'; import { useStickersActions } from '../ducks/stickers';
import { useToastActions } from '../ducks/toast'; import { useToastActions } from '../ducks/toast';
import { isShowingAnyModal } from '../selectors/globalModals';
function renderSmartCompositionRecording( function renderSmartCompositionRecording(
recProps: SmartCompositionRecordingProps recProps: SmartCompositionRecordingProps
@ -107,6 +108,8 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
const errorDialogAudioRecorderType = useSelector( const errorDialogAudioRecorderType = useSelector(
getErrorDialogAudioRecorderType getErrorDialogAudioRecorderType
); );
const hasGlobalModalOpen = useSelector(isShowingAnyModal);
const hasPanelOpen = useSelector(getHasPanelOpen);
const getGroupAdmins = useSelector(getGroupAdminsSelector); const getGroupAdmins = useSelector(getGroupAdminsSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const composerStateForConversationIdSelector = useSelector( const composerStateForConversationIdSelector = useSelector(
@ -126,6 +129,10 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
shouldSendHighQualityAttachments, shouldSendHighQualityAttachments,
} = composerState; } = composerState;
const isActive = useMemo(() => {
return !hasGlobalModalOpen && !hasPanelOpen;
}, [hasGlobalModalOpen, hasPanelOpen]);
const groupAdmins = useMemo(() => { const groupAdmins = useMemo(() => {
return getGroupAdmins(id); return getGroupAdmins(id);
}, [getGroupAdmins, id]); }, [getGroupAdmins, id]);
@ -244,6 +251,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
i18n={i18n} i18n={i18n}
isDisabled={isDisabled} isDisabled={isDisabled}
isFormattingEnabled={isFormattingEnabled} isFormattingEnabled={isFormattingEnabled}
isActive={isActive}
lastEditableMessageId={lastEditableMessageId ?? null} lastEditableMessageId={lastEditableMessageId ?? null}
messageCompositionId={messageCompositionId} messageCompositionId={messageCompositionId}
platform={platform} platform={platform}

View file

@ -15,6 +15,7 @@ export type SmartCompositionTextAreaProps = Pick<
CompositionTextAreaProps, CompositionTextAreaProps,
| 'bodyRanges' | 'bodyRanges'
| 'draftText' | 'draftText'
| 'isActive'
| 'placeholder' | 'placeholder'
| 'onChange' | 'onChange'
| 'onScroll' | 'onScroll'
@ -43,6 +44,7 @@ export const SmartCompositionTextArea = memo(function SmartCompositionTextArea(
{...props} {...props}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
isActive
isFormattingEnabled={isFormattingEnabled} isFormattingEnabled={isFormattingEnabled}
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}

View file

@ -61,6 +61,7 @@ export const SmartConversationView = memo(
<ConversationView <ConversationView
conversationId={conversationId} conversationId={conversationId}
hasOpenModal={hasOpenModal} hasOpenModal={hasOpenModal}
hasOpenPanel={activePanel != null}
isSelectMode={isSelectMode} isSelectMode={isSelectMode}
onExitSelectMode={onExitSelectMode} onExitSelectMode={onExitSelectMode}
processAttachments={processAttachments} processAttachments={processAttachments}