Skip to content

Instantly share code, notes, and snippets.

@cefqrn
Last active November 23, 2023 01:50
Show Gist options
  • Save cefqrn/069d702ba83826199c90b9ec748f777c to your computer and use it in GitHub Desktop.
Save cefqrn/069d702ba83826199c90b9ec748f777c to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Header Buttons
// @version 0.36
// @description Adds like and share buttons to the headers of shared posts
// @author cefqrn
// @match https://cohost.org/*
// ==/UserScript==
const SHARE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" class="h-6 w-6 co-action-button"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"></path></svg>`
const HEART_PATH_ACTIVE = `<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z"></path>`
const HEART_PATH_INACTIVE = `<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"></path>`
const SVG_NS = "http://www.w3.org/2000/svg";
(async function() {
"use strict";
// get the user id and handle from the user's profile picture
const avatar = await new Promise((resolve) => {
const intervalID = setInterval(() => {
const result = document.querySelector("button[id^=headlessui-listbox-button] div img");
if (result) {
clearInterval(intervalID);
resolve(result);
}
}, 100);
});
// the user id is the first part of the avatar's filename
const userID = parseInt(avatar.src.match(/avatar\/(\d+)/)[1]);
// the handle is used as alt text for profile pictures
const handle = avatar.alt;
// posts on the current page
const posts = {};
function initializeHeader(header) {
const postURL = header.querySelector("time a").href;
if (postURL === "https://cohost.org/") {
// post is deleted
return;
}
const postID = parseInt(postURL.match(/post\/(\d+)/)[1]);
const iconContainer = document.createElement("div");
iconContainer.classList.add("flex", "items-center", "justify-end", "gap-3", "ml-auto");
header.appendChild(iconContainer);
const shareLink = document.createElement("a");
shareLink.href = `https://cohost.org/${handle}/post/compose?shareOf=${postID}`;
shareLink.title = `share this post as ${handle}`;
shareLink.innerHTML = SHARE_ICON;
iconContainer.appendChild(shareLink);
const likeButton = document.createElement("button");
likeButton.classList.add("w-6", "h-6", "pointer", "relative");
likeButton.title = `like this post as ${handle}`
iconContainer.appendChild(likeButton);
const activeLikeIcon = document.createElementNS(SVG_NS, "svg");
activeLikeIcon.setAttributeNS(null, "fill", "currentColor");
activeLikeIcon.setAttributeNS(null, "viewBox", "0 0 24 24");
activeLikeIcon.setAttributeNS(null, "aria-hidden", "true");
activeLikeIcon.classList.add("w-6", "h-6", "pointer", "absolute", "top-0", "left-0", "text-cherry", "invisible");
activeLikeIcon.innerHTML = HEART_PATH_ACTIVE;
likeButton.appendChild(activeLikeIcon);
const inactiveLikeIcon = document.createElementNS(SVG_NS, "svg");
inactiveLikeIcon.setAttributeNS(null, "fill", "none");
inactiveLikeIcon.setAttributeNS(null, "viewBox", "0 0 24 24");
inactiveLikeIcon.setAttributeNS(null, "stroke-width", "1.5");
inactiveLikeIcon.setAttributeNS(null, "stroke", "currentColor");
inactiveLikeIcon.setAttributeNS(null, "aria-hidden", "true");
inactiveLikeIcon.classList.add("w-6", "h-6", "pointer", "absolute", "top-0", "left-0", "co-action-button", "visible");
inactiveLikeIcon.innerHTML = HEART_PATH_INACTIVE;
likeButton.appendChild(inactiveLikeIcon);
function updateLikeIcon() {
const liked = posts[postID].isLiked;
likeButton.title = `${liked ? "unlike" : "like"} this post as ${handle}`
if (liked) {
activeLikeIcon.classList.replace("invisible", "visible");
inactiveLikeIcon.classList.replace("visible", "invisible");
} else {
inactiveLikeIcon.classList.replace("invisible", "visible");
activeLikeIcon.classList.replace("visible", "invisible");
}
}
if (postID in posts) {
posts[postID].likeUpdateFunctions.push(updateLikeIcon);
updateLikeIcon();
} else {
posts[postID] = { isLiked: false, likeUpdateFunctions: [updateLikeIcon] };
}
likeButton.onclick = (event) => {
const post = posts[postID];
const liked = post.isLiked;
fetch(
`https://cohost.org/api/v1/trpc/relationships.${liked ? "unlike" : "like"}?batch=1`,
{
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify({"0": {fromProjectId: userID, toPostId: postID}})
}
).then(res => {
if (res.ok) {
post.isLiked = !liked;
post.likeUpdateFunctions.forEach(func => func());
}
});
}
header.dataset.headerLikeButtonInitialized = true;
}
function updatePosts() {
Array.from(document.querySelectorAll("[id^=post] + div"))
.filter(div => div.className !== "" && !div.dataset.headerLikeButtonInitialized)
.map(initializeHeader);
}
// initialize posts every time the page updates
(new MutationObserver(updatePosts)).observe(document, {subtree: true, childList: true});
// initialize the posts that were loaded before the script started
updatePosts();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment