└── cohost-dedup.user.js /cohost-dedup.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Cohost Dedup 3 | // @namespace https://nex-3.com 4 | // @version 1.4 5 | // @description Deduplicate posts you've already seen on Cohost 6 | // @author Natalie Weizenbaum 7 | // @match https://cohost.org/* 8 | // @match https://*.cohost.org/* 9 | // @exclude https://cohost.org/*/post/* 10 | // @exclude https://cohost.org/rc/search 11 | // @exclude https://cohost.org/rc/project/* 12 | // @exclude https://cohost.org/rc/user/* 13 | // @exclude https://cohost.org/rc/posts/unpublished* 14 | // @exclude https://cohost.org/rc/liked-posts 15 | // ==/UserScript== 16 | 17 | // Should be compatible with Firefox (desktop and mobile) and Chrome. To use, 18 | // install Tampermonkey from https://www.tampermonkey.net/, then visit 19 | // https://github.com/nex3/cohost-dedup/blob/main/cohost-dedup.user.js and click 20 | // "Raw" in the top right. 21 | 22 | const hiddenChostsHeight = '150px'; 23 | 24 | const style = document.createElement("style"); 25 | style.innerText = ` 26 | @property --cohost-dedup-opacity { 27 | syntax: ''; 28 | initial-value: 1; 29 | inherits: false; 30 | } 31 | 32 | .-cohost-dedup-hidden-chost, .-cohost-dedup-hidden-thread { 33 | display: none; 34 | } 35 | 36 | .-cohost-dedup-hidden-chost.-cohost-dedup-last { 37 | display: block; 38 | height: ${hiddenChostsHeight}; 39 | position: relative; 40 | overflow: hidden; 41 | margin-bottom: -${hiddenChostsHeight}; 42 | } 43 | 44 | .-cohost-dedup-hidden-chost.-cohost-dedup-last > :not(div:not(.flex)) { 45 | display: none; 46 | } 47 | 48 | .-cohost-dedup-hidden-chost.-cohost-dedup-last > div:not(.flex) { 49 | position: absolute; 50 | bottom: 0; 51 | } 52 | 53 | :is(.-cohost-dedup-hidden-chost, .-cohost-dedup-link) + .prose, 54 | :is(.-cohost-dedup-hidden-chost, .-cohost-dedup-link) + .prose + hr { 55 | display: none; 56 | } 57 | 58 | .-cohost-dedup-link { 59 | --cohost-dedup-opacity: 0.5; 60 | color: rgb(130 127 124 / var(--cohost-dedup-opacity)); 61 | font-size: 2rem; 62 | display: block; 63 | text-align: center; 64 | height: ${hiddenChostsHeight}; 65 | padding-top: calc(${hiddenChostsHeight} - 35px); 66 | background: linear-gradient(0deg, 67 | rgb(255 255 255 / calc(1 - var(--cohost-dedup-opacity))), white); 68 | position: relative; 69 | transition: --cohost-dedup-opacity 0.5s; 70 | margin-bottom: 10px; 71 | } 72 | 73 | .-cohost-dedup-link:hover { 74 | --cohost-dedup-opacity: 1; 75 | } 76 | `; 77 | document.head.appendChild(style); 78 | 79 | function getChosts(thread) { 80 | return thread.querySelectorAll(":scope > article > div"); 81 | } 82 | 83 | function getChostLink(chost) { 84 | return chost.querySelector(":scope > :nth-child(2) time > a")?.href ?? 85 | chost.parentElement.querySelector(":scope > header time > a").href; 86 | } 87 | 88 | function hasTags(chost) { 89 | return !!chost.querySelector("a.inline-block.text-gray-400"); 90 | } 91 | 92 | function previousSiblingThroughShowHide(element) { 93 | const prev = element.previousSibling; 94 | if (prev.nodeName !== 'HR') return prev; 95 | 96 | const next = prev.previousSibling; 97 | return next.innerText.match(/^(show|hide) /) ? next.previousSibling : null; 98 | } 99 | 100 | function hideChost(chost) { 101 | chost.classList.add('-cohost-dedup-hidden-chost'); 102 | chost.classList.add('-cohost-dedup-last'); 103 | const prev = previousSiblingThroughShowHide(chost); 104 | if (prev?.classList?.contains("-cohost-dedup-link")) { 105 | prev.previousSibling.classList.remove('-cohost-dedup-last'); 106 | prev.href = getChostLink(chost); 107 | prev.before(chost); 108 | } else { 109 | const a = document.createElement("a"); 110 | a.classList.add("-cohost-dedup-link"); 111 | a.href = getChostLink(chost); 112 | a.innerText = "..."; 113 | chost.after(a); 114 | a.onclick = event => { 115 | const prev = a.previousSibling; 116 | prev.classList.remove("-cohost-dedup-hidden-chost"); 117 | prev.classList.remove("-cohost-dedup-last"); 118 | 119 | const next = previousSiblingThroughShowHide(prev); 120 | if (next?.classList?.contains("-cohost-dedup-hidden-chost")) { 121 | next.classList.add("-cohost-dedup-last"); 122 | next.after(a); 123 | } else { 124 | a.remove(); 125 | } 126 | 127 | return false; 128 | }; 129 | } 130 | 131 | if (chost.nextSibling.nextSibling.nodeName !== 'DIV') { 132 | chost.parentElement.parentElement.parentElement.classList.add( 133 | '-cohost-dedup-hidden-thread'); 134 | } 135 | } 136 | 137 | class SessionStoreSet { 138 | constructor(name) { 139 | this.name = name; 140 | const stored = window.sessionStorage.getItem(name); 141 | this.set = stored === null ? new Set() : new Set(JSON.parse(stored)); 142 | } 143 | 144 | has(value) { 145 | return this.set.has(value); 146 | } 147 | 148 | add(value) { 149 | this.set.add(value); 150 | window.sessionStorage.setItem(this.name, JSON.stringify([...this.set])); 151 | } 152 | } 153 | 154 | const seenChostIds = new SessionStoreSet('-cohost-dedup-seen-chost-ids'); 155 | const shownChostFullIds = 156 | new SessionStoreSet('-cohost-dedup-shown-chost-full-ids'); 157 | function checkThread(thread) { 158 | const threadId = thread.dataset.testid; 159 | if (!threadId) return; 160 | console.log(`Checking ${threadId}`); 161 | 162 | for (const chost of getChosts(thread)) { 163 | const id = getChostLink(chost); 164 | const fullId = `${threadId} // ${id}`; 165 | if (seenChostIds.has(id) && !shownChostFullIds.has(fullId)) { 166 | console.log(`Hiding chost ${id}`); 167 | hideChost(chost); 168 | } else { 169 | seenChostIds.add(id); 170 | shownChostFullIds.add(fullId); 171 | } 172 | } 173 | } 174 | 175 | const observer = new MutationObserver(mutations => { 176 | for (const mutation of mutations) { 177 | for (const node of mutation.addedNodes) { 178 | if (!(node instanceof Element)) continue; 179 | if (node.dataset.view === 'post-preview') { 180 | checkThread(node); 181 | } else { 182 | for (const thread of 183 | node.querySelectorAll('[data-view=post-preview]')) { 184 | checkThread(thread); 185 | } 186 | } 187 | } 188 | } 189 | }); 190 | 191 | observer.observe(document.body, {subtree: true, childList: true}); 192 | --------------------------------------------------------------------------------