TwiShell

Enhance Twitter Web with lots of features.

当前为 2014-05-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name TwiShell
  3. // @namespace TwiShell
  4. // @description Enhance Twitter Web with lots of features.
  5. // @match http://twitter.com/*
  6. // @match https://twitter.com/*
  7. // @version 3.9
  8. // @run-at document-start
  9. // ==/UserScript==
  10.  
  11. //noinspection ThisExpressionReferencesGlobalObjectJS
  12. (function (window) {
  13. 'use strict';
  14. var document = window.document;
  15. var ELEMENT_NODE = 1,
  16. TEXT_NODE = 3;
  17.  
  18. var click = function (node) {
  19. var event = document.createEvent("Event");
  20. event.initEvent("click", true, true);
  21. node.dispatchEvent(event);
  22. };
  23.  
  24. var hasClass = function (node, cls) {
  25. return node.classList && node.classList.contains(cls);
  26. };
  27.  
  28. var addClass = function (node, cls) {
  29. node.classList.add(cls);
  30. };
  31.  
  32. var removeClass = function (node, cls) {
  33. node.classList.remove(cls);
  34. };
  35.  
  36. var toggleClass = function (node, cls) {
  37. node.classList.toggle(cls);
  38. };
  39.  
  40. var wantsMoreTimelineItems = function () {
  41. var event = document.createEvent("Event");
  42. event.initEvent("uiNearTheBottom", true, true);
  43. document.dispatchEvent(event);
  44. };
  45.  
  46. // throttle function call in specified interval
  47. var throttle = function (fn, interval) {
  48. var fnTimer;
  49. var repeatCalling = false;
  50. return function wrapper() {
  51. if (!fnTimer) {
  52. fn();
  53. fnTimer = setTimeout(function () {
  54. fnTimer = null;
  55. if (repeatCalling) {
  56. repeatCalling = false;
  57. wrapper();
  58. }
  59. }, interval);
  60. }
  61. else {
  62. repeatCalling = true;
  63. }
  64. };
  65. };
  66.  
  67. var globalDialog,
  68. retweetDialog;
  69.  
  70. var getOriginalTweetText = function (tweetNode) {
  71. var originalTweetText = Array.prototype.reduce.call(
  72. tweetNode.querySelector("div.content p.js-tweet-text").childNodes,
  73. function (acc, childNode) {
  74. var text;
  75. if (childNode.nodeType === TEXT_NODE) {
  76. text = childNode.textContent;
  77. }
  78. else if (childNode.nodeType === ELEMENT_NODE) {
  79. text = childNode.getAttribute("data-expanded-url") || childNode.textContent;
  80. }
  81. if (text) {
  82. acc.push(text);
  83. }
  84. return acc;
  85. },
  86. []
  87. ).join("");
  88. var screenName = tweetNode.querySelector("div.stream-item-header span.username").textContent.trim();
  89. return "RT " + screenName + ": " + originalTweetText.split("\n").map(function (s) {
  90. return "<div>" + s + "</div>";
  91. }).join("");
  92. };
  93.  
  94. var hideRetweetDialog = function () {
  95. click(document.querySelector("#retweet-tweet-dialog button.modal-close"));
  96. };
  97.  
  98. var showGlobalTweetDialog = function () {
  99. globalDialog.querySelector(".draggable").setAttribute(
  100. "style",
  101. retweetDialog.querySelector(".draggable").getAttribute("style")
  102. );
  103. click(document.getElementById("global-new-tweet-button"));
  104. globalDialog.querySelector(".modal-title").innerHTML =
  105. retweetDialog.querySelector(".modal-title").innerHTML;
  106. };
  107.  
  108. var prepareRT = function (tweetNode) {
  109. hideRetweetDialog();
  110. showGlobalTweetDialog();
  111. var text = getOriginalTweetText(tweetNode);
  112. fillInTweetBox(text);
  113. };
  114.  
  115. var fillInTweetBox = function (text) {
  116. var tweetBox = globalDialog.querySelector("div.tweet-box"),
  117. range = document.createRange(),
  118. selection = window.getSelection();
  119.  
  120. tweetBox.innerHTML = text;
  121. tweetBox.focus();
  122. // Place cursor in the front.
  123. range.selectNodeContents(tweetBox);
  124. range.collapse(true);
  125. selection.removeAllRanges();
  126. selection.addRange(range);
  127. };
  128.  
  129. var replaceCancelButton = function () {
  130. if (!retweetDialog) { // sometimes it will become null
  131. return;
  132. }
  133. var btnCancel = retweetDialog.querySelector("button.cancel-action");
  134. addClass(btnCancel, "rt-action");
  135. removeClass(btnCancel, "cancel-action");
  136. btnCancel.innerHTML = "RT";
  137. btnCancel.addEventListener("click", function () {
  138. prepareRT(retweetDialog.querySelector(".tweet"));
  139. }, false);
  140. };
  141.  
  142. var isAttachedMap = Object.create(null);
  143. var attachProtectedTweet = function () {
  144. Array.prototype.forEach.call(document.querySelectorAll(".tweet[data-protected=true]"), function (protectedTweet) {
  145. var itemId = protectedTweet.getAttribute("data-item-id");
  146. if (!(isAttachedMap[itemId])) {
  147. isAttachedMap[itemId] = true;
  148. protectedTweet.querySelector(".retweet.cannot-retweet").addEventListener("click", function (evt) {
  149. if (evt.button == 2) { // Ignore right-clicks
  150. return;
  151. }
  152. evt.stopPropagation();
  153. evt.preventDefault();
  154. for (var tweet = evt.target.parentNode; !(hasClass(tweet, "tweet")); tweet = tweet.parentNode) {}
  155. prepareRT(tweet);
  156. }, false);
  157. }
  158. });
  159. };
  160.  
  161. var tcoMatcher = /^http(?:s)?:\/\/t\.co\/[0-9A-Za-z]+$/i;
  162. var expandAllUrl = function () {
  163. Array.prototype.forEach.call(
  164. document.querySelectorAll("a.twitter-timeline-link:not(.url-expanded)"),
  165. function (tlLink) {
  166. var expandedUrl = tlLink.getAttribute("data-expanded-url");
  167. if (expandedUrl && tcoMatcher.test(tlLink.getAttribute("href"))) {
  168. tlLink.setAttribute("href", expandedUrl);
  169. }
  170. addClass(tlLink, "url-expanded");
  171. }
  172. );
  173. };
  174.  
  175. document.addEventListener("DOMContentLoaded", function () {
  176. globalDialog = document.getElementById("global-tweet-dialog");
  177. retweetDialog = document.getElementById("retweet-tweet-dialog");
  178. replaceCancelButton();
  179. }, false);
  180.  
  181. var styles = [
  182. "@media screen {",
  183. ".cannot-retweet{display: inline !important;}", // rt for protected tweet
  184. ".content-main, .profile-page-header {float: left !important;}",
  185. "body.three-col .wrapper {width: 900px !important;}",
  186. ".dashboard {float: right !important;}",
  187. "#suggested-users {clear: none !important;}",
  188. "li.stream-item .has-cards .js-media-container {max-height: 100%; transition-property: all; transition-duration: 0.2s;}",
  189. "li.stream-item:not(.open) .stream-item-footer:before, li.stream-item:not(.open) .stream-item-footer:after {display: none;}",
  190. "li.stream-item:not(.open) .has-cards .js-media-container {max-height: 0; overflow-y: hidden; padding: 0; margin: 0; border: 0;}",
  191. "li.stream-item:not(.open) .has-cards .expanded-content {display: none;}",
  192. "li.stream-item:not(.open) .has-cards .bottom-tweet-actions {margin-top: 0;}",
  193. "}"
  194. ].join("");
  195.  
  196. var addStyle = function (css) {
  197. var node = document.createElement("style");
  198. node.type = "text/css";
  199. node.appendChild(document.createTextNode(css));
  200. document.documentElement.appendChild(node);
  201. node = null;
  202. };
  203.  
  204. addStyle(styles);
  205.  
  206. var throttledExpandUrl = throttle(expandAllUrl, 100);
  207. var throttledAttachProtectedTweet = throttle(attachProtectedTweet, 100);
  208. new MutationObserver(function (mutations) {
  209. mutations.forEach(function (mutation) {
  210. if (mutation.addedNodes) {
  211. throttledExpandUrl();
  212. throttledAttachProtectedTweet();
  213. }
  214. });
  215. }).observe(document, {
  216. childList: true,
  217. subtree: true
  218. });
  219. })(this);