// ==UserScript==
// @name Twitter Linkify Trends
// @description Make Twitter trends links (again)
// @author chocolateboy
// @copyright chocolateboy
// @version 3.0.1
// @namespace https://github.com/chocolateboy/userscripts
// @license GPL
// @include https://mobile.x.com/
// @include https://mobile.x.com/*
// @include https://x.com/
// @include https://x.com/*
// @require https://code.jquery.com/jquery-3.7.1.slim.min.js
// @require https://unpkg.com/[email protected]/dist/index.iife.min.js
// @require https://unpkg.com/@chocolateboy/[email protected]/dist/polyfill.iife.min.js
// @require https://unpkg.com/[email protected]/dist/index.umd.min.js
// @require https://unpkg.com/[email protected]/dist/flru.min.js
// @grant GM_log
// @run-at document-start
// ==/UserScript==
// NOTE This file is generated from src/twitter-linkify-trends.user.ts and should not be edited directly.
"use strict";
(() => {
// src/lib/util.ts
var constant = (value) => (..._args) => value;
// src/lib/observer.ts
var INIT = { childList: true, subtree: true };
var done = constant(false);
var resume = constant(true);
var observe = (...args) => {
const [target, init, callback] = args.length === 3 ? args : args.length === 2 ? args[0] instanceof Element ? [args[0], INIT, args[1]] : [document.body, args[0], args[1]] : [document.body, INIT, args[0]];
const onMutate = (mutations, observer2) => {
observer2.disconnect();
const resume2 = callback({ mutations, observer: observer2, target });
if (resume2 !== false) {
observer2.observe(target, init);
}
};
const observer = new MutationObserver(onMutate);
queueMicrotask(() => onMutate([], observer));
return observer;
};
// src/twitter-linkify-trends.user.ts
// @license GPL
var CACHE = exports.default(128);
var DISABLED_EVENTS = "click touch";
var EVENT_DATA = "data.explore_page.body.initialTimeline.timeline.timeline.instructions[-1].entries[1].content.items.*.item.itemContent";
var EVENT_DATA_ENDPOINT = "/ExplorePage";
var EVENT = 'div[role="link"][data-testid="trend"]:has([data-testid^="UserAvatar-Container"]):not([data-linked])';
var TREND = 'div[role="link"][data-testid="trend"]:not(:has([data-testid^="UserAvatar-Container"])):not([data-linked])';
var VIDEO = 'div[role="presentation"] div[role="link"][data-testid^="media-tweet-card-"]:not([data-linked])';
var SELECTOR = [EVENT, TREND, VIDEO].join(", ");
function disableAll(e) {
e.stopPropagation();
}
function disableSome(e) {
const $target = $(e.target);
const $caret = $target.closest('[data-testid="caret"]', this);
if (!$caret.length) {
e.stopPropagation();
}
}
function hookXHROpen(oldOpen) {
return function open(_method, url) {
const $url = URL.parse(url);
if ($url.pathname.endsWith(EVENT_DATA_ENDPOINT)) {
this.addEventListener("load", () => processEventData(this.responseText));
}
return GMCompat.apply(this, oldOpen, arguments);
};
}
function linkFor(href) {
return $("<a></a>").attr({ href, role: "link", "data-focusable": true }).css({ color: "inherit", textDecoration: "inherit" });
}
function onElement(el) {
const $el = $(el);
let fixPointer = true;
let linked = true;
if ($el.is(EVENT)) {
$el.on(DISABLED_EVENTS, disableAll);
linked = onEventElement($el);
} else if ($el.is(TREND)) {
$el.on(DISABLED_EVENTS, disableSome);
onTrendElement($el);
} else if ($el.is(VIDEO)) {
fixPointer = false;
$el.on(DISABLED_EVENTS, disableAll);
onVideoElement($el);
}
if (linked) {
$el.attr("data-linked", "true");
}
if (fixPointer) {
$el.css("cursor", "auto");
}
}
function onEventElement($event) {
const { target, title } = targetFor($event);
const url = CACHE.get(title);
if (!url) {
return false;
}
console.debug(`element (event):`, JSON.stringify(title));
const $link = linkFor(url);
$(target).parent().wrap($link);
return true;
}
function onTrendElement($trend) {
const { target, title } = targetFor($trend);
const param = /\s+/.test(title) ? '"' + title.replace(/"/g, "") + '"' : title;
console.debug("element (trend):", param);
const query = encodeURIComponent(param);
const url = `${location.origin}/search?q=${query}&src=trend_click&vertical=trends`;
$(target).wrap(linkFor(url));
}
function onVideoElement($link) {
const id = $link.data("testid").split("-").at(-1);
const url = `${location.origin}/i/web/status/${id}`;
$link.wrap(linkFor(url));
}
function processEventData(json) {
const data = JSON.parse(json);
const events = exports.get(data, EVENT_DATA, []);
for (const event of events) {
const title = event.name;
const uri = event.trend_url.url;
const url = uri.replace(/^twitter:\/\//, `${location.origin}/i/`);
console.debug("data (event):", { title, url });
CACHE.set(title, url);
}
}
function targetFor($el) {
const targets = $el.find('div[dir="ltr"] > span').filter((_, el) => {
const fontWeight = Number($(el).parent().css("fontWeight") || 0);
return fontWeight >= 700;
});
const target = targets.get().pop();
const title = $(target).text().trim();
return { target, title };
}
function run() {
const target = document.getElementById("react-root");
if (!target) {
console.warn("can't find react-root element");
return;
}
observe(target, () => {
for (const el of $(SELECTOR)) {
onElement(el);
}
});
}
var xhrProto = GMCompat.unsafeWindow.XMLHttpRequest.prototype;
xhrProto.open = GMCompat.export(hookXHROpen(xhrProto.open));
$(run);
})();