twitter-add-to-list-button

adds "add to list" buttons (edit the variable "listNames"!)

  1. // ==UserScript==
  2. // @name twitter-add-to-list-button
  3. // @name:ja twitter-add-to-list-button
  4. // @namespace NegUtl
  5. // @version 0.2.2
  6. // @description adds "add to list" buttons (edit the variable "listNames"!)
  7. // @description:ja リストにワンクリックで追加するボタンを表示します(変数"listNames"を必ず編集してください)
  8. // @author NegUtl
  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.  
  21. // be sure to change to the name of your lists (not IDs)
  22. const listNames = ['list1', 'list2', 'list3'];
  23.  
  24. const addButtonsInterval = 512; // ms
  25. const tryClickInterval = 32; // ms
  26.  
  27.  
  28. function onClick(userProfileURL, listName) {
  29.  
  30. const newTab = open(userProfileURL);
  31.  
  32. const steps = [
  33. () => { // open meatballs menu
  34. const target = newTab.document.querySelector('[data-testid="userActions"]');
  35. if (target === null) return false;
  36. target.click();
  37. return true;
  38. },
  39. () => { // click "Add/remove @xxxx from Lists"
  40. const target = newTab.document.querySelector('[href="/i/lists/add_member"]');
  41. if (target === null) return false;
  42. target.click();
  43. return true;
  44. },
  45. () => { // click the list
  46. const listCells = newTab.document.querySelectorAll('div[data-testid="listCell"]');
  47. if (listCells.length == 0) return false;
  48. for (const listCell of listCells) {
  49. const labelSpan = listCell.querySelector('span');
  50. if (labelSpan.textContent !== listName) continue;
  51. if (listCell.ariaChecked === 'true') {
  52. newTab.alert(`The user is already in "${listName}"`);
  53. newTab.close();
  54. return true;
  55. }
  56. listCell.click();
  57. return true;
  58. }
  59. newTab.alert(`"${listName}" was not found`);
  60. newTab.close();
  61. return true;
  62. },
  63. () => { // click "Save" button
  64. if (newTab.closed === true) return true;
  65. let modal = newTab.document.querySelector('div[aria-modal="true"]');
  66. if (modal === null) modal = newTab.document.querySelector('main');
  67. const target = modal.querySelectorAll('button')[1];
  68. target.click();
  69. return true;
  70. },
  71. () => { // close the tab
  72. if (newTab.closed !== true) newTab.close();
  73. return true;
  74. }
  75. ];
  76.  
  77. let current_step = 0;
  78.  
  79. const intervalID = setInterval(() => {
  80. if (steps[current_step]()) ++current_step;
  81. if (current_step === steps.length) clearInterval(intervalID);
  82. }, tryClickInterval);
  83. }
  84.  
  85.  
  86. function ListButton(userProfile, listName) {
  87. const button = document.createElement('button');
  88. const styles = {
  89. fontSize: '82%',
  90. margin: '0 0.25em',
  91. };
  92. for (const prop in styles) {
  93. button.style[prop] = styles[prop];
  94. }
  95. button.textContent = listName;
  96. button.addEventListener('click', onClick.bind(null, userProfile, listName));
  97. return button;
  98. }
  99.  
  100.  
  101. function ListButtons(userProfile) {
  102. const buttons = document.createElement('div');
  103. const styles = {
  104. position: 'absolute',
  105. left: '50%',
  106. transform: 'translateX(-50%)',
  107. };
  108. for (const prop in styles) {
  109. buttons.style[prop] = styles[prop];
  110. }
  111. for (const listName of listNames) {
  112. buttons.appendChild(ListButton(userProfile, listName));
  113. }
  114. buttons.classList.add('listButtons');
  115. return buttons;
  116. }
  117.  
  118.  
  119. function isMyAccount(node) { // check if the node is one of the "Change Account" item
  120. const parent = node.parentNode;
  121. if (parent.nodeName === 'DIV' && parent.dataset.testid === 'HoverCard') return true;
  122. if (parent.nodeName === 'BODY') return false;
  123. return isMyAccount(parent);
  124. }
  125.  
  126.  
  127. function getUserProfileURL(node) {
  128. for (const child of node.children) {
  129. if (child.nodeName === 'A') return child.href;
  130. const result = getUserProfileURL(child);
  131. if (result) return result;
  132. }
  133. return false;
  134. }
  135.  
  136.  
  137. function addButtons() {
  138. const selector = '[data-testid="UserCell"]:not(:has(.listButtons))';
  139. const nodes = document.querySelectorAll(selector);
  140. for (const node of nodes) {
  141. if (isMyAccount(node)) continue;
  142. const userProfileURL = getUserProfileURL(node);
  143. node.appendChild(ListButtons(userProfileURL));
  144. }
  145. }
  146.  
  147.  
  148. setInterval(addButtons, addButtonsInterval)
  149.  
  150.  
  151. })();