├── .gitignore ├── src ├── screenshots │ ├── dimming.png │ ├── focussed.png │ ├── composing.png │ ├── potential.png │ ├── configuration.png │ ├── hide-things.png │ └── showing-sides.png ├── focussed-twitter │ ├── icons │ │ ├── focussed_twitter_16.png │ │ ├── focussed_twitter_32.png │ │ ├── focussed_twitter_48.png │ │ └── focussed_twitter_128.png │ ├── styles │ │ ├── popup.css │ │ └── focussed-twitter.css │ ├── manifest.firefox.json │ ├── manifest.chrome.json │ ├── scripts │ │ ├── background.js │ │ ├── popup.js │ │ └── focussed-twitter.js │ ├── manifest.firefox.dev.json │ └── pages │ │ └── popup.html └── assets │ ├── icon-plain.svg │ └── icon.svg ├── Makefile ├── LICENSE ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | src/focussed-twitter/manifest.json -------------------------------------------------------------------------------- /src/screenshots/dimming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/screenshots/dimming.png -------------------------------------------------------------------------------- /src/screenshots/focussed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/screenshots/focussed.png -------------------------------------------------------------------------------- /src/screenshots/composing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/screenshots/composing.png -------------------------------------------------------------------------------- /src/screenshots/potential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/screenshots/potential.png -------------------------------------------------------------------------------- /src/screenshots/configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/screenshots/configuration.png -------------------------------------------------------------------------------- /src/screenshots/hide-things.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/screenshots/hide-things.png -------------------------------------------------------------------------------- /src/screenshots/showing-sides.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/screenshots/showing-sides.png -------------------------------------------------------------------------------- /src/focussed-twitter/icons/focussed_twitter_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/focussed-twitter/icons/focussed_twitter_16.png -------------------------------------------------------------------------------- /src/focussed-twitter/icons/focussed_twitter_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/focussed-twitter/icons/focussed_twitter_32.png -------------------------------------------------------------------------------- /src/focussed-twitter/icons/focussed_twitter_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/focussed-twitter/icons/focussed_twitter_48.png -------------------------------------------------------------------------------- /src/focussed-twitter/icons/focussed_twitter_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/focussed-twitter/HEAD/src/focussed-twitter/icons/focussed_twitter_128.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FILES = src/focussed-twitter/icons/ src/focussed-twitter/pages/ src/focussed-twitter/scripts/ src/focussed-twitter/styles/ src/focussed-twitter/manifest.json 2 | 3 | help: 4 | @grep -E '^[a-zA-Z\._-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' 5 | 6 | dev-chrome: # Copies manifest.chrome.json to manifest.json 7 | cp -f src/focussed-twitter/manifest.chrome.json src/focussed-twitter/manifest.json 8 | dev-firefox: # Copies manifest.firefox.dev.json to manifest.json 9 | cp -f src/focussed-twitter/manifest.firefox.dev.json src/focussed-twitter/manifest.json 10 | prod-firefox: # Copies manifest.firefox.json to manifest.json 11 | cp -f src/focussed-twitter/manifest.firefox.json src/focussed-twitter/manifest.json 12 | build-chrome: # Builds Chrome version of extension for upload 13 | make dev-chrome && zip -r focussed-twitter.chrome.zip $(FILES) 14 | build-firefox: # Builds Firefox version of extension for upload 15 | make prod-firefox && zip -r focussed-twitter.firefox.zip $(FILES) -------------------------------------------------------------------------------- /src/focussed-twitter/styles/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #fafafa; 3 | background: rgb(21, 32, 43); 4 | padding: 1rem; 5 | width: 300px; 6 | } 7 | 8 | h2 { 9 | margin: 0 0 1.25rem 0; 10 | font-size: 1rem; 11 | position: relative; 12 | } 13 | 14 | h2 img { 15 | position: absolute; 16 | top: 50%; 17 | right: 0; 18 | transform: translate(-50%, -50%); 19 | } 20 | 21 | form ul { 22 | list-style-type: none; 23 | } 24 | 25 | form > * + * { 26 | margin-top: 1rem; 27 | } 28 | 29 | span { 30 | font-weight: bold; 31 | } 32 | 33 | li { 34 | display: grid; 35 | grid-template-rows: 1fr 1fr; 36 | grid-template-columns: 1fr 1fr; 37 | } 38 | 39 | input, 40 | label { 41 | cursor: pointer; 42 | } 43 | 44 | ul { 45 | padding: 0; 46 | margin: 0; 47 | display: grid; 48 | grid-gap: 0.5rem; 49 | } 50 | 51 | .no-value label, 52 | input { 53 | grid-column: 1 / -1; 54 | } 55 | 56 | summary { 57 | outline: transparent; 58 | cursor: pointer; 59 | padding: 1rem 0rem; 60 | } 61 | 62 | details > ul { 63 | padding-left: 1rem; 64 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 jh3y 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 5 | 6 | ## [0.10] - 2021-03-11 7 | - Remove breaking change with Tweet borders 8 | ## [0.9] - 2021-03-10 9 | ### Changed 10 | - Fixed Tweet borders 11 | - Allows hiding of Messages Dock 12 | - Hide Metrics for Likes, Retweets, Replies. 13 | - Allow mobile.twitter.com 14 | 15 | ## [0.8] - 2020-06-23 16 | ### Changed 17 | - Fixed the focus on scroll feature by binding event handling to the class. 18 | 19 | ## [0.7] - 2020-06-23 20 | ### Changed 21 | - Updated separator logic to also hide border under page title. 22 | - Added version for Firefox 23 | - Updated README 24 | - Synced implementations into one folder 25 | 26 | ## [0.6] - 2020-06-23 27 | ### Added 28 | - Focussed composition where rest of UI is hidden when composing tweets. 29 | - Relies on MutationObserver to detect DOM structure changes. 30 | - Use visibility toggling on click in the composer. 31 | - Hide separator bars on toggle. 32 | 33 | 34 | ### Removed 35 | - Unnecessary CSS and redundant CSS for thread lines. 36 | - Thread lines attempted featured. -------------------------------------------------------------------------------- /src/focussed-twitter/manifest.firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Focussed Twitter", 3 | "version": "0.12", 4 | "short_name": "focussedtwitter", 5 | "description": "Let's focus on the tweets!", 6 | "manifest_version": 2, 7 | "content_scripts": [ 8 | { 9 | "run_at": "document_start", 10 | "matches": [ 11 | "https://x.com/*", 12 | "https://mobile.x.com/*" 13 | ], 14 | "css": ["styles/focussed-twitter.css"], 15 | "js": ["scripts/focussed-twitter.js"] 16 | } 17 | ], 18 | "background": { 19 | "scripts": ["scripts/background.js"], 20 | "persistent": false 21 | }, 22 | "permissions": ["declarativeContent", "storage"], 23 | "browser_action": { 24 | "default_popup": "pages/popup.html", 25 | "matches": [ 26 | "https://x.com/*", 27 | "https://mobile.x.com/*" 28 | ], 29 | "default_icon": { 30 | "16": "icons/focussed_twitter_16.png", 31 | "32": "icons/focussed_twitter_32.png", 32 | "48": "icons/focussed_twitter_48.png", 33 | "128": "icons/focussed_twitter_128.png" 34 | } 35 | }, 36 | "icons": { 37 | "16": "icons/focussed_twitter_16.png", 38 | "32": "icons/focussed_twitter_32.png", 39 | "48": "icons/focussed_twitter_48.png", 40 | "128": "icons/focussed_twitter_128.png" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/focussed-twitter/manifest.chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Focussed Twitter", 3 | "version": "0.12", 4 | "short_name": "focussedtwitter", 5 | "description": "Let's focus on the tweets!", 6 | "manifest_version": 2, 7 | "content_scripts": [ 8 | { 9 | "run_at": "document_start", 10 | "matches": [ 11 | "https://x.com/*", 12 | "https://mobile.x.com/*" 13 | ], 14 | "css": [ 15 | "styles/focussed-twitter.css" 16 | ], 17 | "js": [ 18 | "scripts/focussed-twitter.js" 19 | ] 20 | } 21 | ], 22 | "background": { 23 | "scripts": ["scripts/background.js"], 24 | "persistent": false 25 | }, 26 | "permissions": [ 27 | "declarativeContent", 28 | "storage" 29 | ], 30 | "page_action": { 31 | "default_popup": "pages/popup.html", 32 | "matches": [ 33 | "https://x.com/*", 34 | "https://mobile.x.com/*" 35 | ], 36 | "default_icon": { 37 | "16": "icons/focussed_twitter_16.png", 38 | "32": "icons/focussed_twitter_32.png", 39 | "48": "icons/focussed_twitter_48.png", 40 | "128": "icons/focussed_twitter_128.png" 41 | } 42 | }, 43 | "icons": { 44 | "16": "icons/focussed_twitter_16.png", 45 | "32": "icons/focussed_twitter_32.png", 46 | "48": "icons/focussed_twitter_48.png", 47 | "128": "icons/focussed_twitter_128.png" 48 | } 49 | } -------------------------------------------------------------------------------- /src/focussed-twitter/scripts/background.js: -------------------------------------------------------------------------------- 1 | const DEFAULTS = { 2 | opacity: 0.05, 3 | transition: 0.2, 4 | fadeBack: 100, 5 | sideBar: false, 6 | bordered: false, 7 | columnBorder: false, 8 | margin: 5, 9 | dimmed: false, 10 | separator: false, 11 | compose: true, 12 | likes: false, 13 | retweets: false, 14 | dms: false, 15 | } 16 | chrome.runtime.onInstalled.addListener(function () { 17 | chrome.storage.sync.get( 18 | [ 19 | 'opacity', 20 | 'transition', 21 | 'fadeBack', 22 | 'sideBar', 23 | 'bordered', 24 | 'columnBorder', 25 | 'margin', 26 | 'dimmed', 27 | 'separator', 28 | 'compose', 29 | 'likes', 30 | 'retweets', 31 | 'dms', 32 | ], 33 | (options) => { 34 | chrome.storage.sync.set({ ...DEFAULTS, ...options }) 35 | chrome.declarativeContent.onPageChanged.removeRules( 36 | undefined, 37 | function () { 38 | chrome.declarativeContent.onPageChanged.addRules([ 39 | { 40 | conditions: [ 41 | new chrome.declarativeContent.PageStateMatcher({ 42 | pageUrl: { hostEquals: 'x.com' }, 43 | }), 44 | ], 45 | actions: [new chrome.declarativeContent.ShowPageAction()], 46 | }, 47 | ]) 48 | } 49 | ) 50 | } 51 | ) 52 | }) 53 | -------------------------------------------------------------------------------- /src/focussed-twitter/manifest.firefox.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Focussed Twitter", 3 | "version": "0.12", 4 | "short_name": "focussedtwitter", 5 | "description": "Let's focus on the tweets!", 6 | "manifest_version": 2, 7 | "content_scripts": [ 8 | { 9 | "run_at": "document_start", 10 | "matches": [ 11 | "https://x.com/*", 12 | "https://mobile.x.com/*" 13 | ], 14 | "css": ["styles/focussed-twitter.css"], 15 | "js": ["scripts/focussed-twitter.js"] 16 | } 17 | ], 18 | "background": { 19 | "scripts": ["scripts/background.js"], 20 | "persistent": false 21 | }, 22 | "permissions": ["declarativeContent", "storage"], 23 | "browser_action": { 24 | "default_popup": "pages/popup.html", 25 | "matches": [ 26 | "https://x.com/*", 27 | "https://mobile.x.com/*" 28 | ], 29 | "default_icon": { 30 | "16": "icons/focussed_twitter_16.png", 31 | "32": "icons/focussed_twitter_32.png", 32 | "48": "icons/focussed_twitter_48.png", 33 | "128": "icons/focussed_twitter_128.png" 34 | } 35 | }, 36 | "applications": { 37 | "gecko": { 38 | "id": "1234567890@example.com", 39 | "strict_min_version": "53.0" 40 | } 41 | }, 42 | "icons": { 43 | "16": "icons/focussed_twitter_16.png", 44 | "32": "icons/focussed_twitter_32.png", 45 | "48": "icons/focussed_twitter_48.png", 46 | "128": "icons/focussed_twitter_128.png" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/assets/icon-plain.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/focussed-twitter/scripts/popup.js: -------------------------------------------------------------------------------- 1 | // Will get hit with max write operations per minute if not throttled 2 | // You are allowed 120 per minute so that 60000 /120 3 | const throttle = (func, limit) => { 4 | let lastFunc 5 | let lastRan 6 | return function () { 7 | const context = this 8 | const args = arguments 9 | if (!lastRan) { 10 | func.apply(context, args) 11 | lastRan = Date.now() 12 | } else { 13 | clearTimeout(lastFunc) 14 | lastFunc = setTimeout(function () { 15 | if (Date.now() - lastRan >= limit) { 16 | func.apply(context, args) 17 | lastRan = Date.now() 18 | } 19 | }, limit - (Date.now() - lastRan)) 20 | } 21 | } 22 | } 23 | 24 | const keys = [ 25 | 'sideBar', 26 | 'bordered', 27 | 'columnBorder', 28 | 'dimmed', 29 | 'separator', 30 | 'compose', 31 | 'dms', 32 | 'likes', 33 | 'retweets', 34 | 'replies', 35 | ] 36 | 37 | const update = throttle((e) => { 38 | chrome.storage.sync.set({ 39 | [e.target.name]: 40 | e.target[keys.includes(e.target.name) ? 'checked' : 'value'], 41 | }) 42 | 43 | // Updates the value span for a value input. For example, avatarRadius && avatarRadius-value 44 | if ( 45 | !keys.includes(e.target.name) && 46 | document.querySelector(`[id="${e.target.name}-value"]`) 47 | ) { 48 | document.querySelector(`[id="${e.target.name}-value"]`).innerText = 49 | e.target.value 50 | } 51 | }, 60000 / 120) 52 | 53 | const all = [ 54 | 'transition', 55 | 'opacity', 56 | 'fadeBack', 57 | 'sideBar', 58 | 'margin', 59 | 'bordered', 60 | 'dimmed', 61 | 'columnBorder', 62 | 'separator', 63 | 'compose', 64 | 'likes', 65 | 'retweets', 66 | 'replies', 67 | 'dms', 68 | 'avatarRadius', 69 | 'nfts', 70 | ] 71 | const filtered = [ 72 | 'sideBar', 73 | 'bordered', 74 | 'columnBorder', 75 | 'dimmed', 76 | 'separator', 77 | 'compose', 78 | 'likes', 79 | 'retweets', 80 | 'dms', 81 | 'replies', 82 | ] 83 | chrome.storage.sync.get(all, (d) => { 84 | Object.keys(d).forEach((n) => { 85 | // Setting whether checked or has a specific value for a range slider 86 | document.querySelector(`[name="${n}"]`)[ 87 | filtered.includes(n) ? 'checked' : 'value' 88 | ] = d[n] 89 | if (!filtered.includes(n) && document.querySelector(`[id="${n}-value"]`)) { 90 | document.querySelector(`[id="${n}-value"]`).innerText = d[n] 91 | } 92 | }) 93 | }) 94 | document.querySelector('form').addEventListener('input', update) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![focussed twitter logo](src/focussed-twitter/icons/focussed_twitter_48.png) 2 | # Focussed Twitter 🐦 3 | 4 | ![potential](src/screenshots/showing-sides.png) 5 | 6 | A browser extension for reducing the noise on Twitter 🙌 7 | _Works in Firefox and Chromium_ 8 | 9 | - [Chrome Extension](https://chrome.google.com/webstore/detail/focussed-twitter/efldegaojlekkkoegoeakkgknaagjeoj) 10 | - [Firefox Add-On](https://addons.mozilla.org/en-US/firefox/addon/focussed-twitter/) 11 | 12 | ## Motivation 13 | With the new Twitter layout hitting our screens, some weren't too happy about the "noise". 14 | 15 | FWIW, I don't mind the new layout. I've almost forgotten how the old one looked now 😂 16 | However, the new one is a little noisy when scrolling. 17 | 18 | Enter "Focussed Twitter"! 19 | 20 | What started as a bookmarklet is now a browser extension! 21 | 22 | ![dimming noise](src/screenshots/dimming.png) 23 | 24 | ## Principles + Concept 25 | The idea for "Focussed Twitter" is to make it easier to focus on the tweets. 26 | 27 | The original idea for better UX was that if I scrolled Twitter, I wanted everything else to fade away for a moment. 28 | 29 | No need to remove elements or break the behavior/layout 👍 30 | 31 | Everything that "Focussed Twitter" does can be switched off or configured to your liking in the extension options 💪 32 | 33 | ![focussed](src/screenshots/focussed.png) 34 | 35 | ## Features 36 | - Dim sides on scroll 37 | - Set the dim 38 | - Hide the sidebar 39 | - Hide separators 40 | - Remove tweet borders 41 | - Increase the margin between tweets 42 | - Permanently dim sides 43 | - Focussed composing 44 | - Hide metric counts 45 | - Hide DMs drawer 46 | 47 | ![composition](src/screenshots/composing.png) 48 | 49 | ## How does it work 50 | `Focussed Twitter' is powered by CSS variables and MutationObserver. When an option is changed, the script updates inline CSS variables on the document. 51 | These then do things like trigger opacity changes and update transition timings 🤓 52 | 53 | ![configuration](src/screenshots/configuration.png) 54 | 55 | ## Contributions 56 | I'd love some! ❤️ Any PRs are welcome or suggestions. 57 | 58 | Ideas include: 59 | - Creating a better icon 😅 60 | - Porting to other browsers besides Firefox && Chrome 61 | - Cleaner styling for the popup window 😅 62 | 63 | The code is in a place where it's not too heavy. There is a reason for this. If Twitter decides to change the UI structure significantly at some point, the extension will need updating. 64 | 65 | ## Development 66 | ### Chrome 67 | - Rename `manifest.chrome.json` to `manifest.json`. 68 | - Load the unpacked extension into Chrome by pointing at the focussed twitter directory. 69 | ### Firefox 70 | - Rename `manifest.firefox.dev.json` to `manifest.json`. 71 | - Load the add on into Firefox by pointing at the manifest file. 72 | 73 | --- 74 | 75 | Made in haste by @jh3y 😅 -------------------------------------------------------------------------------- /src/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 43 | 46 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 64 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/focussed-twitter/styles/focussed-twitter.css: -------------------------------------------------------------------------------- 1 | /* Set the header opacity transition */ 2 | [data-focussed-nav] { 3 | transition: opacity calc(var(--focussed-twitter-transition, 0.5) * 1s) ease 0s; 4 | } 5 | 6 | /* Sets the visibility of the side bar with trends etc. Uses this to calculate a clip-path */ 7 | [data-testid='sidebarColumn'] { 8 | transition: opacity calc(var(--focussed-twitter-transition, 0.5) * 1s) ease 0s, 9 | clip-path calc(var(--focussed-twitter-transition, 0.5) * 1s) ease 0s; 10 | clip-path: inset(0 0 0 calc(var(--focussed-twitter-show-sidebar) * 100%)); 11 | } 12 | 13 | /* 14 | Sets the opacity of the side bars based on the opacity variables. 15 | Uses the permanently dimmed value to calculate the correct opacity. 16 | */ 17 | [data-focussed-nav], 18 | [data-testid='sidebarColumn'] { 19 | opacity: calc( 20 | (var(--focussed-twitter-dimmed, 1) * var(--focussed-twitter-dim-sides, 1)) + 21 | var(--focussed-twitter-opacity, 0.05) 22 | ); 23 | } 24 | 25 | /* Ensures that the side bars have full opacity when hovered */ 26 | [data-focussed-nav]:hover, 27 | [data-testid='sidebarColumn']:hover { 28 | opacity: 1; 29 | } 30 | 31 | div[data-testid='primaryColumn'] { 32 | border-color: rgba(47, 51, 54, var(--focussed-twitter-column-border, 1)); 33 | } 34 | 35 | [data-focussed-everybody-is-an-nft="true"] [data-focussed-avatar], 36 | [data-focussed-everybody-is-an-nft="true"] [data-focussed-avatar] * { 37 | -webkit-clip-path: url("#nft-clip-path") !important; 38 | clip-path: url("#nft-clip-path") !important; 39 | } 40 | 41 | [data-focussed-everybody-is-an-nft="false"] [data-focussed-avatar], 42 | [data-focussed-everybody-is-an-nft="false"] [data-focussed-avatar] * { 43 | -webkit-clip-path: none !important; 44 | clip-path: none !important; 45 | } 46 | 47 | [data-focussed-avatar], 48 | [data-focussed-avatar] * { 49 | border-radius: calc(var(--focussed-twitter-avatar-radius, 50) * 1%) !important; 50 | } 51 | 52 | 53 | 54 | /* Applies margins to tweets and the compose tweet box */ 55 | [data-focussed-composer], 56 | [data-testid='primaryColumn'] article, 57 | [data-focussed-timeline] article { 58 | padding-top: calc(var(--focussed-twitter-tweet-margin, 0) * 1rem); 59 | padding-bottom: calc(var(--focussed-twitter-tweet-margin, 0) * 1rem); 60 | transition: padding calc(var(--focussed-twitter-transition, 0.5) * 1s) ease 0s; 61 | } 62 | 63 | /* .r-gu4em3 { 64 | opacity: var(--focussed-twitter-tweet-border, 1); 65 | } */ 66 | 67 | /* Set tweet borders */ 68 | /* NOTE:: This is the most brittle part of the styling */ 69 | /* Often needs updating with the selector that's currently setting border-bottom-color */ 70 | div[data-testid='primaryColumn'] section > div > div > div > div > div, 71 | div[data-testid='primaryColumn'] .r-j5o65s, 72 | div[data-focussed-page-header] { 73 | border-bottom-color: rgba( 74 | 47, 75 | 51, 76 | 54, 77 | var(--focussed-twitter-tweet-border, 1) 78 | ) !important; 79 | } 80 | 81 | div[data-focussed-page-header] { 82 | border-color: rgba(47, 51, 54, var(--focussed-twitter-separator, 1)) !important; 83 | } 84 | 85 | /* Covers the pinned tweet too */ 86 | [data-focussed-headerbar] { 87 | opacity: var(--focussed-twitter-separator, 1); 88 | } 89 | 90 | /* Hide Metrics */ 91 | div[data-testid="retweet"] > div > div:nth-of-type(2) { 92 | opacity: calc(1 - var(--focussed-twitter-hide-retweets, 0)); 93 | } 94 | div[data-testid="like"] > div > div:nth-of-type(2) { 95 | opacity: calc(1 - var(--focussed-twitter-hide-likes, 0)); 96 | } 97 | div[data-testid="reply"] > div > div:nth-of-type(2) { 98 | opacity: calc(1 - var(--focussed-twitter-hide-replies, 0)); 99 | } 100 | 101 | div[data-testid="DMDrawer"] { 102 | transition: opacity calc(var(--focussed-twitter-transition, 0.5) * 1s) ease 0s, 103 | clip-path calc(var(--focussed-twitter-transition, 0.5) * 1s) ease 0s; 104 | clip-path: inset(calc(var(--focussed-twitter-hide-dms) * 100%) 0 0 0); 105 | opacity: calc(1 - var(--focussed-twitter-hide-dms, 0)); 106 | } -------------------------------------------------------------------------------- /src/focussed-twitter/pages/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Focussed Twitter 0.11logo

