├── .idea ├── .gitignore ├── diconx.iml └── modules.xml ├── LICENSE ├── README.md ├── content.js ├── ext-logo.png ├── index.css ├── manifest.json ├── popup.html └── popup.js /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | /vcs.xml 7 | -------------------------------------------------------------------------------- /.idea/diconx.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Abhinav Singhal 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # disconX 2 | 3 | ## About 4 | 5 | Chrome extension to Unfollow people who don't follow back and Add them to a private list of your choice 6 | 7 | --- 8 | 9 | ## [License](./License) 10 | 11 | ⚖️ MIT License 12 | 13 | Copyright (c) [Abhinav Singhal](https://twitter.com/umunBeing)/2024 14 | 15 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | var previousHeight = 0; 2 | var followers = [] 3 | 4 | function getDataFromFollowers(_followers) { 5 | const data = { 6 | total: 0, 7 | followsBack: 0, 8 | followBackUsers: [], 9 | doesntFollowBack: 0, 10 | doesntFollowBackUsers: [] 11 | } 12 | Object.keys(_followers).forEach(key => { 13 | const user = _followers[key]; 14 | // console.log(user?.name, key, user?.followsBack) 15 | data.total ++; 16 | if(user?.followsBack) { 17 | data.followsBack++; 18 | data.followBackUsers[user?.handle] = user; 19 | } else { 20 | data.doesntFollowBack++; 21 | data.doesntFollowBackUsers[user?.handle] = user; 22 | } 23 | }) 24 | return data; 25 | } 26 | 27 | function parseUsers() { 28 | const userDivs = document.querySelectorAll('[aria-label="Timeline: Following"] div[tabindex="0"][role="button"]'); 29 | // console.log("Users length", userDivs?.length) 30 | for (var i = 0; i < userDivs?.length; i += 2) { 31 | // console.log("Whole Div", userDivs[i]) 32 | const userDiv = userDivs[i]; 33 | if (userDiv) { 34 | const name = userDiv.querySelector('a[role=link] div[dir="ltr"] span span')?.innerHTML 35 | const followsHTML = userDiv.querySelector('div[data-testid="userFollowIndicator"] span')?.innerHTML 36 | const userHandle = userDiv.querySelector('a[tabindex="-1"] span')?.innerHTML; 37 | if (!followers[userHandle]) { 38 | followers[userHandle] = { 39 | handle: userHandle, 40 | name: name, 41 | followsBack: followsHTML == "Follows you" 42 | } 43 | } 44 | } 45 | // console.log(userDivs[i].querySelector('a[tabindex="-1"] span')?.innerHTML) 46 | // console.log("Follow Button", userDivs[i+1]) 47 | } 48 | 49 | // After processing all followers 50 | chrome.runtime.sendMessage({ message: "followersUpdated", data: JSON.stringify(getDataFromFollowers(followers)) }, (response) => { 51 | // ... (optional: handle response) 52 | }); 53 | } 54 | 55 | 56 | function slowScroll(startHeight, targetHeight, duration = 500) { 57 | const distanceToScroll = targetHeight - startHeight; 58 | const scrollAmountPerInterval = distanceToScroll / (duration / 10); // Assuming 10ms interval 59 | let currentHeight = startHeight; 60 | 61 | const scrollInterval = setInterval(() => { 62 | currentHeight += scrollAmountPerInterval; 63 | window.scrollBy(0, Math.round(scrollAmountPerInterval)); // Round for smoother effect 64 | parseUsers(); 65 | if (currentHeight >= targetHeight) { 66 | clearInterval(scrollInterval); 67 | window.scrollTo(0, targetHeight); // Ensure final position 68 | } 69 | }, 100); // Adjust interval for desired scroll speed (lower for slower) 70 | } 71 | 72 | const scrollToBottom = () => { 73 | 74 | if (previousHeight == document.documentElement.scrollHeight) { 75 | previousHeight = 0; 76 | const data = getDataFromFollowers(followers); 77 | // console.log("Total Following", Object.keys(followers)?.length) 78 | console.log("🔥 Final Data 🔥") 79 | console.log("Total Following: ", data.total) 80 | console.log("Following Back: ", data.followsBack) 81 | console.log("Not Following Back: ", data.doesntFollowBack) 82 | 83 | chrome.runtime.sendMessage({ message: "analysisComplete", data: JSON.stringify(data) }, (response) => { 84 | // ... (optional: handle response) 85 | }); 86 | } else { 87 | // console.log("Go More") 88 | // window.scrollTo(previousHeight, document.documentElement.scrollHeight); 89 | slowScroll(previousHeight, document.documentElement.scrollHeight); 90 | previousHeight = document.documentElement.scrollHeight; 91 | setTimeout(scrollToBottom, 2000) 92 | } 93 | }; 94 | 95 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 96 | if (request["action"] === "scrollToBottom") { 97 | scrollToBottom(); 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /ext-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAbhinav/disconX/b796f91c31316b68127f60cd235a1f16f155889c/ext-logo.png -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | body{ 7 | font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; 8 | padding: 1rem; 9 | font-size: 1rem; 10 | width: 600px; 11 | } 12 | 13 | :root{ 14 | --primary-color:rgb(0, 190, 219); 15 | --secondary-color:#df6e12; 16 | --warning-color:yellow; 17 | --danger-color:red; 18 | --primary-font:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; 19 | --primary-radius:.2rem 20 | } 21 | 22 | /* General CSS */ 23 | 24 | button{ 25 | background: none; 26 | padding: .5rem; 27 | outline: none; 28 | background-color: var(--primary-color); 29 | border-radius: var(--primary-radius); 30 | font-family: var(--primary-font); 31 | color: white; 32 | } 33 | 34 | button:hover{ 35 | cursor: pointer; 36 | } 37 | 38 | .header{ 39 | display: flex; 40 | align-items: center; 41 | justify-content: space-between; 42 | margin: 0 1rem; 43 | } 44 | .brand{ 45 | display: flex; 46 | align-items: center; 47 | } 48 | 49 | .header .logo{ 50 | width: 40px; 51 | height: 40px; 52 | } 53 | 54 | .header .logo img{ 55 | margin-top: 2px; 56 | width: 100%; 57 | height: 100%; 58 | border-radius: var(--primary-radius)} 59 | .btn-container{ 60 | display: flex; 61 | justify-content: center; 62 | } 63 | #message{ 64 | padding: .5rem; 65 | border: 2px dashed red; 66 | border-radius: var(--primary-radius); 67 | width: max-content; 68 | margin: 1rem auto; 69 | 70 | } 71 | #message p{ 72 | max-inline-size: 50ch; 73 | text-wrap: balance; 74 | } 75 | 76 | 77 | .followers-details{ 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | gap: 1rem; 82 | text-align: center; 83 | } 84 | 85 | .follower-count{ 86 | width: 10rem; 87 | background-color: var(--secondary-color); 88 | padding: .2rem; 89 | border-radius: var(--primary-radius); 90 | color: #FFF; 91 | } 92 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "DisconX", 4 | "version": "0.0.5", 5 | "description": "Unfollow people who don't follow back. Add them to a private list of your choice", 6 | "action": { 7 | "default_popup": "popup.html", 8 | "default_icon": { 9 | "16": "ext-logo.png", 10 | "48": "ext-logo.png", 11 | "128": "ext-logo.png" 12 | } 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "https://twitter.com/*/following" 18 | ], 19 | "js": ["content.js"] 20 | } 21 | ], 22 | "permissions": [ 23 | "activeTab", 24 | "storage" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | DisconX Popup 12 | 13 | 14 | 15 |
16 |
17 | 20 |

