├── .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 |
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 |
--------------------------------------------------------------------------------