├── .gitignore ├── assets ├── 16.png ├── 32.png ├── 48.png ├── 256.png ├── 512.png ├── icon.png ├── example.png ├── intro-1.png ├── intro-2.png ├── intro-3.png └── intro-4.png ├── options ├── icon.png ├── options.html ├── options.js └── options.css ├── manifest.v2.json ├── manifest.json ├── LICENSE ├── update_version_and_tag.ps1 ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── README.md ├── content.js └── script.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .release/ -------------------------------------------------------------------------------- /assets/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/16.png -------------------------------------------------------------------------------- /assets/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/32.png -------------------------------------------------------------------------------- /assets/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/48.png -------------------------------------------------------------------------------- /assets/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/256.png -------------------------------------------------------------------------------- /assets/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/512.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/example.png -------------------------------------------------------------------------------- /assets/intro-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/intro-1.png -------------------------------------------------------------------------------- /assets/intro-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/intro-2.png -------------------------------------------------------------------------------- /assets/intro-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/intro-3.png -------------------------------------------------------------------------------- /assets/intro-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/intro-4.png -------------------------------------------------------------------------------- /options/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/options/icon.png -------------------------------------------------------------------------------- /manifest.v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Eight Dollars", 4 | "version": "2.0", 5 | "description": "Eight Dollars can help you tell the difference between actual verified accounts and twitter blue users.", 6 | "icons": { 7 | "16": "./assets/16.png", 8 | "32": "./assets/32.png", 9 | "48": "./assets/48.png", 10 | "256": "./assets/256.png", 11 | "512": "./assets/512.png" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": ["https://twitter.com/*", "https://mobile.twitter.com/*", "https://x.com/*", "https://mobile.x.com/*"], 16 | "js": ["content.js"] 17 | } 18 | ], 19 | "web_accessible_resources": ["script.js", "data/verified.txt"], 20 | "permissions": ["storage"], 21 | "browser_action": { 22 | "default_icon": "./assets/32.png", 23 | "default_title": "Eight-Dollars", 24 | "default_popup": "options/options.html" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Eight Dollars", 4 | "version": "2.0", 5 | "description": "Eight Dollars can help you tell the difference between actual verified accounts and twitter blue users.", 6 | "icons": { 7 | "16": "./assets/16.png", 8 | "32": "./assets/32.png", 9 | "48": "./assets/48.png", 10 | "256": "./assets/256.png", 11 | "512": "./assets/512.png" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": ["https://twitter.com/*", "https://mobile.twitter.com/*", "https://x.com/*", "https://mobile.x.com/*"], 16 | "js": ["content.js"] 17 | } 18 | ], 19 | "web_accessible_resources": [ 20 | { 21 | "resources": ["script.js", "data/verified.txt"], 22 | "matches": ["https://twitter.com/*", "https://mobile.twitter.com/*", "https://x.com/*", "https://mobile.x.com/*"] 23 | } 24 | ], 25 | "action": { 26 | "default_icon": { 27 | "16": "./assets/16.png", 28 | "32": "./assets/32.png" 29 | }, 30 | "default_title": "Eight Dollars - Options", 31 | "default_popup": "options/options.html" 32 | }, 33 | "permissions": ["storage"] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 William Seagar & Walter Lim 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 | -------------------------------------------------------------------------------- /update_version_and_tag.ps1: -------------------------------------------------------------------------------- 1 | # Prompt for version number 2 | $versionNumber = Read-Host -Prompt "Enter the version number" 3 | 4 | # Update version in manifest.json 5 | $manifestPath = "manifest.json" 6 | $manifest = Get-Content -Path $manifestPath | ConvertFrom-Json 7 | $manifest.version = $versionNumber 8 | $manifest | ConvertTo-Json -Depth 100 | Set-Content -Path $manifestPath 9 | 10 | # Check if Git is installed 11 | try { 12 | git --version | Out-Null 13 | } catch { 14 | Write-Host "Git is not installed or not in the PATH. Please install Git and try again." 15 | exit 1 16 | } 17 | 18 | # Check if the working directory is a Git repository 19 | try { 20 | git rev-parse --is-inside-work-tree | Out-Null 21 | } catch { 22 | Write-Host "The current directory is not a Git repository. Please initialize a Git repository and try again." 23 | exit 1 24 | } 25 | 26 | # Add all files to the Git staging area 27 | git add -A 28 | 29 | # Create a commit with the release number as the message 30 | $commitMessage = "Release v$versionNumber" 31 | git commit -m $commitMessage 32 | 33 | # Create a tag with the release number 34 | $tagName = "v$versionNumber" 35 | git tag $tagName 36 | 37 | Write-Host "Git commit and tag created successfully for the current release number." -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: noway, wseagar 7 | 8 | --- 9 | 10 | **IMPORTANT PLEASE READ (REMOVE THIS WHEN SUBMITTING A REPORT)** 11 | 12 | Before reporting a bug please make sure you are using the latest browser extension from the appropriate extension store. 13 | 14 | Extensions automatically update but may require a browser restart. Make sure that the issue persists after restarting your browser. 15 | 16 | **Info (please complete the following information):** 17 | - Browser [e.g. chrome, firefox] 18 | - Platform [e.g. Windows, Mac, Linux] 19 | - Extension Version [e.g. 1.3] 20 | - Location [e.g. US] (Twitter can roll out features in different regions at different times so this info is important to us) 21 | - Twitter Language (Include if using a non-English language) 22 | 23 | 24 | **What happened** 25 | Steps to reproduce the behavior: 26 | 1. Go to '...' 27 | 2. Click on '....' 28 | 3. Scroll down to '....' 29 | 4. See error 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Expected behavior** 35 | A clear and concise description of what you expected to happen. 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eight-dollars 2 | 3 | eight-dollars can help you tell the difference between actual verified accounts and twitter blue users. Just install the extension and see the difference. 4 | 5 | Maintained by [Will Seagar](https://twitter.com/willseagar), [Walter Lim](https://twitter.com/iWaltzAround), and [Ilia Sidorenko](https://twitter.com/noway421). 6 | 7 | Found this useful? [Buy us a $8 coffee here](https://www.buymeacoffee.com/eightdollars). 8 | 9 |  10 | 11 | 12 | ## Installation Instructions 13 | 14 | ### How to install on Chrome/Brave 15 | 16 | [Download it on the Chrome Web Store](https://chrome.google.com/webstore/detail/eight-dollars/fjbponfbognnefnmbffcfllkibbbobki) 17 | 18 | ### How to install on Firefox 19 | 20 | [Download it on the Firefox Browser Extensions page](https://addons.mozilla.org/en-US/firefox/addon/eightdollars/) 21 | 22 | ### How to install on Edge 23 | 24 | [Download it on the Microsoft Edge Add-ons page](https://microsoftedge.microsoft.com/addons/detail/eight-dollars/ehfacgbckjlegnlledgpkmkfbemhkknh) 25 | 26 | ### How to install on Opera 27 | 28 | 1. [Vist the Chrome Web Store page](https://chrome.google.com/webstore/detail/eight-dollars/fjbponfbognnefnmbffcfllkibbbobki) 29 | 2. Click "Add to Opera" 30 | 3. Click "Install" 31 | 32 | ### How to install on Safari 33 | 34 | Coming soon 35 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = { 2 | memeMode: false, 3 | textEnabled: true, 4 | removeBlueVerification: false, 5 | textOptions: { 6 | verifiedLabel: "Verified", 7 | twitterBlueLabel: "Paid", 8 | enableBorder: true, 9 | }, 10 | }; 11 | 12 | async function loadVerifiedUsers() { 13 | const url = chrome.runtime.getURL("data/verified.txt"); 14 | const response = await fetch(url); 15 | const text = await response.text(); 16 | return text.split("\n"); 17 | } 18 | 19 | function createSettingsDomNode(items) { 20 | const settingsDomNode = document.createElement("div"); 21 | settingsDomNode.id = "eight-dollars-settings"; 22 | settingsDomNode.style.display = "none"; 23 | settingsDomNode.innerText = JSON.stringify(items); 24 | document.body.appendChild(settingsDomNode); 25 | } 26 | 27 | function injectScript() { 28 | const s = document.createElement("script", { id: "eight-dollars" }); 29 | s.src = chrome.runtime.getURL("script.js"); 30 | s.onload = function () { 31 | this.remove(); 32 | }; 33 | document.head.appendChild(s); 34 | } 35 | 36 | function injectVerifiedUsers(verifiedUsers) { 37 | const verifiedUsersDomNode = document.createElement("div"); 38 | verifiedUsersDomNode.id = "eight-dollars-verified-users"; 39 | verifiedUsersDomNode.style.display = "none"; 40 | verifiedUsersDomNode.innerText = JSON.stringify(verifiedUsers); 41 | document.body.appendChild(verifiedUsersDomNode); 42 | } 43 | 44 | async function main() { 45 | const verifiedUsers = await loadVerifiedUsers(); 46 | 47 | if (typeof chrome !== "undefined" && chrome.storage) { 48 | chrome.storage.local.get(defaultConfig, function (items) { 49 | createSettingsDomNode(items); 50 | injectScript(); 51 | injectVerifiedUsers(verifiedUsers); 52 | }); 53 | } else { 54 | createSettingsDomNode(defaultConfig); 55 | injectScript(); 56 | injectVerifiedUsers(verifiedUsers); 57 | } 58 | } 59 | 60 | main(); 61 | -------------------------------------------------------------------------------- /options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |${TEXT_VERIFIED_LABEL}
198 | `; 199 | try { 200 | if (prependHTML !== "") { 201 | // Ideally, we wouldn't mutate the parent element, because those 2 styles won't be managed by us further on. 202 | // That is, if the `aria-label`-selected element changes, the parent styles won't be properly updated. 203 | // This approach is tolerable, because it's unlikely that a `aria-label`-selected element changes from a 204 | // "React text node(s) + React element node" sibling configuration to a "single React element node" sibling configuration. 205 | elm.style.display = "inline-flex"; 206 | elm.style.alignItems = "center"; 207 | } 208 | if (isSmall || !TEXT_ENABLED) { 209 | elm.innerHTML = `${prependHTML}${small}`; 210 | } else { 211 | elm.innerHTML = `${prependHTML}${big}`; 212 | } 213 | } catch (e) { 214 | console.error("error changing verified", e); 215 | } 216 | } 217 | 218 | function changeBlueVerified(prependHTML, elm, isSmall) { 219 | if (elm.dataset.eightDollarsStatus === "blueVerified") { 220 | // already replaced this element 221 | return; 222 | } 223 | 224 | if (REMOVE_TWITTER_BLUE_VERIFICATION) { 225 | elm.style.display = "none"; 226 | elm.setAttribute("data-eight-dollars-status", "blueVerified"); 227 | return; 228 | } 229 | 230 | const small = MEME_MODE 231 | ? `${COMIC_SANS_BLUE_DOLLAR_SVG(true, getOriginalClasses(elm))}` 232 | : `${REGULAR_BLUE_DOLLAR_SVG(true, getOriginalClasses(elm))}`; 233 | const smallInnerElement = MEME_MODE 234 | ? `${COMIC_SANS_BLUE_DOLLAR_SVG(false, getOriginalClasses(elm))}` 235 | : `${REGULAR_BLUE_DOLLAR_SVG(false, getOriginalClasses(elm))}`; 236 | const big = ` 237 | 246 | ${smallInnerElement} 247 |${TEXT_TWITTER_BLUE_LABEL}
248 | `; 249 | try { 250 | if (prependHTML !== "") { 251 | // Ideally, we wouldn't mutate the parent element, because those 2 styles won't be managed by us further on. 252 | // That is, if the `aria-label`-selected element changes, the parent styles won't be properly updated. 253 | // This approach is tolerable, because it's unlikely that a `aria-label`-selected element changes from a 254 | // "React text node(s) + React element node" sibling configuration to a "single React element node" sibling configuration. 255 | elm.style.display = "inline-flex"; 256 | elm.style.alignItems = "center"; 257 | } 258 | if (isSmall || !TEXT_ENABLED) { 259 | elm.innerHTML = `${prependHTML}${small}`; 260 | } else { 261 | elm.innerHTML = `${prependHTML}${big}`; 262 | } 263 | } catch (e) { 264 | console.error("error changing blue verified", e); 265 | } 266 | } 267 | 268 | function querySelectorAllIncludingMe(node, selector) { 269 | if (node.matches(selector)) { 270 | return [node]; 271 | } 272 | return [...node.querySelectorAll(selector)]; 273 | } 274 | 275 | const trackingTweets = new Set(); 276 | const trackingHovercards = new Set(); 277 | const trackingUsercells = new Set(); 278 | const trackingConversation = new Set(); 279 | const trackingDmDrawHeader = new Set(); 280 | const trackingUserName = new Set(); 281 | const trackingHeadings = new Set(); 282 | 283 | function collectAndTrackElements(node) { 284 | const tweets = querySelectorAllIncludingMe(node, '[data-testid="tweet"]'); 285 | for (const tweet of tweets) { 286 | trackingTweets.add(tweet); 287 | } 288 | const hovercards = querySelectorAllIncludingMe( 289 | node, 290 | '[data-testid="HoverCard"]' 291 | ); 292 | for (const hovercard of hovercards) { 293 | trackingHovercards.add(hovercard); 294 | } 295 | const usercells = querySelectorAllIncludingMe( 296 | node, 297 | '[data-testid="UserCell"]' 298 | ); 299 | for (const usercell of usercells) { 300 | trackingUsercells.add(usercell); 301 | } 302 | const conversations = querySelectorAllIncludingMe( 303 | node, 304 | '[data-testid="conversation"]' 305 | ); 306 | for (const conversation of conversations) { 307 | trackingConversation.add(conversation); 308 | } 309 | const dmDrawHeader = querySelectorAllIncludingMe( 310 | node, 311 | '[data-testid="DMDrawerHeader"]' 312 | ); 313 | for (const dm of dmDrawHeader) { 314 | trackingDmDrawHeader.add(dm); 315 | } 316 | const userName = querySelectorAllIncludingMe( 317 | node, 318 | '[data-testid="UserName"]' 319 | ); 320 | for (const name of userName) { 321 | trackingUserName.add(name); 322 | } 323 | const headings = querySelectorAllIncludingMe( 324 | node, 325 | '[data-testid="primaryColumn"] [role="heading"][aria-level="2"][dir="ltr"]' 326 | ); 327 | for (const heading of headings) { 328 | trackingHeadings.add(heading); 329 | } 330 | } 331 | 332 | function handleProfileModification(containerElement, isBig) { 333 | const ltr = containerElement.querySelectorAll('[dir="ltr"]'); 334 | 335 | const checkmarkContainer = ltr[0]; 336 | const spans = checkmarkContainer.querySelectorAll("span"); 337 | const checkmarkSpan = spans[spans.length - 1]; 338 | const protectd = checkmarkSpan.querySelectorAll('[aria-label="Protected account"]')[0] 339 | 340 | if (!checkmarkSpan || protectd) { 341 | console.error("no checkmark span found"); 342 | return; 343 | } 344 | 345 | let hasVerifiedIcon = checkmarkSpan.children.length > 0; 346 | 347 | const handleContainer = ltr[1]; 348 | const handle = handleContainer.innerText; 349 | if (!handle) { 350 | return; 351 | } 352 | const parsedHandle = handle.replace("@", "").toLowerCase(); 353 | const isLegacyVerifed = VERIFIED_USERS.has(parsedHandle); 354 | 355 | if (isLegacyVerifed) { 356 | changeVerified("", checkmarkSpan, isBig); 357 | } else if (hasVerifiedIcon) { 358 | changeBlueVerified("", checkmarkSpan, isBig); 359 | } 360 | 361 | if (DEBUG) { 362 | if (isLegacyVerifed) { 363 | containerElement.style = "border: 1px solid green;"; 364 | } else if (hasVerifiedIcon) { 365 | containerElement.style = "border: 1px solid red;"; 366 | } 367 | 368 | // add a plain text node to the tweet 369 | containerElement.appendChild( 370 | document.createTextNode( 371 | `isLegacyVerified: ${isLegacyVerifed.toString()}\n verifiedIcon: ${hasVerifiedIcon.toString()}\n` 372 | ) 373 | ); 374 | } 375 | } 376 | 377 | function waitForElm(selector) { 378 | return new Promise((resolve) => { 379 | if (document.querySelector(selector)) { 380 | return resolve(document.querySelector(selector)); 381 | } 382 | 383 | const observer = new MutationObserver((mutations) => { 384 | if (document.querySelector(selector)) { 385 | resolve(document.querySelector(selector)); 386 | observer.disconnect(); 387 | } 388 | }); 389 | 390 | observer.observe(document.body, { 391 | childList: true, 392 | subtree: true, 393 | }); 394 | }); 395 | } 396 | 397 | async function handleHeadingModification(containerElement, isBig) { 398 | const profile = await waitForElm('[data-testid="UserName"]'); 399 | if (!profile) { 400 | console.error("no profile found"); 401 | return; 402 | } 403 | const ltr = profile.querySelectorAll('[dir="ltr"]'); 404 | const handleContainer = ltr[1]; 405 | const handle = handleContainer.innerText; 406 | if (!handle) { 407 | return; 408 | } 409 | const parsedHandle = handle.replace("@", "").toLowerCase(); 410 | const isLegacyVerifed = VERIFIED_USERS.has(parsedHandle); 411 | 412 | const spans = containerElement.querySelectorAll("span"); 413 | const checkmarkSpan = spans[spans.length - 1]; 414 | const protectd = checkmarkSpan.querySelectorAll('[aria-label="Protected account"]')[0] 415 | 416 | if (!checkmarkSpan || protectd) { 417 | console.error("no checkmark span found"); 418 | return; 419 | } 420 | 421 | let hasVerifiedIcon = checkmarkSpan.children.length > 0; 422 | 423 | if (isLegacyVerifed) { 424 | changeVerified("", checkmarkSpan, isBig); 425 | } else if (hasVerifiedIcon) { 426 | changeBlueVerified("", checkmarkSpan, isBig); 427 | } 428 | 429 | if (DEBUG) { 430 | if (isLegacyVerifed) { 431 | containerElement.style = "border: 1px solid green;"; 432 | } else if (hasVerifiedIcon) { 433 | containerElement.style = "border: 1px solid red;"; 434 | } 435 | 436 | // add a plain text node to the tweet 437 | containerElement.appendChild( 438 | document.createTextNode( 439 | `isLegacyVerified: ${isLegacyVerifed.toString()}\n verifiedIcon: ${hasVerifiedIcon.toString()}\n` 440 | ) 441 | ); 442 | } 443 | } 444 | 445 | function handleModification( 446 | containerElement, 447 | checkmarkIndex, 448 | handleIndex, 449 | isBig 450 | ) { 451 | const ltr = containerElement.querySelectorAll('[dir="ltr"]'); 452 | const checkmarkContainer = ltr[checkmarkIndex]; 453 | const checkmarkSpan = checkmarkContainer.children?.[0]; 454 | const protectd = checkmarkSpan.querySelectorAll('[aria-label="Protected account"]')[0] 455 | 456 | if (!checkmarkSpan || protectd) { 457 | return; 458 | } 459 | 460 | let hasVerifiedIcon = checkmarkSpan.children.length > 0; 461 | 462 | const handleContainer = ltr[handleIndex]; 463 | const handle = handleContainer.innerText; 464 | if (!handle) { 465 | return; 466 | } 467 | const parsedHandle = handle.replace("@", "").toLowerCase(); 468 | const isLegacyVerifed = VERIFIED_USERS.has(parsedHandle); 469 | 470 | if (isLegacyVerifed) { 471 | changeVerified("", checkmarkSpan, isBig); 472 | } else if (hasVerifiedIcon) { 473 | changeBlueVerified("", checkmarkSpan, isBig); 474 | } 475 | 476 | if (DEBUG) { 477 | if (isLegacyVerifed) { 478 | containerElement.style = "border: 1px solid green;"; 479 | } else if (hasVerifiedIcon) { 480 | containerElement.style = "border: 1px solid red;"; 481 | } 482 | 483 | // add a plain text node to the tweet 484 | containerElement.appendChild( 485 | document.createTextNode( 486 | `isLegacyVerified: ${isLegacyVerifed.toString()}\n verifiedIcon: ${hasVerifiedIcon.toString()}\n` 487 | ) 488 | ); 489 | } 490 | } 491 | 492 | async function main() { 493 | const observer = new MutationObserver(function (mutations, observer) { 494 | try { 495 | for (const mutation of mutations) { 496 | if (mutation.type === "attributes") { 497 | collectAndTrackElements(mutation.target); 498 | } 499 | for (const node of mutation.addedNodes) { 500 | if (node.nodeType === 1) { 501 | collectAndTrackElements(node); 502 | } 503 | } 504 | } 505 | 506 | for (const tweet of trackingTweets) { 507 | if (!tweet.dataset.processed) { 508 | // includes subtweets 509 | const userNameContainer = tweet.querySelectorAll( 510 | '[data-testid="User-Name"]' 511 | ); 512 | for (const container of userNameContainer) { 513 | handleModification(container, 1, 2, false); 514 | } 515 | 516 | tweet.dataset.processed = true; 517 | } 518 | } 519 | for (const hovercard of trackingHovercards) { 520 | if (!hovercard.dataset.processed) { 521 | handleModification(hovercard, 2, 3, false); 522 | 523 | hovercard.dataset.processed = true; 524 | } 525 | } 526 | for (const usercell of trackingUsercells) { 527 | if (!usercell.dataset.processed) { 528 | handleModification(usercell, 1, 2, true); 529 | usercell.dataset.processed = true; 530 | } 531 | } 532 | for (const conversation of trackingConversation) { 533 | if (!conversation.dataset.processed) { 534 | handleModification(conversation, 1, 2, true); 535 | 536 | conversation.dataset.processed = true; 537 | } 538 | } 539 | for (const dm of trackingDmDrawHeader) { 540 | if (!dm.dataset.processed) { 541 | //handleModification(dm, 2, 2, true); 542 | 543 | dm.dataset.processed = true; 544 | } 545 | } 546 | for (const name of trackingUserName) { 547 | if (!name.dataset.processed) { 548 | handleProfileModification(name); 549 | 550 | name.dataset.processed = true; 551 | } 552 | } 553 | for (const heading of trackingHeadings) { 554 | if (!heading.dataset.processed) { 555 | handleHeadingModification(heading); 556 | 557 | heading.dataset.processed = true; 558 | } 559 | } 560 | } catch (error) { 561 | console.log("uncaught mutation error", error); 562 | } 563 | }); 564 | 565 | observer.observe(document, { 566 | childList: true, 567 | subtree: true, 568 | attributes: true, 569 | }); 570 | } 571 | 572 | main(); 573 | --------------------------------------------------------------------------------