Skip to content

Instantly share code, notes, and snippets.

@8ullyMaguire
Created December 19, 2025 09:12
Show Gist options
  • Select an option

  • Save 8ullyMaguire/4a1971165dc81904f190597850d81d06 to your computer and use it in GitHub Desktop.

Select an option

Save 8ullyMaguire/4a1971165dc81904f190597850d81d06 to your computer and use it in GitHub Desktop.
Bluesky Reposts-to-Likes Percentage
```javascript
// ==UserScript==
// @name Bluesky Reposts-to-Likes Percentage
// @namespace https://example.com/bsky-repost-like-percentage
// @version 1.0.0
// @description Show reposts as a percentage of likes on Bluesky (bsky.app) by appending a parenthesized percentage next to the repost count.
// @match https://bsky.app/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
/* ----- Utilities ----- */
// Parse a human-friendly count like "1.2K", "1,234", "1.2M" into a Number.
function parseCount(text) {
if (!text) return 0;
// remove surrounding whitespace and commas, handle NBSPs
const s = String(text).trim().replace(/\u00A0/g, '').replace(/,/g, '');
const m = /^([\d,.]*\d)([KkMmBb])?$/.exec(s);
if (!m) {
// fallback: try to parse float directly
const v = parseFloat(s);
return Number.isFinite(v) ? v : 0;
}
const num = parseFloat(m[1]);
const suffix = (m[2] || '').toUpperCase();
switch (suffix) {
case 'K': return num * 1e3;
case 'M': return num * 1e6;
case 'B': return num * 1e9;
default: return num;
}
}
// Format percentage: integer for >=10%, one decimal for <10% (e.g. "9.8%"), clamp, no weird values
function formatPercent(raw) {
if (!isFinite(raw) || raw < 0) return '—';
if (raw >= 100) return Math.round(raw) + '%';
if (raw >= 10) return Math.round(raw) + '%';
// show one decimal for small percentages
return (Math.round(raw * 10) / 10).toFixed(1) + '%';
}
// Find an ancestor that contains both repostCount and likeCount nodes
function findSharedContainer(repostEl) {
let el = repostEl;
while (el && el !== document.documentElement) {
if (el.querySelector && el.querySelector('[data-testid="repostCount"]') && el.querySelector('[data-testid="likeCount"]')) {
return el;
}
el = el.parentElement;
}
return null;
}
/* ----- Main logic ----- */
// Process a single repost count element: compute percent and append/update UI node
function processRepostElement(repostEl) {
try {
if (!repostEl || !(repostEl instanceof Element)) return;
const container = findSharedContainer(repostEl);
if (!container) return;
const likeEl = container.querySelector('[data-testid="likeCount"]');
if (!likeEl) return;
const repostText = repostEl.textContent || '';
const likeText = likeEl.textContent || '';
const repostNum = parseCount(repostText);
const likeNum = parseCount(likeText);
// compute percent (reposts / likes * 100)
let percentText;
if (likeNum === 0) {
percentText = '—';
} else {
const raw = (repostNum / likeNum) * 100;
percentText = formatPercent(raw);
}
// create or update appended span
// we append the span as a sibling inside the same parent as repostEl when possible.
// use a stable class and data attribute so we can update it without duplicating.
const APP_CLASS = 'bsky-repost-like-percentage';
let span = repostEl.parentElement && repostEl.parentElement.querySelector(`.${APP_CLASS}[data-source-id="${getElementUniqueId(repostEl)}"]`);
if (!span) {
span = document.createElement('span');
span.className = APP_CLASS;
span.setAttribute('data-source-id', getElementUniqueId(repostEl));
// visual styling: small, muted, and ensuring separation from the number
span.style.marginLeft = '6px';
span.style.fontSize = '12px';
span.style.fontWeight = '500';
span.style.opacity = '0.85';
span.style.userSelect = 'none';
// ensure consistent color with existing UI by inheriting current color
span.style.color = 'inherit';
// put the text in parentheses per request
span.textContent = `(${percentText})`;
// append after repostEl within the same parent if possible
if (repostEl.nextSibling) {
repostEl.parentElement.insertBefore(span, repostEl.nextSibling);
} else {
repostEl.parentElement.appendChild(span);
}
} else {
span.textContent = `(${percentText})`;
}
} catch (err) {
// defensive: don't break the page if anything goes wrong
console.error('Repost->Like percentage worker error:', err);
}
}
// Create a stable unique id for an element to identify appended span.
// We prefer to cache on the element using a Symbol to avoid collisions.
const UID_SYMBOL = Symbol('bsky-repost-uid');
let uidCounter = 1;
function getElementUniqueId(el) {
if (!el) return 'unknown';
if (!el[UID_SYMBOL]) {
el[UID_SYMBOL] = String(uidCounter++);
}
return el[UID_SYMBOL];
}
// Find all repostCount elements on the page and process them.
function processAllRepostElements() {
const repostEls = document.querySelectorAll('[data-testid="repostCount"]');
repostEls.forEach(processRepostElement);
}
// Debounced mutation processing to avoid thrashing.
let scheduled = null;
function scheduleProcessAll(delay = 120) {
if (scheduled) clearTimeout(scheduled);
scheduled = setTimeout(() => {
scheduled = null;
processAllRepostElements();
}, delay);
}
/* ----- Observe DOM changes ----- */
const observer = new MutationObserver((mutations) => {
// If any mutation touches attributes or adds/removes nodes, we rescan (debounced)
let relevant = false;
for (const m of mutations) {
if (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) {
relevant = true;
break;
}
if (m.type === 'characterData' || m.type === 'attributes') {
relevant = true;
break;
}
}
if (relevant) {
scheduleProcessAll();
}
});
// Start observing a sensible root (document.body). If body not ready, wait until it is.
function startObserver() {
if (!document.body) {
window.addEventListener('DOMContentLoaded', startObserver, { once: true });
return;
}
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ['data-testid', 'class', 'aria-pressed'] // monitor likely-changing attributes
});
// initial pass
scheduleProcessAll(50);
}
// Kick off
startObserver();
// Also re-run when window gains focus (useful if counts updated while tab was backgrounded)
window.addEventListener('focus', () => scheduleProcessAll(50));
})();
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment