LINUX DO Auto Nested View

Automatically redirect linux.do topic pages and topic links to nested view.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         LINUX DO Auto Nested View
// @name:zh-CN   LINUX DO 自动嵌套视图
// @namespace    https://github.com/kai-wei-kfuse/linux-do-auto-nested-view
// @version      1.5.4
// @description  Automatically redirect linux.do topic pages and topic links to nested view.
// @description:zh-CN 自动将 linux.do 主题页和主题链接切换到嵌套视图。
// @author       kai-wei-kfuse
// @license      MIT
// @match        https://linux.do/*
// @homepageURL  https://github.com/kai-wei-kfuse/linux-do-auto-nested-view
// @supportURL   https://github.com/kai-wei-kfuse/linux-do-auto-nested-view/issues
// @run-at       document-start
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// ==/UserScript==

(function () {
  "use strict";

  const ENABLED_STORAGE_KEY = "linuxdo-auto-nested-view-enabled";
  const TITLE_STORAGE_PREFIX = "linuxdo-auto-nested-view-topic-title-";
  const PRIVATE_MESSAGE_STORAGE_PREFIX =
    "linuxdo-auto-nested-view-private-message-";
  const SITE_TITLE = "LINUX DO";
  const pageWindow =
    typeof unsafeWindow === "undefined" ? window : unsafeWindow;

  let lastHandledUrl = "";
  let observer = null;
  let menuCommandId = null;
  let autoConvertEnabled = loadAutoConvertEnabled();
  const rewrittenLinks = new WeakSet();

  function loadAutoConvertEnabled() {
    try {
      return GM_getValue(ENABLED_STORAGE_KEY, true) !== false;
    } catch {
      return true;
    }
  }

  function saveAutoConvertEnabled(enabled) {
    autoConvertEnabled = enabled;

    try {
      GM_setValue(ENABLED_STORAGE_KEY, enabled);
    } catch {
      // Ignore storage failures so the script still works with its default state.
    }
  }

  function registerToggleMenu() {
    if (typeof GM_registerMenuCommand !== "function") {
      return;
    }

    if (
      menuCommandId !== null &&
      typeof GM_unregisterMenuCommand === "function"
    ) {
      GM_unregisterMenuCommand(menuCommandId);
    }

    const status = autoConvertEnabled ? "开启" : "关闭";
    menuCommandId = GM_registerMenuCommand(
      `自动转换嵌套视图:${status}`,
      () => {
        saveAutoConvertEnabled(!autoConvertEnabled);
        registerToggleMenu();
        applyAutoConvertState();

        if (!autoConvertEnabled) {
          pageWindow.location.reload();
        }
      }
    );
  }

  function stopObserver() {
    if (!observer) {
      return;
    }

    observer.disconnect();
    observer = null;
  }

  function applyAutoConvertState() {
    lastHandledUrl = "";

    if (!autoConvertEnabled) {
      stopObserver();
      return;
    }

    startObserver();
    rewriteTopicLinks();
    redirectToNested();
    syncDocumentTitle();
  }

  function extractTopicId(pathname) {
    const nestedMatch = pathname.match(/^\/n\/topic\/(\d+)(?:\/.*)?$/);
    if (nestedMatch) {
      return nestedMatch[1];
    }

    const topicMatch = pathname.match(/^\/t\/[^/]+\/(\d+)(?:\/.*)?$/);
    if (topicMatch) {
      return topicMatch[1];
    }

    return null;
  }

  function isTopicPage(pathname) {
    return pathname.startsWith("/t/") && Boolean(extractTopicId(pathname));
  }

  function isNestedPage(pathname) {
    return pathname.startsWith("/n/topic/") && Boolean(extractTopicId(pathname));
  }

  function toNestedUrl(urlLike) {
    const url = new URL(urlLike, pageWindow.location.origin);
    const topicId = extractTopicId(url.pathname);
    if (!topicId) {
      return null;
    }

    url.pathname = `/n/topic/${topicId}`;
    return url.toString();
  }

  function getTopicIdFromUrl(urlLike) {
    try {
      const url = new URL(urlLike, pageWindow.location.origin);
      return extractTopicId(url.pathname);
    } catch {
      return null;
    }
  }

  function normalizeTopicTitle(text) {
    const title = text?.replace(/\s+/g, " ").trim();
    if (!title || title.toUpperCase() === "LINUXDO") {
      return null;
    }

    return title.replace(/\s+-\s+LINUX\s*DO$/i, "").trim() || null;
  }

  function extractTitleFromLink(link) {
    if (!link) {
      return null;
    }

    const titleNode = link.querySelector?.(
      ".title, .topic-title, [data-topic-title]"
    );
    return (
      normalizeTopicTitle(titleNode?.textContent) ||
      normalizeTopicTitle(link.getAttribute("title")) ||
      normalizeTopicTitle(link.getAttribute("aria-label")) ||
      normalizeTopicTitle(link.textContent)
    );
  }

  function rememberTopicTitle(link) {
    const topicId = getTopicIdFromUrl(link?.href);
    const title = extractTitleFromLink(link);
    if (!topicId || !title) {
      return;
    }

    try {
      sessionStorage.setItem(`${TITLE_STORAGE_PREFIX}${topicId}`, title);
    } catch {
      // Ignore storage failures; page DOM title detection can still recover.
    }
  }

  function loadRememberedTopicTitle(topicId) {
    try {
      return normalizeTopicTitle(
        sessionStorage.getItem(`${TITLE_STORAGE_PREFIX}${topicId}`)
      );
    } catch {
      return null;
    }
  }

  function rememberPrivateMessageTopic(link) {
    const topicId = getTopicIdFromUrl(link?.href);
    if (!topicId) {
      return;
    }

    try {
      sessionStorage.setItem(`${PRIVATE_MESSAGE_STORAGE_PREFIX}${topicId}`, "1");
    } catch {
      // Ignore storage failures; context checks still protect visible menu links.
    }
  }

  function isRememberedPrivateMessageUrl(urlLike) {
    const topicId = getTopicIdFromUrl(urlLike);
    if (!topicId) {
      return false;
    }

    try {
      return (
        sessionStorage.getItem(`${PRIVATE_MESSAGE_STORAGE_PREFIX}${topicId}`) ===
        "1"
      );
    } catch {
      return false;
    }
  }

  function isPrivateMessageContext(target) {
    if (!(target instanceof Element)) {
      return false;
    }

    return Boolean(
      target.closest(
        [
          "#quick-access-messages",
          ".quick-access-messages",
          ".user-menu-messages-list",
          ".user-messages-list",
          ".private-messages-list",
          ".messages-list",
          ".message-list",
          ".menu-panel.messages",
          ".menu-panel.private-messages",
          ".quick-access-panel.messages",
          ".quick-access-panel.private-messages",
          "[data-user-menu-tab='messages']",
          "[data-tab='messages']",
          "[data-section='messages']",
          "[data-name='messages']",
          "[data-type='private_message']",
          "[data-notification-type='private_message']",
          "[data-archetype='private_message']",
        ].join(",")
      )
    );
  }

  function shouldSkipNestedConversion(link) {
    if (!link) {
      return false;
    }

    if (isRememberedPrivateMessageUrl(link.href)) {
      return true;
    }

    if (!isPrivateMessageContext(link)) {
      return false;
    }

    rememberPrivateMessageTopic(link);
    return true;
  }

  function findTopicTitleInPage() {
    const metaTitle =
      normalizeTopicTitle(
        document.querySelector('meta[property="og:title"]')?.content
      ) ||
      normalizeTopicTitle(
        document.querySelector('meta[name="twitter:title"]')?.content
      );
    if (metaTitle) {
      return metaTitle;
    }

    const titleSelectors = [
      "h1 .fancy-title",
      "h1.topic-title",
      ".topic-title h1",
      ".topic-title",
      ".title-wrapper h1",
      "h1",
    ];

    for (const selector of titleSelectors) {
      const title = normalizeTopicTitle(
        document.querySelector(selector)?.textContent
      );
      if (title) {
        return title;
      }
    }

    return null;
  }

  function syncDocumentTitle() {
    if (!autoConvertEnabled || !isNestedPage(pageWindow.location.pathname)) {
      return;
    }

    const topicId = extractTopicId(pageWindow.location.pathname);
    const title = findTopicTitleInPage() || loadRememberedTopicTitle(topicId);
    if (!title) {
      return;
    }

    const nextTitle = `${title} - ${SITE_TITLE}`;
    if (document.title !== nextTitle) {
      document.title = nextTitle;
    }
  }

  function rewriteTopicLinks(root = document) {
    if (!autoConvertEnabled) {
      return;
    }

    const links = root.querySelectorAll('a[href]');
    for (const link of links) {
      if (shouldSkipNestedConversion(link)) {
        continue;
      }

      const nestedUrl = toNestedUrl(link.href);
      if (!nestedUrl) {
        continue;
      }

      if (link.href === nestedUrl && rewrittenLinks.has(link)) {
        continue;
      }

      rememberTopicTitle(link);
      link.href = nestedUrl;
      rewrittenLinks.add(link);
    }
  }

  function rewriteLinkElement(link) {
    if (!autoConvertEnabled) {
      return;
    }

    if (!link || !link.href) {
      return;
    }

    if (shouldSkipNestedConversion(link)) {
      return;
    }

    const nestedUrl = toNestedUrl(link.href);
    if (!nestedUrl || link.href === nestedUrl) {
      return;
    }

    rememberTopicTitle(link);
    link.href = nestedUrl;
    rewrittenLinks.add(link);
  }

  function findAnchorFromEventTarget(target) {
    if (!(target instanceof Element)) {
      return null;
    }

    return target.closest("a[href]");
  }

  function isNotificationArea(target) {
    if (!(target instanceof Element)) {
      return false;
    }

    return Boolean(
      target.closest(
        [
          "#quick-access-notifications",
          ".user-notifications-list",
          ".notifications-list",
          ".notification-history",
          ".menu-panel.notifications",
          ".notification",
          "[data-notification-id]",
        ].join(",")
      )
    );
  }

  function isBookmarkArea(target) {
    if (!(target instanceof Element)) {
      return false;
    }

    return Boolean(
      target.closest(
        [
          "#quick-access-bookmarks",
          ".quick-access-bookmarks",
          ".user-menu-bookmarks-list",
          ".bookmarks-list",
          ".menu-panel.bookmarks",
          ".quick-access-panel.bookmarks",
          "[data-user-menu-tab='bookmarks']",
          "[data-tab='bookmarks']",
          "[data-section='bookmarks']",
          "[data-name='bookmarks']",
        ].join(",")
      )
    );
  }

  function shouldForceNestedNavigation(target) {
    return isNotificationArea(target) || isBookmarkArea(target);
  }

  function forceMenuTopicNavigation(event) {
    if (!autoConvertEnabled) {
      return;
    }

    if (!shouldForceNestedNavigation(event.target)) {
      return;
    }

    const link = findAnchorFromEventTarget(event.target);
    if (!link) {
      return;
    }

    if (shouldSkipNestedConversion(link)) {
      return;
    }

    const nestedUrl = toNestedUrl(link.href);
    if (!nestedUrl) {
      return;
    }

    rememberTopicTitle(link);
    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation?.();
    pageWindow.location.assign(nestedUrl);
  }

  function interceptNavigationEvent(event) {
    if (!autoConvertEnabled) {
      return;
    }

    const link = findAnchorFromEventTarget(event.target);
    if (!link) {
      return;
    }

    rewriteLinkElement(link);
  }

  function redirectToNested() {
    if (!autoConvertEnabled) {
      lastHandledUrl = "";
      return;
    }

    const { pathname, href } = pageWindow.location;

    if (href === lastHandledUrl) {
      syncDocumentTitle();
      return;
    }

    if (isNestedPage(pathname)) {
      const normalizedNestedUrl = toNestedUrl(href);
      if (normalizedNestedUrl && normalizedNestedUrl !== href) {
        lastHandledUrl = href;
        pageWindow.location.replace(normalizedNestedUrl);
        return;
      }

      lastHandledUrl = href;
      syncDocumentTitle();
      return;
    }

    if (!isTopicPage(pathname)) {
      lastHandledUrl = href;
      return;
    }

    if (isRememberedPrivateMessageUrl(href)) {
      lastHandledUrl = href;
      return;
    }

    const link = document.querySelector("a.nested-view-link");
    if (link && link.href) {
      lastHandledUrl = href;
      pageWindow.location.href = link.href;
      return;
    }

    const nestedUrl = toNestedUrl(href);
    if (nestedUrl && nestedUrl !== href) {
      lastHandledUrl = href;
      pageWindow.location.href = nestedUrl;
    }
  }

  function startObserver() {
    if (!autoConvertEnabled) {
      stopObserver();
      return;
    }

    if (observer) {
      observer.disconnect();
    }

    observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== Node.ELEMENT_NODE) {
            continue;
          }

          if (node.matches?.("a[href]")) {
            rewriteTopicLinks(node.parentElement ?? document);
            continue;
          }

          rewriteTopicLinks(node);
        }
      }

      redirectToNested();
      syncDocumentTitle();
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });
  }

  function hookHistoryMethod(methodName) {
    const original = pageWindow.history[methodName];
    pageWindow.history[methodName] = function (...args) {
      if (
        autoConvertEnabled &&
        args.length >= 3 &&
        args[2] != null &&
        !isRememberedPrivateMessageUrl(args[2])
      ) {
        const nestedUrl = toNestedUrl(args[2]);
        if (nestedUrl) {
          args[2] = nestedUrl;
        }
      }

      const result = original.apply(this, args);
      if (autoConvertEnabled) {
        setTimeout(redirectToNested, 0);
        setTimeout(syncDocumentTitle, 0);
      }
      return result;
    };
  }

  registerToggleMenu();
  hookHistoryMethod("pushState");
  hookHistoryMethod("replaceState");
  window.addEventListener("popstate", () => setTimeout(redirectToNested, 0));
  window.addEventListener("DOMContentLoaded", () => {
    rewriteTopicLinks();
    redirectToNested();
    syncDocumentTitle();
  });
  window.addEventListener("click", forceMenuTopicNavigation, true);
  window.addEventListener("pointerdown", interceptNavigationEvent, true);
  window.addEventListener("mousedown", interceptNavigationEvent, true);
  window.addEventListener("click", interceptNavigationEvent, true);
  window.addEventListener("auxclick", interceptNavigationEvent, true);

  applyAutoConvertState();
})();