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);
})();