// ==UserScript==
// @name Declutter Pinterest
// @namespace August4067
// @version 1.0.0-alpha
// @description Removes intrusive Pinterest shopping promotions, ads, and clutter, and makes the website more user-friendly
// @license MIT
// @match https://www.pinterest.com/*
// @match https://*.pinterest.com/*
// @match https://*.pinterest.co.uk/*
// @match https://*.pinterest.fr/*
// @match https://*.pinterest.de/*
// @match https://*.pinterest.ca/*
// @match https://*.pinterest.jp/*
// @match https://*.pinterest.it/*
// @match https://*.pinterest.au/*
// @icon https://www.pinterest.com/favicon.ico
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @sandbox Javascript
// ==/UserScript==
/*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
that detects and handles AJAXed content.
Usage example:
waitForKeyElements (
"div.comments"
, commentCallbackFunction
);
//--- Page-specific function to do what we want when the node is found.
function commentCallbackFunction (jNode) {
jNode.text ("This comment changed by waitForKeyElements().");
}
IMPORTANT: This function requires your script to have loaded jQuery.
*/
// Pulled from: https://gist.github.com/raw/2625891/waitForKeyElements.js
function waitForKeyElements(
selectorTxt /* Required: The jQuery selector string that
specifies the desired element(s).
*/,
actionFunction /* Required: The code to run when elements are
found. It is passed a jNode to the matched
element.
*/,
bWaitOnce /* Optional: If false, will continue to scan for
new elements even after the first match is
found.
*/,
iframeSelector /* Optional: If set, identifies the iframe to
search.
*/,
) {
var targetNodes, btargetsFound;
if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt);
else targetNodes = $(iframeSelector).contents().find(selectorTxt);
if (targetNodes && targetNodes.length > 0) {
btargetsFound = true;
/*--- Found target node(s). Go through each and act if they
are new.
*/
targetNodes.each(function () {
var jThis = $(this);
var alreadyFound = jThis.data("alreadyFound") || false;
if (!alreadyFound) {
//--- Call the payload function.
var cancelFound = actionFunction(jThis);
if (cancelFound) btargetsFound = false;
else jThis.data("alreadyFound", true);
}
});
} else {
btargetsFound = false;
}
//--- Get the timer-control variable for this selector.
var controlObj = waitForKeyElements.controlObj || {};
var controlKey = selectorTxt.replace(/[^\w]/g, "_");
var timeControl = controlObj[controlKey];
//--- Now set or clear the timer as appropriate.
if (btargetsFound && bWaitOnce && timeControl) {
//--- The only condition where we need to clear the timer.
clearInterval(timeControl);
delete controlObj[controlKey];
} else {
//--- Set a timer, if needed.
if (!timeControl) {
timeControl = setInterval(function () {
waitForKeyElements(
selectorTxt,
actionFunction,
bWaitOnce,
iframeSelector,
);
}, 300);
controlObj[controlKey] = timeControl;
}
}
waitForKeyElements.controlObj = controlObj;
}
// We will set the Pinterest page title to this, to remove
// the flashing title notifications like Pinterest (2)
const ORIGINAL_TITLE = "Pinterest";
const SETTINGS_CONFIG = {
disableVideoAutoplay: {
displayName: "Disable video autoplay",
default: true,
},
removeShoppablePins: {
displayName: "Remove shoppable pins",
default: true,
},
};
class Setting {
constructor(name, config) {
this.name = name;
this.displayName = config.displayName;
this.default = config.default;
}
currentValue() {
return GM_getValue(this.name, this.default);
}
toggleSetting() {
GM_setValue(this.name, !this.currentValue());
}
}
// Create settings object by mapping config to Setting instances
const SETTINGS = Object.fromEntries(
Object.entries(SETTINGS_CONFIG).map(([name, config]) => [
name,
new Setting(name, config),
]),
);
// MENU SETTINGS
function toggleMenuSetting(settingName) {
var setting = SETTINGS[settingName];
setting.toggleSetting();
updateSettingsMenu();
console.debug(`Setting ${settingName} set to: ${setting.currentValue()}}`);
location.reload();
}
function updateSettingsMenu() {
for (const [setting_name, setting] of Object.entries(SETTINGS)) {
GM_registerMenuCommand(
`${setting.displayName}: ${setting.currentValue() ? "Enabled" : "Disabled"}`,
() => {
toggleMenuSetting(setting_name);
},
);
}
}
// HELPER FUNCTIONS
function waitAndRemove(selector, removeFunction) {
if (removeFunction == undefined) {
removeFunction = (elem) => hideElement(elem);
}
waitForKeyElements(selector, function (node) {
if (node && node.length > 0) {
removeFunction(node[0]);
}
});
}
/**
* Hide an element by setting its display style to "none" if it exists
* @param {HTMLElement} element - The DOM element to hide
*/
function hideElement(element) {
if (element) {
element.style.setProperty("display", "none", "important");
}
}
function isFeaturedBoard(pin) {
if (
pin.textContent.trim().toLowerCase().startsWith("explore featured boards")
) {
return true;
}
return false;
}
function isShoppingCard(pin) {
if (
pin
.querySelector("h2#comments-heading")
?.textContent.toLowerCase()
.startsWith("shop")
) {
return true;
} else if (
pin
.querySelector("a")
?.getAttribute("aria-label")
?.toLowerCase()
.startsWith("shop")
) {
return true;
} else if (
pin.querySelector("h2")?.textContent.trim().toLowerCase().startsWith("shop")
) {
return true;
} else if (pin.textContent.trim().toLowerCase().startsWith("shop similar")) {
return true;
}
return false;
}
function isShoppablePin(pin) {
if (SETTINGS.removeShoppablePins.currentValue()) {
return pin.querySelector('[aria-label="Shoppable Pin indicator"]') != null;
}
return false;
}
function isSponsoredPin(pin) {
return pin.querySelector('div[title="Sponsored"]') != null;
}
// FUNCTIONS THAT REMOVE
function removeClutterPins(pins) {
const filters = [
isShoppingCard,
isShoppablePin,
isFeaturedBoard,
isSponsoredPin,
];
pins.forEach((pin) => {
if (filters.some((test) => test(pin))) {
hideElement(pin);
}
});
}
// In the #SuggestionsMenu
function removePopularOnPinterestSearchSuggestions() {
waitAndRemove('div[data-test-id="search-story-suggestions-container"]');
}
function setupSearchSuggestionsRemovalForPopularSuggestions() {
waitAndRemove("#searchBoxContainer", (node) => {
const observer = new MutationObserver(() => {
removePopularOnPinterestSearchSuggestions();
});
observer.observe(node, { childList: true, subtree: true });
});
}
// FUNCTION THAT SETUP OBSERVERS
function setupPinFiltering() {
waitAndRemove('div[role="list"]', (node) => {
var pinListMutationObserver = new MutationObserver(
(mutations, observer) => {
removeClutterPins(node.querySelectorAll('div[role="listitem"]'));
},
);
pinListMutationObserver.observe(node, {
childList: true,
subtree: true,
});
});
}
function setupShopButtonRemovalFromBoardTools() {
waitAndRemove('div[data-test-id="board-tools"] div[data-test-id="Shop"]');
}
function removeExploreTabNotificationsIcon() {
// --- Remove notification icon from Explore tab in the top nav (old behavior)
var exploreTab = document.querySelector('div[data-test-id="today-tab"]');
if (exploreTab) {
var notificationsIcon = exploreTab.querySelector(
'div[aria-label="Notifications"]',
);
hideElement(notificationsIcon);
}
// --- Remove notification badge from Explore tab in the sidebar (new behavior)
// Find the Explore tab link in the sidebar
var exploreTabLink = document.querySelector('a[data-test-id="today-tab"]');
if (exploreTabLink) {
// The parent of the link is the icon container, its parent is the sidebar item
var iconContainer = exploreTabLink.closest('div[class*="XiG"]');
var sidebarItem = iconContainer?.parentElement?.parentElement;
if (sidebarItem) {
// The notification badge is a sibling div with class "MIw" and pointer-events: none
var notificationBadge = sidebarItem.parentElement?.querySelector(
'.MIw[style*="pointer-events: none"]',
);
if (notificationBadge) {
hideElement(notificationBadge);
}
}
}
}
function setupSidebarObserverForExploreNotifications() {
// Find the sidebar navigation container (adjust selector if needed)
const sidebarNav =
document.querySelector('nav[id="VerticalNavContent"]') ||
document.querySelector('div[role="navigation"]');
if (!sidebarNav) {
// Try again later if sidebar not yet loaded
setTimeout(setupSidebarObserverForExploreNotifications, 500);
return;
}
// Remove any existing badge immediately
removeExploreTabNotificationsIcon();
// Set up observer
const observer = new MutationObserver(() => {
removeExploreTabNotificationsIcon();
});
observer.observe(sidebarNav, { childList: true, subtree: true });
}
function removeShopByBanners() {
waitForKeyElements('div[data-test-id="sf-header-heading"]', function (node) {
var shopByBannerAtTopOfBoard = node[0].closest(
'div[class="PKX zI7 iyn Hsu"]',
);
hideElement(shopByBannerAtTopOfBoard);
if (node[0].closest('div[data-test-id="base-board-pin-grid"]')) {
var shopByBannerAtBottomOfBoard = node[0].closest(
'div[class="gcw zI7 iyn Hsu"]',
);
hideElement(shopByBannerAtBottomOfBoard);
}
var shopByBannerAtTopOfSearch = node[0].closest('div[role="listitem"]');
hideElement(shopByBannerAtTopOfSearch);
});
}
function disableVideoAutoplay() {
waitForKeyElements(
"div[role='list'] video",
function (videoNode) {
if (videoNode && videoNode.length > 0) {
videoNode.prop("autoplay", false);
videoNode.prop("muted", false);
videoNode.prop("loop", false);
videoNode.prop("playsinline", false);
videoNode.prop("preload", "metadata");
videoNode[0].pause();
console.debug("Stopped video autoplay");
}
},
false,
);
}
function setupDisablingVideoAutoplay() {
waitForKeyElements(
"div[role='list']",
function (listNode) {
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.addedNodes.length > 0) {
disableVideoAutoplay();
}
}
});
observer.observe(listNode[0], { childList: true, subtree: true });
disableVideoAutoplay();
},
false,
);
}
function main() {
"use strict";
updateSettingsMenu();
setupPinFiltering();
setupShopButtonRemovalFromBoardTools();
setupSidebarObserverForExploreNotifications();
setupSearchSuggestionsRemovalForPopularSuggestions();
removeShopByBanners();
disableVideoAutoplay();
}
main();
let lastUrl = window.location.href;
setInterval(() => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
console.debug(
`Detected new page, currentURL=${currentUrl}, previousURL=${lastUrl}`,
);
lastUrl = currentUrl;
main();
}
}, 750);