- // ==UserScript==
- // @name Facebook All Comments Helper
- // @name:zh-TW FB全部留言小幫手
- // @name:zh-CN FB全部留言小帮手
- // @namespace http://tampermonkey.net/
- // @version 2.3
- // @description Easy way to show all comments.
- // @description:zh-tw 讓您更快打開全部留言
- // @description:zh-cn 让您更快打开全部留言
- // @author Xuitty
- // @match https://www.facebook.com/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=facebook.com
- // @grant GM_registerMenuCommand
- // @grant GM_setValue
- // @grant GM_getValue
- // @run-at document-end
- // @license MIT
- // ==/UserScript==
-
- /**
- * string array for detecting the menu button
- */
-
- const langs = {
- de: ["Relevanteste", "Top-Kommentare", "Am zutreffendsten", "Neueste zuerst", "Neueste", "Alle Kommentare"],
- en: ["Top comments", "Most relevant", "Most applicable", "Most recent", "Newest", "All comments"],
- es: ["Comentarios destacados", "Más relevantes", "Más pertinentes", "Más recientes", "Más recientes", "Todos los comentarios"],
- 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"],
- ja: ["トップコメント", "関連度の高い順", "最も適切", "新しい順", "新しい順", "すべてのコメント"],
- ko: ["관련성 높은 댓글", "참여도 높은 댓글", "적합성 높은 순", "최신순", "날짜 내림차순", "모든 댓글"],
- fr: ["Plus pertinents", "Les meilleurs commentaires", "Les plus pertinents", "Plus récents", "Les plus récents", "Tous les commentaires"],
- sk: ["Top komentáre", "Najrelevantnejšie", "Najvhodnejšie", "Najnovšie", "Najnovšie", "Všetky komentáre"],
- sl: ["Najbolj priljubljeni komentarji", "Najustreznejši", "Najustreznejše", "Najnovejši", "Najnovejši", "Vsi komentarji"],
- "zh-Hans": ["热门评论", "最相关", "最合适", "从新到旧", "最新", "所有评论"],
- "zh-Hant": ["最熱門留言", "最相關", "最相關", "最新", "由新到舊", "所有留言"],
- };
-
- /**
- * string array for notification
- */
-
- const notificationStr = {
- de: ["Wechseln zu allen Kommentaren!", "Wechseln zu den neuesten Kommentaren!"],
- en: ["Switch to All Comments!", "Switch to Latest Comments!"],
- es: ["Cambiar a todos los comentarios!", "Cambiar a los comentarios más recientes!"],
- hu: ["Váltás az összes hozzászólásra!", "Váltás a legújabb hozzászólásokra!"],
- ja: ["すべてのコメントに切り替え!", "最新のコメントに切り替え!"],
- ko: ["모든 댓글로 전환!", "최신 댓글로 전환!"],
- fr: ["Passer à tous les commentaires!", "Passer aux commentaires les plus récents!"],
- sk: ["Prepnúť na všetky komentáre!", "Prepnúť na najnovšie komentáre!"],
- sl: ["Preklopite na vse komentarje!", "Preklopite na najnovejše komentarje!"],
- "zh-Hant": ["切換到所有留言!", "切換到最新留言!"],
- "zh-Hans": ["切换到所有评论!", "切换到最新评论!"],
- };
-
- /**
- * string array for settings
- */
-
- const settingsStr = {
- "zh-Hant": {
- settings: "設定",
- autoDetect: "自動切換為全部/最新留言",
- notifyEnabled: "操作通知",
- isAll: "全部留言/最新留言",
- isScroll: "是否開啟捲動",
- scrollBehavior: "捲動特效",
- hideSettings: "隱藏選單",
- smooth: "平滑",
- auto: "無",
- openMenu: "開啟選單",
- needRefresh: "需要重新整理頁面應用自動切換",
- },
- "zh-Hans": {
- settings: "设置",
- autoDetect: "自动切换为全部/最新评论",
- notifyEnabled: "操作通知",
- isAll: "全部评论/最新评论",
- isScroll: "是否开启滚动",
- scrollBehavior: "滚动特效",
- hideSettings: "隐藏菜单",
- smooth: "平滑",
- auto: "无",
- openMenu: "打开菜单",
- needRefresh: "需要重新刷新页面应用自动切换",
- },
- en: {
- settings: "Settings",
- autoDetect: "Auto detect all/latest comments",
- notifyEnabled: "Notify after action",
- isAll: "All comments/Latest comments",
- isScroll: "Enable scroll effect",
- scrollBehavior: "Scroll behavior",
- hideSettings: "Hide settings",
- smooth: "Smooth",
- auto: "None",
- openMenu: "Open menu",
- needRefresh: "Need to refresh the page to apply auto detection",
- },
- de: {
- settings: "Einstellungen",
- autoDetect: "Automatisch alle/neuesten Kommentare erkennen",
- notifyEnabled: "Nach Aktion benachrichtigen",
- isAll: "Alle Kommentare/Neueste Kommentare",
- isScroll: "Bildlauf aktivieren",
- scrollBehavior: "Bildlaufverhalten",
- hideSettings: "Einstellungen ausblenden",
- smooth: "Sanft",
- auto: "Keine",
- openMenu: "Menü öffnen",
- needRefresh: "Die Seite muss aktualisiert werden, um die automatische Erkennung anzuwenden",
- },
- es: {
- settings: "Ajustes",
- autoDetect: "Detectar automáticamente todos/los comentarios más recientes",
- notifyEnabled: "Notificar después de la acción",
- isAll: "Todos los comentarios/Comentarios más recientes",
- isScroll: "Activar efecto de desplazamiento",
- scrollBehavior: "Comportamiento de desplazamiento",
- hideSettings: "Ocultar ajustes",
- smooth: "Suave",
- auto: "Ninguno",
- openMenu: "Abrir menú",
- needRefresh: "Necesita actualizar la página para aplicar la detección automática",
- },
- fr: {
- settings: "Paramètres",
- autoDetect: "Détecter automatiquement tous/les derniers commentaires",
- notifyEnabled: "Notifier après l'action",
- isAll: "Tous les commentaires/Derniers commentaires",
- isScroll: "Activer l'effet de défilement",
- scrollBehavior: "Comportement de défilement",
- hideSettings: "Masquer les paramètres",
- smooth: "Doux",
- auto: "Aucun",
- openMenu: "Ouvrir le menu",
- needRefresh: "Besoin de rafraîchir la page pour appliquer la détection automatique",
- },
- hu: {
- settings: "Beállítások",
- autoDetect: "Az összes/legújabb hozzászólás automatikus észlelése",
- notifyEnabled: "Értesítés az akció után",
- isAll: "Minden megjegyzés/Legfrissebb megjegyzések",
- isScroll: "Gördítési hatás engedélyezése",
- scrollBehavior: "Gördülési viselkedés",
- hideSettings: "Beállítások elrejtése",
- smooth: "Simít",
- auto: "Nincs",
- openMenu: "Menü megnyitása",
- needRefresh: "Az automatikus észlelés alkalmazásához frissíteni kell az oldalt",
- },
- ja: {
- settings: "設定",
- autoDetect: "すべて/最新のコメントを自動検出",
- notifyEnabled: "アクション後に通知",
- isAll: "すべてのコメント/最新のコメント",
- isScroll: "スクロール効果を有効にする",
- scrollBehavior: "スクロール動作",
- hideSettings: "設定を非表示",
- smooth: "スムーズ",
- auto: "なし",
- openMenu: "メニューを開く",
- needRefresh: "自動検出を適用するにはページを更新する必要があります",
- },
- ko: {
- settings: "설정",
- autoDetect: "모든/최신 댓글 자동 감지",
- notifyEnabled: "작업 후 알림",
- isAll: "모든 댓글/최신 댓글",
- isScroll: "스크롤 효과 사용",
- scrollBehavior: "스크롤 동작",
- hideSettings: "설정 숨기기",
- smooth: "부드러운",
- auto: "없음",
- openMenu: "메뉴 열기",
- needRefresh: "자동 감지를 적용하려면 페이지를 새로 고쳐야 합니다",
- },
- sk: {
- settings: "Nastavenia",
- autoDetect: "Automaticky zistiť všetky/najnovšie komentáre",
- notifyEnabled: "Upozorniť po akcii",
- isAll: "Všetky komentáre/Najnovšie komentáre",
- isScroll: "Povoliť posuvný efekt",
- scrollBehavior: "Správanie posuvu",
- hideSettings: "Skryť nastavenia",
- smooth: "Hladký",
- auto: "Žiadny",
- openMenu: "Otvoriť menu",
- needRefresh: "Na použitie automatického zistenia je potrebné obnoviť stránku",
- },
- sl: {
- settings: "Nastavitve",
- autoDetect: "Samodejno zaznani vsi/najnovejši komentarji",
- notifyEnabled: "Obvesti po dejanju",
- isAll: "Vsi komentarji/Najnovejši komentarji",
- isScroll: "Omogoči učinek drsenja",
- scrollBehavior: "Vedenje drsenja",
- hideSettings: "Skrij nastavitve",
- smooth: "Gladko",
- auto: "Brez",
- openMenu: "Odpri meni",
- needRefresh: "Za uporabo samodejnega zaznavanja je treba osvežiti stran",
- },
- };
-
- /**
- * get the language of the fb
- * @returns the language of the fb
- */
- function detectLang() {
- return document.getElementById("facebook")?.getAttribute("lang") || "en";
- }
-
- /**
- * get the settings string array
- * @returns the settings string array
- */
-
- function getSettingsStr() {
- return settingsStr[detectLang()] || settingsStr.en;
- }
-
- /**
- * get the xpath for menu
- * @returns the xpath for menu
- */
-
- function getMenuButtonXPath() {
- const lang = langs[detectLang()] || langs.en;
- 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]}')]`;
- }
-
- /**
- * handle the click the comment button or the right bottom comment count
- */
-
- function handleClickOutside() {
- if (settings.isAll) showAllComment();
- else showLatestComment();
- }
-
- /**
- * wait for the element to appear in the DOM
- * @param {string} xpath
- * @param {Function} callback
- * @param {number} timeout
- * @param {number} interval
- */
-
- function waitForElement(xpath, callback, timeout = 3000, intervalTime = 100) {
- const startTime = Date.now();
- const interval = setInterval(() => {
- const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
- if (element) {
- clearInterval(interval);
- callback(element);
- } else if (Date.now() - startTime > timeout) {
- clearInterval(interval);
- console.warn("Timeout: Element not found for XPath", xpath);
- }
- }, intervalTime);
- }
-
- /**
- *
- * @param {Element} element
- */
-
- function safeClick(element) {
- try {
- element.click();
- } catch (err) {
- console.error("Error clicking element:", err);
- }
- }
-
- /**
- * show the notification to user after the action
- * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
- * @param {boolean} reverse
- */
-
- async function notifyUser(reverse = false) {
- const notification = document.createElement("div");
- notification.setAttribute("id", "FBAllCommentsHelperNotification");
- notification.style.position = "fixed";
- notification.style.bottom = "20px";
- notification.style.left = "20px";
- notification.style.backgroundColor = "rgba(0,0,0,1)";
- notification.style.color = "white";
- notification.style.padding = "10px";
- notification.style.borderRadius = "5px";
- notification.style.zIndex = "9999";
- const notifyingTimeout = Number(await GM.getValue("notifyingTimeout"));
- if (reverse) {
- notification.textContent = notificationStr[detectLang()][settings.isAll ? 1 : 0] || notificationStr.en[settings.isAll ? 1 : 0];
- } else {
- notification.textContent = notificationStr[detectLang()][settings.isAll ? 0 : 1] || notificationStr.en[settings.isAll ? 0 : 1];
- }
- document.body.appendChild(notification);
- if (notifyingTimeout) {
- document.getElementById("FBAllCommentsHelperNotification").remove();
- clearTimeout(notifyingTimeout);
- }
- const id = setTimeout(async () => {
- notification.remove();
- await GM.deleteValue("notifyingTimeout");
- }, 3000);
- await GM.setValue("notifyingTimeout", id);
- }
-
- /**
- * parse the user action
- * @param {*} e
- * @returns
- */
-
- function actionParser(e) {
- if (e.type === "dblclick" && e.ctrlKey) {
- !settings.isAll ? showAllComment(true) : showLatestComment(true);
- return;
- }
- if (e.type === "dblclick") {
- settings.isAll ? showAllComment() : showLatestComment();
- return;
- }
- if (e.code === "Insert" && e.ctrlKey) {
- !settings.isAll ? showAllComment(true) : showLatestComment(true);
- return;
- }
- if (e.code === "Insert") {
- settings.isAll ? showAllComment() : showLatestComment();
- return;
- }
- }
-
- /**
- * detecting the changes in the DOM
- * @param {Function} callback
- */
-
- function observeDOM(callback) {
- const observer = new MutationObserver((mutations) => {
- mutations.forEach(() => callback());
- });
- observer.observe(document.body, { childList: true, subtree: true });
- }
-
- /**
- * show all comments
- */
-
- function showAllComment(reverse = false) {
- waitForElement(
- //getting the menu
- getMenuButtonXPath(),
- (element) => {
- if (settings.isScroll) {
- element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
- }
- setTimeout(() => {
- safeClick(element);
- }, 100);
- waitForElement(
- //getting the items in menu
- "//*[@role='menuitem']",
- (element) => {
- const menuItems = document.querySelectorAll('*[role="menuitem"]');
- if (menuItems.length > 1) {
- safeClick(menuItems[menuItems.length - 1]);
- if (settings.notifyEnabled) {
- notifyUser(reverse);
- }
- }
- }
- );
- }
- );
- }
-
- /**
- * show latest comment
- * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
- * @param {boolean} reverse
- */
- function showLatestComment(reverse = false) {
- waitForElement(
- //getting the menu
- getMenuButtonXPath(),
- (element) => {
- if (settings.isScroll) {
- element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
- }
- setTimeout(() => {
- safeClick(element);
- }, 100);
- waitForElement(
- //getting the items in menu
- "//*[@role='menuitem']",
- (element) => {
- const menuItems = document.querySelectorAll('*[role="menuitem"]');
- if (menuItems.length > 1) {
- safeClick(menuItems[menuItems.length - 2]);
- if (settings.notifyEnabled) {
- notifyUser(reverse);
- }
- }
- }
- );
- }
- );
- }
-
- /**
- * bind the event for detected object after the DOM changes
- */
-
- function bindForDetected() {
- let commentRightBottomBtn = document.querySelectorAll("div[role='button'][tabindex='0'][id^=':']");
- let commentBtn = document.querySelectorAll("span[data-ad-rendering-role='comment_button']");
- commentRightBottomBtn.forEach((btn) => {
- btn.addEventListener("click", handleClickOutside);
- });
- commentBtn.forEach((btn) => {
- btn.parentElement.parentElement.parentElement.addEventListener("click", handleClickOutside);
- });
- }
-
- /**
- * default settings
- */
-
- const settings = {
- autoDetect: true, // auto detect all/latest comments
- scrollBehavior: "smooth", // scroll behavior
- notifyEnabled: true, // notify after action
- isAll: true, // all comments/latest comments
- isHidden: false, // settings panel hidden
- isScroll: false, // enable scroll effect
- };
-
- /**
- * save settings to gm storage
- */
-
- async function saveSettings() {
- await GM.setValue("fbAllCommentsHelperSettings", JSON.stringify(settings));
- }
-
- /**
- * load settings from gm storage
- */
-
- async function loadSettings() {
- const storedSettings = await GM.getValue("fbAllCommentsHelperSettings");
- if (storedSettings) {
- Object.assign(settings, JSON.parse(storedSettings));
- }
- }
-
- /**
- * create settings panel
- */
-
- function createSettingsPanel() {
- const panel = document.createElement("div");
- panel.id = "settingsPanel";
- panel.style.position = "fixed";
- panel.style.top = "10px";
- panel.style.right = "10px";
- panel.style.backgroundColor = "rgba(0, 0, 0, 1)";
- panel.style.padding = "10px";
- panel.style.borderRadius = "5px";
- panel.style.zIndex = "9999";
- panel.style.fontSize = "14px";
-
- panel.innerHTML = `
- <h4 style="margin: 0 0 10px;color: white;">${getSettingsStr().settings}</h4>
- <label style="color: white;">
- <input type="checkbox" id="autoDetect" ${settings.autoDetect ? "checked" : ""}>
- ${getSettingsStr().autoDetect}
- </label><br>
- <label style="color: white;">
- <input type="checkbox" id="notifyEnabled" ${settings.notifyEnabled ? "checked" : ""}>
- ${getSettingsStr().notifyEnabled}
- </label><br>
- <label style="color: white;">
- <input type="checkbox" id="isAll" ${settings.isAll ? "checked" : ""}>
- ${getSettingsStr().isAll}
- </label><br>
- <label style="color: white;">
- <input type="checkbox" id="isScroll" ${settings.isScroll ? "checked" : ""}>
- ${getSettingsStr().isScroll}
- </label><br>
- <label style="color: white;">
- ${getSettingsStr().scrollBehavior}
- <select id="scrollBehavior">
- <option value="smooth" ${settings.scrollBehavior === "smooth" ? "selected" : ""}>${getSettingsStr().smooth}</option>
- <option value="auto" ${settings.scrollBehavior === "auto" ? "selected" : ""}>${getSettingsStr().auto}</option>
- </select>
- </label><br>
- <button id="hideSettings" style="margin-top: 10px;">${getSettingsStr().hideSettings}</button>
- `;
-
- document.body.appendChild(panel);
-
- // add panel buttons event listeners
- document.getElementById("autoDetect").addEventListener("change", (e) => {
- settings.autoDetect = e.target.checked;
- saveSettings();
- alert(getSettingsStr().needRefresh);
- });
- document.getElementById("notifyEnabled").addEventListener("change", (e) => {
- settings.notifyEnabled = e.target.checked;
- saveSettings();
- });
- document.getElementById("isAll").addEventListener("change", (e) => {
- settings.isAll = e.target.checked;
- saveSettings();
- });
- document.getElementById("isScroll").addEventListener("change", (e) => {
- settings.isScroll = e.target.checked;
- saveSettings();
- });
- document.getElementById("scrollBehavior").addEventListener("change", (e) => {
- settings.scrollBehavior = e.target.value;
- saveSettings();
- });
- document.getElementById("hideSettings").addEventListener("click", () => {
- settings.isHidden = true;
- document.getElementById("settingsPanel").remove();
- saveSettings();
- });
- }
-
- (async function () {
- "use strict";
- window.addEventListener("load", async () => {
- await loadSettings();
- await saveSettings();
- await GM_registerMenuCommand(getSettingsStr().openMenu, () => {
- // gm menu command
- createSettingsPanel();
- settings.isHidden = false;
- });
- if (!settings.isHidden) {
- // create settings panel when isHidden is false
- createSettingsPanel();
- }
- document.addEventListener("dblclick", actionParser); //handle dblclick event
- document.addEventListener("keydown", actionParser); //handle keydown event
- bindForDetected();
- if (settings.autoDetect) {
- // for auto detect
- observeDOM(bindForDetected);
- }
- });
- })();