diff --git a/source/content-scripts/features/exports.ts b/source/content-scripts/features/exports.ts index 1bb0126..d28bbef 100644 --- a/source/content-scripts/features/exports.ts +++ b/source/content-scripts/features/exports.ts @@ -9,5 +9,6 @@ export * from "./jump-to-new-comment.js"; export * from "./markdown-toolbar.js"; export * from "./themed-logo.js"; export * from "./topic-info-ignore.js"; +export * from "./unignore-all-button.js"; export * from "./user-labels.js"; export * from "./username-colors.js"; diff --git a/source/content-scripts/features/unignore-all-button.tsx b/source/content-scripts/features/unignore-all-button.tsx new file mode 100644 index 0000000..9b40707 --- /dev/null +++ b/source/content-scripts/features/unignore-all-button.tsx @@ -0,0 +1,114 @@ +import {Component, render} from "preact"; +import {log, querySelectorAll, sleep} from "../../utilities/exports.js"; + +export function runUnignoreAllButtonFeature(): void { + if (addUnignoreAllButton()) { + log("Added Unignore All button."); + } +} + +function addUnignoreAllButton(): boolean { + // Only add the button when we're on the ignore list page and the ignore list + // isn't empty. + if ( + window.location.pathname !== "/ignored_topics" && + document.querySelector("main > .empty") === null + ) { + return false; + } + + const heading = document.querySelector(".heading-main") ?? undefined; + if (heading === undefined) { + return false; + } + + const button = document.createDocumentFragment(); + render(, button); + heading.after(button); + + return true; +} + +type Props = Record; + +type State = { + isRunning: boolean; + remaining: number; + total: number; + wasCanceled: boolean; +}; + +class UnignoreAllButton extends Component { + constructor(props: Props) { + super(props); + + this.state = { + isRunning: false, + remaining: 0, + total: 0, + wasCanceled: false, + }; + } + + click = () => { + if (this.state.isRunning) { + // If we're already running, cancel the run. + this.setState({isRunning: false, wasCanceled: true}); + window.setTimeout(() => { + // And after 5 seconds, return back to the default state. + this.setState({wasCanceled: false}); + }, 5000); + return; + } + + // Select the ignore buttons that have a HTTP DELETE method set. Since we're + // going to have Intercooler do all the work for us, we don't want to + // accidentally also select ignore buttons that would ignore the topics + // again. + const unignoreButtons = querySelectorAll( + 'button[name="topic-actions-ignore"][data-ic-delete-from]', + ); + this.setState({ + isRunning: true, + remaining: unignoreButtons.length, + total: unignoreButtons.length, + }); + void this.unignoreAll(unignoreButtons); + }; + + unignoreAll = async (buttons: HTMLButtonElement[]) => { + let remaining = buttons.length; + for (const ignoreButton of buttons) { + // Stop the loop if the user canceled it. + if (!this.state.isRunning && this.state.wasCanceled) { + return; + } + + ignoreButton.click(); + remaining--; + this.setState({remaining}); + await sleep(250); + } + + this.setState({isRunning: false}); + }; + + render() { + const {isRunning, remaining, total, wasCanceled} = this.state; + let text = "Unignore All"; + + if (isRunning) { + // When we're running show how many topics are remaining. + text = `Unignoring topics, ${remaining} out of ${total} remaining`; + } else if (wasCanceled) { + // If the user canceled, say that. + text = "Canceled unignoring all topics"; + } + + return ( + + ); + } +} diff --git a/source/content-scripts/setup.tsx b/source/content-scripts/setup.tsx index 164b3be..51a1370 100644 --- a/source/content-scripts/setup.tsx +++ b/source/content-scripts/setup.tsx @@ -24,6 +24,7 @@ import { runMarkdownToolbarFeature, runThemedLogoFeature, runTopicInfoIgnore, + runUnignoreAllButtonFeature, runUsernameColorsFeature, } from "./features/exports.js"; @@ -176,6 +177,13 @@ async function initialize() { runTopicInfoIgnore(); } + if ( + miscEnabled.value.has(MiscellaneousFeature.UnignoreAllButton) && + isLoggedIn + ) { + runUnignoreAllButtonFeature(); + } + // Insert a placeholder at the end of the body first, then render the rest // and use that as the replacement element. Otherwise render() would put it // at the beginning of the body which causes a bunch of different issues. diff --git a/source/options/components/miscellaneous.tsx b/source/options/components/miscellaneous.tsx index 22bbd89..71dbd7c 100644 --- a/source/options/components/miscellaneous.tsx +++ b/source/options/components/miscellaneous.tsx @@ -42,6 +42,14 @@ function FeatureDescription({ ); } + if (feature === MiscellaneousFeature.UnignoreAllButton) { + return ( +

+ Add an "Unignore All" button to your list of ignored topics. +

+ ); + } + return <>; } diff --git a/source/storage/enums.ts b/source/storage/enums.ts index 1f541fc..aaf6e77 100644 --- a/source/storage/enums.ts +++ b/source/storage/enums.ts @@ -23,6 +23,7 @@ export enum MiscellaneousFeature { CommentAnchorFix = "comment-anchor-fix", GroupListSubscribeButtons = "group-list-subscribe-buttons", TopicInfoIgnore = "topic-info-ignore", + UnignoreAllButton = "unignore-all-button", } /**