Last active
November 23, 2023 01:26
-
-
Save cefqrn/583593fb8ac1c78df7ffa71425d09adb to your computer and use it in GitHub Desktop.
see https://cohost.org/cefqrn/post/1391089-expand-comments-user for details
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Expand Comments | |
// @version 0.48 | |
// @description Adds a button to expand comments on chosts | |
// @author cefqrn | |
// @match https://cohost.org/* | |
// @exclude https://cohost.org/*/post/* | |
// ==/UserScript== | |
// ----- CONFIG ----- | |
// absolute URL toggle | |
// false: the links move the page to put themselves in view (default) | |
// true: the links go to the original post then move the page | |
const USE_ABSOLUTE_LINKS = false; | |
// ----- END OF CONFIG ----- | |
// reuse the arrow from the page switcher but make it the same color as the other icons | |
const ARROW = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="h-6 w-6 co-action-button transition-transform ui-open:rotate-180"><path fill-rule="evenodd" d="M12.53 16.28a.75.75 0 01-1.06 0l-7.5-7.5a.75.75 0 011.06-1.06L12 14.69l6.97-6.97a.75.75 0 111.06 1.06l-7.5 7.5z" clip-rule="evenodd"></path></svg>`; | |
const units = { | |
second: 1000, | |
minute: 60, | |
hour: 60, | |
day: 24, | |
month: 30.4375, // thanks, duckduckgo (365.25 / 12) | |
year: 12 | |
}; | |
const dateFormatter = new Intl.DateTimeFormat("en", { weekday: "short", year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "numeric", hour12: true }); | |
const relativeDateFormatter = new Intl.RelativeTimeFormat("en", { style: "short", numeric: "always" }); | |
function toRelativeDate(date) { | |
const now = new Date; | |
let difference = date - now; | |
let prev; | |
for (const [unit, prevUnitCount] of Object.entries(units)) { | |
difference = Math.ceil(difference / prevUnitCount); | |
if (!difference) break; | |
prev = relativeDateFormatter.format(difference, unit); | |
} | |
return prev; | |
} | |
function createElement({tagName, classList = [], children = [], parent, dataset = {}, ...attributes}) { | |
const element = document.createElement(tagName); | |
if (classList.length > 0) { | |
element.classList.add(...classList); | |
} | |
for (const [attribute, value] of Object.entries(dataset)) { | |
element.dataset[attribute] = value; | |
} | |
for (const [attribute, value] of Object.entries(attributes)) { | |
element[attribute] = value; | |
} | |
children.forEach(child => createElement({...child, parent: element})); | |
if (parent) { | |
parent.appendChild(element); | |
} | |
return element; | |
} | |
function parseMarkdown(text) { | |
return text; | |
} | |
function parseComment({comment, poster}, mainPostURL, depth = 0) { | |
const postDate = new Date(comment.postedAtISO); | |
let commentElement; | |
if (poster === undefined) { | |
// deleted comments (temporary (hopefully lol)) | |
commentElement = { | |
tagName: "div", | |
classList: ["flex","flex-col", "gap-4"], | |
children: [ | |
{ | |
tagName: "article", | |
classList: ["relative", "flex", "flex-row", "gap-4"], | |
dataSet: {commentId: comment.commentId}, | |
children: [ | |
{ // the link to the comment is still there even though the time doesn't have a link anymore | |
tagName: "div", | |
classList: ["absolute", "-top-16"], | |
id: `comment-${comment.commentId}` | |
}, | |
{ | |
tagName: "div", | |
classList: ["flex", "min-w-0", "flex-1", "flex-col"], | |
children: [ | |
{ | |
tagName: "div", | |
classList: ["flex", "flex-row", "gap-4"], | |
children: [ | |
{ | |
tagName: "div", | |
classList: ["flex", "min-w-0", "flex-1", "flex-col", "justify-start", "gap-2"], | |
children: [ | |
{ // header | |
tagName: "div", | |
classList: ["flex", "flex-row", "flex-wrap", "items-center", "gap-2"], | |
children: [ | |
{ // display name | |
tagName: "span", | |
// added a class so it shows better on dark mode | |
classList: ["co-project-display-name"], | |
innerText: "[deleted]", | |
}, | |
{ // time | |
tagName: "time", | |
classList: ["expand-comments-time", "block", "flex-none", "text-xs", "tabular-nums", "text-gray-500"], | |
title: dateFormatter.format(postDate), | |
innerText: toRelativeDate(postDate), | |
dataset: {postDate: postDate} | |
} | |
] | |
}, | |
{ // comment body | |
tagName: "div", | |
classList: ["co-prose", "prose", "overflow-hidden", "break-words"] | |
} | |
] | |
} | |
] | |
} | |
] | |
} | |
] | |
} | |
] | |
} | |
} else { | |
commentElement = { | |
tagName: "div", | |
classList: ["flex","flex-col", "gap-4"], | |
children: [ | |
{ | |
tagName: "article", | |
classList: ["relative", "flex", "flex-row", "gap-4"], | |
dataSet: {commentId: comment.commentId}, | |
children: [ | |
{ // for links to comments (not visible) | |
tagName: "div", | |
classList: ["absolute", "-top-16"], | |
id: `comment-${comment.commentId}` | |
}, | |
{ | |
tagName: "div", | |
classList: ["flex", "min-w-0", "flex-1", "flex-col"], | |
children: [ | |
{ | |
tagName: "div", | |
classList: ["flex", "flex-row", "gap-4"], | |
children: [ | |
{ // big avatar (appears on wide screens) | |
tagName: "a", | |
classList: ["flex-0", "mask", "relative", "aspect-square", "cohost-shadow-light", "dark:cohost-shadow-dark", "hidden", "h-12", "w-12", "lg:block"], | |
href: `/${poster.handle}`, | |
title: `@${poster.handle}`, | |
children: [ | |
{ | |
tagName: "img", | |
classList: ["mask", `mask-${poster.avatarShape}`, "h-full", "w-full", "object-cover"], | |
src: poster.avatarURL + "?dpr=2&width=80&height=80&fit=cover&auto=webp", | |
alt: poster.handle | |
} | |
] | |
}, | |
{ | |
tagName: "div", | |
classList: ["flex", "min-w-0", "flex-1", "flex-col", "justify-start", "gap-2"], | |
children: [ | |
{ // header | |
tagName: "div", | |
classList: ["flex", "flex-row", "flex-wrap", "items-center", "gap-2"], | |
children: [ | |
{ // small avatar | |
tagName: "div", | |
classList: ["flex-0", "mask", "relative", "aspect-square", "h-8", "w-8", "lg:hidden", "inline-block"], | |
children: [ | |
{ | |
tagName: "img", | |
classList: ["mask", `mask-${poster.avatarShape}`, "h-full", "w-full", "object-cover"], | |
src: poster.avatarURL + "?dpr=2&width=80&height=80&fit=cover&auto=webp", | |
alt: poster.handle | |
} | |
] | |
}, | |
...(poster.displayName !== "" ? [{ // display name | |
tagName: "a", | |
classList: ["co-project-display-name", "max-w-full", "flex-shrink", "truncate", "font-atkinson", "font-bold", "hover:underline"], | |
rel: "author", | |
href: `/${poster.handle}`, | |
title: poster.displayName, | |
innerText: poster.displayName, | |
}] : []), | |
{ // handle | |
tagName: "a", | |
classList: ["co-project-handle", "font-atkinson", "font-normal", "hover:underline"], | |
href: `/${poster.handle}`, | |
innerText: `@${poster.handle}` | |
}, | |
{ // time | |
tagName: "time", | |
classList: ["block", "flex-none", "text-xs", "tabular-nums", "text-gray-500"], | |
children: [ | |
{ | |
tagName: "a", | |
classList: ["expand-comments-time", "hover:underline"], | |
title: dateFormatter.format(postDate), | |
href: `${mainPostURL}#comment-${comment.commentId}`, | |
innerText: toRelativeDate(postDate), | |
dataset: {postDate: postDate} | |
} | |
] | |
} | |
] | |
}, | |
{ // comment body | |
tagName: "div", | |
// the prose class also gives it a max width | |
classList: ["co-prose", "prose", "overflow-hidden", "break-words"], | |
children: comment.body.split("\n\n").map(paragraphText => ({ | |
tagName: "p", | |
innerText: paragraphText | |
})) | |
} | |
] | |
} | |
] | |
} | |
] | |
} | |
] | |
} | |
] | |
} | |
} | |
if (comment.children.length > 0) { | |
// add children comments | |
commentElement.children.push( | |
{ | |
tagName: "div", | |
classList: ["co-hairline", "ml-0", "flex", "flex-col", "gap-4", "border-l", "pl-6", "lg:ml-6", "lg:pl-4"], | |
children: comment.children.map(childComment => parseComment(childComment, mainPostURL, depth + 1)) | |
} | |
); | |
} | |
if (depth === 0) { | |
// add shadow to first comment | |
commentElement = { | |
tagName: "div", | |
classList: ["co-themed-box", "co-comment-box", "cohost-shadow-light", "dark:cohost-shadow-dark", "flex", "w-full", "min-w-0", "max-w-full", "flex-col", "gap-4", "rounded-lg", "p-3", "lg:max-w-prose"], | |
children: [ | |
commentElement | |
] | |
}; | |
} | |
return commentElement; | |
} | |
function initializePost(node) { | |
if (node.dataset.expandCommentsIsInitialized) return; | |
node.dataset.expandCommentsIsInitialized = true; | |
const article = node.querySelector("article"); | |
const footer = article.querySelector("footer"); | |
const button = document.createElement("button"); | |
button.innerHTML = ARROW; | |
const buttonContainer = footer.firstChild.firstChild; | |
buttonContainer.classList.add("gap-3", "flex"); | |
buttonContainer.insertBefore(button, buttonContainer.firstChild); | |
const link = footer.querySelector("div div a"); | |
const postURL = link.getAttribute("href"); | |
const [_, author, postId] = postURL.match(/([^\/]+)\/post\/(\d+)/); | |
let commentHolderElement; | |
button.onclick = (event) => { | |
{ | |
const dataset = button.dataset; | |
if (dataset.headlessuiState === "open") { | |
dataset.headlessuiState = ""; | |
if (commentHolderElement) { | |
commentHolderElement.remove(); | |
} | |
return; | |
} | |
dataset.headlessuiState = "open"; | |
} | |
fetch(`https://cohost.org/api/v1/trpc/posts.singlePost?batch=1&input={"0":{"handle":"${author}","postId":${postId}}}`) | |
.then(response => response.json()) | |
.then(response => { | |
const { post, comments } = response[0].result.data; | |
if (!Object.values(comments).map(commentArray => commentArray.length).some(commentCount => commentCount > 0)) { | |
return; | |
} | |
const posts = { [parseInt(postId)]: post }; | |
post.shareTree.forEach(sharedPost => { | |
posts[sharedPost.postId] = sharedPost | |
}); | |
const commentHolder = { | |
tagName: "div", | |
classList: ["flex", "min-w-0", "flex-col", "gap-2", "p-3"], | |
parent: article, | |
children: [] | |
}; | |
const mainPostURL = USE_ABSOLUTE_LINKS ? post.singlePostPageUrl : ""; | |
for (const [postRepliedToID, commentArray] of Object.entries(comments)) { | |
if (commentArray.length < 1) continue; | |
commentHolder.children.push( | |
{ | |
tagName: "h4", | |
// display name class just to give it the correct color | |
classList: ["px-3", "co-project-display-name", "lg:px-0"], | |
innerText: "in reply to ", | |
children: [ | |
{ | |
tagName: "a", | |
// text-secondary doesn't change with the individual post | |
classList: ["font-bold", "text-secondary", "hover:underline"], | |
href: `${mainPostURL}#post-${postRepliedToID}`, | |
innerText: `@${posts[commentArray[0].comment.postId].postingProject.handle}'s post:` // lol | |
} | |
] | |
}, | |
...commentArray.map((comment, _) => parseComment(comment, mainPostURL)) | |
); | |
} | |
commentHolderElement = createElement(commentHolder); | |
}); | |
}; | |
} | |
function isPost(node) { | |
return node.nodeType === Node.ELEMENT_NODE && node.dataset.view && node.querySelector("article"); | |
} | |
(function() { | |
"use strict"; | |
(new MutationObserver((_) => { | |
Array.from(document.querySelectorAll(".renderIfVisible")) | |
.map(node => node.firstChild) | |
.filter(isPost) | |
.forEach(initializePost); | |
})).observe(document.body, {subtree: true, childList: true}); | |
setInterval(() => { | |
document.querySelectorAll(".expand-comments-time") | |
.forEach(element => { | |
const newText = toRelativeDate(new Date(element.dataset.postDate)); | |
if (element.innerText !== newText) { | |
element.innerText = newText; | |
} | |
}) | |
}, 1000); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment