x-twitter-add-to-list-button

1-click "add user to [xyz] list" button next to usernames while scrolling your x (twitter) feed (be sure to edit the variable "lists")

  1. // ==UserScript==
  2. // @name x-twitter-add-to-list-button
  3. // @name:ja x-twitter-add-to-list-button
  4. // @namespace x-twitter
  5. // @version 0.2.2
  6. // @description 1-click "add user to [xyz] list" button next to usernames while scrolling your x (twitter) feed (be sure to edit the variable "lists")
  7. // @description:ja リストにワンクリックで追加するボタンを表示します(変数"lists"を必ず編集してください)
  8. // @author fuwawascoco
  9. // @match https://twitter.com/*
  10. // @match https://mobile.twitter.com/*
  11. // @match https://x.com/*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  13. // @grant none
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. const lists = ['waitlist', 'illustrators', 'animators']; // be sure to change to the NAME of your lists (not IDs)
  21. const checkInterval = 512; // ms
  22. const tryInterval = 64; // ms
  23. const maxRetries = 100; // maximum number of retries to avoid infinite loops
  24.  
  25. function createNotification(message) {
  26. const notification = document.createElement('div');
  27. notification.style.position = 'fixed';
  28. notification.style.bottom = '10px';
  29. notification.style.right = '10px';
  30. notification.style.padding = '10px';
  31. notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
  32. notification.style.color = 'white';
  33. notification.style.borderRadius = '5px';
  34. notification.style.zIndex = '10000';
  35. notification.textContent = message;
  36. document.body.appendChild(notification);
  37. setTimeout(() => {
  38. document.body.removeChild(notification);
  39. }, 5000);
  40. }
  41.  
  42. function retryUntilSuccess(newTab, selector, innerText, callback) {
  43. let attempts = 0;
  44. const intervalID = setInterval(() => {
  45. let element;
  46. const elements = newTab.document.querySelectorAll(selector);
  47. if (innerText != '') {
  48. element = Array.from(elements).find(el => el.textContent.trim() === innerText);
  49. } else {
  50. element = elements[0];
  51. }
  52. if (element && element.offsetParent != null) { // Check if element is visible
  53. clearInterval(intervalID);
  54. callback(element);
  55. }
  56. if (++attempts >= maxRetries) {
  57. clearInterval(intervalID);
  58. console.error(`Failed to find element: ${selector} with innerText: ${innerText}`);
  59. createNotification(`"${innerText}" was not found, please edit the script to update the variable "lists" with your own names.`);
  60. newTab.close();
  61. }
  62. }, tryInterval);
  63. }
  64.  
  65. function onClick(userProfile, list) {
  66. const newTab = open(userProfile);
  67. newTab.addEventListener('beforeunload', () => clearInterval(intervalID));
  68.  
  69. retryUntilSuccess(newTab, 'button[aria-label="More"][data-testid="userActions"]', '', moreButton => {
  70. moreButton.click();
  71. retryUntilSuccess(newTab, 'a[href="/i/lists/add_member"][role="menuitem"]', '', listButton => {
  72. listButton.click();
  73. retryUntilSuccess(newTab, '[aria-modal="true"]', '', modal => {
  74. let attempts = 0;
  75. const intervalID = setInterval(() => {
  76. const listSpan = Array.from(modal.getElementsByTagName('span')).find(span => span.textContent === list);
  77. if (listSpan) {
  78. clearInterval(intervalID);
  79. const checkbox = listSpan.closest('[role="checkbox"]');
  80. if (checkbox && checkbox.getAttribute('aria-checked') === 'false') {
  81. checkbox.click();
  82. }
  83. } else if (++attempts >= maxRetries) {
  84. clearInterval(intervalID);
  85. createNotification(`"${list}" was not found, please edit the script to update the variable "lists" with your own names.`);
  86. newTab.close();
  87. }
  88. }, tryInterval);
  89. });
  90. });
  91. });
  92. }
  93.  
  94. function createButton(userProfile, list) {
  95. const button = document.createElement('button');
  96. button.style.fontSize = '90%';
  97. button.style.margin = '0 0.25em';
  98. button.textContent = list;
  99. button.addEventListener('click', () => onClick(userProfile, list));
  100. return button;
  101. }
  102.  
  103. function createButtonContainer(userProfile) {
  104. const container = document.createElement('div');
  105. container.style.position = 'relative';
  106. container.style.left = '2%';
  107. container.style.opacity = 0.5;
  108. lists.forEach(list => container.appendChild(createButton(userProfile, list)));
  109. container.classList.add('listButtons');
  110. return container;
  111. }
  112.  
  113. function addButtons() {
  114. const nodes = document.querySelectorAll('[data-testid="User-Name"]:not(:has(.listButtons))');
  115. nodes.forEach(node => {
  116. const userProfile = node.querySelector('a')?.href || '';
  117. if (userProfile) {
  118. node.appendChild(createButtonContainer(userProfile));
  119. }
  120. });
  121. }
  122.  
  123. setInterval(addButtons, checkInterval);
  124. })();