Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autolink when edititng comments except wehn explicit link removal #13551

Merged
merged 9 commits into from
Dec 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 71 additions & 4 deletions src/libs/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,68 @@ function deleteReportComment(reportID, reportAction) {
API.write('DeleteComment', parameters, {optimisticData, successData, failureData});
}

/**
* @param {String} comment
* @returns {Array}
*/
const extractLinksInMarkdownComment = (comment) => {
const regex = /\[[^[\]]*\]\(([^()]*)\)/gm;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #18911:
I understand it's difficult to find edge cases but this regex didn't handle such cases like [[test]](test.com)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, we're now using markdown link regex exported from expensify-common.

const matches = [...comment.matchAll(regex)];

// Element 1 from match is the regex group if it exists which contains the link URLs
const links = _.map(matches, match => match[1]);
return links;
};

/**
* Compares two markdown comments and returns a list of the links removed in a new comment.
*
* @param {String} oldComment
* @param {String} newComment
* @returns {Array}
*/
const getRemovedMarkdownLinks = (oldComment, newComment) => {
const linksInOld = extractLinksInMarkdownComment(oldComment);
const linksInNew = extractLinksInMarkdownComment(newComment);
return _.difference(linksInOld, linksInNew);
};

/**
* Removes the links in a markdown comment.
* example:
* comment="test [link](https://www.google.com) test",
* links=["https://www.google.com"]
* returns: "test link test"
* @param {String} comment
* @param {Array} links
* @returns {String}
*/
const removeLinks = (comment, links) => {
let commentCopy = comment.slice();
_.forEach(links, (link) => {
const regex = new RegExp(`\\[([^\\[\\]]*)\\]\\(${link}\\)`, 'gm');
const linkMatch = regex.exec(commentCopy);
const linkText = linkMatch && linkMatch[1];
commentCopy = commentCopy.replace(`[${linkText}](${link})`, linkText);
});
return commentCopy;
};

/**
* This function will handle removing only links that were purposely removed by the user while editing.
* @param {String} newCommentText text of the comment after editing.
* @param {Array} originalHtml original html of the comment before editing
* @returns {String}
*/
const handleUserDeletedLinks = (newCommentText, originalHtml) => {
const parser = new ExpensiMark();
const htmlWithAutoLinks = parser.replace(newCommentText);
const markdownWithAutoLinks = parser.htmlToMarkdown(htmlWithAutoLinks);
const markdownOriginalComment = parser.htmlToMarkdown(originalHtml);
const removedLinks = getRemovedMarkdownLinks(markdownOriginalComment, newCommentText);
return removeLinks(markdownWithAutoLinks, removedLinks);
};

/**
* Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI.
*
Expand All @@ -904,9 +966,13 @@ function editReportComment(reportID, originalReportAction, textForNewComment) {

// Do not autolink if someone explicitly tries to remove a link from message.
// https://github.com/Expensify/App/issues/9090
// https://github.com/Expensify/App/issues/13221
const originalCommentHTML = lodashGet(originalReportAction, 'message[0].html');
const markdownForNewComment = handleUserDeletedLinks(textForNewComment, originalCommentHTML);

const autolinkFilter = {filterRules: _.filter(_.pluck(parser.rules, 'name'), name => name !== 'autolink')};
const htmlForNewComment = parser.replace(textForNewComment, autolinkFilter);
const originalMessageHTML = parser.replace(originalReportAction.message[0].html, autolinkFilter);
const htmlForNewComment = parser.replace(markdownForNewComment, autolinkFilter);
const parsedOriginalCommentHTML = parser.replace(originalCommentHTML, autolinkFilter);

// Delete the comment if it's empty
if (_.isEmpty(htmlForNewComment)) {
Expand All @@ -915,7 +981,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) {
}

// Skip the Edit if message is not changed
if (originalMessageHTML === htmlForNewComment.trim()) {
if (parsedOriginalCommentHTML === htmlForNewComment.trim()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a case where resaving the edited message without changing the content will parse the link again #29225. Instead, it should not auto-link when it is removed explicitly and the content is not changed.

return;
}

Expand All @@ -927,7 +993,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) {
message: [{
isEdited: true,
html: htmlForNewComment,
text: textForNewComment,
text: markdownForNewComment,
type: originalReportAction.message[0].type,
}],
},
Expand Down Expand Up @@ -1362,6 +1428,7 @@ export {
broadcastUserIsTyping,
togglePinnedState,
editReportComment,
handleUserDeletedLinks,
saveReportActionDraft,
deleteReportComment,
getSimplifiedIOUReport,
Expand Down
47 changes: 47 additions & 0 deletions tests/actions/ReportTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,4 +409,51 @@ describe('actions/Report', () => {
expect(report.lastMessageText).toBe('Current User Comment 2');
});
});

it('Should properly update comment with links', () => {
/* This tests a variety of scenarios when a user edits a comment.
* We should generate a link when editing a message unless the link was
* already in the comment and the user deleted it on purpose.
*/

// User edits comment to add link
// We should generate link
let originalCommentHTML = 'Original Comment';
let afterEditCommentText = 'Original Comment www.google.com';
let newCommentMarkdown = Report.handleUserDeletedLinks(afterEditCommentText, originalCommentHTML);
let expectedOutput = 'Original Comment [www.google.com](https://www.google.com)';
expect(newCommentMarkdown).toBe(expectedOutput);

// User deletes www.google.com link from comment but keeps link text
// We should not generate link
originalCommentHTML = 'Comment <a href="https://www.google.com" target="_blank">www.google.com</a>';
afterEditCommentText = 'Comment www.google.com';
newCommentMarkdown = Report.handleUserDeletedLinks(afterEditCommentText, originalCommentHTML);
expectedOutput = 'Comment www.google.com';
expect(newCommentMarkdown).toBe(expectedOutput);

// User Delete only () part of link but leaves the []
// We should not generate link
originalCommentHTML = 'Comment <a href="https://www.google.com" target="_blank">www.google.com</a>';
afterEditCommentText = 'Comment [www.google.com]';
newCommentMarkdown = Report.handleUserDeletedLinks(afterEditCommentText, originalCommentHTML);
expectedOutput = 'Comment [www.google.com]';
expect(newCommentMarkdown).toBe(expectedOutput);

// User Generates multiple links in one edit
// We should generate both links
originalCommentHTML = 'Comment';
afterEditCommentText = 'Comment www.google.com www.facebook.com';
newCommentMarkdown = Report.handleUserDeletedLinks(afterEditCommentText, originalCommentHTML);
expectedOutput = 'Comment [www.google.com](https://www.google.com) [www.facebook.com](https://www.facebook.com)';
expect(newCommentMarkdown).toBe(expectedOutput);

// Comment has two links but user deletes only one of them
// Should not generate link again for the deleted one
originalCommentHTML = 'Comment <a href="https://www.google.com" target="_blank">www.google.com</a> <a href="https://www.facebook.com" target="_blank">www.facebook.com</a>';
afterEditCommentText = 'Comment www.google.com [www.facebook.com](https://www.facebook.com)';
newCommentMarkdown = Report.handleUserDeletedLinks(afterEditCommentText, originalCommentHTML);
expectedOutput = 'Comment www.google.com [www.facebook.com](https://www.facebook.com)';
expect(newCommentMarkdown).toBe(expectedOutput);
});
});
Loading