Displays the post ID number after the username/date and enables chan-style greentext formatting on x.com.
// ==UserScript==
// @name xitter digits observer
// @namespace https://x.com/
// @version 1.4
// @description Displays the post ID number after the username/date and enables chan-style greentext formatting on x.com.
// @match https://x.com/*
// @match https://twitter.com/*
// @author @gigagroid
// @license GNU GPLv3
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// =========================
// GREENTEXT (UNCHANGED CORE)
// =========================
function processGreentext(tweet) {
if (tweet.dataset.gtDone === "1") return;
tweet.dataset.gtDone = "1";
const textContainer = tweet.querySelector('[data-testid="tweetText"]');
if (!textContainer) return;
const walker = document.createTreeWalker(
textContainer,
NodeFilter.SHOW_TEXT,
null,
false
);
const nodes = [];
let n;
while ((n = walker.nextNode())) nodes.push(n);
nodes.forEach(textNode => {
const text = textNode.nodeValue;
if (!text.includes('>')) return;
const frag = document.createDocumentFragment();
const parts = text.split(/(\n)/);
for (const part of parts) {
if (part === '\n') {
frag.appendChild(document.createTextNode(part));
continue;
}
if (/^\s*>/.test(part)) {
const span = document.createElement('span');
span.style.color = 'rgb(90, 150, 65)';
span.textContent = part;
frag.appendChild(span);
} else {
frag.appendChild(document.createTextNode(part));
}
}
textNode.parentNode.replaceChild(frag, textNode);
});
}
// =========================
// POST ID (NATIVE INLINE STYLE)
// =========================
function getTweetKey(tweet) {
const link = tweet.querySelector('a[href*="/status/"]');
return link?.href || null;
}
function processTweet(tweet) {
processGreentext(tweet);
const key = getTweetKey(tweet);
if (!key) return;
const match = key.match(/status\/(\d+)/);
if (!match) return;
const postId = match[1];
const nameContainer = tweet.querySelector('[data-testid="User-Name"]');
if (!nameContainer) return;
// prevent duplicates even after X re-render
if (tweet.dataset.xPostId === postId) return;
tweet.dataset.xPostId = postId;
// IMPORTANT: find last metadata text node (where " · time" lives)
const walker = document.createTreeWalker(
nameContainer,
NodeFilter.SHOW_TEXT,
null,
false
);
let lastTextNode = null;
let node;
while ((node = walker.nextNode())) {
lastTextNode = node;
}
// fallback if structure changes
const target = lastTextNode || nameContainer;
// insert EXACT native-style separator + id
const injection = document.createTextNode(` · >>${postId}`);
// if we have a text node, append to it; otherwise append to container
if (target.nodeType === Node.TEXT_NODE) {
target.nodeValue += ` · >>${postId}`;
} else {
target.appendChild(injection);
}
}
// =========================
// RESILIENT SCANNER
// =========================
function scan(root = document) {
root.querySelectorAll('[data-testid="tweet"]').forEach(processTweet);
}
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.matches?.('[data-testid="tweet"]')) {
processTweet(node);
} else {
node.querySelectorAll?.('[data-testid="tweet"]')?.forEach(processTweet);
}
}
}
});
scan();
observer.observe(document.body, {
childList: true,
subtree: true
});
// backup for full React tree rewrites
setInterval(scan, 5000);
})();