diff options
author | 2020-10-19 00:32:06 -0500 | |
---|---|---|
committer | 2020-10-19 00:32:06 -0500 | |
commit | b1229bbaeb8cf071f0711bc2ed1b40dd96cd7a05 (patch) | |
tree | a64f1d3eb500fef632d114eee537eccbdd548145 | |
parent | 017c17b36565186d87edb7618d781b0e87b206f3 (diff) | |
download | refined-github-b1229bbaeb8cf071f0711bc2ed1b40dd96cd7a05.tar.gz refined-github-b1229bbaeb8cf071f0711bc2ed1b40dd96cd7a05.tar.zst refined-github-b1229bbaeb8cf071f0711bc2ed1b40dd96cd7a05.zip |
Refactor `highest-rated-comment` (#3669)
-rw-r--r-- | source/features/highest-rated-comment.css | 5 | ||||
-rw-r--r-- | source/features/highest-rated-comment.tsx | 119 | ||||
-rw-r--r-- | source/features/releases-tab.tsx | 4 | ||||
-rw-r--r-- | source/helpers/loose-parse-int.ts | 6 |
4 files changed, 56 insertions, 78 deletions
diff --git a/source/features/highest-rated-comment.css b/source/features/highest-rated-comment.css index 5a712705..b8326e4f 100644 --- a/source/features/highest-rated-comment.css +++ b/source/features/highest-rated-comment.css @@ -8,3 +8,8 @@ a.timeline-chosen-answer .timeline-comment-header-text { overflow: hidden; max-width: none; } + +a.timeline-chosen-answer .avatar { + position: absolute; + left: -55px; +} diff --git a/source/features/highest-rated-comment.tsx b/source/features/highest-rated-comment.tsx index ec1374e8..b62aa36c 100644 --- a/source/features/highest-rated-comment.tsx +++ b/source/features/highest-rated-comment.tsx @@ -1,4 +1,5 @@ import './highest-rated-comment.css'; +import mem from 'mem'; import React from 'dom-chef'; import select from 'select-dom'; import CheckIcon from 'octicon/check.svg'; @@ -21,40 +22,37 @@ const negativeReactionsSelector = ` ${commentSelector} [aria-label*="reacted with thumbs down"] `; -function getBestComment(): HTMLElement | undefined { - let highest; - for (const comment of getCommentsWithReactions()) { - const positiveReactions = getCount(getPositiveReactions(comment)); - - // It needs to be upvoted enough times to be considered an useful comment - if (positiveReactions < 10) { - continue; - } +const getPositiveReactions = mem((comment: HTMLElement): number | void => { + const count = selectSum(positiveReactionsSelector, comment); + if ( + // It needs to be upvoted enough times + count >= 10 && - // Controversial comment, ignore - const negativeReactions = getCount(getNegativeReactions(comment)); - if (negativeReactions >= positiveReactions / 2) { - continue; - } + // It can't be a controversial comment + selectSum(negativeReactionsSelector, comment) < count / 2 + ) { + return count; + } +}); - if (!highest || positiveReactions > highest.count) { +function getBestComment(): HTMLElement | undefined { + let highest; + for (const reaction of select.all(positiveReactionsSelector)) { + const comment = reaction.closest<HTMLElement>(commentSelector)!; + const positiveReactions = getPositiveReactions(comment); + if (positiveReactions && (!highest || positiveReactions > highest.count)) { highest = {comment, count: positiveReactions}; } } - if (!highest) { - return undefined; - } - - return highest.comment; + return highest?.comment; } function highlightBestComment(bestComment: Element): void { const avatar = select('.TimelineItem-avatar', bestComment)!; avatar.classList.add('flex-column', 'flex-items-center', 'd-md-flex'); - avatar.append( - <CheckIcon width={24} height={32} className="mt-4 text-green"/> - ); + avatar.append(<CheckIcon width={24} height={32} className="mt-4 text-green"/>); + select('.unminimized-comment', bestComment)!.classList.add('timeline-chosen-answer'); select('.unminimized-comment .timeline-comment-header-text', bestComment)!.before( <span @@ -70,62 +68,33 @@ function linkBestComment(bestComment: HTMLElement): void { // Find position of comment in thread const position = select.all(commentSelector).indexOf(bestComment); // Only link to it if it doesn't already appear at the top of the conversation - if (position >= 3) { - const text = select('.comment-body', bestComment)!.textContent!.slice(0, 100); - const {hash} = select<HTMLAnchorElement>('.js-timestamp', bestComment)!; - - // Copy avatar but link it to the comment - const avatar = select('.TimelineItem-avatar', bestComment)!.cloneNode(true); - const link = select<HTMLAnchorElement>('[data-hovercard-type="user"]', avatar)!; - link.removeAttribute('data-hovercard-type'); - link.removeAttribute('data-hovercard-url'); - link.href = hash; - - // Remove the check icon from the preview #3338 - select('.octicon-check.text-green', avatar)!.remove(); - - // We don't copy the exact timeline item structure, so we need to align the avatar with the other avatars in the timeline. - // TODO: update DOM to match other comments, instead of applying this CSS - avatar.style.left = '-55px'; - - bestComment.parentElement!.firstElementChild!.after(( - <div className="timeline-comment-wrapper pl-0 my-0"> - {avatar} - - <a href={hash} className="no-underline rounded-1 timeline-chosen-answer timeline-comment bg-gray px-2 d-flex flex-items-center"> - <span className="btn btn-sm mr-2"> - <ArrowDownIcon/> - </span> - - <span className="text-gray timeline-comment-header-text"> - Highest-rated comment: <em>{text}</em> - </span> - </a> - </div> - )); + if (position < 3) { + return; } -} -function getCommentsWithReactions(): Set<HTMLElement> { - const comments = getPositiveReactions().map(reaction => reaction.closest<HTMLElement>(commentSelector)!); - return new Set(comments); -} - -function getNegativeReactions(reactionBox?: HTMLElement): HTMLElement[] { - return select.all(negativeReactionsSelector, reactionBox ?? document); -} + const text = select('.comment-body', bestComment)!.textContent!.slice(0, 100); + const {hash} = select<HTMLAnchorElement>('.js-timestamp', bestComment)!; + const avatar = select('img.avatar', bestComment)!.cloneNode(); -function getPositiveReactions(reactionBox?: HTMLElement): HTMLElement[] { - return select.all(positiveReactionsSelector, reactionBox ?? document); + bestComment.parentElement!.firstElementChild!.after( + <div className="timeline-comment-wrapper pl-0 my-0"> + <a href={hash} className="no-underline rounded-1 timeline-chosen-answer timeline-comment bg-gray px-2 d-flex flex-items-center"> + {avatar} + <span className="btn btn-sm mr-2"> + <ArrowDownIcon/> + </span> + + <span className="text-gray timeline-comment-header-text"> + Highest-rated comment: <em>{text}</em> + </span> + </a> + </div> + ); } -function getCount(reactions: HTMLElement[]): number { - let count = 0; - for (const reaction of reactions) { - count += looseParseInt(reaction.textContent!); - } - - return count; +function selectSum(selector: string, container: HTMLElement): number { + // eslint-disable-next-line unicorn/no-reduce -- The alternative `for` loop is too lengthy for a simple sum + return select.all(selector, container).reduce((sum, element) => sum + looseParseInt(element), 0); } function init(): false | void { @@ -134,8 +103,8 @@ function init(): false | void { return false; } - highlightBestComment(bestComment); linkBestComment(bestComment); + highlightBestComment(bestComment); } void features.add({ diff --git a/source/features/releases-tab.tsx b/source/features/releases-tab.tsx index 3345410c..1693df6d 100644 --- a/source/features/releases-tab.tsx +++ b/source/features/releases-tab.tsx @@ -17,13 +17,13 @@ const cacheKey = `releases-count:${getRepoURL()}`; function parseCountFromDom(): number { const releasesCountElement = select('.numbers-summary a[href$="/releases"] .num'); if (releasesCountElement) { - return looseParseInt(releasesCountElement.textContent!); + return looseParseInt(releasesCountElement); } // In "Repository refresh" layout, look for the releases link in the sidebar const moreReleasesCountElement = select('[href$="/tags"] strong'); if (moreReleasesCountElement) { - return looseParseInt(moreReleasesCountElement.textContent!); + return looseParseInt(moreReleasesCountElement); } return 0; diff --git a/source/helpers/loose-parse-int.ts b/source/helpers/loose-parse-int.ts index 5d559853..081d519c 100644 --- a/source/helpers/loose-parse-int.ts +++ b/source/helpers/loose-parse-int.ts @@ -1,3 +1,7 @@ -export default function looseParseInt(text: string): number { +export default function looseParseInt(text: Node | string): number { + if (typeof text !== 'string') { + text = text.textContent!; + } + return Number(text.replace(/\D+/g, '')); } |