├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .npmrc ├── CONTRIBUTING.md ├── .gitignore ├── eslint.config.js ├── bump.js ├── README.md ├── LICENSE ├── package.json ├── .vscode └── settings.json └── index.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [antfu] 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/antfu/contribute 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | languageOptions: { 5 | globals: { 6 | GM_getValue: 'readonly', 7 | GM_setValue: 'readonly', 8 | GM_registerMenuCommand: 'readonly', 9 | }, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /bump.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable antfu/no-top-level-await */ 2 | import fs from 'node:fs/promises' 3 | 4 | const { version } = JSON.parse(await fs.readFile('package.json', 'utf8')) 5 | const content = await fs.readFile('index.js', 'utf8') 6 | const newContent = content.replace(/\/\/ @version\s+(?:\S.*)?$/m, `// @version ${version}`) 7 | await fs.writeFile('index.js', newContent, 'utf8') 8 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v4 21 | 22 | - name: Set node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | 27 | - run: npx changelogithub 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Userscript: Clean Twitter 2 | 3 | ~~Blue check~~, ~~Twitter Blue~~, ~~Verified Organization~~, ~~Communities~~, ~~Trending~~, ~~Search & Explore~~, ~~Who to follow~~, ~~For you~~. **All gone**. 4 | 5 | [Install on Greasyfork](https://greasyfork.org/en/scripts/464719-clean-twitter) 6 | 7 | image 8 | 9 | ## Sponsors 10 | 11 |

12 | 13 | 14 | 15 |

16 | 17 | ## License 18 | 19 | [MIT](./LICENSE) License © 2023 [Anthony Fu](https://github.com/antfu) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "userscript-clean-twitter", 3 | "type": "module", 4 | "version": "0.5.0", 5 | "private": true, 6 | "packageManager": "pnpm@9.11.0", 7 | "author": "Anthony Fu ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/antfu", 10 | "homepage": "https://github.com/antfu/userscript-clean-twitter#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/antfu/userscript-clean-twitter.git" 14 | }, 15 | "bugs": "https://github.com/antfu/userscript-clean-twitter/issues", 16 | "scripts": { 17 | "lint": "eslint .", 18 | "release": "bumpp -x \"node bump.js\" --all" 19 | }, 20 | "devDependencies": { 21 | "@antfu/eslint-config": "^3.7.1", 22 | "@antfu/ni": "^0.23.0", 23 | "@antfu/utils": "^0.7.10", 24 | "@types/node": "^22.6.1", 25 | "bumpp": "^9.5.2", 26 | "eslint": "^9.11.1", 27 | "esno": "^4.7.0", 28 | "lint-staged": "^15.2.10", 29 | "pnpm": "^9.11.0", 30 | "rimraf": "^6.0.1", 31 | "simple-git-hooks": "^2.11.1", 32 | "typescript": "^5.6.2", 33 | "unbuild": "^2.0.0", 34 | "vite": "^5.4.7" 35 | }, 36 | "simple-git-hooks": { 37 | "pre-commit": "pnpm lint-staged" 38 | }, 39 | "lint-staged": { 40 | "*": "eslint --fix" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off" }, 15 | { "rule": "format/*", "severity": "off" }, 16 | { "rule": "*-indent", "severity": "off" }, 17 | { "rule": "*-spacing", "severity": "off" }, 18 | { "rule": "*-spaces", "severity": "off" }, 19 | { "rule": "*-order", "severity": "off" }, 20 | { "rule": "*-dangle", "severity": "off" }, 21 | { "rule": "*-newline", "severity": "off" }, 22 | { "rule": "*quotes", "severity": "off" }, 23 | { "rule": "*semi", "severity": "off" } 24 | ], 25 | 26 | // Enable eslint for all supported languages 27 | "eslint.validate": [ 28 | "javascript", 29 | "javascriptreact", 30 | "typescript", 31 | "typescriptreact", 32 | "vue", 33 | "html", 34 | "markdown", 35 | "json", 36 | "jsonc", 37 | "yaml", 38 | "toml", 39 | "xml", 40 | "gql", 41 | "graphql", 42 | "astro", 43 | "css", 44 | "less", 45 | "scss", 46 | "pcss", 47 | "postcss" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Clean Twitter 3 | // @namespace http://antfu.me/ 4 | // @version 0.5.0 5 | // @description Bring back peace on Twitter 6 | // @author Anthony Fu (https://github.com/antfu) 7 | // @license MIT 8 | // @homepageURL https://github.com/antfu/userscript-clean-twitter 9 | // @supportURL https://github.com/antfu/userscript-clean-twitter 10 | // @match https://twitter.com/** 11 | // @match https://x.com/** 12 | // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com 13 | // @grant GM_getValue 14 | // @grant GM_setValue 15 | // @grant GM_registerMenuCommand 16 | // @grant GM_unregisterMenuCommand 17 | // @run-at document-body 18 | // ==/UserScript== 19 | 20 | (function () { 21 | 'use strict' 22 | 23 | function useOption(key, title, defaultValue) { 24 | if (typeof GM_getValue === 'undefined') { 25 | return { 26 | value: defaultValue, 27 | } 28 | } 29 | 30 | let value = GM_getValue(key, defaultValue) 31 | const ref = { 32 | get value() { 33 | return value 34 | }, 35 | set value(v) { 36 | value = v 37 | GM_setValue(key, v) 38 | location.reload() 39 | }, 40 | } 41 | 42 | GM_registerMenuCommand(`${title}: ${value ? '✅' : '❌'}`, () => { 43 | ref.value = !value 44 | }) 45 | 46 | return ref 47 | } 48 | 49 | const hideHomeTabs = useOption('twitter_hide_home_tabs', 'Hide Home Tabs', true) 50 | const hideBlueBadge = useOption('twitter_hide_blue_badge', 'Hide Blue Badges', true) 51 | const skipDelegateDialog = useOption('twitter_skip_delegate_dialog', 'Skip delegate account switching dialog', true) 52 | const expandPersonalAccounts = useOption('twitter_expand_personal_accounts', 'Expand drawer of personal accounts', true) 53 | 54 | const style = document.createElement('style') 55 | const hides = [ 56 | // menu 57 | '[aria-label="Communities (New items)"], [aria-label="Communities"], [aria-label="Twitter Blue"], [aria-label="Verified"], [aria-label="Timeline: Trending now"], [aria-label="Who to follow"], [aria-label="Search and explore"], [aria-label="Verified Organizations"]', 58 | // submean 59 | '* > [href="/i/verified-orgs-signup"]', 60 | // sidebar 61 | '[aria-label="Trending"] > * > *:nth-child(3), [aria-label="Trending"] > * > *:nth-child(4), [aria-label="Trending"] > * > *:nth-child(5)', 62 | // "Verified" tab 63 | '[role="presentation"]:has(> [href="/notifications/verified"][role="tab"])', 64 | // "Jobs" tab 65 | '[href="/jobs"]', 66 | // verified badge 67 | hideBlueBadge.value && '*:has(> * > [aria-label="Verified account"])', 68 | // Home tabs 69 | hideHomeTabs.value && '[role="tablist"]:has([href="/home"][role="tab"])', 70 | ].filter(Boolean) 71 | 72 | style.innerHTML = [ 73 | `${hides.join(',')}{ display: none !important; }`, 74 | // styling 75 | '[aria-label="Search Twitter"] { margin-top: 20px !important; }', 76 | ].join('') 77 | 78 | document.body.appendChild(style) 79 | 80 | function selectedFollowingTab() { 81 | if (hideHomeTabs.value) { 82 | if (window.location.pathname === '/home') { 83 | const tabs = document.querySelectorAll('[href="/home"][role="tab"]') 84 | if (tabs.length === 2 && tabs[1].getAttribute('aria-selected') === 'false') 85 | tabs[1].click() 86 | } 87 | } 88 | } 89 | 90 | function hideDiscoverMore() { 91 | const conversations = document.querySelector('[aria-label="Timeline: Conversation"]')?.children[0] 92 | if (!conversations) 93 | return 94 | 95 | let hide = false 96 | Array.from(conversations.children).forEach((el) => { 97 | if (hide) { 98 | el.style.display = 'none' 99 | return 100 | } 101 | 102 | const span = el.querySelector('h2 > div > span') 103 | 104 | if (span?.textContent.trim() === 'Discover more') { 105 | hide = true 106 | el.style.display = 'none' 107 | } 108 | }) 109 | } 110 | 111 | // Select "Following" tab on home page, if not 112 | window.addEventListener('load', () => { 113 | setTimeout(() => { 114 | selectedFollowingTab() 115 | hideDiscoverMore() 116 | }, 500) 117 | // TODO: use a better way to detect the tab is loaded 118 | setTimeout(() => { 119 | hideDiscoverMore() 120 | }, 1500) 121 | hideDiscoverMore() 122 | }) 123 | 124 | if (expandPersonalAccounts.value) { 125 | let timer 126 | const ob = new MutationObserver((mut) => { 127 | if (!mut.some(m => m.addedNodes.length)) 128 | return 129 | if (timer) 130 | clearTimeout(timer) 131 | timer = setTimeout(() => { 132 | const personalAccount = [...document.querySelectorAll('svg')].find(i => i.parentElement.textContent?.trim() === 'Personal accounts')?.parentElement 133 | if (!personalAccount || !personalAccount.nextSibling) 134 | return 135 | const nextElement = personalAccount.nextSibling 136 | if (nextElement.tagName !== 'BUTTON') { 137 | personalAccount.click() 138 | } 139 | }, 200) 140 | }) 141 | ob.observe(document.body, { 142 | childList: true, 143 | subtree: true, 144 | }) 145 | } 146 | 147 | if (skipDelegateDialog.value) { 148 | const ob = new MutationObserver((mut) => { 149 | if (!mut.some(m => m.addedNodes.length)) 150 | return 151 | const dialog = document.querySelector('[data-testid="sheetDialog"]') 152 | if (!dialog) 153 | return 154 | const text = dialog.textContent.toLowerCase() 155 | if (!text.includes('switch to a delegate account')) { 156 | // eslint-disable-next-line no-console 157 | console.debug('[Clean Twitter] Dialog is not detected as a "delegate account switching" dialog', dialog) 158 | return 159 | } 160 | 161 | const buttons = [...dialog.querySelectorAll('button')] 162 | 163 | const confrim = buttons.find(el => el.textContent.toLowerCase().includes('switch accounts')) 164 | 165 | if (!confrim) { 166 | // eslint-disable-next-line no-console 167 | console.debug('[Clean Twitter] Confirm button not found', dialog) 168 | return 169 | } 170 | 171 | confrim.click() 172 | }) 173 | 174 | ob.observe(document.body, { 175 | childList: true, 176 | subtree: true, 177 | }) 178 | } 179 | })() 180 | --------------------------------------------------------------------------------