Twitter Linkify Trends

Make Twitter trends links (again)

  1. // ==UserScript==
  2. // @name Twitter Linkify Trends
  3. // @description Make Twitter trends links (again)
  4. // @author chocolateboy
  5. // @copyright chocolateboy
  6. // @version 3.0.1
  7. // @namespace https://github.com/chocolateboy/userscripts
  8. // @license GPL
  9. // @include https://mobile.x.com/
  10. // @include https://mobile.x.com/*
  11. // @include https://x.com/
  12. // @include https://x.com/*
  13. // @require https://code.jquery.com/jquery-3.7.1.slim.min.js
  14. // @require https://unpkg.com/gm-compat@1.1.0/dist/index.iife.min.js
  15. // @require https://unpkg.com/@chocolateboy/uncommonjs@3.2.1/dist/polyfill.iife.min.js
  16. // @require https://unpkg.com/get-wild@3.0.2/dist/index.umd.min.js
  17. // @require https://unpkg.com/flru@1.0.2/dist/flru.min.js
  18. // @grant GM_log
  19. // @run-at document-start
  20. // ==/UserScript==
  21.  
  22. // NOTE This file is generated from src/twitter-linkify-trends.user.ts and should not be edited directly.
  23.  
  24. "use strict";
  25. (() => {
  26. // src/lib/util.ts
  27. var constant = (value) => (..._args) => value;
  28.  
  29. // src/lib/observer.ts
  30. var INIT = { childList: true, subtree: true };
  31. var done = constant(false);
  32. var resume = constant(true);
  33. var observe = (...args) => {
  34. 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]];
  35. const onMutate = (mutations, observer2) => {
  36. observer2.disconnect();
  37. const resume2 = callback({ mutations, observer: observer2, target });
  38. if (resume2 !== false) {
  39. observer2.observe(target, init);
  40. }
  41. };
  42. const observer = new MutationObserver(onMutate);
  43. queueMicrotask(() => onMutate([], observer));
  44. return observer;
  45. };
  46.  
  47. // src/twitter-linkify-trends.user.ts
  48. // @license GPL
  49. var CACHE = exports.default(128);
  50. var DISABLED_EVENTS = "click touch";
  51. var EVENT_DATA = "data.explore_page.body.initialTimeline.timeline.timeline.instructions[-1].entries[1].content.items.*.item.itemContent";
  52. var EVENT_DATA_ENDPOINT = "/ExplorePage";
  53. var EVENT = 'div[role="link"][data-testid="trend"]:has([data-testid^="UserAvatar-Container"]):not([data-linked])';
  54. var TREND = 'div[role="link"][data-testid="trend"]:not(:has([data-testid^="UserAvatar-Container"])):not([data-linked])';
  55. var VIDEO = 'div[role="presentation"] div[role="link"][data-testid^="media-tweet-card-"]:not([data-linked])';
  56. var SELECTOR = [EVENT, TREND, VIDEO].join(", ");
  57. function disableAll(e) {
  58. e.stopPropagation();
  59. }
  60. function disableSome(e) {
  61. const $target = $(e.target);
  62. const $caret = $target.closest('[data-testid="caret"]', this);
  63. if (!$caret.length) {
  64. e.stopPropagation();
  65. }
  66. }
  67. function hookXHROpen(oldOpen) {
  68. return function open(_method, url) {
  69. const $url = URL.parse(url);
  70. if ($url.pathname.endsWith(EVENT_DATA_ENDPOINT)) {
  71. this.addEventListener("load", () => processEventData(this.responseText));
  72. }
  73. return GMCompat.apply(this, oldOpen, arguments);
  74. };
  75. }
  76. function linkFor(href) {
  77. return $("<a></a>").attr({ href, role: "link", "data-focusable": true }).css({ color: "inherit", textDecoration: "inherit" });
  78. }
  79. function onElement(el) {
  80. const $el = $(el);
  81. let fixPointer = true;
  82. let linked = true;
  83. if ($el.is(EVENT)) {
  84. $el.on(DISABLED_EVENTS, disableAll);
  85. linked = onEventElement($el);
  86. } else if ($el.is(TREND)) {
  87. $el.on(DISABLED_EVENTS, disableSome);
  88. onTrendElement($el);
  89. } else if ($el.is(VIDEO)) {
  90. fixPointer = false;
  91. $el.on(DISABLED_EVENTS, disableAll);
  92. onVideoElement($el);
  93. }
  94. if (linked) {
  95. $el.attr("data-linked", "true");
  96. }
  97. if (fixPointer) {
  98. $el.css("cursor", "auto");
  99. }
  100. }
  101. function onEventElement($event) {
  102. const { target, title } = targetFor($event);
  103. const url = CACHE.get(title);
  104. if (!url) {
  105. return false;
  106. }
  107. console.debug(`element (event):`, JSON.stringify(title));
  108. const $link = linkFor(url);
  109. $(target).parent().wrap($link);
  110. return true;
  111. }
  112. function onTrendElement($trend) {
  113. const { target, title } = targetFor($trend);
  114. const param = /\s+/.test(title) ? '"' + title.replace(/"/g, "") + '"' : title;
  115. console.debug("element (trend):", param);
  116. const query = encodeURIComponent(param);
  117. const url = `${location.origin}/search?q=${query}&src=trend_click&vertical=trends`;
  118. $(target).wrap(linkFor(url));
  119. }
  120. function onVideoElement($link) {
  121. const id = $link.data("testid").split("-").at(-1);
  122. const url = `${location.origin}/i/web/status/${id}`;
  123. $link.wrap(linkFor(url));
  124. }
  125. function processEventData(json) {
  126. const data = JSON.parse(json);
  127. const events = exports.get(data, EVENT_DATA, []);
  128. for (const event of events) {
  129. const title = event.name;
  130. const uri = event.trend_url.url;
  131. const url = uri.replace(/^twitter:\/\//, `${location.origin}/i/`);
  132. console.debug("data (event):", { title, url });
  133. CACHE.set(title, url);
  134. }
  135. }
  136. function targetFor($el) {
  137. const targets = $el.find('div[dir="ltr"] > span').filter((_, el) => {
  138. const fontWeight = Number($(el).parent().css("fontWeight") || 0);
  139. return fontWeight >= 700;
  140. });
  141. const target = targets.get().pop();
  142. const title = $(target).text().trim();
  143. return { target, title };
  144. }
  145. function run() {
  146. const target = document.getElementById("react-root");
  147. if (!target) {
  148. console.warn("can't find react-root element");
  149. return;
  150. }
  151. observe(target, () => {
  152. for (const el of $(SELECTOR)) {
  153. onElement(el);
  154. }
  155. });
  156. }
  157. var xhrProto = GMCompat.unsafeWindow.XMLHttpRequest.prototype;
  158. xhrProto.open = GMCompat.export(hookXHROpen(xhrProto.open));
  159. $(run);
  160. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址