Skip to content

Commit

Permalink
Merge pull request #49228 from dominictb/fix/46766
Browse files Browse the repository at this point in the history
fix: pasting long text freezes app
  • Loading branch information
Julesssss authored Dec 12, 2024
2 parents bf38806 + 6d245d7 commit 50c1db8
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 31 deletions.
7 changes: 6 additions & 1 deletion src/components/RNMarkdownTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import React from 'react';
import type {TextInput} from 'react-native';
import Animated from 'react-native-reanimated';
import useTheme from '@hooks/useTheme';
import CONST from '@src/CONST';

// Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet
const AnimatedMarkdownTextInput = Animated.createAnimatedComponent(MarkdownTextInput);

type AnimatedMarkdownTextInputRef = typeof AnimatedMarkdownTextInput & TextInput & HTMLInputElement;

function RNMarkdownTextInputWithRef(props: MarkdownTextInputProps, ref: ForwardedRef<AnimatedMarkdownTextInputRef>) {
function RNMarkdownTextInputWithRef({maxLength, ...props}: MarkdownTextInputProps, ref: ForwardedRef<AnimatedMarkdownTextInputRef>) {
const theme = useTheme();

return (
Expand All @@ -27,6 +28,10 @@ function RNMarkdownTextInputWithRef(props: MarkdownTextInputProps, ref: Forwarde
}}
// eslint-disable-next-line
{...props}
/**
* If maxLength is not set, we should set the it to CONST.MAX_COMMENT_LENGTH + 1, to avoid parsing markdown for large text
*/
maxLength={maxLength ?? CONST.MAX_COMMENT_LENGTH + 1}
/>
);
}
Expand Down
76 changes: 46 additions & 30 deletions src/hooks/useHtmlPaste/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import {useNavigation} from '@react-navigation/native';
import {useCallback, useEffect} from 'react';
import Parser from '@libs/Parser';
import CONST from '@src/CONST';
import type UseHtmlPaste from './types';

const insertByCommand = (text: string) => {
document.execCommand('insertText', false, text);
};
const insertAtCaret = (target: HTMLElement, insertedText: string, maxLength: number) => {
const currentText = target.textContent ?? '';

let availableLength = maxLength - currentText.length;
if (availableLength <= 0) {
return;
}

let text = insertedText;

const insertAtCaret = (target: HTMLElement, text: string) => {
const selection = window.getSelection();
if (selection?.rangeCount) {
const range = selection.getRangeAt(0);
const selectedText = range.toString();
availableLength -= selectedText.length;
if (availableLength <= 0) {
return;
}
text = text.slice(0, availableLength);
range.deleteContents();

const node = document.createTextNode(text);
range.insertNode(node);

Expand All @@ -22,40 +35,43 @@ const insertAtCaret = (target: HTMLElement, text: string) => {

// Dispatch input event to trigger Markdown Input to parse the new text
target.dispatchEvent(new Event('input', {bubbles: true}));
} else {
insertByCommand(text);
}
};

const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false) => {
const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false, maxLength = CONST.MAX_COMMENT_LENGTH + 1) => {
const navigation = useNavigation();

/**
* Set pasted text to clipboard
* @param {String} text
*/
const paste = useCallback((text: string) => {
try {
const textInputHTMLElement = textInputRef.current as HTMLElement;
if (textInputHTMLElement?.hasAttribute('contenteditable')) {
insertAtCaret(textInputHTMLElement, text);
} else {
insertByCommand(text);
}
const paste = useCallback(
(text: string) => {
try {
const textInputHTMLElement = textInputRef.current as HTMLElement;
if (textInputHTMLElement?.hasAttribute('contenteditable')) {
insertAtCaret(textInputHTMLElement, text, maxLength);
} else {
const htmlInput = textInputRef.current as unknown as HTMLInputElement;
const availableLength = maxLength - (htmlInput.value?.length ?? 0);
htmlInput.setRangeText(text.slice(0, availableLength));
}

// Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view.
// To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler
// We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered
textInputHTMLElement.dispatchEvent(
new FocusEvent('focusin', {
bubbles: true,
}),
);
// eslint-disable-next-line no-empty
} catch (e) {}
// We only need to set the callback once.
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
// Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view.
// To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler
// We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered
textInputHTMLElement.dispatchEvent(
new FocusEvent('focusin', {
bubbles: true,
}),
);
// eslint-disable-next-line no-empty
} catch (e) {}
// We only need to set the callback once.
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
},
[maxLength, textInputRef],
);

/**
* Manually place the pasted HTML into Composer
Expand All @@ -64,9 +80,9 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
*/
const handlePastedHTML = useCallback(
(html: string) => {
paste(Parser.htmlToMarkdown(html));
paste(Parser.htmlToMarkdown(html.slice(0, maxLength)));
},
[paste],
[paste, maxLength],
);

/**
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useHtmlPaste/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type UseHtmlPaste = (
textInputRef: MutableRefObject<(HTMLTextAreaElement & TextInput) | TextInput | null>,
preHtmlPasteCallback?: (event: ClipboardEvent) => boolean,
removeListenerOnScreenBlur?: boolean,
maxLength?: number, // Maximum length of the text input value after pasting
) => void;

export default UseHtmlPaste;

0 comments on commit 50c1db8

Please sign in to comment.