summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--source/features/highest-rated-comment.css5
-rw-r--r--source/features/highest-rated-comment.tsx119
-rw-r--r--source/features/releases-tab.tsx4
-rw-r--r--source/helpers/loose-parse-int.ts6
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, ''));
}