Last active
July 30, 2026 15:59
-
-
Save mootari/bf4c7fa1649a3670953c0731006353ce to your computer and use it in GitHub Desktop.
Bookmarklet to show latest channel activity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Run as content snippet to test and to obtain the bookmarklet URL | |
| function run() { | |
| const stores = ((s = {}) => (webpackChunkdiscord_app.push( | |
| [[Symbol()], {}, r => { | |
| for(const c of Object.values(r.c ?? {})) { | |
| for(const e of Object.values(c?.exports ?? {})) { | |
| if(!e || typeof e !== "object") continue; | |
| if(e[Symbol.toStringTag] === "IntlMessagesProxy") continue; | |
| if(e._dispatchToken) s[e.getName()] = e; | |
| } | |
| } | |
| }] | |
| ), s))(); | |
| const tsOf = sfId => Number((BigInt(sfId) >> 22n) + 1420070400000n); | |
| const iconUrlOf = guild => `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=160&quality=lossless`; | |
| const HOUR = 60 * 60 * 1000; | |
| const cutoffTs = Date.now() - 12 * HOUR; | |
| const hoursAgo = ts => (Date.now() - ts) / HOUR; | |
| // guilds in sidebar order | |
| const guildIds = stores.SortedGuildStore.getFlattenedGuildIds(); | |
| const guilds = Object.fromEntries(guildIds.map(id => [id, stores.GuildStore.getGuild(id)])); | |
| const readStates = Object.fromEntries( | |
| Array.from(stores.ReadStateStore.getReadStatesByChannel(), ([cId, c]) => [cId, { | |
| unread: c.unreadCount, | |
| lastTs: c._lastMessageTimestamp, | |
| unreadTs: c._oldestUnreadMessageId ? tsOf(c._oldestUnreadMessageId) : null, | |
| }]) | |
| ); | |
| const channelsOf = gId => Object.values(stores.ChannelStore.getMutableGuildChannelsForGuild(gId)); | |
| const isMuted = (gId, cId) => stores.UserGuildSettingsStore.isChannelMuted(gId, cId); | |
| const relTime = ts => { | |
| const d = Math.round((Date.now() - ts) / 60_000); | |
| const s = d < 60 ? `${d}m` : `${(d / 60).toFixed(0)}h`; | |
| return `${s} ago`; | |
| }; | |
| const absTime = ts => new Date(ts).toString(); | |
| const urlOf = (gId, cId) => `https://discord.com/channels/${gId}/${cId}`; | |
| const rows = guildIds.flatMap( | |
| gId => channelsOf(gId) | |
| .filter(c => readStates[c.id]?.lastTs >= cutoffTs && !isMuted(gId, c.id)) | |
| .map(c => { | |
| const {unread, lastTs} = readStates[c.id]; | |
| return { | |
| guild: guilds[gId].name, | |
| channel: c.name, | |
| unread, | |
| lastTs, | |
| url: urlOf(gId, c.id), | |
| //__guild: guilds[gId], | |
| //__channel: c, | |
| } | |
| }).sort((a, b) => b.lastTs - a.lastTs) | |
| ); | |
| const css = ` | |
| body { font: 13px sans-serif; } | |
| th, td { padding: 4px 8px; } | |
| th { cursor: pointer } | |
| tbody tr:nth-child(2n) { background: #eee; } | |
| td:nth-child(3), td:nth-child(4) { text-align: right } | |
| `; | |
| const srcUrl = URL.createObjectURL(new Blob([`<!doctype html><head><style>${css}`], {type: "text/html"})); | |
| const w = window.open(srcUrl, "_blank"); | |
| w.onload = () => { | |
| w.document.title = "Discord - Latest channels"; | |
| const h = (tag, props, children) => { | |
| const el = w.document.createElement(tag); | |
| if(props) for(const [k, v] of Object.entries(props)) typeof v === "string" ? el.setAttribute(k, v) : el[k] = v; | |
| if(children) Array.isArray(children) ? el.append(...children) : el.append(children); | |
| return el; | |
| }; | |
| const compareStr = key => (a, b) => a[key].localeCompare(b[key]); | |
| const compareNum = key => (a, b) => (b[key] ?? 0) - (a[key] ?? 0); | |
| const sortBy = fn => render(rows.slice().sort(fn)); | |
| const render = rows => w.document.body.replaceChildren( | |
| h("table", null, [ | |
| h("tr", null, [ | |
| h("th", {onclick: () => sortBy(compareStr("guild"))}, "Server"), | |
| h("th", {onclick: () => sortBy(compareStr("channel"))}, "Channel"), | |
| h("th", {onclick: () => sortBy(compareNum("unread"))}, "Unread"), | |
| h("th", {onclick: () => sortBy(compareNum("lastTs"))}, "Latest"), | |
| h("th", {onclick: () => sortBy(() => 0)}, ""), | |
| ]), | |
| h("tbody", null, rows.map(r => h("tr", null, [ | |
| h("td", null, r.guild), | |
| h("td", null, r.channel), | |
| h("td", null, r.unread), | |
| h("td", null, h("span", {title: absTime(r.lastTs)}, relTime(r.lastTs))), | |
| h("td", null, h("a", {target: "_blank", href: r.url}, ["Open"])) | |
| ]))) | |
| ]) | |
| ); | |
| render(rows); | |
| }; | |
| } | |
| run(); | |
| console.log({bookmarkletUrl: "javascript:" + encodeURIComponent(`(${run})()`)}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment