Twitter 主页上的媒体模式

在 Twitter 的主页和列表时间流上删除纯文本 Tweet。当前是 Beta 质量

  1. // vim: set ts=4 sw=4 expandtab:
  2.  
  3. // ==UserScript==
  4. // @name Media mode for Twitter home
  5. // @name:zh-CN Twitter 主页上的媒体模式
  6. // @name:zh-TW Twitter 主頁上的媒體模式
  7. // @name:zh-HK Twitter 主頁上的媒體模式
  8. // @description Remove text-only tweets on the flow of Twitter home/list. It is currently Beta quality.
  9. // @description:zh-CN 在 Twitter 的主页和列表时间流上删除纯文本 Tweet。当前是 Beta 质量
  10. // @description:zh-TW 在 Twitter 的主頁和列表時間流上刪除純文字 Tweet。當前是 Beta 質量
  11. // @description:zh-HK 在 Twitter 的主頁和列表時間流上刪除純文本 Tweet。當前是 Beta 質量
  12. // @icon https://i.imgur.com/bUIPv1O.jpg
  13. // @namespace https://github.com/UtopicPanther/userscript-twitter-home-media
  14. // @supportURL https://github.com/UtopicPanther/userscript-twitter-home-media/issues
  15. // @version 0.7.1
  16. // @author UtopicPanther
  17. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
  18. // @match https://twitter.com/*
  19. // @match https://mobile.twitter.com/*
  20. // @grant GM_registerMenuCommand
  21. // @run-at document-idle
  22. // ==/UserScript==
  23.  
  24. /*
  25. * Copyright (C) 2020 UtopicPanther
  26. *
  27. * This program is free software: you can redistribute it and/or modify
  28. * it under the terms of the GNU General Public License as published by
  29. * the Free Software Foundation, either version 3 of the License, or
  30. * (at your option) any later version.
  31. *
  32. * This program is distributed in the hope that it will be useful,
  33. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  34. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  35. * GNU General Public License for more details.
  36. *
  37. * You should have received a copy of the GNU General Public License
  38. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  39. *
  40. * This script contains an additional exemption. When this script is
  41. * injected into the site with the original setting of `@match`,
  42. * other user scripts running in the same space are not required to
  43. * be compatible with GPL 3.
  44. *
  45. * 这个脚本包含一个附加豁免。当此脚本被注入到原有设置 `@match` 的
  46. * 站点时,不要求同一空间中运行的其他用户脚本与 GPL 3 兼容。
  47. */
  48.  
  49. (function() {
  50. 'use strict';
  51.  
  52. let hide = true;
  53.  
  54. const removeTweet = article => {
  55. article.classList.add('mmfth_hide');
  56. const div = article.parentElement.parentElement;
  57. div.style.background = "red";
  58. if (hide)
  59. div.style.display = "none";
  60. }
  61.  
  62. const showTweet = article => {
  63. console.log("showTweet: %O", article)
  64. article.classList.remove('mmfth_hide');
  65. const div = article.parentElement.parentElement;
  66. div.style.background = "";
  67. div.style.display = "";
  68. }
  69.  
  70. const isTweetOnlyText = i => {
  71. if (Array.from(i.querySelectorAll('img')).some(img => {
  72. if (!img.src.match(/^[a-z]*:\/\/[^\/]*\/profile_images/) &&
  73. !img.src.match(/^[a-z]*:\/\/[^\/]*\/emoji/)) {
  74. return true;
  75. }
  76. })) return false;
  77.  
  78. if (i.querySelector('div[data-testid=tweetPhoto]') != null) return false;
  79.  
  80. if (Array.from(i.querySelectorAll('a')).some(a => {
  81. if (a.getAttribute('href').match(/^\/[^\/]*\/status\/[0-9]*\/photo\//)) {
  82. return true;
  83. }
  84. })) return false;
  85.  
  86. if (i.querySelector('video') != null) return false;
  87.  
  88. if (i.querySelector('div[role=progressbar]') != null) return false;
  89.  
  90. let emptyMiddle = true;
  91.  
  92. try {
  93. const tweet = i.querySelector('div[data-testid=tweet]');
  94. const tmp = tweet.children[1].children[1];
  95. const middle = tmp.children[tmp.length - 2];
  96.  
  97. if (middle.children.length > 0)
  98. emptyMiddle = false;
  99. } catch (e) {}
  100.  
  101. return emptyMiddle;
  102. }
  103.  
  104. const findTweetsForRemove = () => {
  105. if (location.pathname.startsWith('/home') ||
  106. location.pathname.startsWith('/i/lists/')) {
  107. document.querySelectorAll('article:not(.mmfth_hide)').forEach(i => {
  108. if (isTweetOnlyText(i)) {
  109. removeTweet(i);
  110. }
  111. });
  112. document.querySelectorAll('.mmfth_hide').forEach(i => {
  113. if (!isTweetOnlyText(i)) {
  114. showTweet(i);
  115. }
  116. });
  117. }
  118. }
  119.  
  120. const startObserver = () => {
  121. //const targetNode = document.querySelector('article').parentElement.parentElement.parentElement.parentElement;
  122. const targetNode = document.documentElement || document.body;
  123.  
  124. findTweetsForRemove();
  125.  
  126. const config = { childList: true, subtree: true };
  127. const observer = new MutationObserver((mutationsList, observer) => {
  128. findTweetsForRemove();
  129. });
  130. observer.observe(targetNode, config);
  131. }
  132.  
  133. GM_registerMenuCommand("Show/Hide text-only tweets", () => {
  134. hide = !hide;
  135. alert("Text-only tweers will be " + (hide ? "hidden" : "shown (with red background)"));
  136. document.querySelectorAll('.mmfth_hide').forEach(i => {
  137. i.parentElement.parentElement.style.display = (hide ? "none" : "");
  138. });
  139. });
  140.  
  141. setTimeout(() => {
  142. startObserver();
  143. }, 6000);
  144. })();