Improved SpaceHey Dates

SpaceHey timestamps with better relative dates and full dates on hover. Shows relative time for recent posts (< 30 days) and exact dates for older content.

目前為 2025-02-01 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Improved SpaceHey Dates
// @namespace    https://vasi.dev
// @version      1.0.1
// @description  SpaceHey timestamps with better relative dates and full dates on hover. Shows relative time for recent posts (< 30 days) and exact dates for older content.
// @author       Vasilis
// @match        https://*.spacehey.com/*
// @icon         https://static.spacehey.net/icons/clock.png
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const dateFormatterWithTime = new Intl.DateTimeFormat(undefined, {
    dateStyle: "long",
    timeStyle: "medium",
  });

  const dateFormatterNoTime = new Intl.DateTimeFormat(undefined, {
    dateStyle: "medium",
  });

  function formatDate(timestamp, showTime = true) {
    const date = new Date(timestamp * 1000);
    return (showTime ? dateFormatterWithTime : dateFormatterNoTime).format(
      date
    );
  }

  function getRelativeTime(timestamp) {
    const now = Math.floor(Date.now() / 1000);
    const diff = timestamp - now;

    const MS_IN_SECOND = 1000;
    const MS_IN_MINUTE = 60 * MS_IN_SECOND;
    const MS_IN_HOUR = 60 * MS_IN_MINUTE;
    const MS_IN_DAY = 24 * MS_IN_HOUR;
    const MS_IN_MONTH = 30 * MS_IN_DAY;

    const diffMs = Math.abs(diff * 1000);

    if (diffMs < MS_IN_MINUTE) return "just now";

    if (diffMs < MS_IN_HOUR) {
      const minutes = Math.round(diffMs / MS_IN_MINUTE);
      return diff > 0
        ? `in ${minutes === 1 ? "1 minute" : minutes + " minutes"}`
        : `${minutes === 1 ? "1 minute" : minutes + " minutes"} ago`;
    }

    if (diffMs < MS_IN_DAY) {
      const hours = Math.round(diffMs / MS_IN_HOUR);
      return diff > 0
        ? `in ${hours === 1 ? "1 hour" : hours + " hours"}`
        : `${hours === 1 ? "1 hour" : hours + " hours"} ago`;
    }

    if (diffMs < MS_IN_MONTH) {
      const days = Math.round(diffMs / MS_IN_DAY);
      if (days === 1) return diff > 0 ? "tomorrow" : "yesterday";
      return diff > 0 ? `in ${days} days` : `${days} days ago`;
    }

    return false; // otherwise we will show the exact date
  }

  function setTimestamp(element) {
    const timestamp = element.getAttribute("data-timestamp");
    const relativeTime = getRelativeTime(timestamp);
    element.textContent = relativeTime || formatDate(timestamp, false);
    element.title = formatDate(timestamp);
    element.dataset.timeProcessed = "true";
  }

  function updateTimeElements() {
    const timeElements = document.querySelectorAll("time[data-timestamp]");
    timeElements.forEach((element) => {
      if (!element.dataset.timeProcessed) {
        setTimestamp(element);
      }
    });
  }

  // spacehey updates its timestamps every 6 seconds, resulting in replacing our changes
  // we need to observe the elements and update them again
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.target.matches("time[data-timestamp]")) {
        const element = mutation.target;
        if (element.dataset.timeProcessed === "true") {
          setTimestamp(element);
        }
      }
    });
  });

  // required for IMs since timestamps are loaded dynamically
  const documentObserver = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          const timeElements = node.querySelectorAll("time[data-timestamp]");
          timeElements.forEach((element) => {
            setTimestamp(element);
            observer.observe(element, { characterData: true, childList: true });
          });
        }
      });
    });
  });

  documentObserver.observe(document.body, { childList: true, subtree: true });
  updateTimeElements();

  document.querySelectorAll("time[data-timestamp]").forEach((element) => {
    observer.observe(element, { characterData: true, childList: true });
  });
})();