LinkedIn Exact Post Date

Show exact post dates on hover for LinkedIn posts

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         LinkedIn Exact Post Date
// @namespace    https://github.com/chr1sx
// @version      1.0.3
// @description  Show exact post dates on hover for LinkedIn posts
// @author       chr1sx
// @match        https://www.linkedin.com/*
// @grant        none
// @license      MIT
// @icon         https://www.google.com/s2/favicons?domain=linkedin.com&sz=64
// ==/UserScript==

(function() {
    'use strict';

    const TIME_PATTERN = /^\s*\d+\s*(?:s|m|h|d|w|mo|yr|y)\b/i;
    const LINKEDIN_EPOCH = new Date('2003-01-01').getTime();
    const ID_PATTERN = /(?:activity|ugcPost|share)[:-](\d{18,19})/;

    function extractTimestampFromId(id) {
        try {
            const binaryStr = BigInt(id).toString(2);
            const timestamp = parseInt(binaryStr.substring(0, 41), 2);
            if (timestamp > LINKEDIN_EPOCH && timestamp <= Date.now()) return timestamp;
        } catch(e) {}
        return null;
    }

    function extractTimestampFromUrl(url) {
        const m = url.match(ID_PATTERN);
        if (m) return extractTimestampFromId(m[1]);
        return null;
    }

    function formatDate(timestamp) {
        return new Date(timestamp).toLocaleString('en-US', {
            year: 'numeric', month: 'short', day: 'numeric',
            hour: '2-digit', minute: '2-digit'
        });
    }

    function getIdFromTrackingScope(attrValue) {
        try {
            const items = JSON.parse(attrValue);
            let postId = null;
            let commentId = null;
            for (const item of items) {
                const dataBytes = item?.breadcrumb?.content?.data;
                if (!Array.isArray(dataBytes)) continue;
                const inner = String.fromCharCode(...dataBytes);
                if (item.topicName === 'CommentServedEvent') {
                    const m = inner.match(/comment:\(urn:li:activity:\d+,(\d{18,19})\)/);
                    if (m) commentId = m[1];
                } else if (item.topicName === 'FeedUpdateServedEvent') {
                    const m = inner.match(ID_PATTERN);
                    if (m) postId = m[1];
                }
            }
            return commentId || postId;
        } catch(e) {}
        return null;
    }

    function findPostUrl(element) {
        const PLAIN_URN_ATTRS = [
            'data-urn', 'data-attributed-urn', 'data-id',
            'data-semaphore-content-urn', 'data-activity-urn', 'data-entity-urn',
        ];

        let node = element;
        while (node && node !== document.body) {
            const ck = node.getAttribute && (node.getAttribute('componentkey') || '');
            const cm = ck.match(/comment:\([^,]+,(\d{18,19})\)/);
            if (cm) return `activity:${cm[1]}`;
            node = node.parentElement;
        }

        let trackingScopeId = null;
        node = element;

        while (node && node !== document.body) {
            if (!node.getAttribute) { node = node.parentElement; continue; }

            for (const attr of PLAIN_URN_ATTRS) {
                const val = node.getAttribute(attr);
                if (val) {
                    const m = val.match(ID_PATTERN);
                    if (m) return `activity:${m[1]}`;
                }
            }

            if (node.querySelectorAll) {
                const reshareLink = node.querySelector('a[data-view-name="feed-original-share-description"]');
                if (reshareLink) {
                    const hasTrackingScope = !!node.querySelector('[data-view-tracking-scope]');
                    if (!hasTrackingScope) {
                        const m = reshareLink.getAttribute('href').match(ID_PATTERN);
                        if (m) return `activity:${m[1]}`;
                    }
                }

                for (const a of node.querySelectorAll(
                    'a[href*="feed/update"]:not([data-view-name="feed-original-share-description"]),' +
                    'a[href*="activity:"]:not([data-view-name="feed-original-share-description"]),' +
                    'a[href*="ugcPost"]:not([data-view-name="feed-original-share-description"])'
                )) {
                    const m = a.getAttribute('href').match(ID_PATTERN);
                    if (m) return `activity:${m[1]}`;
                }
            }

            if (!trackingScopeId) {
                const scope = node.getAttribute('data-view-tracking-scope');
                if (scope) trackingScopeId = getIdFromTrackingScope(scope);
            }

            node = node.parentElement;
        }

        if (trackingScopeId) return `activity:${trackingScopeId}`;
        return window.location.href;
    }

    function isTimeElement(element) {
        const knownClasses = [
            'update-components-actor__sub-description',
            'comment__duration-since',
            'comments-comment-meta__data'
        ];
        if (knownClasses.some(c => element.classList.contains(c))) return true;
        if (element.tagName === 'TIME') return true;

        const directText = Array.from(element.childNodes)
            .filter(n => n.nodeType === Node.TEXT_NODE)
            .map(n => n.textContent)
            .join('');
        return TIME_PATTERN.test(directText);
    }

    function processTimeElements() {
        document.querySelectorAll('time, p, span').forEach(element => {
            if (element.dataset.linkedinDateProcessed) return;
            if (!isTimeElement(element)) return;

            const postUrl = findPostUrl(element);
            if (!postUrl) return;

            const timestamp = extractTimestampFromUrl(postUrl);
            if (!timestamp) return;

            element.title = formatDate(timestamp);
            element.dataset.linkedinDateProcessed = 'true';
            element.style.cursor = 'help';
        });
    }

    setTimeout(processTimeElements, 1500);
    const observer = new MutationObserver(() => processTimeElements());
    observer.observe(document.body, { childList: true, subtree: true });
    setInterval(processTimeElements, 3000);
})();