Here’s another Tampermonkey script for Gander Social. This is a modification of the script hides posts containing hashtags, but instead only shows posts you’ve liked.
// ==UserScript==
// @name Gander Social - Liked Posts Only
// @namespace https://shawnhooper.ca
// @version 1.2
// @description Toggles the feed between all posts and only posts you've liked
// @author Shawn M. Hooper
// @match https://*.gander.social/*
// @icon https://gander.social/favicon.png
// @run-at document-end
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- Config ---------------------------------------------------------------
// Posts are <article> elements carrying the "post" class.
const POST_SELECTOR = 'article.post';
// A liked post renders its like button with this label.
const UNLIKE_SELECTOR = 'button[aria-label="Unlike"]';
// The toggle is inserted immediately before the settings block, so it sits to
// its left in the same row.
const ANCHOR_SELECTORS = [
'div.hc__settings',
'.hc__settings',
];
// Used only if no settings icon can be found.
const FALLBACK_SELECTOR = '.home__top';
const STORAGE_KEY = 'gandermonkey.likedOnly';
const DEBUG = false;
// --- Hiding mechanism -----------------------------------------------------
// A stylesheet + data attribute rather than an inline style: Gander is a Vue
// app and manages the article's `style` attribute, so an inline display:none
// gets wiped on re-patch. Vue leaves unknown data-* attributes alone.
const HIDDEN_ATTR = 'data-gmlo-hidden';
const BUTTON_ID = 'gmlo-toggle';
const style = document.createElement('style');
style.textContent = `
[${HIDDEN_ATTR}="1"] { display: none !important; }
#${BUTTON_ID} {
display: inline-flex; align-items: center; gap: 4px; flex: none;
margin: 0 10px 0 0; padding: 4px 8px; white-space: nowrap;
font: inherit; font-size: 13px; font-weight: 600; line-height: 1;
cursor: pointer; border: 0; border-radius: 999px;
background: transparent; color: inherit;
}
#${BUTTON_ID}:hover { opacity: 0.7; }
#${BUTTON_ID}[aria-pressed="true"] { color: #C30B0D; }
#${BUTTON_ID} .gmlo-count { opacity: 0.7; font-weight: 500; font-size: 12px; }
`;
(document.head || document.documentElement).appendChild(style);
// --- State ----------------------------------------------------------------
let likedOnly = false;
try {
likedOnly = localStorage.getItem(STORAGE_KEY) === '1';
} catch (e) {
console.warn('[gandermonkey] could not read ' + STORAGE_KEY, e);
}
function persist() {
try {
localStorage.setItem(STORAGE_KEY, likedOnly ? '1' : '0');
} catch (e) {
console.warn('[gandermonkey] could not persist filter state (session only)', e);
}
}
// --- Matching -------------------------------------------------------------
// Fix quoted post likes
function isLiked(post) {
for (const btn of post.querySelectorAll(UNLIKE_SELECTOR)) {
if (btn.closest(POST_SELECTOR) === post) return true;
}
return false;
}
// Only consider top-level posts, so hiding a parent doesn't fight with a
// nested quote.
function topLevelPosts() {
return [...document.querySelectorAll(POST_SELECTOR)].filter(
(p) => !p.parentElement?.closest(POST_SELECTOR)
);
}
let shown = 0;
let hidden = 0;
function evaluate(post) {
const hide = likedOnly && !isLiked(post);
if (hide) {
if (post.getAttribute(HIDDEN_ATTR) !== '1') post.setAttribute(HIDDEN_ATTR, '1');
hidden++;
} else {
// Only ever unhide posts we hid ourselves.
if (post.hasAttribute(HIDDEN_ATTR)) post.removeAttribute(HIDDEN_ATTR);
shown++;
}
}
let queued = false;
function scheduleScan() {
if (queued) return;
queued = true;
requestAnimationFrame(() => {
queued = false;
shown = 0;
hidden = 0;
topLevelPosts().forEach(evaluate);
mountButton();
updateButton();
if (DEBUG) console.debug('[gandermonkey] shown', shown, 'hidden', hidden);
});
}
// --- Toggle button --------------------------------------------------------
let button = null;
let warnedNoAnchor = false;
function findAnchor() {
for (const sel of ANCHOR_SELECTORS) {
let el;
try {
el = document.querySelector(sel);
} catch (e) {
continue; // e.g. older engines rejecting the case-insensitive flag
}
if (el && !el.closest(POST_SELECTOR)) return el; // ignore anything inside a post
}
return null;
}
function mountButton() {
const anchor = findAnchor();
if (anchor) {
// Already sitting immediately to the anchor's left — nothing to do.
if (button && button.isConnected && anchor.previousElementSibling === button) return;
if (!button) button = createButton();
anchor.insertAdjacentElement('beforebegin', button);
return;
}
const fallback = document.querySelector(FALLBACK_SELECTOR);
if (!fallback) return;
if (!warnedNoAnchor) {
warnedNoAnchor = true;
console.warn('[gandermonkey] .hc__settings not found; mounting the liked-only ' +
'toggle in ' + FALLBACK_SELECTOR + ' instead. Adjust ANCHOR_SELECTORS.');
}
// Vue may re-render and drop our button; re-mount if so.
if (button && button.isConnected && fallback.contains(button)) return;
if (!button) button = createButton();
fallback.appendChild(button);
}
function createButton() {
const el = document.createElement('button');
el.id = BUTTON_ID;
el.type = 'button';
el.title = 'Show only posts you\u2019ve liked';
// Children are created once, here. updateButton() only ever touches their
// textContent, and only when it actually changes — writing innerHTML on
// every scan would generate childList mutations and spin the observer.
el.appendChild(document.createElement('span')).className = 'gmlo-label';
el.appendChild(document.createElement('span')).className = 'gmlo-count';
el.addEventListener('click', () => setLikedOnly(!likedOnly));
return el;
}
function setText(el, text) {
if (el && el.textContent !== text) el.textContent = text;
}
function updateButton() {
if (!button) return;
const pressed = likedOnly ? 'true' : 'false';
if (button.getAttribute('aria-pressed') !== pressed) {
button.setAttribute('aria-pressed', pressed);
}
setText(
button.querySelector('.gmlo-label'),
(likedOnly ? '\u2665' : '\u2661') + ' Liked Posts Only'
);
setText(
button.querySelector('.gmlo-count'),
likedOnly ? `${shown}/${shown + hidden}` : ''
);
}
function setLikedOnly(next) {
likedOnly = !!next;
persist();
scheduleScan();
}
// --- Cross-tab sync -------------------------------------------------------
window.addEventListener('storage', (e) => {
if (e.key !== STORAGE_KEY) return;
likedOnly = e.newValue === '1';
scheduleScan();
});
// --- Observer -------------------------------------------------------------
new MutationObserver((records) => {
// Belt and braces against self-triggering: ignore batches that only
// describe changes inside our own toggle button.
if (button && records.every((r) => button === r.target || button.contains(r.target))) return;
scheduleScan();
}).observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['aria-label'],
});
scheduleScan();
})();
The script does not include posts where you liked a comment.
