LinkedIn Exact Post Date

Show exact post dates on hover for LinkedIn posts

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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