Twitter Block With Love

Block or mute all the Twitter users who like or RT a specific tweet, with love.

目前为 2022-07-11 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Twitter Block With Love
  3. // @namespace https://www.eolstudy.com
  4. // @version 2.8.0
  5. // @description Block or mute all the Twitter users who like or RT a specific tweet, with love.
  6. // @author Eol, OverflowCat, yuanLeeMidori
  7. // @run-at document-end
  8. // @grant GM_registerMenuCommand
  9. // @match https://twitter.com/*
  10. // @match https://mobile.twitter.com/*
  11. // @match https://tweetdeck.twitter.com/*
  12. // @exclude https://twitter.com/account/*
  13. // @require https://cdn.jsdelivr.net/npm/axios@0.25.0/dist/axios.min.js
  14. // @require https://cdn.jsdelivr.net/npm/qs@6.10.3/dist/qs.min.js
  15. // @require https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js
  16. // ==/UserScript==
  17.  
  18. /* global axios $ Qs */
  19.  
  20. (_ => {
  21. /* Begin of Dependencies */
  22. /* eslint-disable */
  23.  
  24. // https://gist.githubusercontent.com/BrockA/2625891/raw/9c97aa67ff9c5d56be34a55ad6c18a314e5eb548/waitForKeyElements.js
  25. /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
  26. that detects and handles AJAXed content.
  27.  
  28. Usage example:
  29.  
  30. waitForKeyElements (
  31. "div.comments"
  32. , commentCallbackFunction
  33. );
  34.  
  35. //--- Page-specific function to do what we want when the node is found.
  36. function commentCallbackFunction (jNode) {
  37. jNode.text ("This comment changed by waitForKeyElements().");
  38. }
  39.  
  40. IMPORTANT: This function requires your script to have loaded jQuery.
  41. */
  42. function waitForKeyElements (
  43. selectorTxt, /* Required: The jQuery selector string that
  44. specifies the desired element(s).
  45. */
  46. actionFunction, /* Required: The code to run when elements are
  47. found. It is passed a jNode to the matched
  48. element.
  49. */
  50. bWaitOnce, /* Optional: If false, will continue to scan for
  51. new elements even after the first match is
  52. found.
  53. */
  54. iframeSelector /* Optional: If set, identifies the iframe to
  55. search.
  56. */
  57. ) {
  58. var targetNodes, btargetsFound;
  59.  
  60. if (typeof iframeSelector == "undefined")
  61. targetNodes = $(selectorTxt);
  62. else
  63. targetNodes = $(iframeSelector).contents ()
  64. .find (selectorTxt);
  65.  
  66. if (targetNodes && targetNodes.length > 0) {
  67. btargetsFound = true;
  68. /*--- Found target node(s). Go through each and act if they
  69. are new.
  70. */
  71. targetNodes.each ( function () {
  72. var jThis = $(this);
  73. var alreadyFound = jThis.data ('alreadyFound') || false;
  74.  
  75. if (!alreadyFound) {
  76. //--- Call the payload function.
  77. var cancelFound = actionFunction (jThis);
  78. if (cancelFound)
  79. btargetsFound = false;
  80. else
  81. jThis.data ('alreadyFound', true);
  82. }
  83. } );
  84. }
  85. else {
  86. btargetsFound = false;
  87. }
  88.  
  89. //--- Get the timer-control variable for this selector.
  90. var controlObj = waitForKeyElements.controlObj || {};
  91. var controlKey = selectorTxt.replace (/[^\w]/g, "_");
  92. var timeControl = controlObj [controlKey];
  93.  
  94. //--- Now set or clear the timer as appropriate.
  95. if (btargetsFound && bWaitOnce && timeControl) {
  96. //--- The only condition where we need to clear the timer.
  97. clearInterval (timeControl);
  98. delete controlObj [controlKey]
  99. }
  100. else {
  101. //--- Set a timer, if needed.
  102. if ( ! timeControl) {
  103. timeControl = setInterval ( function () {
  104. waitForKeyElements ( selectorTxt,
  105. actionFunction,
  106. bWaitOnce,
  107. iframeSelector
  108. );
  109. },
  110. 300
  111. );
  112. controlObj [controlKey] = timeControl;
  113. }
  114. }
  115. waitForKeyElements.controlObj = controlObj;
  116. }
  117. /* eslint-enable */
  118. /* End of Dependencies */
  119.  
  120. let lang = document.documentElement.lang
  121. if (lang == 'en-US') {
  122. lang = 'en' // TweetDeck
  123. }
  124. const translations = {
  125. // Please submit a feedback on Greasyfork.com if your language is not in the list bellow
  126. en: {
  127. lang_name: 'English',
  128. like_title: 'Liked by',
  129. like_list_identifier: 'Timeline: Liked by',
  130. retweet_title: 'Retweeted by',
  131. retweet_list_identifier: 'Timeline: Retweeted by',
  132. block_btn: 'Block all',
  133. block_success: 'All users blocked!',
  134. mute_btn: 'Mute all',
  135. mute_success: 'All users muted!',
  136. include_original_tweeter: 'Include the original Tweeter',
  137. logs: 'Logs',
  138. list_members: 'List members',
  139. list_members_identifier: 'Timeline: List members',
  140. block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.'
  141. },
  142. 'en-GB': {
  143. lang_name: 'British English',
  144. like_title: 'Liked by',
  145. like_list_identifier: 'Timeline: Liked by',
  146. retweet_title: 'Retweeted by',
  147. retweet_list_identifier: 'Timeline: Retweeted by',
  148. block_btn: 'Block all',
  149. block_success: 'All users blocked!',
  150. mute_btn: 'Mute all',
  151. mute_btn: 'Mute all',
  152. mute_success: 'All users muted!',
  153. mute_success: 'All users muted!',
  154. include_original_tweeter: 'Include the original Tweeter',
  155. logs: 'Logs',
  156. list_members: 'List members',
  157. list_members_identifier: 'Timeline: List members',
  158. block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.'
  159. },
  160. zh: {
  161. lang_name: '简体中文',
  162. like_title: '喜欢者',
  163. like_list_identifier: '时间线:喜欢者',
  164. retweet_title: '转推',
  165. retweet_list_identifier: '时间线:转推者',
  166. block_btn: '全部屏蔽',
  167. mute_btn: '全部隐藏',
  168. block_success: '列表用户已全部屏蔽!',
  169. mute_success: '列表用户已全部隐藏!',
  170. include_original_tweeter: '包括推主',
  171. logs: '操作记录',
  172. list_members: '列表成员',
  173. list_members_identifier: '时间线:列表成员',
  174. block_retweets_notice: 'Twitter Block with Love 仅屏蔽了不带评论转推的用户。\n请手动屏蔽引用推文的用户。'
  175. },
  176. 'zh-Hant': {
  177. lang_name: '正體中文',
  178. like_title: '已被喜歡',
  179. like_list_identifier: '時間軸:已被喜歡',
  180. retweet_title: '轉推',
  181. retweet_list_identifier: '時間軸:已被轉推',
  182. block_btn: '全部封鎖',
  183. mute_btn: '全部靜音',
  184. block_success: '列表用戶已全部封鎖!',
  185. mute_success: '列表用戶已全部靜音!',
  186. include_original_tweeter: '包括推主',
  187. logs: '活動記錄',
  188. list_members: '列表成員',
  189. list_members_identifier: '時間軸:列表成員',
  190. block_retweets_notice: 'Twitter Block with Love 僅封鎖了不帶評論轉推的使用者。\n請手動封鎖引用推文的使用者。'
  191. },
  192. ja: {
  193. lang_name: '日本語',
  194. like_list_identifier: 'タイムライン: いいねしたユーザー',
  195. like_title: 'いいねしたユーザー',
  196. retweet_list_identifier: 'タイムライン: リツイートしたユーザー',
  197. retweet_title: 'リツイート',
  198. block_btn: '全部ブロック',
  199. mute_btn: '全部ミュート',
  200. block_success: '全てブロックしました!',
  201. mute_success: '全てミュートしました!',
  202. include_original_tweeter: 'スレ主',
  203. logs: '操作履歴を表示',
  204. list_members: 'リストに追加されているユーザー',
  205. list_members_identifier: 'タイムライン: リストに追加されているユーザー',
  206. block_retweets_notice: 'TBWLは、コメントなしでリツイートしたユーザーのみをブロックしました。\n引用ツイートしたユーザーを手動でブロックしてください。'
  207. },
  208. vi: {
  209. // translation by Ly Hương
  210. lang_name: 'Tiếng Việt',
  211. like_list_identifier: 'Dòng thời gian: Được thích bởi',
  212. like_title: 'Được thích bởi',
  213. retweet_list_identifier: 'Dòng thời gian: Được Tweet lại bởi',
  214. retweet_title: 'Được Tweet lại bởi',
  215. block_btn: 'Tắt tiếng tất cả',
  216. mute_btn: 'Chặn tất cả',
  217. block_success: 'Tất cả tài khoản đã bị chặn!',
  218. mute_success: 'Tất cả tài khoản đã bị tắt tiếng!',
  219. include_original_tweeter: 'Tweeter gốc',
  220. logs: 'Lịch sử',
  221. list_members: 'Thành viên trong danh sách',
  222. list_members_identifier: 'Dòng thời gian: Thành viên trong danh sách',
  223. block_retweets_notice: 'TBWL chỉ chặn tài khoản đã retweet không bình luận. Những tài khoản retweet bằng bình luận thì xin hãy chặn bằng tay.'
  224. },
  225. ko: {
  226. // translation by hellojo011
  227. lang_name: '한국어',
  228. like_list_identifier: '타임라인: 마음에 들어 함',
  229. like_title: '마음에 들어 함',
  230. retweet_list_identifier: '타임라인: 리트윗함',
  231. retweet_title: '리트윗',
  232. block_btn: '모두 차단',
  233. mute_btn: '모두 뮤트',
  234. block_success: '모두 차단했습니다!',
  235. mute_success: '모두 뮤트했습니다!',
  236. include_original_tweeter: '글쓴이',
  237. logs: '활동',
  238. list_members: '리스트 멤버',
  239. list_members_identifier: '타임라인: 리스트 멤버',
  240. block_retweets_notice: '저희는 리트윗하신 사용자분들을 차단 했으나 트윗 인용하신 사용자분들은 직접 차단하셔야 합니다.'
  241. },
  242. de: {
  243. // translation by Wassermäuserich Lúcio
  244. lang_name: 'Deutsch',
  245. like_title: 'Gefällt',
  246. like_list_identifier: 'Timeline: Gefällt',
  247. retweet_title: 'Retweetet von',
  248. retweet_list_identifier: 'Timeline: Retweetet von',
  249. block_btn: 'Alle blockieren',
  250. mute_btn: 'Alle stummschalten',
  251. block_success: 'Alle wurden blockiert!',
  252. mute_success: 'Alle wurden stummgeschaltet!',
  253. include_original_tweeter: 'Original-Hochlader einschließen',
  254. logs: 'Betriebsaufzeichnung',
  255. list_members: 'Listenmitglieder',
  256. list_members_identifier: 'Timeline: Listenmitglieder',
  257. block_retweets_notice: 'TBWL hat nur Benutzer blockiert, die ohne Kommentare retweetet haben.\nBitte blockieren Sie Benutzer, die mit Kommentaren retweetet haben, manuell.',
  258. enabled: 'Aktiviert!',
  259. disabled: 'Behindert!',
  260. },
  261. fr: {
  262. lang_name: 'French',
  263. like_title: 'Aimé par',
  264. like_list_identifier: 'Fil d\'actualités : Aimé par',
  265. retweet_title: 'Retweeté par',
  266. retweet_list_identifier: 'Fil d\'actualités : Retweeté par',
  267. block_btn: 'Bloquer tous',
  268. block_success: 'Tous les utilisateurs sont bloqués !',
  269. mute_btn: 'Masquer tous',
  270. mute_success: 'Tous les utilisateurs sont masqués !',
  271. include_original_tweeter: 'Inclure l’auteur original',
  272. logs: 'Logs',
  273. list_members: 'Membres de la liste',
  274. list_members_identifier: 'Fil d\'actualités : Membres de la liste',
  275. block_retweets_notice: 'TBWL a seulement bloqué les utilisateurs qui ont retweeté sans commenter.\n Vous devez bloquer manuellement les retweets avec commentaire.'
  276. },
  277. }
  278. let i18n = translations[lang]
  279. let no_local = false
  280. // lang is empty in some error pages, so check lang first
  281. if (lang && !i18n) {
  282. i18n = translations['en']
  283. no_local = true
  284. if (false) {
  285. let langnames = []
  286. Object.values(translations).forEach(language => langnames.push(language.lang_name))
  287. langnames = langnames.join(', ')
  288. let issue = confirm(
  289. 'Twitter Block With Love userscript does not support your language (language code: "' + lang + '").\n' +
  290. 'Please send feedback at Greasyfork.com or open an issue at Github.com.\n' +
  291. 'Before that, you can edit the userscript yourself or just switch the language of Twitter Web App to any of the following languages: ' +
  292. langnames + '.\n\nDo you want to open an issue?'
  293. )
  294. if (issue) {
  295. window.location.replace("https://github.com/E011011101001/Twitter-Block-With-Love/issues/new/")
  296. }
  297. }
  298. }
  299.  
  300. function get_theme_color () {
  301. const close_icon = $('div[aria-label] > div[dir="auto"] > svg[viewBox="0 0 24 24"]')[0]
  302. return window.getComputedStyle(close_icon).color
  303. }
  304.  
  305. function component_to_hex (c) {
  306. if (typeof(c) === 'string') c = Number(c)
  307. const hex = c.toString(16)
  308. return hex.length === 1 ? ("0" + hex) : hex
  309. }
  310.  
  311. function rgb_to_hex (r, g, b) {
  312. return "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b)
  313. }
  314.  
  315. function get_cookie (cname) {
  316. let name = cname + '='
  317. let ca = document.cookie.split(';')
  318. for (let i = 0; i < ca.length; ++i) {
  319. let c = ca[i].trim()
  320. if (c.indexOf(name) === 0) {
  321. return c.substring(name.length, c.length)
  322. }
  323. }
  324. return ''
  325. }
  326.  
  327. function get_ancestor (dom, level) {
  328. for (let i = 0; i < level; ++i) {
  329. dom = dom.parent()
  330. }
  331. return dom
  332. }
  333.  
  334. const ajax = axios.create({
  335. baseURL: 'https://api.twitter.com',
  336. withCredentials: true,
  337. headers: {
  338. Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  339. 'X-Twitter-Auth-Type': 'OAuth2Session',
  340. 'X-Twitter-Active-User': 'yes',
  341. 'X-Csrf-Token': get_cookie('ct0')
  342. }
  343. })
  344.  
  345. function get_tweet_id () {
  346. // https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567
  347. return location.href.split('status/')[1].split('/')[0]
  348. }
  349.  
  350. function get_list_id () {
  351. // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567
  352. return location.href.split('lists/')[1].split('/')[0]
  353. }
  354.  
  355. // fetch_likers and fetch_no_comment_retweeters need to be merged into one function
  356. async function fetch_likers (tweetId) {
  357. const users = await ajax.get(`/2/timeline/liked_by.json?tweet_id=${tweetId}`).then(
  358. res => res.data.globalObjects.users
  359. )
  360.  
  361. let likers = []
  362. Object.keys(users).forEach(user => likers.push(user)) // keys of users are id strings
  363. return likers
  364. }
  365.  
  366. async function fetch_no_comment_retweeters (tweetId) {
  367. const users = (await ajax.get(`/2/timeline/retweeted_by.json?tweet_id=${tweetId}`)).data.globalObjects.users
  368.  
  369. let targets = []
  370. Object.keys(users).forEach(user => targets.push(user))
  371. return targets
  372. }
  373.  
  374. async function fetch_list_members (listId) {
  375. const users = (await ajax.get(`/1.1/lists/members.json?list_id=${listId}`)).data.users
  376. let members = []
  377. members = users.map(u => u.id_str)
  378. return members
  379. }
  380.  
  381. function block_user (id) {
  382. ajax.post('/1.1/blocks/create.json', Qs.stringify({
  383. user_id: id
  384. }), {
  385. headers: {
  386. 'Content-Type': 'application/x-www-form-urlencoded'
  387. }
  388. })
  389. }
  390.  
  391. function mute_user (id) {
  392. ajax.post('/1.1/mutes/users/create.json', Qs.stringify({
  393. user_id: id
  394. }), {
  395. headers: {
  396. 'Content-Type': 'application/x-www-form-urlencoded'
  397. }
  398. })
  399. }
  400.  
  401. async function get_tweeter (tweetId) {
  402. const screen_name = location.href.split('twitter.com/')[1].split('/')[0]
  403. const tweetData = (await ajax.get(`/2/timeline/conversation/${tweetId}.json`)).data
  404. // Find the tweeter by username
  405. const users = tweetData.globalObjects.users
  406. for (let key in users) {
  407. if (users[key].screen_name === screen_name) {
  408. return key
  409. }
  410. }
  411. return undefined
  412. }
  413.  
  414. function inlude_tweeter () {
  415. return $("#bwl-include-tweeter").checked
  416. }
  417.  
  418. // block_all_liker and block_no_comment_retweeters need to be merged
  419. async function block_all_likers () {
  420. const tweetId = get_tweet_id()
  421. const likers = await fetch_likers(tweetId)
  422. if (inlude_tweeter()) {
  423. const tweeter = await get_tweeter(tweetId)
  424. if (tweeter) likers.push(tweeter)
  425. }
  426. likers.forEach(id => block_user(id))
  427. }
  428.  
  429. async function mute_all_likers () {
  430. const tweetId = get_tweet_id()
  431. const likers = await fetch_likers(tweetId)
  432. if (inlude_tweeter()) {
  433. const tweeter = await get_tweeter(tweetId)
  434. if (tweeter) likers.push(tweeter)
  435. }
  436. likers.forEach(id => mute_user(id))
  437. }
  438.  
  439. async function block_no_comment_retweeters () {
  440. const tweetId = get_tweet_id()
  441. const retweeters = await fetch_no_comment_retweeters(tweetId)
  442. if (inlude_tweeter()) {
  443. const tweeter = await get_tweeter(tweetId)
  444. if (tweeter) retweeters.push(tweeter)
  445. }
  446. retweeters.forEach(id => block_user(id))
  447.  
  448. const tabName = location.href.split('retweets/')[1]
  449. if (tabName === 'with_comments') {
  450. if (!block_no_comment_retweeters.hasAlerted) {
  451. block_no_comment_retweeters.hasAlerted = true
  452. alert(i18n.block_rt_notice)
  453. }
  454. }
  455. }
  456.  
  457. async function mute_no_comment_retweeters () {
  458. const tweetId = get_tweet_id()
  459. const retweeters = await fetch_no_comment_retweeters(tweetId)
  460. if (inlude_tweeter()) {
  461. const tweeter = await get_tweeter(tweetId)
  462. if (tweeter) retweeters.push(tweeter)
  463. }
  464. retweeters.forEach(id => mute_user(id))
  465.  
  466. const tabName = location.href.split('retweets/')[1]
  467. if (tabName === 'with_comments') {
  468. if (!block_no_comment_retweeters.hasAlerted) {
  469. block_no_comment_retweeters.hasAlerted = true
  470. alert(
  471. 'TBWL has only muted users that retweeted without comments.\n Please mute users retweeting with comments manually.'
  472. )
  473. }
  474. }
  475. }
  476.  
  477. async function block_list_members () {
  478. const listId = get_list_id()
  479. const members = await fetch_list_members(listId)
  480. members.forEach(id => block_user(id))
  481. }
  482.  
  483. async function mute_list_members () {
  484. const listId = get_list_id()
  485. const members = await fetch_list_members(listId)
  486. members.forEach(id => mute_user(id))
  487. }
  488.  
  489. function get_notifier_of (msg) {
  490. return _ => {
  491. const banner = $(`
  492. <div id="bwl-notice" style="right:0px; position:fixed; left:0px; bottom:0px; display:flex; flex-direction:column;">
  493. <div class="tbwl-notice">
  494. <span>${msg}</span>
  495. </div>
  496. </div>
  497. `)
  498. const closeButton = $(`
  499. <span id="bwl-close-button" style="font-weight:700; margin-left:12px; margin-right:12px; cursor:pointer;">
  500. Close
  501. </span>
  502. `)
  503. closeButton.click(_ => banner.remove())
  504. $(banner).children('.tbwl-notice').append(closeButton)
  505.  
  506. $('#layers').append(banner)
  507. setTimeout(() => banner.remove(), 5000)
  508. $('div[data-testid="app-bar-close"]').click()
  509. }
  510. }
  511.  
  512. function mount_switch (parentDom, name) {
  513. const backgroundColor = $('body').css('background-color')
  514. const textColors = {
  515. 'rgb(255, 255, 255)': '#000000',
  516. 'rgb(21, 32, 43)': '#ffffff',
  517. 'rgb(0, 0, 0)': '#ffffff'
  518. }
  519. const textColor = textColors[backgroundColor] || '#000000'
  520. let themeColor = get_theme_color()
  521. let _rgb = themeColor.replace('rgb(', '').replace(')', '').split(', ')
  522. let themeColor_hex = rgb_to_hex(_rgb[0], _rgb[1], _rgb[2])
  523. $('head').append(`
  524. <style>
  525. .container {
  526. margin-top: 0px;
  527. margin-left: 0px;
  528. margin-right: 5px;
  529. }
  530. .checkbox {
  531. width: 100%;
  532. margin: 0px auto;
  533. position: relative;
  534. display: block;
  535. }
  536.  
  537. .checkbox input[type="checkbox"] {
  538. width: auto;
  539. opacity: 0.00000001;
  540. position: absolute;
  541. left: 0;
  542. margin-left: 0px;
  543. }
  544. .checkbox label:before {
  545. content: '';
  546. position: absolute;
  547. left: 0;
  548. top: 0;
  549. margin: 0px;
  550. width: 22px;
  551. height: 22px;
  552. transition: transform 0.2s ease;
  553. border-radius: 3px;
  554. border: 2px solid ${themeColor_hex};
  555. }
  556. .checkbox label:after {
  557. content: '';
  558. display: block;
  559. width: 10px;
  560. height: 5px;
  561. border-bottom: 2px solid ${themeColor_hex};
  562. border-left: 2px solid ${themeColor_hex};
  563. -webkit-transform: rotate(-45deg) scale(0);
  564. transform: rotate(-45deg) scale(0);
  565. transition: transform ease 0.2s;
  566. will-change: transform;
  567. position: absolute;
  568. top: 8px;
  569. left: 6px;
  570. }
  571. .checkbox input[type="checkbox"]:checked ~ label::before {
  572. color: ${themeColor_hex};
  573. }
  574.  
  575. .checkbox input[type="checkbox"]:checked ~ label::after {
  576. -webkit-transform: rotate(-45deg) scale(1);
  577. transform: rotate(-45deg) scale(1);
  578. }
  579.  
  580. .checkbox label {
  581. position: relative;
  582. display: block;
  583. padding-left: 31px;
  584. margin-bottom: 0;
  585. font-weight: normal;
  586. cursor: pointer;
  587. vertical-align: sub;
  588. width:fit-content;
  589. width:-webkit-fit-content;
  590. width:-moz-fit-content;
  591. }
  592. .checkbox label span {
  593. position: relative;
  594. top: 50%;
  595. color: ${textColor};
  596. -webkit-transform: translateY(-50%);
  597. transform: translateY(-50%);
  598. }
  599. .checkbox input[type="checkbox"]:focus + label::before {
  600. outline: 0;
  601. }
  602. </style>`)
  603. const button = $(`
  604. <div class="container">
  605. <div class="checkbox">
  606. <input type="checkbox" id="bwl-include-tweeter" name="" value="">
  607. <label for="bwl-include-tweeter"><span>${name}</span></label>
  608. </div>
  609. </div>
  610. `)
  611.  
  612. parentDom.append(button)
  613. }
  614.  
  615. function mount_button (parentDom, name, executer, success_notifier) {
  616. let themeColor = get_theme_color()
  617. const hoverColor = themeColor.replace(/rgb/i, "rgba").replace(/\)/, ', 0.1)')
  618. const mousedownColor = themeColor.replace(/rgb/i, "rgba").replace(/\)/, ', 0.2)')
  619. const btn_mousedown = 'bwl-btn-mousedown'
  620. const btn_hover = 'bwl-btn-hover'
  621.  
  622. $('head').append(`
  623. <style>
  624. .bwl-btn-base {
  625. min-height: 30px;
  626. padding-left: 1em;
  627. padding-right: 1em;
  628. border: 1px solid ${themeColor} !important;
  629. border-radius: 9999px;
  630. }
  631. .${btn_mousedown} {
  632. background-color: ${mousedownColor};
  633. cursor: pointer;
  634. }
  635. .${btn_hover} {
  636. background-color: ${hoverColor};
  637. cursor: pointer;
  638. }
  639. .bwl-btn-inner-wrapper {
  640. font-weight: bold;
  641. -webkit-box-align: center;
  642. align-items: center;
  643. -webkit-box-flex: 1;
  644. flex-grow: 1;
  645. color: ${themeColor};
  646. display: flex;
  647. }
  648. .bwl-text-font {
  649. font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
  650. color: ${themeColor};
  651. }
  652. </style>
  653. `)
  654.  
  655. const button = $(`
  656. <div
  657. aria-haspopup="true"
  658. role="button"
  659. data-focusable="true"
  660. class="bwl-btn-base"
  661. style="margin:3px"
  662. >
  663. <div class="bwl-btn-inner-wrapper">
  664. <span>
  665. <span class="bwl-text-font">${name}</span>
  666. </span>
  667. </div>
  668. </div>
  669. `)
  670. .addClass(parentDom.prop('classList')[0])
  671. .hover(function () {
  672. $(this).addClass(btn_hover)
  673. }, function () {
  674. $(this).removeClass(btn_hover)
  675. $(this).removeClass(btn_mousedown)
  676. })
  677. .on('selectstart', function () {
  678. return false
  679. })
  680. .mousedown(function () {
  681. $(this).removeClass(btn_hover)
  682. $(this).addClass(btn_mousedown)
  683. })
  684. .mouseup(function () {
  685. $(this).removeClass(btn_mousedown)
  686. if ($(this).is(':hover')) {
  687. $(this).addClass(btn_hover)
  688. }
  689. })
  690. .click(executer)
  691. .click(success_notifier)
  692.  
  693. parentDom.append(button)
  694. }
  695.  
  696. function insert_css () {
  697. // TODO: Move all CSS classes here
  698. $('head').append(`<style>
  699. .tbwl-notice {
  700. align-self: center;
  701. display: flex;
  702. flex-direction: row;
  703. padding: 12px;
  704. margin-bottom: 32px;
  705. border-radius: 4px;
  706. color:rgb(255, 255, 255);
  707. background-color: rgb(29, 155, 240);
  708. font-family:TwitterChirp, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  709. font-size:15px;
  710. line-height:20px;
  711. overflow-wrap: break-word;
  712. }
  713. </style>`)
  714. }
  715.  
  716. function main () {
  717. insert_css()
  718.  
  719. const notice_block_success = get_notifier_of ('Successfully blocked.')
  720. const notice_mute_success = get_notifier_of ('Successfully muted.')
  721.  
  722. if (no_local) {
  723. /**
  724. * Two results: Tweet, Retweeters/Likers popup
  725. * document.querySelectorAll('div > div > div > div > div > h2[dir="auto"][aria-level="2"][role="heading"] > span')
  726. * The exact popup:
  727. * document.querySelectorAll('div > div > div > div > div > h2#modal-header[dir="auto"][aria-level="2"][role="heading"] > span')
  728. * But now we still do not know whether it is a "retweeters" popup or a "likers" one and cannot mount the correct button. However, we can check the current page URL! While doing such a check can be done when clicking buttons, let's to do it here in case we want to use different styles, labels, etc. for buttons.
  729.  
  730. * P.S. there is a window.onpopstate event, and since Twitter does not use that, we can set a trigger like this:
  731. * window.onpopstate = () => console.log(new Date().toLocaleDateString())
  732. * It will only be triggered when the user clicks the back button, though, and that is of no use for us.
  733. */
  734. waitForKeyElements('h2#modal-header[dir="auto"][aria-level="2"][role="heading"]', ele => {
  735. const ancestor = get_ancestor(ele, 3)
  736. const currentURL = window.location.href
  737. if (/\/status\/[0-9]+\/likes$/.test(currentURL)) {
  738. mount_switch(ancestor, i18n.include_original_tweeter)
  739. mount_button(ancestor, i18n.mute_btn, mute_all_likers, notice_mute_success)
  740. mount_button(ancestor, i18n.block_btn, block_all_likers, notice_block_success)
  741. } else if (currentURL.endsWith("/retweets")) {
  742. mount_switch(ancestor, i18n.include_original_tweeter)
  743. mount_button(ancestor, i18n.mute_btn, mute_no_comment_retweeters, notice_mute_success)
  744. mount_button(ancestor, i18n.block_btn, block_no_comment_retweeters, notice_block_success)
  745. } else if (/\/lists\/[0-9]+\/members$/.test(currentURL)) {
  746. mount_switch(ancestor, i18n.include_original_tweeter)
  747. mount_button(ancestor, i18n.mute_btn, mute_list_members, notice_mute_success)
  748. mount_button(ancestor, i18n.block_btn, block_list_members, notice_block_success)
  749. }
  750. })
  751. } else {
  752. // Old approach when lang is supported
  753. waitForKeyElements('h2:has(> span:contains(' + i18n.like_title + '))', ele => {
  754. const ancestor = get_ancestor(ele, 3) // ele is h2
  755. mount_switch(ancestor, i18n.include_original_tweeter)
  756. mount_button(ancestor, i18n.mute_btn, mute_all_likers, notice_mute_success)
  757. mount_button(ancestor, i18n.block_btn, block_all_likers, notice_block_success)
  758. })
  759.  
  760. waitForKeyElements('h2:has(> span:contains(' + i18n.retweet_title + '))', ele => {
  761. const ancestor = get_ancestor(ele, 3)
  762. mount_switch(ancestor, i18n.include_original_tweeter)
  763. mount_button(ancestor, i18n.mute_btn, mute_no_comment_retweeters, notice_mute_success)
  764. mount_button(ancestor, i18n.block_btn, block_no_comment_retweeters, notice_block_success)
  765. })
  766. }
  767.  
  768. waitForKeyElements('h2:has(> span:contains(' + i18n.list_members + '))', ele => {
  769. const ancestor = get_ancestor(ele, 3)
  770. mount_button(ancestor, i18n.mute_btn, mute_list_members, notice_mute_success)
  771. mount_button(ancestor, i18n.block_btn, block_list_members, notice_block_success)
  772. })
  773. }
  774.  
  775. main()
  776. })()