From 533dac94b42e3b2d3242e6d12468a048cbe944dd Mon Sep 17 00:00:00 2001 From: Dmitry Trubin <1437843+dtruebin@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:13:19 +0100 Subject: [PATCH] refactor(SFR): try Chain of Responsibility pattern --- .../strava-feed-refined.user.js | 159 +++++++++++++----- 1 file changed, 116 insertions(+), 43 deletions(-) diff --git a/strava-feed-refined/strava-feed-refined.user.js b/strava-feed-refined/strava-feed-refined.user.js index 66917f6..ab11ceb 100644 --- a/strava-feed-refined/strava-feed-refined.user.js +++ b/strava-feed-refined/strava-feed-refined.user.js @@ -64,13 +64,13 @@ this._cache = {}; } - // Marks this item as processed + /** Marks this item as processed. */ markAsProcessed() { this.el.dataset.processed = "true"; } - // Hides this item and logs the description of what is being hidden /** + * Hides this item (also marking as processed) and logs the description of what is being hidden. * @param {string} description */ hide(description) { @@ -132,64 +132,137 @@ } } - // === Main function === - function hideUnwantedEntries(root = document) { - root.querySelectorAll(`div[role="button"]:not([data-processed]):has(${SELECTORS.feedEntry})`) - .forEach((div) => { - const item = new FeedItem(/** @type {HTMLElement} */ (div)); + class Handler { + /** @param {Handler} handler */ + setNext(handler) { + this.nextHandler = handler; + return handler; + } - if (item.isChallenge) { - item.hide(`challenge progress: ${item.challengeInfo}`); - return; - } + // ToDo Consider redesign: in subclasses, each `hide` or `markAsProcessed` should be followed by + // ToDo `return;` to stop processing further down the chain - boilerplate & error-prone. + /** @param {FeedItem} feedItem */ + handle(feedItem) { + this.nextHandler?.handle(feedItem); + } + } - if (!item.isActivity) { - item.markAsProcessed(); - return; - } + class ChallengeHandler extends Handler { + /** @param {FeedItem} item */ + handle(item) { + if (item.isChallenge) { + item.hide(`challenge progress: ${item.challengeInfo}`); + return; + } + super.handle(item); + } + } - if (item.isFromFavoriteAthlete) { - if (!document.URL.includes("/athletes/")) { - console.log(`skipping further processing of ${item.athleteName}'s ⭐ activity: ${item.activityName}`); - } - item.markAsProcessed(); - return; - } + class ValidActivityHandler extends Handler { + /** @param {FeedItem} item */ + handle(item) { + if (!item.isActivity) { + item.markAsProcessed(); + return; + } + super.handle(item); + } + } - for (const tag of item.tags) { - if (CONFIG.unwantedTags.has(tag)) { - if ((tag === "Commute" || tag === "Регулярный маршрут") && item.hasPhoto) { - console.log(`not hiding commute activity with photo(s): ${item.activityName}`); - item.markAsProcessed(); - return; - } - item.hide(`activity by tag "${tag}": ${item.activityName}`); - return; - } + class FavoriteAthleteWhitelistHandler extends Handler { + /** @param {FeedItem} item */ + handle(item) { + if (item.isFromFavoriteAthlete) { + if (!document.URL.includes("/athletes/")) { + console.log(`skipping further processing of ${item.athleteName}'s ⭐ activity: ${item.activityName}`); } + item.markAsProcessed(); + return; + } + super.handle(item); + } + } - for (const tag of item.partnerTags) { - if (CONFIG.unwantedPartnerTags.has(tag)) { - item.hide(`activity by partner tag "${tag}": ${item.activityName}`); + class TagHandler extends Handler { + /** @param {FeedItem} item */ + handle(item) { + for (const tag of item.tags) { + if (CONFIG.unwantedTags.has(tag)) { + if ((tag === "Commute" || tag === "Регулярный маршрут") && item.hasPhoto) { + console.log(`not hiding commute activity with photo(s): ${item.activityName}`); + item.markAsProcessed(); return; } - } - - if (CONFIG.unwantedDevices.has(item.deviceName)) { - item.hide(`activity by device "${item.deviceName}": ${item.activityName}`); + item.hide(`activity by tag "${tag}": ${item.activityName}`); return; } + } + super.handle(item); + } + } - if (CONFIG.unwantedNames.some(name => item.activityName.toLowerCase().includes(name))) { - item.hide(`activity by name: ${item.activityName}`); + class PartnerTagHandler extends Handler { + /** @param {FeedItem} item */ + handle(item) { + for (const tag of item.partnerTags) { + if (CONFIG.unwantedPartnerTags.has(tag)) { + item.hide(`activity by partner tag "${tag}": ${item.activityName}`); return; } + } + super.handle(item); + } + } - item.markAsProcessed(); + class DeviceHandler extends Handler { + /** @param {FeedItem} item */ + handle(item) { + if (CONFIG.unwantedDevices.has(item.deviceName)) { + item.hide(`activity by device "${item.deviceName}": ${item.activityName}`); + return; + } + super.handle(item); + } + } + + class ActivityNameHandler extends Handler { + /** @param {FeedItem} item */ + handle(item) { + if (CONFIG.unwantedNames.some(name => item.activityName.toLowerCase().includes(name))) { + item.hide(`activity by name: ${item.activityName}`); + return; + } + super.handle(item); + } + } + + class FallbackHandler extends Handler { + /** @param {FeedItem} item */ + handle(item) { + item.markAsProcessed(); + } + } + + const rootHandler = new ChallengeHandler(); + rootHandler + .setNext(new ValidActivityHandler()) + .setNext(new FavoriteAthleteWhitelistHandler()) + .setNext(new TagHandler()) + .setNext(new PartnerTagHandler()) + .setNext(new DeviceHandler()) + .setNext(new ActivityNameHandler()) + .setNext(new FallbackHandler()); + + // === Main function === + function processFeed(root = document) { + root.querySelectorAll(`div[role="button"]:not([data-processed]):has(${SELECTORS.feedEntry})`) + .forEach((div) => { + const item = new FeedItem(/** @type {HTMLElement} */(div)); + rootHandler.handle(item); }); } - const observer = new MutationObserver(() => hideUnwantedEntries()); + const observer = new MutationObserver(() => processFeed()); observer.observe(document.body, { childList: true, subtree: true,