Nextdoor.com - Hide annoying content in the News Feed

This is a content filter that will hide annoying content in the Nextdoor News Feed and add an instant "Hide" button to all posts that are shown. Customize with enable-flags and title phrases. It seems like about 99% of the content on Nextdoor is garbage but that 1% of useful posts are worth digging through all the junk. This script helps improve the ratio of gold to garbage.

目前為 2020-01-06 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Nextdoor.com - Hide annoying content in the News Feed
// @namespace    Lepricon
// @version      0.3.1
// @description  This is a content filter that will hide annoying content in the Nextdoor News Feed and add an instant "Hide" button to all posts that are shown. Customize with enable-flags and title phrases. It seems like about 99% of the content on Nextdoor is garbage but that 1% of useful posts are worth digging through all the junk. This script helps improve the ratio of gold to garbage.
// @author       Lepricon
// @match        https://nextdoor.com/*
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM.setValue
// @grant        GM.getValue
// @run-at       document-start
// jshint esversion: 6
// ==/UserScript==

(() => {
    'use strict';

    const getValue = (settingName, defaultValue) => {
        let value = undefined;
        if (window.GM_getValue) {
            value = GM_getValue(settingName);
        }

        if (value === undefined) {
            value = defaultValue;

            if (window.GM_setValue) {
                GM_setValue(settingName, value);
            }
        }

        return value;
    }

    // --- Configurations start
    const enableHidePaidAds = getValue("enableHidePaidAds", true);
    const enableHideSponsoredAds = getValue("enableHideSponsoredAds", true);
    const enableHideNewNeighborAnnouncements = getValue("enableHideNewNeighborAnnouncements", true);
    const enableHideNonFreeClassifiedAds = getValue("enableHideNonFreeClassifiedAds", true);
    const enableAddHideLinkToPosts = getValue("enableAddHideLinkToPosts", true);

    // The following setting will hide any posts that have a matching tag set. Must be all lower-case.
    const hideTaggedInterestNames = getValue("hideTaggedInterestNames", ["dogs", "cats", "yoga", "animal adoption"]);

    // The following setting will hide any posts that have titles that contain any of these key phrases. Use array for multiple words in any order. Must be all lower-case.
    const hideKeyPhrases = getValue("hideKeyPhrases", [["dog", "lost"], ["dog", "missing"], ["dog", "found"], ["dog", "loose"], ["dog", "walk"], ["cat", "lost"], ["cat", "missing"], ["cat", "found"], "coyote", "handyman", "plumber", "roommate", "electrician"]);

    // The following setting will hide any posts with any of these author names. Name match exactly, including case. Useful for those sneaky "pay to read" news sites like San Diego Union-Tribune that otherwise look like legitimate posts.
    const hideAuthorsNames = getValue("hideAuthorsNames", ["The San Diego Union-Tribune"]);

    // Set the following setting true to send a request to the nextdoor server to hide posts permanently. Only applies to key phrases, authors, and individual "new neighbor announcements".
    const enablePermanentHide = getValue("enablePermanentHide", false);
    // --- Configurations end

    const shouldHidePostItem = (postItem) => {
        if (enableHideNonFreeClassifiedAds) {
            // Single classified item
            if (postItem.classified && (postItem.classified.price || "").toLowerCase() !== "free") {
                return true;
            }

            // Multiple classified items
            if (postItem.classified_items && postItem.classified_items.filter((classifiedItem) => (classifiedItem.price || "").toLowerCase() === "free").length === 0) { // count of free items
                return true;
            }
        }

        return false;
    }

    const shouldHideFeedItem = (feedItem) => {
        if (enableHideNewNeighborAnnouncements) {
            // Multiple people
            if (feedItem.nmas && feedItem.nmas.find((nma) => nma.presentation_features && nma.presentation_features.welcome)) {
                return true;
            }

            // Single person
            if (feedItem.presentation_features && feedItem.presentation_features.welcome) {
                return true;
            }
        }

        if (hideAuthorsNames && hideAuthorsNames.length > 0) {
            if (feedItem.author && feedItem.author.name) {
                if (hideAuthorsNames.includes(feedItem.author.name)) {
                    return true;
                }
            }
        }

        if (hideTaggedInterestNames && hideTaggedInterestNames.length > 0) {
            if (feedItem.tagged_interest && feedItem.tagged_interest.name) {
                if (hideTaggedInterestNames.includes(feedItem.tagged_interest.name.toLowerCase())) {
                    return true;
                }
            }
        }

        if (hideKeyPhrases && hideKeyPhrases.length > 0) {
            if (feedItem.subject) {
                const subject = feedItem.subject.toLowerCase();
                const keyPhraseStrings = hideKeyPhrases.filter((keyPhrase) => typeof keyPhrase === "string");
                if (keyPhraseStrings.find((keyPhraseString) => subject.includes(keyPhraseString))) {
                    return true;
                }

                const keyPhraseArrays = hideKeyPhrases.filter((keyPhrase) => Array.isArray(keyPhrase));
                if (keyPhraseArrays.find((keyPhraseArray) => keyPhraseArray.filter((keyPhrase) => subject.includes(keyPhrase)).length === keyPhraseArray.length)) {
                    return true;
                }
            }
        }

        return false;
    }

    // create XMLHttpRequest proxy object
    var oldXMLHttpRequest = unsafeWindow.XMLHttpRequest;

    // define constructor for an proxy object to intercept all AJAX traffic for this site so we can filter it
    unsafeWindow.XMLHttpRequest = function() {
        var actual = new oldXMLHttpRequest();
        var self = this;

        this.onreadystatechange = null;

        // this is the actual handler on the real XMLHttpRequest object
        actual.onreadystatechange = function() {
            if (this.readyState == 4) {
                // Intercept the responses
                // actual.responseText is the ajax result

                // Only apply to the actual news_feed. Email article links contain the base news_feed URL so we do not want to block those.
                const isTargetPage = unsafeWindow.location.href === "https://nextdoor.com/news_feed/"
                        || unsafeWindow.location.href === "https://nextdoor.com/news_feed/?"
                        || unsafeWindow.location.href.includes("nextdoor.com/news_feed/?ordering=");

                if (!actual.responseText) {
                    self.responseText = actual.responseText;
                }
                else {
                    try {
                        let changes = 0;
                        let responseJson;

                        if (isTargetPage) {
                            responseJson = JSON.parse(actual.responseText);

                            if (responseJson.posts && responseJson.posts.length > 0) {
                                for (let i = responseJson.posts.length - 1; i >= 0; i--) {
                                    const postItem = responseJson.posts[i];
                                    if (postItem && shouldHidePostItem(postItem)) {
                                        responseJson.posts.splice(i, 1);
                                        responseJson.feed_items.splice(i, 1);
                                        changes++;
                                        if (enablePermanentHide) {
                                            if (postItem.classified && postItem.classified.id) {
                                                const postId = postItem.classified.id;
                                                setTimeout(() => {
                                                    try {
                                                        unsafeWindow.jQuery.post(`/api/classifieds/${postId}/hide`); // tell the server to hide this post
                                                    }
                                                    catch (error) {
                                                        console.log(`Post to /api/classifieds/${postId}/hide failed.`)
                                                    }
                                                }, 250);
                                            }
                                        }
                                    }
                                }
                            }

                            if (responseJson.feed_items && responseJson.feed_items.length > 0) {
                                for (let i = responseJson.feed_items.length - 1; i >= 0; i--) {
                                    const feedItem = responseJson.feed_items[i];
                                    if (feedItem && shouldHideFeedItem(feedItem)) {
                                        responseJson.posts.splice(i, 1);
                                        responseJson.feed_items.splice(i, 1);
                                        changes++;
                                        if (enablePermanentHide) {
                                            if (feedItem.nmas) {
                                                for (let j = 0; j < feedItem.nmas.length; j++) {
                                                    const nma = feedItem.nmas[j];
                                                    if (!nma) {
                                                        continue;
                                                    }

                                                    const postId = nma.id;
                                                    if (!postId) {
                                                        continue;
                                                    }
                                                    setTimeout(() => {
                                                        try {
                                                            unsafeWindow.jQuery.post("/ajax/confirm_mute_post/", `post_id=${postId}`); // tell the server to hide this post
                                                        }
                                                        catch (error) {
                                                            console.log(`Post to /ajax/confirm_mute_post/ for post_id=${postId} failed.`)
                                                        }
                                                    }, 250);
                                                }
                                            }
                                            else if (feedItem.id) {
                                                const postId = feedItem.paged_comments.post_id;
                                                setTimeout(() => {
                                                    try {
                                                        unsafeWindow.jQuery.post("/ajax/confirm_mute_post/", `post_id=${postId}`); // tell the server to hide this post
                                                    }
                                                    catch (error) {
                                                        console.log(`Post to /ajax/confirm_mute_post/ for post_id=${postId} failed.`)
                                                    }
                                                }, 250);
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        if (changes > 0) {
                            self.responseText = JSON.stringify(responseJson);
                        }
                        else {
                            self.responseText = actual.responseText;
                        }

                        setTimeout(() => {
                            if (enableAddHideLinkToPosts) {
                                addHideLinkToPosts();
                            }
                            // Do some imperceptible micro scrolling to load more data. Nextdoor looks for scrolling to determine if it should try to load more data. Will only load if the "Load More" area is visible at the bottom of the posts.
                            unsafeWindow.scrollBy(0, 1);
                            unsafeWindow.scrollBy(0, -1);
                        }, 100);
                    }
                    catch (error) {
                        self.responseText = actual.responseText; // Let the actual response pass through
                    }
                }
            }
            if (self.onreadystatechange) {
                return self.onreadystatechange();
            }
        };

        // add all proxy getters
        ["status", "statusText", "responseType", "response",
         "readyState", "responseXML", "upload"].forEach(function(item) {
            Object.defineProperty(self, item, {
                get: function() {return actual[item];}
            });
        });

        // add all proxy getters/setters
        ["ontimeout, timeout", "withCredentials", "onload", "onerror", "onprogress"].forEach(function(item) {
            Object.defineProperty(self, item, {
                get: function() {return actual[item];},
                set: function(val) {actual[item] = val;}
            });
        });

        // add all pure proxy pass-through methods
        ["addEventListener", "send", "open", "abort", "getAllResponseHeaders",
         "getResponseHeader", "overrideMimeType", "setRequestHeader"].forEach(function(item) {
            Object.defineProperty(self, item, {
                value: function() {return actual[item].apply(actual, arguments);}
            });
        });
    }

    let oldXHROpen = unsafeWindow.XMLHttpRequest.prototype.open;
    unsafeWindow.XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
        // do something with the method, url and etc.
        this.addEventListener('load', function() {
            // do something with the response text
            this.responseText = "";
            console.log('load: ' + this.responseText);
        });

        return oldXHROpen.apply(this, arguments);
    }

    const addHideLinkToPosts = () => {
        const $ = unsafeWindow.jQuery; // Use jQuery object provided by the site

        const allPosts = $("article.post-container, div.classifieds-single-item-content");
        allPosts.each((index, el) => {
            const post = $(el);
            const existingHideLink = post.find("a.addon-hide-link");
            if (existingHideLink.length > 0) {
                return; // The ide link already exists
            }

            // Add a "Hide" link since it does not already exist
            const caretMenu = post.find("div.story-caret-menu").first();
            const hideLink = $('<span><a class="addon-hide-link" href="javascript:void(0)">Hide</a> &nbsp; </span>');
            hideLink.click(() => {
                if (post.hasClass("classifieds-single-item-content")) {
                    const postId = post.parent().attr("id").substring(2);
                    $.post(`/api/classifieds/${postId}/hide`); // tell the server to hide this post
                }
                else {
                    const postId = post.attr("id").substring(2);
                    $.post("/ajax/confirm_mute_post/", `post_id=${postId}`); // tell the server to hide this post
                }

                post.remove(); // hide the post now
            });
            hideLink.insertBefore(caretMenu);
        });
    }

    // CSS styles to hide ads
    let styleTag = " div.feed-container { min-height: 2000px !important; } ";
    if (enableHidePaidAds) {
        styleTag += " article.gam-ad-outer-container { display: none !important; } ";
    }

    if (enableHideSponsoredAds) {
        styleTag += " div.ad-wrapper, div.programmatic-promo-container { display: none !important; } ";
    }

    if (styleTag) {
        styleTag = `<style>${styleTag}</style>`;
        const head = document.querySelector("head").innerHTML += styleTag;
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址