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