DisconX

21 |
22 | 23 |
24 | 27 |
28 |
29 | 30 | 31 |
32 |

33 | Do not close the extension, or move away from the tab while we analyse 34 | all the accounts you are following. 35 |

36 |
37 |
38 |
39 |

Total

40 |

0

41 |
42 |
43 |

Follows Back

44 |

0

45 |
46 |
47 |

Doesn't FollowBack

48 |

0

49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | document.getElementById("startAnalyzingBtn").addEventListener("click", () => { 2 | chrome.tabs.query({active: true, currentWindow: true}, function (tabs) { 3 | chrome.tabs.sendMessage(tabs[0].id, {action: "scrollToBottom"}, function (response) { 4 | // console.error("Message Sent") 5 | }); 6 | }); 7 | }); 8 | 9 | 10 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 11 | if (message.message === "followersUpdated") { 12 | const data = JSON.parse(message?.data); 13 | document.getElementById("followingCount").innerHTML = data?.total 14 | document.getElementById("followsBackCount").innerHTML = data?.followsBack 15 | document.getElementById("doesntFollowBackCount").innerHTML = data?.doesntFollowBack 16 | // console.error(JSON.parse(message?.data)) 17 | // console.error("Received message from web page:", message.data?.length); 18 | sendResponse({response: "Hello from extension!"}); 19 | } else if (message.message === "analysisComplete") { 20 | document.getElementById("message").innerHTML = "Analysis Complete!" 21 | sendResponse({response: "Hello from extension!"}); 22 | } 23 | return false; // Indicate the message is not handled synchronously 24 | }); 25 | --------------------------------------------------------------------------------