1
Fork 0

Compare commits

..

2 Commits

Author SHA1 Message Date
Bauke 061df73df8
Add the Unignore All miscellaneous feature. 2023-07-16 14:03:20 +02:00
Bauke c818362773
Add a sleep utility function. 2023-07-15 21:19:41 +02:00
7 changed files with 142 additions and 0 deletions

View File

@ -9,5 +9,6 @@ export * from "./jump-to-new-comment.js";
export * from "./markdown-toolbar.js"; export * from "./markdown-toolbar.js";
export * from "./themed-logo.js"; export * from "./themed-logo.js";
export * from "./topic-info-ignore.js"; export * from "./topic-info-ignore.js";
export * from "./unignore-all-button.js";
export * from "./user-labels.js"; export * from "./user-labels.js";
export * from "./username-colors.js"; export * from "./username-colors.js";

View File

@ -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(<UnignoreAllButton />, button);
heading.after(button);
return true;
}
type Props = Record<string, unknown>;
type State = {
isRunning: boolean;
remaining: number;
total: number;
wasCanceled: boolean;
};
class UnignoreAllButton extends Component<Props, State> {
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<HTMLButtonElement>(
'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 (
<button class="btn" onClick={this.click}>
{text}
</button>
);
}
}

View File

@ -24,6 +24,7 @@ import {
runMarkdownToolbarFeature, runMarkdownToolbarFeature,
runThemedLogoFeature, runThemedLogoFeature,
runTopicInfoIgnore, runTopicInfoIgnore,
runUnignoreAllButtonFeature,
runUsernameColorsFeature, runUsernameColorsFeature,
} from "./features/exports.js"; } from "./features/exports.js";
@ -176,6 +177,13 @@ async function initialize() {
runTopicInfoIgnore(); runTopicInfoIgnore();
} }
if (
miscEnabled.value.has(MiscellaneousFeature.UnignoreAllButton) &&
isLoggedIn
) {
runUnignoreAllButtonFeature();
}
// Insert a placeholder at the end of the body first, then render the rest // 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 // 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. // at the beginning of the body which causes a bunch of different issues.

View File

@ -42,6 +42,14 @@ function FeatureDescription({
); );
} }
if (feature === MiscellaneousFeature.UnignoreAllButton) {
return (
<p class="description">
Add an "Unignore All" button to your list of ignored topics.
</p>
);
}
return <></>; return <></>;
} }

View File

@ -23,6 +23,7 @@ export enum MiscellaneousFeature {
CommentAnchorFix = "comment-anchor-fix", CommentAnchorFix = "comment-anchor-fix",
GroupListSubscribeButtons = "group-list-subscribe-buttons", GroupListSubscribeButtons = "group-list-subscribe-buttons",
TopicInfoIgnore = "topic-info-ignore", TopicInfoIgnore = "topic-info-ignore",
UnignoreAllButton = "unignore-all-button",
} }
/** /**

View File

@ -7,6 +7,7 @@ export * from "./http.js";
export * from "./logging.js"; export * from "./logging.js";
export * from "./query-selectors.js"; export * from "./query-selectors.js";
export * from "./report-a-bug.js"; export * from "./report-a-bug.js";
export * from "./sleep.js";
export * from "./text.js"; export * from "./text.js";
export * from "./user.js"; export * from "./user.js";
export * from "./validators.js"; export * from "./validators.js";

View File

@ -0,0 +1,9 @@
/**
* Promisified {@linkcode window.setTimeout}.
* @param timeout The amount of time in milliseconds to sleep for.
*/
export async function sleep(timeout: number): Promise<void> {
return new Promise((resolve) => {
window.setTimeout(resolve, timeout);
});
}