8 |
9 | 39 |
40 | Borders 41 |
    42 |
  • 43 | 44 | 45 |
  • 46 |
  • 47 | 48 | 49 |
  • 50 |
  • 51 | 52 | 53 |
  • 54 |
55 |
56 |
57 | Distractions 58 |
    59 |
  • 60 | 61 | 62 |
  • 63 |
  • 64 | 65 | 66 |
  • 67 |
68 |
69 |
70 | Metrics 71 |
    72 |
  • 73 | 74 | 75 |
  • 76 |
  • 77 | 78 | 79 |
  • 80 |
  • 81 | 82 | 83 |
  • 84 |
85 |
86 |
87 | Experimental 88 |
    89 |
  • 90 | 91 | 96 |
  • 97 |
  • 98 | 99 | 100 | 101 |
  • 102 |
103 |
104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /src/focussed-twitter/scripts/focussed-twitter.js: -------------------------------------------------------------------------------- 1 | const VAR_MAP = { 2 | OPACITY: '--focussed-twitter-opacity', 3 | TRANSITION: '--focussed-twitter-transition', 4 | SIDEBAR: '--focussed-twitter-show-sidebar', 5 | TWEET_BORDER: '--focussed-twitter-tweet-border', 6 | COL_BORDER: '--focussed-twitter-column-border', 7 | TWEET_MARGIN: '--focussed-twitter-tweet-margin', 8 | DIM_SIDES: '--focussed-twitter-dim-sides', 9 | DIMMED: '--focussed-twitter-dimmed', 10 | SEPARATOR: '--focussed-twitter-separator', 11 | RETWEETS: '--focussed-twitter-hide-retweets', 12 | LIKES: '--focussed-twitter-hide-likes', 13 | REPLIES: '--focussed-twitter-hide-replies', 14 | DMS: '--focussed-twitter-hide-dms', 15 | AVATAR_RADIUS: '--focussed-twitter-avatar-radius', 16 | } 17 | 18 | const CLASSES = { 19 | AVATAR: '.css-1dbjc4n.r-1adg3ll.r-bztko3', 20 | } 21 | 22 | const EL_MAP = { 23 | HEADER_BAR: '.css-1dbjc4n.r-z32n2g.r-1or9b2r', 24 | NAV_CONTAINER: 'header[role="banner"]', 25 | COMPOSER_AREA: '[data-testid="tweetTextarea_0"]', 26 | SIDE_BAR: '[data-testid="sidebarColumn"]', 27 | COLUMN: '[data-testid="primaryColumn"]', 28 | CONTAINER: '#react-root', 29 | MODAL: '[aria-modal]', 30 | EMOJI_PICKER: '#emoji_picker_categories_dom_id', 31 | TIMELINE: '[data-focussed-timeline]', 32 | POLL_CONTAINER: '[data-testid="attachments"]', 33 | LAYERS_AVATAR: `#layers ${CLASSES.AVATAR}`, 34 | ACCOUNT_SWITCHER_AVATAR: `[data-testid="SideNav_AccountSwitcher_Button"] ${CLASSES.AVATAR}`, 35 | CONVERSATION_AVATAR: `[data-testid="conversation"] ${CLASSES.AVATAR}`, 36 | MESSAGE_ENTRY_AVATAR: `[data-testid="messageEntry"] ${CLASSES.AVATAR}`, 37 | COMPOSER_AVATAR: `[data-focussed-composer="true"] ${CLASSES.AVATAR}`, 38 | NOTIFICATION_AVATAR: 39 | 'article .css-1dbjc4n.r-1adg3ll.r-mabqd8.r-4amgru.r-bztko3', 40 | FEED_AVATAR: `[data-testid="tweet"] [id]${CLASSES.AVATAR}`, 41 | USER_CELL_AVATAR: `[data-testid="UserCell"] ${CLASSES.AVATAR}`, 42 | AVATAR: 43 | '[data-testid="primaryColumn"] .css-1dbjc4n.r-1adg3ll.r-16l9doz.r-6gpygo.r-2o1y69.r-1v6e3re.r-bztko3.r-1xce0ei', 44 | COMPOSER: 45 | '[data-testid="primaryColumn"] > div > .css-1dbjc4n.r-kemksi.r-184en5c', 46 | } 47 | 48 | const ATTR_MAP = { 49 | NAV: 'data-focussed-nav', 50 | COMPOSER: 'data-focussed-composer', 51 | BOUND: 'data-focussed-composer-bound', 52 | HEADER: 'data-focussed-headerbar', 53 | TIMELINE: 'data-focussed-timeline', 54 | PAGE_HEADER: 'data-focussed-page-header', 55 | AVATAR: 'data-focussed-avatar', 56 | NFT: 'data-focussed-everybody-is-an-nft', 57 | } 58 | 59 | const updateBody = (callback) => { 60 | chrome.storage.sync.get( 61 | [ 62 | 'opacity', 63 | 'transition', 64 | 'fadeBack', 65 | 'sideBar', 66 | 'bordered', 67 | 'columnBorder', 68 | 'margin', 69 | 'dimmed', 70 | 'separator', 71 | 'compose', 72 | 'likes', 73 | 'retweets', 74 | 'replies', 75 | 'dms', 76 | 'nfts', 77 | 'avatarRadius', 78 | ], 79 | (options) => { 80 | const { 81 | opacity, 82 | transition, 83 | // fadeBack, 84 | sideBar, 85 | bordered, 86 | columnBorder, 87 | margin, 88 | dimmed, 89 | separator, 90 | likes, 91 | retweets, 92 | replies, 93 | dms, 94 | nfts, 95 | avatarRadius, 96 | } = options 97 | 98 | if (nfts === 'default') document.body.removeAttribute(ATTR_MAP.NFT) 99 | else 100 | document.body.setAttribute( 101 | ATTR_MAP.NFT, 102 | nfts === 'everybody' ? true : false 103 | ) 104 | 105 | document.body.style.setProperty(VAR_MAP.OPACITY, opacity) 106 | document.body.style.setProperty(VAR_MAP.TRANSITION, transition) 107 | document.body.style.setProperty(VAR_MAP.SIDEBAR, sideBar ? 0 : 1) 108 | document.body.style.setProperty(VAR_MAP.TWEET_BORDER, bordered ? 1 : 0) 109 | document.body.style.setProperty(VAR_MAP.COL_BORDER, columnBorder ? 1 : 0) 110 | document.body.style.setProperty(VAR_MAP.TWEET_MARGIN, margin) 111 | document.body.style.setProperty(VAR_MAP.DIM_SIDES, dimmed ? 0 : 1) 112 | document.body.style.setProperty(VAR_MAP.SEPARATOR, separator ? 0 : 1) 113 | document.body.style.setProperty(VAR_MAP.LIKES, likes ? 1 : 0) 114 | document.body.style.setProperty(VAR_MAP.RETWEETS, retweets ? 1 : 0) 115 | document.body.style.setProperty(VAR_MAP.REPLIES, replies ? 1 : 0) 116 | document.body.style.setProperty(VAR_MAP.DMS, dms ? 1 : 0) 117 | document.body.style.setProperty(VAR_MAP.AVATAR_RADIUS, avatarRadius) 118 | if (callback) callback(options) 119 | } 120 | ) 121 | } 122 | 123 | class FocussedTwitter { 124 | constructor() { 125 | this.focussing = false 126 | this.updating = false 127 | this.removeFocusTimeout = undefined 128 | this.removeDelay = 0 129 | this.sideBar = document.querySelector(EL_MAP.SIDE_BAR) 130 | this.nav = document.querySelector(EL_MAP.NAV_CONTAINER) 131 | this.headerBar = document.querySelector(EL_MAP.HEADER_BAR) 132 | this.composer = document.querySelector(EL_MAP.COMPOSER) 133 | this.column = document.querySelector(EL_MAP.COLUMN) 134 | this.container = document.querySelector(EL_MAP.CONTAINER) 135 | this.setupTrueFocus() 136 | this.update() 137 | this.bindHandlers() 138 | this.addClipPath() 139 | } 140 | addClipPath() { 141 | const SVGNS = 'http://www.w3.org/2000/svg' 142 | 143 | const SVG = document.createElementNS(SVGNS, 'svg') 144 | const DEFS = document.createElementNS(SVGNS, 'defs') 145 | const CLIP = document.createElementNS(SVGNS, 'clipPath') 146 | CLIP.setAttributeNS(null, 'clipPathUnits', 'objectBoundingBox') 147 | CLIP.setAttributeNS(null, 'id', 'nft-clip-path') 148 | CLIP.setAttributeNS(null, 'transform', 'scale(0.005 0.005319148936170213)') 149 | 150 | const PATH = document.createElementNS(SVGNS, 'path') 151 | PATH.setAttributeNS( 152 | null, 153 | 'd', 154 | 'M193.248 69.51C185.95 54.1634 177.44 39.4234 167.798 25.43L164.688 20.96C160.859 15.4049 155.841 10.7724 149.998 7.3994C144.155 4.02636 137.633 1.99743 130.908 1.46004L125.448 1.02004C108.508 -0.340012 91.4873 -0.340012 74.5479 1.02004L69.0879 1.46004C62.3625 1.99743 55.8413 4.02636 49.9981 7.3994C44.155 10.7724 39.1367 15.4049 35.3079 20.96L32.1979 25.47C22.5561 39.4634 14.0458 54.2034 6.74789 69.55L4.39789 74.49C1.50233 80.5829 0 87.2441 0 93.99C0 100.736 1.50233 107.397 4.39789 113.49L6.74789 118.43C14.0458 133.777 22.5561 148.517 32.1979 162.51L35.3079 167.02C39.1367 172.575 44.155 177.208 49.9981 180.581C55.8413 183.954 62.3625 185.983 69.0879 186.52L74.5479 186.96C91.4873 188.32 108.508 188.32 125.448 186.96L130.908 186.52C137.638 185.976 144.163 183.938 150.006 180.554C155.85 177.17 160.865 172.526 164.688 166.96L167.798 162.45C177.44 148.457 185.95 133.717 193.248 118.37L195.598 113.43C198.493 107.337 199.996 100.676 199.996 93.93C199.996 87.1841 198.493 80.5229 195.598 74.43L193.248 69.51Z' 155 | ) 156 | 157 | CLIP.appendChild(PATH) 158 | DEFS.appendChild(CLIP) 159 | SVG.appendChild(DEFS) 160 | 161 | document.body.appendChild(SVG) 162 | } 163 | 164 | setupTrueFocus() { 165 | const NAV = this.nav 166 | // Nav doesn't require a MutationObserver because it's always there 167 | NAV.setAttribute(ATTR_MAP.NAV, true) 168 | 169 | // Observe the entire app container in case we switch tabs in Twitter 170 | const targetNode = this.container 171 | 172 | // Options for the observer (which mutations to observe) 173 | // Watch all changes 174 | const config = { attributes: true, childList: true, subtree: true } 175 | 176 | const intenseFocus = (composer) => (e) => { 177 | const unFocus = (event) => { 178 | const modal = document.querySelector(EL_MAP.MODAL) 179 | const emojiModal = document.querySelector(EL_MAP.EMOJI_PICKER) 180 | 181 | if ( 182 | composer.contains(event.target) || 183 | // Assumption that if a modal is available in the DOM, we aren't done 184 | modal || 185 | // Assumption that if you can see the emoji category picker, the emoji modal is open 186 | emojiModal || 187 | // User hits the remove Poll button? 188 | event.target.closest(EL_MAP.POLL_CONTAINER) !== null 189 | ) { 190 | return 191 | } 192 | // Show all the things again and unfocus 193 | window.removeEventListener('click', unFocus) 194 | // SHOW THINGS HERE 195 | if (this.nav) this.nav.style.visibility = 'visible' 196 | if (document.querySelector(EL_MAP.SIDE_BAR)) { 197 | document.querySelector(EL_MAP.SIDE_BAR).style.visibility = 'visible' 198 | } 199 | 200 | if (document.querySelector(EL_MAP.TIMELINE)) { 201 | document.querySelector(EL_MAP.TIMELINE).style.visibility = 'visible' 202 | } 203 | document.querySelector(EL_MAP.COLUMN).style.position = 'static' 204 | } 205 | 206 | // Only run this if true focus is on 207 | 208 | if (this.composeFocus) { 209 | this.nav.style.visibility = 210 | document.querySelector(EL_MAP.SIDE_BAR).style.visibility = 211 | document.querySelector(EL_MAP.TIMELINE).style.visibility = 212 | 'hidden' 213 | document.querySelector(EL_MAP.COLUMN).style.position = 'fixed' 214 | requestAnimationFrame(() => { 215 | window.addEventListener('click', unFocus) 216 | }) 217 | } 218 | } 219 | 220 | const callback = (mutationsList, override = false) => { 221 | if ((mutationsList.length && this.composeFocus) || override) { 222 | const COL = document.querySelector(EL_MAP.COLUMN) 223 | if (!COL || COL === null) return 224 | const composer = [...COL.querySelector('div').children][1] 225 | if ( 226 | composer && 227 | !composer.hasAttribute(ATTR_MAP.COMPOSER) && 228 | composer.querySelector(EL_MAP.COMPOSER_AREA) !== null 229 | ) { 230 | composer.setAttribute(ATTR_MAP.COMPOSER, true) 231 | } else if ( 232 | composer && 233 | composer.hasAttribute(ATTR_MAP.COMPOSER) && 234 | !composer.hasAttribute(ATTR_MAP.BOUND) 235 | ) { 236 | NAV.style.visibility = 'visible' 237 | composer.setAttribute(ATTR_MAP.BOUND, true) 238 | composer.previousElementSibling.setAttribute( 239 | ATTR_MAP.PAGE_HEADER, 240 | true 241 | ) 242 | this.headerBar = composer ? composer.nextElementSibling : null 243 | this.timeline = this.headerBar 244 | ? this.headerBar.nextElementSibling 245 | : null 246 | if (this.headerBar && !this.headerBar.hasAttribute(ATTR_MAP.HEADER)) { 247 | this.headerBar.setAttribute(ATTR_MAP.HEADER, true) 248 | } 249 | 250 | if (this.timeline && !this.timeline.hasAttribute(ATTR_MAP.TIMELINE)) { 251 | this.timeline.setAttribute(ATTR_MAP.TIMELINE, true) 252 | } 253 | composer.addEventListener('click', intenseFocus(composer)) 254 | } 255 | } 256 | const avatars = [ 257 | ...document.querySelectorAll(EL_MAP.AVATAR), 258 | ...document.querySelectorAll(EL_MAP.USER_CELL_AVATAR), 259 | ...document.querySelectorAll(EL_MAP.FEED_AVATAR), 260 | ...document.querySelectorAll(EL_MAP.MESSAGE_ENTRY_AVATAR), 261 | ...document.querySelectorAll(EL_MAP.NOTIFICATION_AVATAR), 262 | ...document.querySelectorAll(EL_MAP.CONVERSATION_AVATAR), 263 | ...document.querySelectorAll(EL_MAP.ACCOUNT_SWITCHER_AVATAR), 264 | ...document.querySelectorAll(EL_MAP.COMPOSER_AVATAR), 265 | ...document.querySelectorAll(EL_MAP.LAYERS_AVATAR), 266 | ] 267 | if ( 268 | (mutationsList.length && 269 | (!this.avatars || avatars.length !== this.avatars.length)) || 270 | override 271 | ) { 272 | this.avatars = avatars 273 | this.avatars.forEach((img) => img.setAttribute(ATTR_MAP.AVATAR, true)) 274 | } 275 | } 276 | 277 | // Create an observer instance linked to the callback function 278 | const observer = new MutationObserver((list) => 279 | requestAnimationFrame(() => callback(list)) 280 | ) 281 | // Start observing the target node for configured mutations 282 | observer.observe(targetNode, config) 283 | } 284 | // A method for setting the dimming coefficient. 285 | // Setting to 0 means (0 * {{Dimmed sides value}}) + dimmed opacity 286 | focus() { 287 | this.focussing = true 288 | document.body.style.setProperty(VAR_MAP.DIMMED, 0) 289 | } 290 | // When we unfocus set the coefficient back to 1 291 | unfocus() { 292 | if (this.removeFocusTimeout) clearTimeout(this.removeFocusTimeout) 293 | if (!this.updating) { 294 | this.focussing = false 295 | document.body.style.setProperty(VAR_MAP.DIMMED, 1) 296 | } 297 | } 298 | handleFocussing() { 299 | if (!this.focussing) this.focus() 300 | if (this.removeFocusTimeout) clearTimeout(this.removeFocusTimeout) 301 | this.removeFocusTimeout = setTimeout( 302 | this.unfocus.bind(this), 303 | this.removeDelay 304 | ) 305 | this.updating = false 306 | } 307 | handleScroll() { 308 | const handleFocussing = this.handleFocussing 309 | // Only do anything on scroll when not perma-dimmed and not updating 310 | if (this.updating || this.dimmed) return 311 | else { 312 | this.updating = true 313 | requestAnimationFrame(handleFocussing.bind(this)) 314 | } 315 | } 316 | // Send through an update to process variables from the popup 317 | // The callback is used to update class references to the popup 318 | // Especially useful for true focus mode 319 | update() { 320 | updateBody(({ dimmed, fadeBack, compose }) => { 321 | this.dimmed = dimmed 322 | this.removeDelay = fadeBack 323 | this.composeFocus = compose 324 | }) 325 | } 326 | // 1. Hook up the scroll handler for dimming the sides on scroll 327 | // 2. Hook into when storage values change in the popup 328 | bindHandlers() { 329 | const handleScroll = this.handleScroll 330 | chrome.storage.onChanged.addListener(this.update) 331 | window.addEventListener('scroll', handleScroll.bind(this)) 332 | } 333 | } 334 | 335 | // Call once before initiating so that we don't get FOUC effect 336 | // when CSS variables change. 337 | // This uses variables from within Chrome Storage 338 | updateBody() 339 | const initiate = () => { 340 | if ( 341 | !document.querySelector(EL_MAP.NAV_CONTAINER) || 342 | !document.querySelector(EL_MAP.SIDE_BAR) 343 | ) { 344 | requestAnimationFrame(initiate) 345 | } else { 346 | // Once the nav and sidebar are available, initialize focussed Twitter 347 | new FocussedTwitter() 348 | } 349 | } 350 | initiate() 351 | --------------------------------------------------------------------------------