Skip to content

Instantly share code, notes, and snippets.

@raelga
Created August 25, 2026 14:39
Show Gist options
  • Select an option

  • Save raelga/18cce3b479be63ccdf55103fd68720ee to your computer and use it in GitHub Desktop.

Select an option

Save raelga/18cce3b479be63ccdf55103fd68720ee to your computer and use it in GitHub Desktop.
Archive LinkedIn conversations whose latest activity is older than one month
(async () => {
const DELAY_MS = 1200;
const ACTION_TIMEOUT_MS = 5000;
const cutoff = new Date();
cutoff.setHours(0, 0, 0, 0);
cutoff.setMonth(cutoff.getMonth() - 1);
if (
!location.hostname.endsWith("linkedin.com") ||
!location.pathname.startsWith("/messaging")
) {
throw new Error("Open https://www.linkedin.com/messaging/ first.");
}
if (!confirm(
`Archive conversations last active before ${cutoff.toLocaleDateString()}?`
)) return;
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function waitFor(predicate, timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) return true;
await sleep(100);
}
return false;
}
function isVisible(element) {
if (!element) return false;
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return (
style.display !== "none" &&
style.visibility !== "hidden" &&
rect.width > 0 &&
rect.height > 0
);
}
const list = document.querySelector(
".msg-conversations-container__conversations-list, " +
'[class*="conversations-list"]'
);
if (!list) {
throw new Error("Conversation list not found. LinkedIn changed its UI.");
}
function findScroller(element) {
for (let node = element; node; node = node.parentElement) {
const style = getComputedStyle(node);
if (
/(auto|scroll)/.test(style.overflowY) &&
node.scrollHeight > node.clientHeight
) return node;
}
throw new Error("Scrollable conversation container not found.");
}
function parseDate(value) {
if (!value) return null;
const text = value.trim();
const now = new Date();
if (/^today$/i.test(text)) return now;
if (/^yesterday$/i.test(text)) {
return new Date(now.getTime() - 86400000);
}
const relative = text.match(/^(\d+)\s*(min|m|h|d|w|mo|y)$/i);
if (relative) {
const amount = Number(relative[1]);
const unit = relative[2].toLowerCase();
const milliseconds = {
min: 60000,
m: 60000,
h: 3600000,
d: 86400000,
w: 604800000,
mo: 2629800000,
y: 31557600000
}[unit];
return new Date(now.getTime() - amount * milliseconds);
}
if (/^\d{4}-\d{2}-\d{2}/.test(text) || /\b\d{4}\b/.test(text)) {
const parsed = new Date(text);
if (!Number.isNaN(parsed.getTime())) return parsed;
}
const months = {
jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11
};
const shortDate = text.match(/^([a-z]{3,9})\s+(\d{1,2})$/i);
if (!shortDate) return null;
const month = months[shortDate[1].slice(0, 3).toLowerCase()];
if (month === undefined) return null;
const result = new Date(now.getFullYear(), month, Number(shortDate[2]));
if (result > now) result.setFullYear(result.getFullYear() - 1);
return result;
}
function rowDate(row) {
const time = row.querySelector("time");
if (!time) return null;
for (const value of [
time.getAttribute("datetime"),
time.getAttribute("title"),
time.getAttribute("aria-label"),
time.textContent
]) {
const parsed = parseDate(value);
if (parsed) return parsed;
}
return null;
}
function rowName(row) {
return row.querySelector(
".msg-conversation-listitem__participant-names, " +
'[class*="participant-names"]'
)?.textContent.trim() || "Unnamed conversation";
}
function rowKey(row) {
const date = row.querySelector("time")?.textContent.trim() || "";
const snippet = row.querySelector(
".msg-conversation-card__message-snippet"
)?.textContent.trim() || "";
return `${rowName(row)}|${date}|${snippet}`;
}
function conversationRows() {
const cards = [...list.querySelectorAll(".msg-conversation-card")];
if (cards.length) return cards.filter(row => row.querySelector("time"));
return [...list.querySelectorAll("li")].filter(row => row.querySelector("time"));
}
function menuButton(row) {
return row.querySelector(
"button.msg-thread-actions__control, " +
"button.msg-conversation-card__dropdown-trigger, " +
"button.artdeco-dropdown__trigger"
) || [...row.querySelectorAll("button")].find(button =>
/open the options list/i.test(button.textContent)
);
}
function visibleArchiveAction() {
const openDropdown =
":is(.msg-thread-actions__dropdown-options, " +
".msg-thread-actions__dropdown-options--inbox-shortcuts)" +
".artdeco-dropdown__content--is-open[aria-hidden='false']";
return [...document.querySelectorAll(
`${openDropdown} ` +
"[data-view-name='message-toolbar-dropdown-toggle-archive']"
)].find(isVisible) || [...document.querySelectorAll(
`${openDropdown} [role="button"], ` +
`${openDropdown} button, ` +
`${openDropdown} [role="menuitem"]`
)].find(element =>
isVisible(element) &&
/^archive(?: conversation)?$/i.test(element.textContent.trim())
);
}
const scroller = findScroller(list);
const seen = new Set();
let archived = 0;
let idleRounds = 0;
console.log(
"[Started]",
`Archiving conversations older than ${cutoff.toLocaleDateString()}.`
);
scroller.scrollTop = 0;
await sleep(500);
while (idleRounds < 6) {
let discovered = false;
let archivedOne = false;
for (const row of conversationRows()) {
const key = rowKey(row);
if (seen.has(key)) continue;
seen.add(key);
discovered = true;
const date = rowDate(row);
if (!date) {
console.warn("Could not parse conversation date:", rowName(row), row);
continue;
}
console.log("[Scanned]", rowName(row), date.toLocaleDateString());
if (date >= cutoff) continue;
const name = rowName(row);
row.scrollIntoView({ block: "center" });
row.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
await sleep(150);
const menu = menuButton(row);
if (!menu) {
throw new Error(`Menu button not found for "${name}".`);
}
menu.click();
const menuOpened = await waitFor(
() => menu.getAttribute("aria-expanded") === "true" ||
Boolean(visibleArchiveAction()),
2000
);
if (!menuOpened) {
throw new Error(`Options menu did not open for "${name}".`);
}
const archiveAction = visibleArchiveAction();
if (!archiveAction) {
const visibleOptions = [...document.querySelectorAll(
".artdeco-dropdown__content"
)]
.filter(isVisible)
.map(element => element.textContent.trim())
.filter(Boolean)
.join(" | ");
throw new Error(
`Archive action not found for "${name}". Visible options: ${visibleOptions}`
);
}
console.log("[Archiving]", name, date.toLocaleDateString());
archiveAction.click();
const actionAccepted = await waitFor(
() =>
!row.isConnected ||
!list.contains(row) ||
menu.getAttribute("aria-expanded") !== "true" ||
!isVisible(archiveAction),
ACTION_TIMEOUT_MS
);
if (!actionAccepted) {
throw new Error(
`LinkedIn did not accept the Archive click for "${name}".`
);
}
archived++;
archivedOne = true;
console.log("[Archive submitted]", name);
await sleep(DELAY_MS);
break;
}
if (archivedOne) {
idleRounds = 0;
continue;
}
const previousTop = scroller.scrollTop;
scroller.scrollBy({ top: scroller.clientHeight * 0.8 });
await sleep(700);
if (!discovered && scroller.scrollTop === previousTop) idleRounds++;
else idleRounds = 0;
}
console.log(`Complete: ${archived} conversations submitted for archiving.`);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment