Created
April 3, 2026 18:20
-
-
Save abear1538/eb801c21dbf33130667dc91c1a5ac835 to your computer and use it in GitHub Desktop.
Nextdoor Cleanup Userscriptt
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
| // ==UserScript== | |
| // @name Nextdoor - Enhanced Feed Filter | |
| // @namespace https://greasyfork.org/users/majik | |
| // @version 2.1.0 | |
| // @description Hides ads, sponsored content, and specific keywords/authors from the Nextdoor feed. Adds a quick "Hide" button to posts and a graphical settings panel. | |
| // @author Gemini / Cleanup: majik | |
| // @match https://nextdoor.com/* | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // --- Configuration Defaults --- | |
| const DEFAULT_SETTINGS = { | |
| hidePaidAds: true, | |
| hidePromos: true, | |
| hideSponsored: true, | |
| hideNewNeighbors: true, | |
| hideNonFreeClassifieds: true, | |
| addHideLink: true, | |
| hideTaggedInterests: ['dogs', 'cats', 'yoga', 'animal adoption'], | |
| hideKeyPhrases: [ | |
| 'dog lost', 'dog missing', 'dog found', 'dog loose', | |
| 'cat lost', 'cat missing', 'cat found', | |
| 'coyote', 'handyman', 'plumber', 'roommate', 'electrician' | |
| ], | |
| hideAuthors: ['The San Diego Union-Tribune'] | |
| }; | |
| // Load Settings | |
| let settings = { ...DEFAULT_SETTINGS, ...GM_getValue('nd_settings', {}) }; | |
| // --- CSS Injection --- | |
| const style = document.createElement('style'); | |
| style.innerHTML = ` | |
| .gm-hide-btn { | |
| color: #6d6e71; | |
| font-size: 13px; | |
| cursor: pointer; | |
| margin-left: 10px; | |
| text-decoration: underline; | |
| } | |
| .gm-hide-btn:hover { color: #000; } | |
| .gm-hidden-post { display: none !important; } | |
| /* Settings Panel Styles */ | |
| #gm-settings-panel { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: white; | |
| border: 1px solid #ccc; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.2); | |
| z-index: 99999; | |
| width: 450px; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| padding: 20px; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| display: none; | |
| } | |
| #gm-settings-overlay { | |
| position: fixed; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 99998; | |
| display: none; | |
| } | |
| .gm-settings-header { | |
| font-size: 18px; | |
| font-weight: bold; | |
| margin-bottom: 15px; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 10px; | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .gm-settings-section { margin-bottom: 15px; } | |
| .gm-settings-section label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; } | |
| .gm-settings-textarea { | |
| width: 100%; | |
| height: 80px; | |
| margin-top: 5px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| padding: 8px; | |
| font-size: 12px; | |
| resize: vertical; | |
| } | |
| .gm-settings-footer { | |
| margin-top: 20px; | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 10px; | |
| } | |
| .gm-btn { | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| border: none; | |
| cursor: pointer; | |
| font-weight: 600; | |
| } | |
| .gm-btn-save { background: #2e8231; color: white; } | |
| .gm-btn-cancel { background: #eee; color: #333; } | |
| `; | |
| document.head.appendChild(style); | |
| // --- Helpers --- | |
| const saveSettings = (newSettings) => { | |
| settings = newSettings; | |
| GM_setValue('nd_settings', settings); | |
| }; | |
| const containsPhrases = (text, phrases) => { | |
| const lowerText = text.toLowerCase(); | |
| return phrases.some(phrase => { | |
| if (Array.isArray(phrase)) { | |
| return phrase.every(word => lowerText.includes(word.toLowerCase())); | |
| } | |
| return lowerText.includes(phrase.toLowerCase()); | |
| }); | |
| }; | |
| // --- Settings UI Logic --- | |
| const createSettingsPanel = () => { | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'gm-settings-overlay'; | |
| const panel = document.createElement('div'); | |
| panel.id = 'gm-settings-panel'; | |
| panel.innerHTML = ` | |
| <div class="gm-settings-header"> | |
| <span>Filter Settings</span> | |
| <span style="cursor:pointer" id="gm-close-x">×</span> | |
| </div> | |
| <div class="gm-settings-section"> | |
| <label><input type="checkbox" id="gm-hideSponsored" ${settings.hideSponsored ? 'checked' : ''}> Hide Sponsored Ads</label> | |
| </div> | |
| <div class="gm-settings-section"> | |
| <label><input type="checkbox" id="gm-hideNonFreeClassifieds" ${settings.hideNonFreeClassifieds ? 'checked' : ''}> Hide Non-Free Classifieds</label> | |
| </div> | |
| <div class="gm-settings-section"> | |
| <label><input type="checkbox" id="gm-addHideLink" ${settings.addHideLink ? 'checked' : ''}> Show "Quick Hide" on posts</label> | |
| </div> | |
| <div class="gm-settings-section"> | |
| <div style="font-size: 13px; font-weight: 600;">Keywords to Hide (one per line):</div> | |
| <textarea id="gm-hideKeyPhrases" class="gm-settings-textarea">${settings.hideKeyPhrases.join('\n')}</textarea> | |
| </div> | |
| <div class="gm-settings-section"> | |
| <div style="font-size: 13px; font-weight: 600;">Authors to Hide (one per line):</div> | |
| <textarea id="gm-hideAuthors" class="gm-settings-textarea">${settings.hideAuthors.join('\n')}</textarea> | |
| </div> | |
| <div class="gm-settings-footer"> | |
| <button class="gm-btn gm-btn-cancel" id="gm-btn-cancel">Cancel</button> | |
| <button class="gm-btn gm-btn-save" id="gm-btn-save">Save & Reload</button> | |
| </div> | |
| `; | |
| document.body.appendChild(overlay); | |
| document.body.appendChild(panel); | |
| const close = () => { | |
| panel.style.display = 'none'; | |
| overlay.style.display = 'none'; | |
| }; | |
| document.getElementById('gm-btn-cancel').onclick = close; | |
| document.getElementById('gm-close-x').onclick = close; | |
| document.getElementById('gm-btn-save').onclick = () => { | |
| const updated = { | |
| ...settings, | |
| hideSponsored: document.getElementById('gm-hideSponsored').checked, | |
| hideNonFreeClassifieds: document.getElementById('gm-hideNonFreeClassifieds').checked, | |
| addHideLink: document.getElementById('gm-addHideLink').checked, | |
| hideKeyPhrases: document.getElementById('gm-hideKeyPhrases').value.split('\n').filter(s => s.trim()), | |
| hideAuthors: document.getElementById('gm-hideAuthors').value.split('\n').filter(s => s.trim()) | |
| }; | |
| saveSettings(updated); | |
| location.reload(); | |
| }; | |
| }; | |
| const showSettings = () => { | |
| let panel = document.getElementById('gm-settings-panel'); | |
| if (!panel) { | |
| createSettingsPanel(); | |
| panel = document.getElementById('gm-settings-panel'); | |
| } | |
| panel.style.display = 'block'; | |
| document.getElementById('gm-settings-overlay').style.display = 'block'; | |
| }; | |
| // --- Core Logic --- | |
| const processPost = (post) => { | |
| if (post.dataset.gmProcessed) return; | |
| post.dataset.gmProcessed = "true"; | |
| const postText = post.innerText || ""; | |
| let shouldHide = false; | |
| let hideReason = ""; | |
| // 1. Sponsored / Ad Detection | |
| const isSponsored = postText.includes('Sponsored') || | |
| post.querySelector('[class*="sponsored"]') || | |
| post.querySelector('svg[aria-label="Sponsored"]'); | |
| if (settings.hideSponsored && isSponsored) { | |
| shouldHide = true; | |
| hideReason = "Sponsored Content"; | |
| } | |
| // 2. Keyword/Phrase Filter | |
| if (!shouldHide && containsPhrases(postText, settings.hideKeyPhrases)) { | |
| shouldHide = true; | |
| hideReason = "Keyword Match"; | |
| } | |
| // 3. Author Filter | |
| if (!shouldHide && settings.hideAuthors.some(author => author && postText.includes(author))) { | |
| shouldHide = true; | |
| hideReason = "Blocked Author"; | |
| } | |
| // 4. Classifieds Filter (Non-Free) | |
| if (!shouldHide && settings.hideNonFreeClassifieds && (postText.includes('For Sale') || postText.includes('Classifieds'))) { | |
| const hasFree = postText.toLowerCase().includes('free'); | |
| const hasPrice = /\$\d+/.test(postText); | |
| if (hasPrice && !hasFree) { | |
| shouldHide = true; | |
| hideReason = "Paid Classified"; | |
| } | |
| } | |
| // Apply Hiding | |
| if (shouldHide) { | |
| console.log(`[Nextdoor Filter] Hiding post: ${hideReason}`); | |
| post.classList.add('gm-hidden-post'); | |
| return; | |
| } | |
| // 5. Add Manual Hide Link | |
| if (settings.addHideLink) { | |
| const actionBar = post.querySelector('div[data-testid="post-footer"], .post-footer, [class*="footer"]'); | |
| if (actionBar && !post.querySelector('.gm-hide-btn')) { | |
| const hideBtn = document.createElement('span'); | |
| hideBtn.className = 'gm-hide-btn'; | |
| hideBtn.innerText = 'Quick Hide'; | |
| hideBtn.onclick = (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| post.style.transition = 'opacity 0.3s'; | |
| post.style.opacity = '0'; | |
| setTimeout(() => post.classList.add('gm-hidden-post'), 300); | |
| }; | |
| actionBar.appendChild(hideBtn); | |
| } | |
| } | |
| }; | |
| // --- Observer Engine --- | |
| const observer = new MutationObserver((mutations) => { | |
| for (const mutation of mutations) { | |
| for (const node of mutation.addedNodes) { | |
| if (node.nodeType !== 1) continue; | |
| const posts = node.querySelectorAll('div[data-testid="post-container"], article, [class*="FeedPost"]'); | |
| if (node.matches('div[data-testid="post-container"], article, [class*="FeedPost"]')) { | |
| processPost(node); | |
| } | |
| posts.forEach(processPost); | |
| } | |
| } | |
| }); | |
| const startObserver = () => { | |
| const feedContainer = document.querySelector('main, #newsfeed_wrapper, [role="main"]'); | |
| if (feedContainer) { | |
| observer.observe(feedContainer, { childList: true, subtree: true }); | |
| feedContainer.querySelectorAll('div[data-testid="post-container"], article, [class*="FeedPost"]').forEach(processPost); | |
| } else { | |
| setTimeout(startObserver, 1000); | |
| } | |
| }; | |
| // Menu Commands | |
| GM_registerMenuCommand("Open Filter Settings", showSettings); | |
| GM_registerMenuCommand("Reset to Defaults", () => { | |
| if(confirm("Are you sure you want to reset all filter settings?")) { | |
| GM_setValue('nd_settings', DEFAULT_SETTINGS); | |
| location.reload(); | |
| } | |
| }); | |
| startObserver(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment