Skip to content

Instantly share code, notes, and snippets.

@cefqrn
Last active November 23, 2023 01:26
Show Gist options
  • Save cefqrn/583593fb8ac1c78df7ffa71425d09adb to your computer and use it in GitHub Desktop.
Save cefqrn/583593fb8ac1c78df7ffa71425d09adb to your computer and use it in GitHub Desktop.
// ==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