Skip to content

[Hold for payment 2024-02-13] [$500] Private Note - Code block paste in private notes does not work (works fine in native apps) #29806

Closed

Description

@kbecciv

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: 1.3.85.0
Reproducible in staging?: y
Reproducible in production?: y
If this was caught during regression testing, add the test name, ID and link from TestRail:
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @dhanashree-sawant
Slack conversation: https://expensify.slack.com/archives/C049HHMV9SM/p1697530399668279

Action Performed:

  1. Open the app
  2. Open any user report
  3. Send 3 backtick code block eg: Hii
  4. Hover and click copy to clipboard
  5. Click on header to open user details
  6. Click on privates notes-> my notes
  7. Paste the copied text and observe that app removes backticks while pasting

Expected Result:

App should paste text with backticks in private notes

Actual Result:

App does not paste text with private notes (works fine only in native apps)

Workaround:

Unknown

Platforms:

Which of our officially supported platforms is this issue occurring on?

  • Android: Native
  • Android: mWeb Chrome
  • iOS: Native
  • iOS: mWeb Safari
  • MacOS: Chrome / Safari
  • MacOS: Desktop

Screenshots/Videos

Android: Native
Android: mWeb Chrome
Android.chrome.copy.to.clipboard.code.block.paste.notes.mp4
iOS: Native
iOS: mWeb Safari
ios.safari.paste.code.block.in.notes.mov
MacOS: Chrome / Safari
windows.chrome.code.block.paste.notes.mp4
mac.chrome.paste.code.block.notes.not.working.mov
MacOS: Desktop
mac.desktop.paste.code.block.notes.mov

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~013e368c7fef7e59bc
  • Upwork Job ID: 1714338003846836224
  • Last Price Increase: 2023-11-07

Activity

  1. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 17, 2023
  2. changed the title [-]Private Note - Code block paste in private notes does not work (works fine in native apps)[/-] [+][$500] Private Note - Code block paste in private notes does not work (works fine in native apps)[/+] on Oct 17, 2023
  3. melvin-bot commented on Oct 17, 2023

    @melvin-bot

    Triggered auto assignment to @garrettmknight (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details.

  4. melvin-bot commented on Oct 17, 2023

    @melvin-bot
  5. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Oct 17, 2023
  6. melvin-bot commented on Oct 17, 2023

    @melvin-bot

    Bug0 Triage Checklist (Main S/O)

    • This "bug" occurs on a supported platform (ensure Platforms in OP are ✅)
    • This bug is not a duplicate report (check E/App issues and #expensify-bugs)
      • If it is, comment with a link to the original report, close the issue and add any novel details to the original issue instead
    • This bug is reproducible using the reproduction steps in the OP. S/O
      • If the reproduction steps are clear and you're unable to reproduce the bug, check with the reporter and QA first, then close the issue.
      • If the reproduction steps aren't clear and you determine the correct steps, please update the OP.
    • This issue is filled out as thoroughly and clearly as possible
      • Pay special attention to the title, results, platforms where the bug occurs, and if the bug happens on staging/production.
    • I have reviewed and subscribed to the linked Slack conversation to ensure Slack/Github stay in sync
  7. melvin-bot commented on Oct 17, 2023

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @ArekChr (External)

  8. FitseTLT commented on Oct 17, 2023

    @FitseTLT
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Code block paste doesn't work properly in private notes text input

    What is the root cause of that problem?

    When user copies code block message we set the code in 'html' prop of the clipboard and the plain text (found by parsing it with ExpensiMark htmToText function) to 'text' prop of the clipboard in here when canSetHtml is true( for web only, that's why it works fine for native apps).

    } else if (content) {
    const parser = new ExpensiMark();
    if (!Clipboard.canSetHtml()) {
    Clipboard.setString(parser.htmlToMarkdown(content));
    } else {
    const plainText = parser.htmlToText(content);
    Clipboard.setHtml(content, plainText);
    }
    }

    So when the user pastes in PrivatesNotesEditPage text input the plain text is copied by default as we have not implemented a code that pastes the html whenever the clipboard has html code inside it as we did in our Composer component.

    What changes do you think we should make in order to solve the problem?

    We need to take the code inside Composer component that handles paste of html code and reimplement it in a reusable code (custom hook or HOC) and then use it both in here privates notes and in composer.
    So for instance we can use a custom hook like this and use it both in Private notes and Composer

    function useCodePaste(textInputRef) {
        const navigation = useNavigation();
    
        /**
         * Set pasted text to clipboard
         * @param {String} text
         */
        const paste = useCallback((text) => {
            try {
                document.execCommand('insertText', false, text);
                // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view.
                textInputRef.current.blur();
                textInputRef.current.focus();
                // eslint-disable-next-line no-empty
            } catch (e) {}
        }, []);
    
        /**
         * Manually place the pasted HTML into Composer
         *
         * @param {String} html - pasted HTML
         */
        const handlePastedHTML = useCallback(
            (html) => {
                const parser = new ExpensiMark();
                paste(parser.htmlToMarkdown(html));
            },
            [paste],
        );
    
        /**
         * Paste the plaintext content into Composer.
         *
         * @param {ClipboardEvent} event
         */
        const handlePastePlainText = useCallback(
            (event) => {
                const plainText = event.clipboardData.getData('text/plain');
                paste(plainText);
            },
            [paste],
        );
    
        const handlePaste = useCallback(
            (event) => {
                const isFocused = textInputRef.current.isFocused();
    
                if (!isFocused) {
                    return;
                }
    
                if (textInputRef.current !== event.target) {
                    // To make sure the text input does not capture paste events from other inputs, we check where the event originated
                    // If it did originate in another input, we return early to prevent the text input from handling the paste
                    const isTargetInput = event.target.nodeName === 'INPUT' || event.target.nodeName === 'TEXTAREA' || event.target.contentEditable === 'true';
                    if (isTargetInput) {
                        return;
                    }
    
                    textInputRef.current.focus();
                }
    
                event.preventDefault();
    
                const {types} = event.clipboardData;
                const TEXT_HTML = 'text/html';
    
                // If paste contains HTML
                if (types.includes(TEXT_HTML)) {
                    const pastedHTML = event.clipboardData.getData(TEXT_HTML);
    
                    const domparser = new DOMParser();
                    const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images;
    
                    // Exclude parsing img tags in the HTML, as fetching the image via fetch triggers a connect-src Content-Security-Policy error.
                    if (embeddedImages.length > 0 && embeddedImages[0].src) {
                        // If HTML has emoji, then treat this as plain text.
                        if (embeddedImages[0].dataset && embeddedImages[0].dataset.stringifyType === 'emoji') {
                            handlePastePlainText(event);
                            return;
                        }
                    }
                    handlePastedHTML(pastedHTML);
                    return;
                }
                handlePastePlainText(event);
            },
            [handlePastedHTML, handlePastePlainText],
        );
    
        useEffect(() => {
            // we need to handle listeners on navigation focus/blur as Composer is not unmounting
            // when navigating away to different report
            const unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste));
            const unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste));
    
            if (textInputRef.current) {
                document.addEventListener('paste', handlePaste);
            }
    
            return () => {
                unsubscribeFocus();
                unsubscribeBlur();
                document.removeEventListener('paste', handlePaste);
                // eslint-disable-next-line es/no-optional-chaining
                if (!textInputRef.current) {
                    return;
                }
            };
            // eslint-disable-next-line react-hooks/exhaustive-deps
        }, []);
    }
    

    And in both PrivateNotesEditPage and Composer we call the custom hook passing the appropriate textInputRef parameter like this (or we can also return the ref from the custom hook and assign it to the ref of our text inputs):

        useCodePaste(privateNotesInput);
    

    In Composer we change

    const {files, types} = event.clipboardData;
    const TEXT_HTML = 'text/html';
    // If paste contains files, then trigger file management
    if (files.length > 0) {
    // Prevent the default so we do not post the file name into the text box
    onPasteFile(event.clipboardData.files[0]);
    return;
    }
    // If paste contains HTML
    if (types.includes(TEXT_HTML)) {
    const pastedHTML = event.clipboardData.getData(TEXT_HTML);
    const domparser = new DOMParser();
    const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images;
    // Exclude parsing img tags in the HTML, as fetching the image via fetch triggers a connect-src Content-Security-Policy error.
    if (embeddedImages.length > 0 && embeddedImages[0].src) {
    // If HTML has emoji, then treat this as plain text.
    if (embeddedImages[0].dataset && embeddedImages[0].dataset.stringifyType === 'emoji') {
    handlePastePlainText(event);
    return;
    }
    }
    handlePastedHTML(pastedHTML);
    return;
    }
    handlePastePlainText(event);
    },
    [onPasteFile, handlePastedHTML, checkComposerVisibility, handlePastePlainText],
    );

    to

                const {files} = event.clipboardData;
    
                // If paste contains files, then trigger file management
                if (files.length > 0) {
                    // Prevent the default so we do not post the file name into the text box
                    onPasteFile(event.clipboardData.files[0]);
                    return;
                }
            },
            [onPasteFile, checkComposerVisibility],
        );
    
        useCodePaste(textInput);
    

    (Optional) We can use also HOC (whichever is appropriate I am just trying to explain the basic idea).
    *** We can also use HOC 👍 ***
    Here is the result:

    10.New.Expensify.1.mp4

    What alternative solutions did you explore? (Optional)

  9. neonbhai commented on Oct 17, 2023

    @neonbhai
    Contributor

    Cannot reproduce on latest main

  10. FitseTLT commented on Oct 19, 2023

    @FitseTLT
    Contributor

    Updated comment
    And also I still can reproduce it in the latest main :-

    3.New.Expensify.2.mp4
  11. 127 remaining items

  12. garrettmknight commented on Feb 21, 2024

    @garrettmknight
    Contributor

    @Santhosh-Sellavel please fill out the checklist when you get a chance and request payment. Dropping to weekly till then.

  13. garrettmknight commented on Mar 1, 2024

    @garrettmknight
    Contributor

    Just waiting on checklist and payment.

  14. garrettmknight commented on Mar 11, 2024

    @garrettmknight
    Contributor

    @Santhosh-Sellavel can you request payment when you get a chance? The checklist was completed in the original issue.

  15. Santhosh-Sellavel commented on Mar 11, 2024

    @Santhosh-Sellavel
    Collaborator

    Feel free to close this one, I'll request later

  16. Santhosh-Sellavel commented on May 22, 2024

    @Santhosh-Sellavel
    Collaborator

    Requested on ND

  17. JmillsExpensify commented on May 31, 2024

    @JmillsExpensify
    Contributor

    $500 approved for @Santhosh-Sellavel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.EngineeringInternalRequires API changes or must be handled by Expensify staffWeeklyKSv2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions