Skip to content

Commit

Permalink
Merge pull request #13551 from markarmanus/linkWhenEditingComment
Browse files Browse the repository at this point in the history
Autolink when edititng comments except wehn explicit link removal
  • Loading branch information
cead22 committed Dec 17, 2022
2 parents b0f6785 + a76c8c3 commit 2483ad0
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 4 deletions.
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;
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()) {
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);
});
});

0 comments on commit 2483ad0

Please sign in to comment.