Untitled
unknown
javascript
a year ago
12 kB
49
Indexable
Never
// ==UserScript== // @name Mastodon - threaded replies // @description Mastodon - threaded replies // @match https://qoto.org/* // @version 1.5 // ==/UserScript== /* jshint -W097 */ 'use strict'; // NOTE: change the URL below and the match above to your own instance. const instanceURL = "https://qoto.org"; const maxIndent = 15; let loc = window.location.toString(); let json = {}; let replyMap = {}; const clonableButton = document.createElement("div"); clonableButton.style.position = "absolute"; clonableButton.style.cursor = "pointer"; clonableButton.style.fontSize = "12px"; clonableButton.style.right = 0; clonableButton.style.bottom = 0; clonableButton.style.width = "100%"; clonableButton.style.textAlign = "center"; clonableButton.style.padding = "6px 0"; clonableButton.style.lineHeight = "0px"; clonableButton.style.fontSize = "0px"; const colorConvert = function(lab) { const L = Math.pow(lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2], 3); const M = Math.pow(lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2], 3); const S = Math.pow(lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2], 3); const rgb = [+4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S, -1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S, -0.0041960863 * L - 0.7034186147 * M + 1.7076147010 * S]; const linearRgbToSrgb = c => c > 0.0031308 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c; return rgb.map(linearRgbToSrgb); }; // generates a "nice" color wheel, with constant lightness / chroma const getRotatedColor = function(index, maxIndex) { const L = 60; // 75; const C = 10; // 12.1; const hueRadians = (2 * Math.PI) * (index / (maxIndex + 1)); const a = C * Math.cos(hueRadians); const b = C * Math.sin(hueRadians); const rgb = colorConvert([L, a, b]); return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; }; const recursiveSetVisibility = function(id, displayProp) { if (!replyMap.hasOwnProperty(id)) { return; } for (const replyId of replyMap[id]) { const replyElement = document.querySelector(`div[data-id="${replyId}"]`); replyElement.parentElement.style.display = displayProp; if (!replyElement.classList.contains("tree-hidden")) { recursiveSetVisibility(replyId, displayProp); } } }; const buttonHover = function(ev) { ev.target.style.backgroundColor = "rgb(128,128,128,0.15)"; ev.target.style.fontSize = "12px"; }; const buttonExit = function(ev) { if (!ev.target.classList.contains("tree-hidden")) { ev.target.style.backgroundColor = ""; ev.target.style.fontSize = "0px"; } }; const buttonClick = function(ev) { ev.stopPropagation(); ev.target.classList.toggle("tree-hidden"); const status = ev.target.previousElementSibling; status.classList.toggle("tree-hidden"); const hide = status.classList.contains("tree-hidden"); const id = status.getAttribute("data-id"); const displayProp = hide ? "none" : ""; ev.target.textContent = hide ? "▼" : "▲"; recursiveSetVisibility(id, displayProp); for (const statusElement of Array.from(status.children).slice(1)) { statusElement.style.display = displayProp; } }; const addToggleButton = function(reply) { // FIXME: is there a better way to check if button has already been added? if (reply.nextElementSibling) { return; } const button = clonableButton.cloneNode(); button.addEventListener("pointerenter", buttonHover); button.addEventListener("pointerout", buttonExit); button.addEventListener("click", buttonClick); button.textContent = "▲"; reply.parentElement.appendChild(button); }; const indentReplies = function() { const replies = document.getElementsByClassName("status-reply"); // race condition avoidance: try again if not all posts are loaded yet // FIXME: could this be flaky if we run while replies are still being added? if (!replies.length) { setTimeout(indentReplies, 100); return; } replyMap = {}; const replyDepth = {}; let maxDepth = 0; // FIXME: this assumes children never appear before parents in reply list for (const reply of json.descendants) { if (!replyMap.hasOwnProperty(reply.in_reply_to_id)) { replyMap[reply.in_reply_to_id] = [reply.id]; } else { replyMap[reply.in_reply_to_id].push(reply.id); } let depth = 0; if (replyDepth.hasOwnProperty(reply.in_reply_to_id)) { depth += replyDepth[reply.in_reply_to_id] + 1; } replyDepth[reply.id] = depth; if (depth > maxDepth) { maxDepth = depth; } } const colorMap = {}; for (let i = 0; i <= maxDepth; i++) { colorMap[i] = getRotatedColor(i, maxDepth); } for (const reply of replies) { const replyID = reply.getAttribute("data-id"); const depth = replyDepth[replyID]; if (depth >= 0) { // set a maximum depth so that indentation doesn't squish too much reply.parentElement.style.marginLeft = `${maxIndent * Math.min(15, depth)}px`; reply.parentElement.style.borderLeft = `5px solid ${colorMap[depth]}`; addToggleButton(reply); } } }; const locationChanged = async function() { // alert("Location changed!"); const pathParts = window.location.pathname.split("/"); if (pathParts.length < 3) { return; } //alert(pathParts[3]); // if (!pathParts[1].startsWith("@")) { // alert("@"); // return; // } // note: matching empty string is deliberate if (!Number(pathParts[3])) { return; } // same origin, shouldn't cause CORS issues const resp = await fetch(`${instanceURL}/api/v1/statuses/${pathParts[3]}/context`); json = await resp.json(); indentReplies(); }; const checkLocation = function() { // alert("Checking location"); if (window.location.toString() !== loc) { loc = window.location.toString(); locationChanged(); } }; //const mutConfig = {attributes: false, childList: true, subtree: false}; //const title = document.head.getElementsByTagName("title")[0]; //alert(title); //const mutObs = new MutationObserver(checkLocation); //mutObs.observe(title, mutConfig); locationChanged(); window.onclick = function() { // alert("Clicked"); checkLocation(); };// ==UserScript== // @name Mastodon - threaded replies // @description Mastodon - threaded replies // @match https://qoto.org/* // @version 1.5 // ==/UserScript== /* jshint -W097 */ 'use strict'; // NOTE: change the URL below and the match above to your own instance. const instanceURL = "https://qoto.org"; const maxIndent = 15; let loc = window.location.toString(); let json = {}; let replyMap = {}; const clonableButton = document.createElement("div"); clonableButton.style.position = "absolute"; clonableButton.style.cursor = "pointer"; clonableButton.style.fontSize = "12px"; clonableButton.style.right = 0; clonableButton.style.bottom = 0; clonableButton.style.width = "100%"; clonableButton.style.textAlign = "center"; clonableButton.style.padding = "6px 0"; clonableButton.style.lineHeight = "0px"; clonableButton.style.fontSize = "0px"; const colorConvert = function(lab) { const L = Math.pow(lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2], 3); const M = Math.pow(lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2], 3); const S = Math.pow(lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2], 3); const rgb = [+4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S, -1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S, -0.0041960863 * L - 0.7034186147 * M + 1.7076147010 * S]; const linearRgbToSrgb = c => c > 0.0031308 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c; return rgb.map(linearRgbToSrgb); }; // generates a "nice" color wheel, with constant lightness / chroma const getRotatedColor = function(index, maxIndex) { const L = 60; // 75; const C = 10; // 12.1; const hueRadians = (2 * Math.PI) * (index / (maxIndex + 1)); const a = C * Math.cos(hueRadians); const b = C * Math.sin(hueRadians); const rgb = colorConvert([L, a, b]); return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; }; const recursiveSetVisibility = function(id, displayProp) { if (!replyMap.hasOwnProperty(id)) { return; } for (const replyId of replyMap[id]) { const replyElement = document.querySelector(`div[data-id="${replyId}"]`); replyElement.parentElement.style.display = displayProp; if (!replyElement.classList.contains("tree-hidden")) { recursiveSetVisibility(replyId, displayProp); } } }; const buttonHover = function(ev) { ev.target.style.backgroundColor = "rgb(128,128,128,0.15)"; ev.target.style.fontSize = "12px"; }; const buttonExit = function(ev) { if (!ev.target.classList.contains("tree-hidden")) { ev.target.style.backgroundColor = ""; ev.target.style.fontSize = "0px"; } }; const buttonClick = function(ev) { ev.stopPropagation(); ev.target.classList.toggle("tree-hidden"); const status = ev.target.previousElementSibling; status.classList.toggle("tree-hidden"); const hide = status.classList.contains("tree-hidden"); const id = status.getAttribute("data-id"); const displayProp = hide ? "none" : ""; ev.target.textContent = hide ? "▼" : "▲"; recursiveSetVisibility(id, displayProp); for (const statusElement of Array.from(status.children).slice(1)) { statusElement.style.display = displayProp; } }; const addToggleButton = function(reply) { // FIXME: is there a better way to check if button has already been added? if (reply.nextElementSibling) { return; } const button = clonableButton.cloneNode(); button.addEventListener("pointerenter", buttonHover); button.addEventListener("pointerout", buttonExit); button.addEventListener("click", buttonClick); button.textContent = "▲"; reply.parentElement.appendChild(button); }; const indentReplies = function() { const replies = document.getElementsByClassName("status-reply"); // race condition avoidance: try again if not all posts are loaded yet // FIXME: could this be flaky if we run while replies are still being added? if (!replies.length) { setTimeout(indentReplies, 100); return; } replyMap = {}; const replyDepth = {}; let maxDepth = 0; // FIXME: this assumes children never appear before parents in reply list for (const reply of json.descendants) { if (!replyMap.hasOwnProperty(reply.in_reply_to_id)) { replyMap[reply.in_reply_to_id] = [reply.id]; } else { replyMap[reply.in_reply_to_id].push(reply.id); } let depth = 0; if (replyDepth.hasOwnProperty(reply.in_reply_to_id)) { depth += replyDepth[reply.in_reply_to_id] + 1; } replyDepth[reply.id] = depth; if (depth > maxDepth) { maxDepth = depth; } } const colorMap = {}; for (let i = 0; i <= maxDepth; i++) { colorMap[i] = getRotatedColor(i, maxDepth); } for (const reply of replies) { const replyID = reply.getAttribute("data-id"); const depth = replyDepth[replyID]; if (depth >= 0) { // set a maximum depth so that indentation doesn't squish too much reply.parentElement.style.marginLeft = `${maxIndent * Math.min(15, depth)}px`; reply.parentElement.style.borderLeft = `5px solid ${colorMap[depth]}`; addToggleButton(reply); } } }; const locationChanged = async function() { // alert("Location changed!"); const pathParts = window.location.pathname.split("/"); if (pathParts.length < 3) { return; } //alert(pathParts[3]); // if (!pathParts[1].startsWith("@")) { // alert("@"); // return; // } // note: matching empty string is deliberate if (!Number(pathParts[3])) { return; } // same origin, shouldn't cause CORS issues const resp = await fetch(`${instanceURL}/api/v1/statuses/${pathParts[3]}/context`); json = await resp.json(); indentReplies(); }; const checkLocation = function() { // alert("Checking location"); if (window.location.toString() !== loc) { loc = window.location.toString(); locationChanged(); } }; //const mutConfig = {attributes: false, childList: true, subtree: false}; //const title = document.head.getElementsByTagName("title")[0]; //alert(title); //const mutObs = new MutationObserver(checkLocation); //mutObs.observe(title, mutConfig); locationChanged(); window.onclick = function() { // alert("Clicked"); checkLocation(); };