Bloque ou mute tous les utilisateurs de Twitter qui aiment ou repostent un post spécifique (Tweet), avec amour.
// ==UserScript== // @name Twitter Block With Love // @namespace https://www.eolstudy.com // @homepage https://github.com/E011011101001/Twitter-Block-With-Love // @icon https://raw.githubusercontent.com/E011011101001/Twitter-Block-With-Love/master/imgs/icon.svg // @version 2024.05.17 // @description Block or mute all the Twitter users who like or repost a specific post(Tweet), with love. // @description:fr Bloque ou mute tous les utilisateurs de Twitter qui aiment ou repostent un post spécifique (Tweet), avec amour. // @description:zh-CN 屏蔽或隐藏所有转发或点赞某条推文的推特用户 // @description:zh-TW 封鎖或靜音所有轉推或喜歡某則推文的推特使用者 // @description:ja あるツイートに「いいね」や「リツイート」をしたTwitterユーザー全員をブロックまたはミュートする機能を追加する // @description:ko 특정 트윗을 좋아하거나 리트윗하는 모든 트위터 사용자 차단 또는 음소거 // @description:de Blockieren Sie alle Twitter-Nutzer, denen ein bestimmter Tweet gefällt oder die ihn retweeten, oder schalten Sie sie stumm - mit Liebe. // @author Eol, OverflowCat, yuanLeeMidori, nwxz // @license MIT // @run-at document-end // @grant GM_registerMenuCommand // @match https://twitter.com/* // @match https://x.com/* // @match https://mobile.twitter.com/* // @match https://tweetdeck.twitter.com/* // @exclude https://twitter.com/account/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/qs.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // ==/UserScript== /* global axios $ Qs */ (_ => { /* Begin of Dependencies */ /* eslint-disable */ // https://gist.githubusercontent.com/BrockA/2625891/raw/9c97aa67ff9c5d56be34a55ad6c18a314e5eb548/waitForKeyElements.js /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts, that detects and handles AJAXed content. Usage example: waitForKeyElements ( "div.comments" , commentCallbackFunction ); //--- Page-specific function to do what we want when the node is found. function commentCallbackFunction (jNode) { jNode.text ("This comment changed by waitForKeyElements()."); } IMPORTANT: This function requires your script to have loaded jQuery. */ function waitForKeyElements ( selectorTxt, /* Required: The jQuery selector string that specifies the desired element(s). */ actionFunction, /* Required: The code to run when elements are found. It is passed a jNode to the matched element. */ bWaitOnce, /* Optional: If false, will continue to scan for new elements even after the first match is found. */ iframeSelector /* Optional: If set, identifies the iframe to search. */ ) { var targetNodes, btargetsFound; if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt); else targetNodes = $(iframeSelector).contents () .find (selectorTxt); if (targetNodes && targetNodes.length > 0) { btargetsFound = true; /*--- Found target node(s). Go through each and act if they are new. */ targetNodes.each ( function () { var jThis = $(this); var alreadyFound = jThis.data ('alreadyFound') || false; if (!alreadyFound) { //--- Call the payload function. var cancelFound = actionFunction (jThis); if (cancelFound) btargetsFound = false; else jThis.data ('alreadyFound', true); } } ); } else { btargetsFound = false; } //--- Get the timer-control variable for this selector. var controlObj = waitForKeyElements.controlObj || {}; var controlKey = selectorTxt.replace (/[^\w]/g, "_"); var timeControl = controlObj [controlKey]; //--- Now set or clear the timer as appropriate. if (btargetsFound && bWaitOnce && timeControl) { //--- The only condition where we need to clear the timer. clearInterval (timeControl); delete controlObj [controlKey] } else { //--- Set a timer, if needed. if ( ! timeControl) { timeControl = setInterval ( function () { waitForKeyElements ( selectorTxt, actionFunction, bWaitOnce, iframeSelector ); }, 300 ); controlObj [controlKey] = timeControl; } } waitForKeyElements.controlObj = controlObj; } /* eslint-enable */ /* End of Dependencies */ let lang = document.documentElement.lang if (lang == 'en-US') { lang = 'en' // TweetDeck } const translations = { // Please submit a feedback on Greasyfork.com if your language is not in the list bellow en: { lang_name: 'English', like_title: 'Liked by', like_list_identifier: 'Timeline: Liked by', retweet_title: 'Retweeted by', retweet_list_identifier: 'Timeline: Retweeted by', block_btn: 'Block all', block_success: 'All users blocked!', mute_btn: 'Mute all', mute_success: 'All users muted!', include_original_tweeter: 'Include the original Tweeter', logs: 'Logs', list_members: 'List members', list_members_identifier: 'Timeline: List members', block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.' }, 'en-GB': { lang_name: 'British English', like_title: 'Liked by', like_list_identifier: 'Timeline: Liked by', retweet_title: 'Retweeted by', retweet_list_identifier: 'Timeline: Retweeted by', block_btn: 'Block all', block_success: 'All users blocked!', mute_btn: 'Mute all', mute_success: 'All users muted!', include_original_tweeter: 'Include the original Tweeter', logs: 'Logs', list_members: 'List members', list_members_identifier: 'Timeline: List members', block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.' }, zh: { lang_name: '简体中文', like_title: '喜欢者', like_list_identifier: '时间线:喜欢者', retweet_title: '转推', retweet_list_identifier: '时间线:转推者', block_btn: '全部屏蔽', mute_btn: '全部隐藏', block_success: '列表用户已全部屏蔽!', mute_success: '列表用户已全部隐藏!', include_original_tweeter: '包括推主', logs: '操作记录', list_members: '列表成员', list_members_identifier: '时间线:列表成员', block_retweets_notice: 'Twitter Block with Love 仅屏蔽了不带评论转推的用户。\n请手动屏蔽引用推文的用户。' }, 'zh-Hant': { lang_name: '正體中文', like_title: '已被喜歡', like_list_identifier: '時間軸:已被喜歡', retweet_title: '轉推', retweet_list_identifier: '時間軸:已被轉推', block_btn: '全部封鎖', mute_btn: '全部靜音', block_success: '列表用戶已全部封鎖!', mute_success: '列表用戶已全部靜音!', include_original_tweeter: '包括推主', logs: '活動記錄', list_members: '列表成員', list_members_identifier: '時間軸:列表成員', block_retweets_notice: 'Twitter Block with Love 僅封鎖了不帶評論轉推的使用者。\n請手動封鎖引用推文的使用者。' }, ja: { lang_name: '日本語', like_list_identifier: 'タイムライン: いいねしたユーザー', like_title: 'いいねしたユーザー', retweet_list_identifier: 'タイムライン: リツイートしたユーザー', retweet_title: 'リツイート', block_btn: '全部ブロック', mute_btn: '全部ミュート', block_success: '全てブロックしました!', mute_success: '全てミュートしました!', include_original_tweeter: 'スレ主', logs: '操作履歴を表示', list_members: 'リストに追加されているユーザー', list_members_identifier: 'タイムライン: リストに追加されているユーザー', block_retweets_notice: 'TBWLは、コメントなしでリツイートしたユーザーのみをブロックしました。\n引用ツイートしたユーザーを手動でブロックしてください。' }, vi: { // translation by Ly Hương lang_name: 'Tiếng Việt', like_list_identifier: 'Dòng thời gian: Được thích bởi', like_title: 'Được thích bởi', retweet_list_identifier: 'Dòng thời gian: Được Tweet lại bởi', retweet_title: 'Được Tweet lại bởi', block_btn: 'Chặn tất cả', mute_btn: 'Tắt tiếng tất cả', block_success: 'Tất cả tài khoản đã bị chặn!', mute_success: 'Tất cả tài khoản đã bị tắt tiếng!', include_original_tweeter: 'Tweeter gốc', logs: 'Lịch sử', list_members: 'Thành viên trong danh sách', list_members_identifier: 'Dòng thời gian: Thành viên trong danh sách', 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.' }, ko: { // translation by hellojo011 lang_name: '한국어', like_list_identifier: '타임라인: 마음에 들어 함', like_title: '마음에 들어 함', retweet_list_identifier: '타임라인: 리트윗함', retweet_title: '리트윗', block_btn: '모두 차단', mute_btn: '모두 뮤트', block_success: '모두 차단했습니다!', mute_success: '모두 뮤트했습니다!', include_original_tweeter: '글쓴이', logs: '활동', list_members: '리스트 멤버', list_members_identifier: '타임라인: 리스트 멤버', block_retweets_notice: '저희는 리트윗하신 사용자분들을 차단 했으나 트윗 인용하신 사용자분들은 직접 차단하셔야 합니다.' }, de: { // translation by Wassermäuserich Lúcio lang_name: 'Deutsch', like_title: 'Gefällt', like_list_identifier: 'Timeline: Gefällt', retweet_title: 'Retweetet von', retweet_list_identifier: 'Timeline: Retweetet von', block_btn: 'Alle blockieren', mute_btn: 'Alle stummschalten', block_success: 'Alle wurden blockiert!', mute_success: 'Alle wurden stummgeschaltet!', include_original_tweeter: 'Original-Hochlader einschließen', logs: 'Betriebsaufzeichnung', list_members: 'Listenmitglieder', list_members_identifier: 'Timeline: Listenmitglieder', block_retweets_notice: 'TBWL hat nur Benutzer blockiert, die ohne Kommentare retweetet haben.\nBitte blockieren Sie Benutzer, die mit Kommentaren retweetet haben, manuell.', enabled: 'Aktiviert!', disabled: 'Behindert!', }, fr: { lang_name: 'French', like_title: 'Aimé par', like_list_identifier: 'Fil d\'actualités : Aimé par', retweet_title: 'Retweeté par', retweet_list_identifier: 'Fil d\'actualités : Retweeté par', block_btn: 'Bloquer tous', block_success: 'Tous les utilisateurs sont bloqués !', mute_btn: 'Masquer tous', mute_success: 'Tous les utilisateurs sont masqués !', include_original_tweeter: 'Inclure l’auteur original', logs: 'Logs', list_members: 'Membres de la liste', list_members_identifier: 'Fil d\'actualités : Membres de la liste', block_retweets_notice: 'TBWL a seulement bloqué les utilisateurs qui ont retweeté sans commenter.\n Vous devez bloquer manuellement les retweets avec commentaire.' }, } let i18n = translations[lang] // lang is empty in some error pages, so check lang first if (lang && !i18n) { i18n = translations.en if (false) { let langnames = [] Object.values(translations).forEach(language => langnames.push(language.lang_name)) langnames = langnames.join(', ') confirm( 'Twitter Block With Love userscript does not support your language (language code: "' + lang + '").\n' + 'Please send feedback at Greasyfork.com or open an issue at Github.com.\n' + 'Before that, you can edit the userscript yourself or just switch the language of Twitter Web App to any of the following languages: ' + langnames + '.\n\nDo you want to open an issue?' ) && window.location.replace('https://github.com/E011011101001/Twitter-Block-With-Love/issues/new/') } } function rgba_to_hex (rgba_str, force_remove_alpha) { return '#' + rgba_str.replace(/^rgba?\(|\s+|\)$/g, '') // Get's rgba / rgb string values .split(',') // splits them at "," .filter((_, index) => !force_remove_alpha || index !== 3) .map(string => parseFloat(string)) // Converts them to numbers .map((number, index) => index === 3 ? Math.round(number * 255) : number) // Converts alpha to 255 number .map(number => number.toString(16)) // Converts numbers to hex .map(string => string.length === 1 ? '0' + string : string) // Adds 0 when length of one number is 1 .join('') .toUpperCase() } function hex_to_rgb (hex_str) { const r###lt = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i.exec(hex_str) return r###lt ? `rgb(${parseInt(r###lt[1], 16)}, ${parseInt(r###lt[2], 16)}, ${parseInt(r###lt[3], 16)})` : '' } function invert_hex (hex) { return '#' + (Number(`0x1${hex.substring(1)}`) ^ 0xFFFFFF).toString(16).substring(1).toUpperCase() } function get_theme_color () { const FALLBACK_COLOR = 'rgb(128, 128, 128)' let bgColor = FALLBACK_COLOR // try { // bgColor = getComputedStyle(document.querySelector('h2 > span')).color // } catch (e) { // console.info('[TBWL] bgColor not found. Falling back to default.') // } let buttonTextColor = hex_to_rgb(invert_hex(rgba_to_hex(bgColor))) for (const ele of document.querySelectorAll('div[role=\'button\']')) { const color = ele?.style?.backgroundColor if (color != '') { bgColor = color const span = ele.querySelector('span') buttonTextColor = getComputedStyle(span)?.color || buttonTextColor } } return { bgColor, buttonTextColor, plainTextColor: $('span').css('color'), hoverColor: bgColor.replace(/rgb/i, 'rgba').replace(/\)/, ', 0.9)'), mousedownColor: bgColor.replace(/rgb/i, 'rgba').replace(/\)/, ', 0.8)') } } function get_cookie (cname) { const name = cname + '=' const ca = document.cookie.split(';') for (let i = 0; i < ca.length; ++i) { const c = ca[i].trim() if (c.indexOf(name) === 0) { return c.substring(name.length, c.length) } } return '' } function get_ancestor (dom, level) { for (let i = 0; i < level; ++i) { dom = dom.parent() } return dom } const ajax = axios.create({ baseURL: 'https://api.x.com', withCredentials: true, headers: { Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', 'X-Twitter-Auth-Type': 'OAuth2Session', 'X-Twitter-Active-User': 'yes', 'X-Csrf-Token': get_cookie('ct0') } }) function get_tweet_id () { // https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567 return location.href.split('status/')[1].split('/')[0] } function get_list_id () { // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567 return location.href.split('lists/')[1].split('/')[0] } const paramsREQ = `features=%7B%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_r###lts_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D` // fetch_likers and fetch_no_comment_reposters need to be merged into one function async function fetch_likers (tweetId) { const response = await ajax.get(`https://x.com/i/api/graphql/-A3YSkEdbCV0rpHTkYZXCA/Favoriters?variables=%7B%22tweetId%22%3A%22${tweetId}%22%2C%22includePromotedContent%22%3Atrue%7D&${paramsREQ}`); const data = response.data; const users = data["data"]["favoriters_timeline"]["timeline"]["instructions"].reduce((acc, instruction) => { if (instruction.type === 'TimelineAddEntries') { instruction.entries.forEach(entry => { if (entry.content && entry.content.entryType === 'TimelineTimelineItem' && entry.content.itemContent && entry.content.itemContent.itemType === 'TimelineUser') { if (entry.content.itemContent.user_r###lts && entry.content.itemContent.user_r###lts.r###lt && typeof entry.content.itemContent.user_r###lts.r###lt.rest_id !== "undefined") { const restId = entry.content.itemContent.user_r###lts.r###lt.rest_id; acc[restId] = true; } } }); } return acc; }, {}); const likers = Object.keys(users); return likers; } async function fetch_no_comment_reposters (tweetId) { const response = await ajax.get(`https://x.com/i/api/graphql/s6LwzbPawe8J04NldDYrQQ/Retweeters?variables=%7B%22tweetId%22%3A%22${tweetId}%22%2C%22includePromotedContent%22%3Atrue%7D&${paramsREQ}`); const data = response.data; const users = data["data"]["retweeters_timeline"]["timeline"]["instructions"].reduce((acc, instruction) => { if (instruction.type === 'TimelineAddEntries') { instruction.entries.forEach(entry => { if (entry.content && entry.content.entryType === 'TimelineTimelineItem' && entry.content.itemContent && entry.content.itemContent.itemType === 'TimelineUser') { if (entry.content.itemContent.user_r###lts && entry.content.itemContent.user_r###lts.r###lt && typeof entry.content.itemContent.user_r###lts.r###lt.rest_id !== "undefined") { const restId = entry.content.itemContent.user_r###lts.r###lt.rest_id; acc[restId] = true; } } }); } return acc; }, {}); const reposters = Object.keys(users); return reposters; } async function fetch_list_members (listId) { const users = (await ajax.get(`/1.1/lists/members.json?list_id=${listId}`)).data.users const members = users.map(u => u.id_str) return members } function block_user (id) { ajax.post('/1.1/blocks/create.json', Qs.stringify({ user_id: id }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) } function mute_user (id) { ajax.post('/1.1/mutes/users/create.json', Qs.stringify({ user_id: id }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) } async function get_tweeter (tweetId) { const screen_name = location.href.split('x.com/')[1].split('/')[0] const tweetData = (await ajax.get(`/2/timeline/conversation/${tweetId}.json`)).data // Find the tweeter by username const users = tweetData.globalObjects.users for (const key in users) { if (users[key].screen_name === screen_name) { return key } } return undefined } function is_poster_included () { return $('#bwl-include-tweeter').prop('checked') } // block_all_liker and block_no_comment_reposters need to be merged async function block_all_likers () { const tweetId = get_tweet_id() const likers = await fetch_likers(tweetId) if (is_poster_included()) { const tweeter = await get_tweeter(tweetId) if (tweeter) { likers.push(tweeter) } } likers.forEach(block_user) } async function mute_all_likers () { const tweetId = get_tweet_id() const likers = await fetch_likers(tweetId) if (is_poster_included()) { const tweeter = await get_tweeter(tweetId) if (tweeter) { likers.push(tweeter) } } likers.forEach(mute_user) } async function block_reposters () { const tweetId = get_tweet_id() const reposters = await fetch_no_comment_reposters(tweetId) if (is_poster_included()) { const tweeter = await get_tweeter(tweetId) if (tweeter) { reposters.push(tweeter) } } reposters.forEach(block_user) } async function mute_reposters () { const tweetId = get_tweet_id() const reposters = await fetch_no_comment_reposters(tweetId) if (is_poster_included()) { const tweeter = await get_tweeter(tweetId) if (tweeter) { reposters.push(tweeter) } } reposters.forEach(mute_user) } async function block_list_members () { const listId = get_list_id() const members = await fetch_list_members(listId) members.forEach(block_user) } async function mute_list_members () { const listId = get_list_id() const members = await fetch_list_members(listId) members.forEach(mute_user) } async function mute () { const url = window.location.href if (url.endsWith('/likes')) { mute_all_likers() } else if (url.endsWith('/retweets')) { mute_reposters() } else { console.error('Mute is not implemented on this page.') } } async function block () { const url = window.location.href if (url.endsWith('/likes')) { block_all_likers() } else if (url.endsWith('/retweets')) { block_reposters() } else { console.error('Block is not implemented on this page.') } } function get_notifier_of (msg) { return _ => { const banner = $(` <div id="bwl-notice" style="right:0px; position:fixed; left:0px; bottom:0px; display:flex; flex-direction:column;"> <div class="tbwl-notice"> <span>${msg}</span> </div> </div> `) const closeButton = $(` <span id="bwl-close-button" style="font-weight:700; margin-left:12px; margin-right:12px; cursor:pointer;"> Close </span> `) closeButton.click(_ => banner.remove()) $(banner).children('.tbwl-notice').append(closeButton) $('#layers').append(banner) // TODO: after the hiding, the tab gets sluggish if revisited $('div[data-testid="cellInnerDiv"]:has(div[role="button"])').parent().hide() setTimeout(() => banner.remove(), 5000) } } function mount_switch (parentDom, name) { const button = $(` <div class="container"> <div class="checkbox"> <input type="checkbox" id="bwl-include-tweeter" name="" value=""> <label for="bwl-include-tweeter"><span>${name}</span></label> </div> </div> `) parentDom.append(button) } function mount_button (parentDom, name, executer, success_notifier) { const btn_mousedown = 'bwl-btn-mousedown' const btn_hover = 'bwl-btn-hover' const button = $(` <div aria-haspopup="true" role="button" data-focusable="true" class="bwl-btn-base" style="margin:3px" > <div class="bwl-btn-inner-wrapper"> <span> <span class="bwl-text-font">${name}</span> </span> </div> </div> `).addClass(parentDom.prop('classList')[0]) .hover(function () { $(this).addClass(btn_hover) }, function () { $(this).removeClass(btn_hover) $(this).removeClass(btn_mousedown) }) .on('selectstart', function () { return false }) .mousedown(function () { $(this).removeClass(btn_hover) $(this).addClass(btn_mousedown) }) .mouseup(function () { $(this).removeClass(btn_mousedown) if ($(this).is(':hover')) { $(this).addClass(btn_hover) } }) .click(executer) .click(success_notifier) parentDom.append(button) } function insert_css () { const FALLBACK_FONT_FAMILY = 'TwitterChirp, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, "Noto Sans CJK SC", "Noto Sans CJK TC", "Noto Sans CJK JP", Arial, sans-serif;' function get_font_family () { for (const ele of document.querySelectorAll('div[role=\'button\']')) { const font_family = getComputedStyle(ele).fontFamily if (font_family) { return font_family + ', ' + FALLBACK_FONT_FAMILY } } return FALLBACK_FONT_FAMILY } const colors = get_theme_color() // switch related $('head').append(`<style> </style>`) // TODO: reduce repeated styles $('head').append(`<style> #tbwl-panel { display:flex; align-items:center; justify-content:center; border-bottom-width:1px;border-bottom-style: solid; border-color: rgb(239, 243, 244); padding-top: 3px; padding-bottom: 3px; } .tbwl-notice { align-self: center; display: flex; flex-direction: row; padding: 12px; margin-bottom: 32px; border-radius: 4px; color:rgb(255, 255, 255); background-color: rgb(29, 155, 240); font-family: ${FALLBACK_FONT_FAMILY}; font-size: 15px; line-height: 20px; overflow-wrap: break-word; } .bwl-btn-base { min-height: 30px; padding-left: 1em; padding-right: 1em; border: 1px solid ${colors.bgColor} !important; border-radius: 9999px; background-color: ${colors.bgColor}; display: flex; align-items: center } .bwl-btn-mousedown { background-color: ${colors.mousedownColor}; cursor: pointer; } .bwl-btn-hover { background-color: ${colors.hoverColor}; cursor: pointer; } .bwl-btn-inner-wrapper { font-weight: bold; -webkit-box-align: center; align-items: center; -webkit-box-flex: 1; flex-grow: 1; color: ${colors.bgColor}; display: flex; } .bwl-text-font { font-family: ${get_font_family()}; color: ${colors.buttonTextColor}; font-size: 14px; } .container { margin-top: 0px; margin-left: 0px; margin-right: 5px; } .checkbox { width: 100%; margin: 0px auto; position: relative; display: block; color: ${colors.plainTextColor}; } .checkbox input[type="checkbox"] { width: auto; opacity: 0.00000001; position: absolute; left: 0; margin-left: 0px; } .checkbox label:before { content: ''; position: absolute; left: 0; top: -5px; margin: 0px; width: 22px; height: 22px; transition: transform 0.2s ease; border-radius: 3px; border: 2px solid ${colors.bgColor}; } .checkbox label:after { content: ''; display: block; width: 10px; height: 5px; border-bottom: 2px solid ${colors.bgColor}; border-left: 2px solid ${colors.bgColor}; -webkit-transform: rotate(-45deg) scale(0); transform: rotate(-45deg) scale(0); transition: transform ease 0.2s; will-change: transform; position: absolute; top: 3px; left: 8px; } .checkbox input[type="checkbox"]:checked ~ label::before { color: ${colors.bgColor}; } .checkbox input[type="checkbox"]:checked ~ label::after { -webkit-transform: rotate(-45deg) scale(1); transform: rotate(-45deg) scale(1); } .checkbox label { position: relative; display: block; padding-left: 31px; margin-bottom: 0; font-weight: normal; cursor: pointer; vertical-align: sub; width:fit-content; width:-webkit-fit-content; width:-moz-fit-content; } .checkbox label span { position: relative; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); } .checkbox input[type="checkbox"]:focus + label::before { outline: 0; } </style>`) } function compose_panel () { const notice_block_success = get_notifier_of('Successfully blocked.') const notice_mute_success = get_notifier_of('Successfully muted.') const TBWLPanel = $(`<div id="tbwl-panel" style=" "></div>`) mount_switch(TBWLPanel, i18n.include_original_tweeter) mount_button(TBWLPanel, i18n.mute_btn, mute, notice_mute_success) mount_button(TBWLPanel, i18n.block_btn, block, notice_block_success) return TBWLPanel.hide() } function main () { const sleepTime = 700 // ms insert_css() const TBWLPanel = compose_panel() let prevURL = undefined setInterval(_ => { const currentURL = window.location.href if (prevURL !== currentURL) { prevURL = currentURL // Attention: /retweets may change to /reposts at any time. // Good job, Elon. // ♪ CEO, entrepreneur, born in 1971, Elon~~ Elon Reeve Musk~~ ♪♪ if (currentURL.endsWith('/likes') || currentURL.endsWith('/retweets')) { if ($('#tbwl-panel').length) { TBWLPanel.slideDown('fast') } else { waitForKeyElements('div[data-testid="primaryColumn"] section, div[data-testid="primaryColumn"] div[data-testid="emptyState"]', ele => { TBWLPanel.insertBefore(ele) TBWLPanel.slideDown() }, true) } } else { TBWLPanel.slideUp('fast') } } }, sleepTime) // TODO: merge into the above way // need a way to hide the include_original_tweeter option waitForKeyElements('h2#modal-header[aria-level="2"][role="heading"]', ele => { const ancestor = get_ancestor(ele, 3) const currentURL = window.location.href if (/\/lists\/[0-9]+\/members$/.test(currentURL)) { mount_switch(ancestor, i18n.include_original_tweeter) const notice_block_success = get_notifier_of('Successfully blocked.') const notice_mute_success = get_notifier_of('Successfully muted.') mount_button(ancestor, i18n.mute_btn, mute_list_members, notice_mute_success) mount_button(ancestor, i18n.block_btn, block_list_members, notice_block_success) } }) } main() })()