Greasy Fork is available in English.
Make Twitter trends links (again)
// ==UserScript==// @name Twitter Linkify Trends// @description Make Twitter trends links (again)// @author chocolateboy// @copyright chocolateboy// @version 2.3.0// @namespace https://github.com/chocolateboy/userscripts// @license GPL// @include https://mobile.twitter.com/// @include https://mobile.twitter.com/*// @include https://mobile.x.com/// @include https://mobile.x.com/*// @include https://twitter.com/// @include https://twitter.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/twitter-linkify-trends.user.ts// @license GPLvar CACHE = exports.default(128);var DEBUG = {};var DISABLED_EVENTS = "click touch";var EVENT_DATA = "/2/guide.json";var EVENT_PATH = "timeline.instructions.*.addEntries.entries.*.content.timelineModule.items.*.item.content.eventSummary";var EVENT_HERO_PATH = "timeline.instructions.*.addEntries.entries.*.content.item.content.eventSummary";var LIVE_EVENT_KEY = "/lex/placeholder_live_nomargin";var EVENT = '[data-testid="sidebarColumn"] div[role="link"]:not([data-testid]):not([data-linked])';var EVENT_IMAGE = `${EVENT} > div > div:nth-child(2):last-child img[src]:not([src=""])`;var EVENT_HERO = 'div[role="link"][data-testid="eventHero"]:not([data-linked])';var EVENT_HERO_IMAGE = `${EVENT_HERO} > div:first-child [data-testid="image"] > img[src]:not([src=""])`;var TREND = 'div[role="link"][data-testid="trend"]:not([data-linked])';var VIDEO = 'div[role="presentation"] div[role="link"][data-testid^="media-tweet-card-"]:not([data-linked])';var EVENT_ANY = [EVENT, EVENT_HERO].join(", ");var SELECTOR = [EVENT_IMAGE, EVENT_HERO_IMAGE, TREND, VIDEO].join(", ");var pluck = exports.getter({ default: [], split: "." });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 = new URL(url);if ($url.pathname.endsWith(EVENT_DATA)) {this.addEventListener("load", () => processEventData(this.responseText));}return GMCompat.apply(this, oldOpen, arguments);};}function keyFor(url) {const { pathname: path } = new URL(url);return path === LIVE_EVENT_KEY ? path : path.split("/")[2];}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 $target;let type;if ($el.is(TREND)) {[$target, type] = [$el, "trend"];$el.on(DISABLED_EVENTS, disableSome);onTrendElement($el);} else if ($el.is(VIDEO)) {[$target, type] = [$el, "video"];$el.on(DISABLED_EVENTS, disableAll);onVideoElement($el);} else {const $event = $el.closest(EVENT_ANY);const wrapImage = $event.is(EVENT);[$target, type] = [$event, "event"];$event.on(DISABLED_EVENTS, disableAll);onEventElement($event, $el, { wrapImage });}$target.attr("data-linked", "true");if (type !== "video") {$target.css("cursor", "auto");}if (DEBUG[type]) {$target.css("backgroundColor", DEBUG[type]);}}function onEventElement($event, $image, options = {}) {const { target, title } = targetFor($event);const key = keyFor($image.attr("src"));console.debug("element (event):", JSON.stringify(title));const url = key === LIVE_EVENT_KEY ? CACHE.get(title) : CACHE.get(key);if (url) {const $link = linkFor(url);$(target).parent().wrap($link);if (options.wrapImage) {$image.wrap($link);}} else {console.warn("Can't find URL for event (element):", JSON.stringify(title));}}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 = `https://x.com/i/web/status/${id}`;$link.wrap(linkFor(url));}function processEventData(json) {const data = JSON.parse(json);const events = pluck(data, EVENT_PATH);const eventHero = pluck(data, EVENT_HERO_PATH);const $events = eventHero.concat(events);const nEvents = $events.length;if (!nEvents) {return;}for (const event of $events) {const { title, url: { url } } = event;const imageURL = event.image?.url;if (!imageURL) {console.warn("Can't find image for event (data):", title);continue;}const key = keyFor(imageURL);console.debug("data (event):", JSON.stringify(title));if (key === LIVE_EVENT_KEY) {CACHE.set(title, url);} else {CACHE.set(key, 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 init = { childList: true, subtree: true };const target = document.getElementById("react-root");if (!target) {console.warn("can't find react-root element");return;}const callback = (_mutations, observer) => {observer.disconnect();for (const el of $(SELECTOR)) {onElement(el);}observer.observe(target, init);};new MutationObserver(callback).observe(target, init);}var xhrProto = GMCompat.unsafeWindow.XMLHttpRequest.prototype;xhrProto.open = GMCompat.export(hookXHROpen(xhrProto.open));$(run);})();