FB全部留言小帮手

让您更快打开全部留言

  1. // ==UserScript==
  2. // @name Facebook All Comments Helper
  3. // @name:zh-TW FB全部留言小幫手
  4. // @name:zh-CN FB全部留言小帮手
  5. // @namespace https://github.com/Xuitty/FBallComments
  6. // @version 2.4
  7. // @description Easy way to show all comments.
  8. // @description:zh-tw 讓您更快打開全部留言
  9. // @description:zh-cn 让您更快打开全部留言
  10. // @author Xuitty
  11. // @match https://www.facebook.com/*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=facebook.com
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM.registerMenuCommand
  17. // @grant GM.setValue
  18. // @grant GM.getValue
  19. // @run-at document-end
  20. // @license MIT
  21. // ==/UserScript==
  22.  
  23. /**
  24. * string array for detecting the menu button
  25. */
  26.  
  27. const langs = {
  28. de: ["Relevanteste", "Top-Kommentare", "Am zutreffendsten", "Neueste zuerst", "Neueste", "Alle Kommentare"],
  29. en: ["Top comments", "Most relevant", "Most applicable", "Most recent", "Newest", "All comments"],
  30. es: ["Comentarios destacados", "Más relevantes", "Más pertinentes", "Más recientes", "Más recientes", "Todos los comentarios"],
  31. hu: ["A legfontosabb hozzászólások", "A legrelevánsabbak", "A témához leginkább illők", "A legújabbak", "A legutóbbiak", "Az összes hozzászólás"],
  32. ja: ["トップコメント", "関連度の高い順", "最も適切", "新しい順", "新しい順", "すべてのコメント"],
  33. ko: ["관련성 높은 댓글", "참여도 높은 댓글", "적합성 높은 순", "최신순", "날짜 내림차순", "모든 댓글"],
  34. fr: ["Plus pertinents", "Les meilleurs commentaires", "Les plus pertinents", "Plus récents", "Les plus récents", "Tous les commentaires"],
  35. sk: ["Top komentáre", "Najrelevantnejšie", "Najvhodnejšie", "Najnovšie", "Najnovšie", "Všetky komentáre"],
  36. sl: ["Najbolj priljubljeni komentarji", "Najustreznejši", "Najustreznejše", "Najnovejši", "Najnovejši", "Vsi komentarji"],
  37. "zh-Hans": ["热门评论", "最相关", "最合适", "从新到旧", "最新", "所有评论"],
  38. "zh-Hant": ["最熱門留言", "最相關", "最相關", "最新", "由新到舊", "所有留言"],
  39. };
  40.  
  41. /**
  42. * string array for notification
  43. */
  44.  
  45. const notificationStr = {
  46. de: ["Wechseln zu allen Kommentaren!", "Wechseln zu den neuesten Kommentaren!"],
  47. en: ["Switch to All Comments!", "Switch to Latest Comments!"],
  48. es: ["Cambiar a todos los comentarios!", "Cambiar a los comentarios más recientes!"],
  49. hu: ["Váltás az összes hozzászólásra!", "Váltás a legújabb hozzászólásokra!"],
  50. ja: ["すべてのコメントに切り替え!", "最新のコメントに切り替え!"],
  51. ko: ["모든 댓글로 전환!", "최신 댓글로 전환!"],
  52. fr: ["Passer à tous les commentaires!", "Passer aux commentaires les plus récents!"],
  53. sk: ["Prepnúť na všetky komentáre!", "Prepnúť na najnovšie komentáre!"],
  54. sl: ["Preklopite na vse komentarje!", "Preklopite na najnovejše komentarje!"],
  55. "zh-Hant": ["切換到所有留言!", "切換到最新留言!"],
  56. "zh-Hans": ["切换到所有评论!", "切换到最新评论!"],
  57. };
  58.  
  59. /**
  60. * string array for settings
  61. */
  62.  
  63. const settingsStr = {
  64. "zh-Hant": {
  65. settings: "設定",
  66. autoDetect: "自動切換為全部/最新留言",
  67. notifyEnabled: "操作通知",
  68. isAll: "全部留言/最新留言",
  69. isScroll: "是否開啟捲動",
  70. scrollBehavior: "捲動特效",
  71. hideSettings: "隱藏選單",
  72. smooth: "平滑",
  73. auto: "無",
  74. openMenu: "開啟選單",
  75. needRefresh: "需要重新整理頁面應用自動切換",
  76. },
  77. "zh-Hans": {
  78. settings: "设置",
  79. autoDetect: "自动切换为全部/最新评论",
  80. notifyEnabled: "操作通知",
  81. isAll: "全部评论/最新评论",
  82. isScroll: "是否开启滚动",
  83. scrollBehavior: "滚动特效",
  84. hideSettings: "隐藏菜单",
  85. smooth: "平滑",
  86. auto: "无",
  87. openMenu: "打开菜单",
  88. needRefresh: "需要重新刷新页面应用自动切换",
  89. },
  90. en: {
  91. settings: "Settings",
  92. autoDetect: "Auto detect all/latest comments",
  93. notifyEnabled: "Notify after action",
  94. isAll: "All comments/Latest comments",
  95. isScroll: "Enable scroll effect",
  96. scrollBehavior: "Scroll behavior",
  97. hideSettings: "Hide settings",
  98. smooth: "Smooth",
  99. auto: "None",
  100. openMenu: "Open menu",
  101. needRefresh: "Need to refresh the page to apply auto detection",
  102. },
  103. de: {
  104. settings: "Einstellungen",
  105. autoDetect: "Automatisch alle/neuesten Kommentare erkennen",
  106. notifyEnabled: "Nach Aktion benachrichtigen",
  107. isAll: "Alle Kommentare/Neueste Kommentare",
  108. isScroll: "Bildlauf aktivieren",
  109. scrollBehavior: "Bildlaufverhalten",
  110. hideSettings: "Einstellungen ausblenden",
  111. smooth: "Sanft",
  112. auto: "Keine",
  113. openMenu: "Menü öffnen",
  114. needRefresh: "Die Seite muss aktualisiert werden, um die automatische Erkennung anzuwenden",
  115. },
  116. es: {
  117. settings: "Ajustes",
  118. autoDetect: "Detectar automáticamente todos/los comentarios más recientes",
  119. notifyEnabled: "Notificar después de la acción",
  120. isAll: "Todos los comentarios/Comentarios más recientes",
  121. isScroll: "Activar efecto de desplazamiento",
  122. scrollBehavior: "Comportamiento de desplazamiento",
  123. hideSettings: "Ocultar ajustes",
  124. smooth: "Suave",
  125. auto: "Ninguno",
  126. openMenu: "Abrir menú",
  127. needRefresh: "Necesita actualizar la página para aplicar la detección automática",
  128. },
  129. fr: {
  130. settings: "Paramètres",
  131. autoDetect: "Détecter automatiquement tous/les derniers commentaires",
  132. notifyEnabled: "Notifier après l'action",
  133. isAll: "Tous les commentaires/Derniers commentaires",
  134. isScroll: "Activer l'effet de défilement",
  135. scrollBehavior: "Comportement de défilement",
  136. hideSettings: "Masquer les paramètres",
  137. smooth: "Doux",
  138. auto: "Aucun",
  139. openMenu: "Ouvrir le menu",
  140. needRefresh: "Besoin de rafraîchir la page pour appliquer la détection automatique",
  141. },
  142. hu: {
  143. settings: "Beállítások",
  144. autoDetect: "Az összes/legújabb hozzászólás automatikus észlelése",
  145. notifyEnabled: "Értesítés az akció után",
  146. isAll: "Minden megjegyzés/Legfrissebb megjegyzések",
  147. isScroll: "Gördítési hatás engedélyezése",
  148. scrollBehavior: "Gördülési viselkedés",
  149. hideSettings: "Beállítások elrejtése",
  150. smooth: "Simít",
  151. auto: "Nincs",
  152. openMenu: "Menü megnyitása",
  153. needRefresh: "Az automatikus észlelés alkalmazásához frissíteni kell az oldalt",
  154. },
  155. ja: {
  156. settings: "設定",
  157. autoDetect: "すべて/最新のコメントを自動検出",
  158. notifyEnabled: "アクション後に通知",
  159. isAll: "すべてのコメント/最新のコメント",
  160. isScroll: "スクロール効果を有効にする",
  161. scrollBehavior: "スクロール動作",
  162. hideSettings: "設定を非表示",
  163. smooth: "スムーズ",
  164. auto: "なし",
  165. openMenu: "メニューを開く",
  166. needRefresh: "自動検出を適用するにはページを更新する必要があります",
  167. },
  168. ko: {
  169. settings: "설정",
  170. autoDetect: "모든/최신 댓글 자동 감지",
  171. notifyEnabled: "작업 후 알림",
  172. isAll: "모든 댓글/최신 댓글",
  173. isScroll: "스크롤 효과 사용",
  174. scrollBehavior: "스크롤 동작",
  175. hideSettings: "설정 숨기기",
  176. smooth: "부드러운",
  177. auto: "없음",
  178. openMenu: "메뉴 열기",
  179. needRefresh: "자동 감지를 적용하려면 페이지를 새로 고쳐야 합니다",
  180. },
  181. sk: {
  182. settings: "Nastavenia",
  183. autoDetect: "Automaticky zistiť všetky/najnovšie komentáre",
  184. notifyEnabled: "Upozorniť po akcii",
  185. isAll: "Všetky komentáre/Najnovšie komentáre",
  186. isScroll: "Povoliť posuvný efekt",
  187. scrollBehavior: "Správanie posuvu",
  188. hideSettings: "Skryť nastavenia",
  189. smooth: "Hladký",
  190. auto: "Žiadny",
  191. openMenu: "Otvoriť menu",
  192. needRefresh: "Na použitie automatického zistenia je potrebné obnoviť stránku",
  193. },
  194. sl: {
  195. settings: "Nastavitve",
  196. autoDetect: "Samodejno zaznani vsi/najnovejši komentarji",
  197. notifyEnabled: "Obvesti po dejanju",
  198. isAll: "Vsi komentarji/Najnovejši komentarji",
  199. isScroll: "Omogoči učinek drsenja",
  200. scrollBehavior: "Vedenje drsenja",
  201. hideSettings: "Skrij nastavitve",
  202. smooth: "Gladko",
  203. auto: "Brez",
  204. openMenu: "Odpri meni",
  205. needRefresh: "Za uporabo samodejnega zaznavanja je treba osvežiti stran",
  206. },
  207. };
  208.  
  209. /**
  210. * get the language of the fb
  211. * @returns the language of the fb
  212. */
  213. function detectLang() {
  214. return document.getElementById("facebook")?.getAttribute("lang") || "en";
  215. }
  216.  
  217. /**
  218. * get the settings string array
  219. * @returns the settings string array
  220. */
  221.  
  222. function getSettingsStr() {
  223. return settingsStr[detectLang()] || settingsStr.en;
  224. }
  225.  
  226. /**
  227. * get the xpath for menu
  228. * @returns the xpath for menu
  229. */
  230.  
  231. function getMenuButtonXPath() {
  232. const lang = langs[detectLang()] || langs.en;
  233. return `//span[not(@style) and (text()='${lang[0]}' or text()='${lang[1]}' or text()='${lang[2]}' or text()='${lang[3]}' or text()='${lang[4]}' or text()='${lang[5]}')]`;
  234. }
  235.  
  236. /**
  237. * handle the click the comment button or the right bottom comment count
  238. */
  239.  
  240. function handleClickOutside() {
  241. if (settings.isAll) showAllComment();
  242. else showLatestComment();
  243. }
  244.  
  245. /**
  246. * wait for the element to appear in the DOM
  247. * @param {string} xpath
  248. * @param {Function} callback
  249. * @param {number} timeout
  250. * @param {number} interval
  251. */
  252.  
  253. function waitForElement(xpath, callback, timeout = 3000, intervalTime = 100) {
  254. const startTime = Date.now();
  255. const interval = setInterval(() => {
  256. const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  257. if (element) {
  258. clearInterval(interval);
  259. callback(element);
  260. } else if (Date.now() - startTime > timeout) {
  261. clearInterval(interval);
  262. console.warn("Timeout: Element not found for XPath", xpath);
  263. }
  264. }, intervalTime);
  265. }
  266.  
  267. /**
  268. *
  269. * @param {Element} element
  270. */
  271.  
  272. function safeClick(element) {
  273. try {
  274. element.click();
  275. } catch (err) {
  276. console.error("Error clicking element:", err);
  277. }
  278. }
  279.  
  280. /**
  281. * show the notification to user after the action
  282. * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
  283. * @param {boolean} reverse
  284. */
  285.  
  286. async function notifyUser(reverse = false) {
  287. const notification = document.createElement("div");
  288. notification.setAttribute("id", "FBAllCommentsHelperNotification");
  289. notification.style.position = "fixed";
  290. notification.style.bottom = "20px";
  291. notification.style.left = "20px";
  292. notification.style.backgroundColor = "rgba(0,0,0,1)";
  293. notification.style.color = "white";
  294. notification.style.padding = "10px";
  295. notification.style.borderRadius = "5px";
  296. notification.style.zIndex = "9999";
  297. const notifyingTimeout = Number(await GM.getValue("notifyingTimeout"));
  298. if (reverse) {
  299. notification.textContent = notificationStr[detectLang()][settings.isAll ? 1 : 0] || notificationStr.en[settings.isAll ? 1 : 0];
  300. } else {
  301. notification.textContent = notificationStr[detectLang()][settings.isAll ? 0 : 1] || notificationStr.en[settings.isAll ? 0 : 1];
  302. }
  303. document.body.appendChild(notification);
  304. if (notifyingTimeout) {
  305. document.getElementById("FBAllCommentsHelperNotification").remove();
  306. clearTimeout(notifyingTimeout);
  307. await GM.setValue("notifyingTimeout", undefined);
  308. }
  309. const id = setTimeout(async () => {
  310. notification.remove();
  311. await GM.setValue("notifyingTimeout", undefined);
  312. }, 3000);
  313. await GM.setValue("notifyingTimeout", id);
  314. }
  315.  
  316. /**
  317. * parse the user action
  318. * @param {*} e
  319. * @returns
  320. */
  321.  
  322. function actionParser(e) {
  323. if (e.type === "dblclick" && e.ctrlKey) {
  324. !settings.isAll ? showAllComment(true) : showLatestComment(true);
  325. return;
  326. }
  327. if (e.type === "dblclick") {
  328. settings.isAll ? showAllComment() : showLatestComment();
  329. return;
  330. }
  331. if (e.code === "Insert" && e.ctrlKey) {
  332. !settings.isAll ? showAllComment(true) : showLatestComment(true);
  333. return;
  334. }
  335. if (e.code === "Insert") {
  336. settings.isAll ? showAllComment() : showLatestComment();
  337. return;
  338. }
  339. }
  340.  
  341. /**
  342. * detecting the changes in the DOM
  343. * @param {Function} callback
  344. */
  345.  
  346. function observeDOM(callback) {
  347. const observer = new MutationObserver((mutations) => {
  348. mutations.forEach(() => callback());
  349. });
  350. observer.observe(document.body, { childList: true, subtree: true });
  351. }
  352.  
  353. /**
  354. * show all comments
  355. */
  356.  
  357. function showAllComment(reverse = false) {
  358. waitForElement(
  359. //getting the menu
  360. getMenuButtonXPath(),
  361. (element) => {
  362. if (settings.isScroll) {
  363. element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
  364. }
  365. setTimeout(() => {
  366. safeClick(element);
  367. }, 100);
  368. waitForElement(
  369. //getting the items in menu
  370. "//*[@role='menuitem']",
  371. (element) => {
  372. const menuItems = document.querySelectorAll('*[role="menuitem"]');
  373. if (menuItems.length > 1) {
  374. safeClick(menuItems[menuItems.length - 1]);
  375. if (settings.notifyEnabled) {
  376. notifyUser(reverse);
  377. }
  378. }
  379. }
  380. );
  381. }
  382. );
  383. }
  384.  
  385. /**
  386. * show latest comment
  387. * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
  388. * @param {boolean} reverse
  389. */
  390. function showLatestComment(reverse = false) {
  391. waitForElement(
  392. //getting the menu
  393. getMenuButtonXPath(),
  394. (element) => {
  395. if (settings.isScroll) {
  396. element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
  397. }
  398. setTimeout(() => {
  399. safeClick(element);
  400. }, 100);
  401. waitForElement(
  402. //getting the items in menu
  403. "//*[@role='menuitem']",
  404. (element) => {
  405. const menuItems = document.querySelectorAll('*[role="menuitem"]');
  406. if (menuItems.length > 1) {
  407. safeClick(menuItems[menuItems.length - 2]);
  408. if (settings.notifyEnabled) {
  409. notifyUser(reverse);
  410. }
  411. }
  412. }
  413. );
  414. }
  415. );
  416. }
  417.  
  418. /**
  419. * bind the event for detected object after the DOM changes
  420. */
  421.  
  422. function bindForDetected(action = "bind") {
  423. let commentRightBottomBtn = document.querySelectorAll("div[role='button'][tabindex='0'][id^=':']");
  424. let commentBtn = document.querySelectorAll("span[data-ad-rendering-role='comment_button']");
  425. commentRightBottomBtn.forEach((btn) => {
  426. if (action === "remove") {
  427. btn.removeEventListener("click", handleClickOutside);
  428. } else {
  429. btn.addEventListener("click", handleClickOutside);
  430. }
  431. });
  432. commentBtn.forEach((btn) => {
  433. if (action === "remove") {
  434. btn.parentElement.parentElement.parentElement.removeEventListener("click", handleClickOutside);
  435. } else {
  436. btn.parentElement.parentElement.parentElement.addEventListener("click", handleClickOutside);
  437. }
  438. });
  439. }
  440.  
  441. /**
  442. * default settings
  443. */
  444.  
  445. const settings = {
  446. autoDetect: true, // auto detect all/latest comments
  447. scrollBehavior: "smooth", // scroll behavior
  448. notifyEnabled: true, // notify after action
  449. isAll: true, // all comments/latest comments
  450. isHidden: false, // settings panel hidden
  451. isScroll: false, // enable scroll effect
  452. };
  453.  
  454. /**
  455. * save settings to gm storage
  456. */
  457.  
  458. async function saveSettings() {
  459. await GM.setValue("fbAllCommentsHelperSettings", JSON.stringify(settings));
  460. }
  461.  
  462. /**
  463. * load settings from gm storage
  464. */
  465.  
  466. async function loadSettings() {
  467. const storedSettings = await GM.getValue("fbAllCommentsHelperSettings");
  468. if (storedSettings) {
  469. Object.assign(settings, JSON.parse(storedSettings));
  470. }
  471. }
  472.  
  473. /**
  474. * create settings panel
  475. */
  476.  
  477. function createSettingsPanel() {
  478. const panel = document.createElement("div");
  479. panel.id = "settingsPanel";
  480. panel.style.position = "fixed";
  481. panel.style.top = "10px";
  482. panel.style.right = "10px";
  483. panel.style.backgroundColor = "rgba(0, 0, 0, 1)";
  484. panel.style.padding = "10px";
  485. panel.style.borderRadius = "5px";
  486. panel.style.zIndex = "9999";
  487. panel.style.fontSize = "14px";
  488.  
  489. panel.innerHTML = `
  490. <h4 style="margin: 0 0 10px;color: white;">${getSettingsStr().settings}</h4>
  491. <label style="color: white;">
  492. <input type="checkbox" id="autoDetect" ${settings.autoDetect ? "checked" : ""}>
  493. ${getSettingsStr().autoDetect}
  494. </label><br>
  495. <label style="color: white;">
  496. <input type="checkbox" id="notifyEnabled" ${settings.notifyEnabled ? "checked" : ""}>
  497. ${getSettingsStr().notifyEnabled}
  498. </label><br>
  499. <label style="color: white;">
  500. <input type="checkbox" id="isAll" ${settings.isAll ? "checked" : ""}>
  501. ${getSettingsStr().isAll}
  502. </label><br>
  503. <label style="color: white;">
  504. <input type="checkbox" id="isScroll" ${settings.isScroll ? "checked" : ""}>
  505. ${getSettingsStr().isScroll}
  506. </label><br>
  507. <label style="color: white;">
  508. ${getSettingsStr().scrollBehavior}
  509. <select id="scrollBehavior">
  510. <option value="smooth" ${settings.scrollBehavior === "smooth" ? "selected" : ""}>${getSettingsStr().smooth}</option>
  511. <option value="auto" ${settings.scrollBehavior === "auto" ? "selected" : ""}>${getSettingsStr().auto}</option>
  512. </select>
  513. </label><br>
  514. <button id="hideSettings" style="margin-top: 10px;">${getSettingsStr().hideSettings}</button>
  515. `;
  516.  
  517. document.body.appendChild(panel);
  518.  
  519. // add panel buttons event listeners
  520. document.getElementById("autoDetect").addEventListener("change", (e) => {
  521. settings.autoDetect = e.target.checked;
  522. saveSettings();
  523. alert(getSettingsStr().needRefresh);
  524. });
  525. document.getElementById("notifyEnabled").addEventListener("change", (e) => {
  526. settings.notifyEnabled = e.target.checked;
  527. saveSettings();
  528. });
  529. document.getElementById("isAll").addEventListener("change", (e) => {
  530. settings.isAll = e.target.checked;
  531. saveSettings();
  532. });
  533. document.getElementById("isScroll").addEventListener("change", (e) => {
  534. settings.isScroll = e.target.checked;
  535. saveSettings();
  536. });
  537. document.getElementById("scrollBehavior").addEventListener("change", (e) => {
  538. settings.scrollBehavior = e.target.value;
  539. saveSettings();
  540. });
  541. document.getElementById("hideSettings").addEventListener("click", () => {
  542. settings.isHidden = true;
  543. document.getElementById("settingsPanel").remove();
  544. saveSettings();
  545. });
  546. }
  547.  
  548. (async function () {
  549. "use strict";
  550. window.addEventListener("load", async () => {
  551. await loadSettings();
  552. await saveSettings();
  553. await GM_registerMenuCommand(getSettingsStr().openMenu, () => {
  554. // gm menu command
  555. createSettingsPanel();
  556. settings.isHidden = false;
  557. });
  558. if (!settings.isHidden) {
  559. // create settings panel when isHidden is false
  560. createSettingsPanel();
  561. }
  562. document.addEventListener("dblclick", actionParser); //handle dblclick event
  563. document.addEventListener("keydown", actionParser); //handle keydown event
  564. bindForDetected("remove");
  565. if (settings.autoDetect) {
  566. bindForDetected();
  567. // for auto detect
  568. observeDOM(bindForDetected);
  569. }
  570. });
  571. })();