wayBetter for X

This commit is contained in:
CRTkafa 2026-10-01 06:01:13 +07:00
commit df5dff619e
720 changed files with 147465 additions and 0 deletions

View file

@ -0,0 +1,95 @@
/**
* @name Exact Timestamps
* @description Shows the full local date and time next to relative post times ("5h" becomes "5h · Sep 27, 2026, 14:03") and on hover. When the header is short on room the date gives way first (it shortens, then hides) and never covers X's buttons.
* @version 1.2.0
* @author wayBetter
* @permissions network:none
*/
module.exports = class ExactTimestamps {
start() {
const api = window.wayBetter.api;
this.scope = api.scope();
// Dates in the app's language; a language switch redraws every date.
const locale = () => (api.i18n ? api.i18n.locale.split('-x-')[0] : undefined);
this.format = new Intl.DateTimeFormat(locale(), { dateStyle: 'medium', timeStyle: 'short' });
if (api.i18n) this.scope.defer(api.i18n.onChange(() => {
this.format = new Intl.DateTimeFormat(locale(), { dateStyle: 'medium', timeStyle: 'short' });
for (const time of document.querySelectorAll('article time[data-bt-exact]')) this.decorate(time);
}));
// The date is its own flex item in X's header row (name · @handle · 2h | Grok ··· ). It shrinks
// a thousand times faster than X's name block, so a narrow header cuts the date (with an
// ellipsis, the full date stays in the tooltip) before it truncates the name, and X's buttons
// on the right never move or get covered.
this.scope.css('exact-timestamps', `
.bt-exact-time { flex: 0 1000 auto; min-width: 0; margin: 0 auto 0 4px; align-self: flex-start;
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
font: 400 15px/20px var(--wb-font, inherit); color: var(--wb-muted, inherit); opacity: .85; }
.bt-exact-time.bt-exact-inline { margin-right: 0; }
`);
this.scope.observe('article time[datetime]', time => this.decorate(time));
}
stop() {
this.scope.dispose();
// Per-element changes are marked in the DOM (no per-element closures that keep old nodes alive).
for (const span of document.querySelectorAll('.bt-exact-time')) span.remove();
for (const time of document.querySelectorAll('time[data-bt-exact]')) {
time.removeAttribute('title');
time.removeAttribute('data-bt-exact');
}
}
/**
* Where the date goes: right after the name block in the post's header row (the row that also
* holds X's Grok / ··· buttons), or, where a header has no buttons (quoted posts), right after the
* time's own box. Returns the node to insert after and whether it is the header row.
*/
static placement(time) {
const article = time.closest('article');
const link = time.closest('a') || time;
const name = link.closest('[data-testid="User-Name"]');
// A quoted post (a role=link card inside the post) has no buttons of its own.
const quoted = link.closest('div[role="link"]');
if (name && article && !(quoted && article.contains(quoted))) {
// Nearest ancestor of the name block whose parent is a row that also holds X's header buttons.
let node = name;
for (let depth = 0; depth < 6 && node.parentElement && node.parentElement !== article; depth += 1, node = node.parentElement) {
const row = node.parentElement;
if (!row.querySelector('[data-testid="caret"], button[aria-label*="Grok" i]')) continue;
if (node.querySelector('[data-testid="caret"]') || !getComputedStyle(row).flexDirection.startsWith('row')) break;
return { after: node, header: true };
}
}
// X keeps the time in a box that never shrinks; the date goes after that box (the outermost
// wrapper holding only the time), where it can shrink, not inside it.
let box = link;
while (name && box.parentElement && box.parentElement !== name && box.parentElement.childElementCount === 1 && name.contains(box.parentElement)) box = box.parentElement;
return { after: box, header: false };
}
decorate(time) {
const date = new Date(time.getAttribute('datetime'));
if (Number.isNaN(date.getTime())) return;
const full = this.format.format(date);
// X renders post times without a title; stop() removes the one set here.
time.setAttribute('data-bt-exact', '');
time.setAttribute('title', full);
// The focused post on a post page already prints the full date ("2:03 PM · Sep 27, 2026").
if ((time.textContent || '').includes('·')) return;
const { after, header } = ExactTimestamps.placement(time);
if (!after.parentNode) return;
const text = `· ${full}`;
const next = after.nextElementSibling;
if (next?.classList.contains('bt-exact-time')) {
if (next.textContent !== text) next.textContent = text;
return;
}
// A header row keeps at most one date (X may re-render the name block next to an old one).
if (header) for (const old of after.parentNode.querySelectorAll(':scope > .bt-exact-time')) old.remove();
const span = document.createElement('span');
span.className = header ? 'bt-exact-time' : 'bt-exact-time bt-exact-inline';
span.title = full;
span.textContent = text;
after.after(span);
}
};