Show exact post dates on hover for LinkedIn posts
// ==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);
})();