Add the content script part of the Hide Topics feature.
This commit is contained in:
parent
17bee78e70
commit
de0730db28
|
@ -7,3 +7,4 @@ export * from "./markdown-toolbar.js";
|
||||||
export * from "./themed-logo.js";
|
export * from "./themed-logo.js";
|
||||||
export * from "./user-labels.js";
|
export * from "./user-labels.js";
|
||||||
export * from "./username-colors.js";
|
export * from "./username-colors.js";
|
||||||
|
export * from "./hide-topics.js";
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
import {Component, render} from "preact";
|
||||||
|
import {
|
||||||
|
fromStorage,
|
||||||
|
Feature,
|
||||||
|
HideTopicMatcher,
|
||||||
|
type UserLabelsData,
|
||||||
|
} from "../../storage/exports.js";
|
||||||
|
import {
|
||||||
|
createElementFromString,
|
||||||
|
log,
|
||||||
|
pluralize,
|
||||||
|
querySelector,
|
||||||
|
querySelectorAll,
|
||||||
|
} from "../../utilities/exports.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for {@link HideTopicMatcher} for code brevity.
|
||||||
|
*/
|
||||||
|
const Matcher = HideTopicMatcher;
|
||||||
|
|
||||||
|
type Props = Record<string, unknown>;
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
hidden: boolean;
|
||||||
|
hiddenTopicsCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide a topic by adding `.trx-hidden` and setting the `data-trx-hide-topics`
|
||||||
|
* attribute.
|
||||||
|
* @param topic The topic to hide.
|
||||||
|
*/
|
||||||
|
function hideTopic(topic: HTMLElement) {
|
||||||
|
if (
|
||||||
|
topic.parentElement?.nextElementSibling?.getAttribute(
|
||||||
|
"data-trx-hide-topics-spacer",
|
||||||
|
) === null
|
||||||
|
) {
|
||||||
|
// Add a spacer to the topic listing so the `li:nth-child(2n)` styling
|
||||||
|
// doesn't become inconsistent when this topic is hidden.
|
||||||
|
topic.parentElement.insertAdjacentElement(
|
||||||
|
"afterend",
|
||||||
|
createElementFromString(
|
||||||
|
'<li class="trx-hidden" data-trx-hide-topics-spacer></li>',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
topic.classList.add("trx-hidden");
|
||||||
|
topic.dataset.trxHideTopics = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the Hide Topics feature.
|
||||||
|
* @param userLabels The {@link UserLabelsData} to use with the UserLabelEquals
|
||||||
|
* {@link HideTopicMatcher}.
|
||||||
|
*/
|
||||||
|
export async function runHideTopicsFeature(
|
||||||
|
userLabels: UserLabelsData,
|
||||||
|
): Promise<void> {
|
||||||
|
const predicates = await fromStorage(Feature.HideTopics);
|
||||||
|
|
||||||
|
// Select all topics not already handled by TRX.
|
||||||
|
const topics = querySelectorAll<HTMLElement>(
|
||||||
|
".topic-listing .topic:not([data-trx-hide-topics])",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Define all the predicates before going through the topics so matching the
|
||||||
|
// topics is easier later.
|
||||||
|
const domainPredicates = [];
|
||||||
|
const titlePredicates = [];
|
||||||
|
const userPredicates = new Set();
|
||||||
|
|
||||||
|
for (const predicate of predicates) {
|
||||||
|
const {matcher, value} = predicate.value;
|
||||||
|
switch (matcher) {
|
||||||
|
case Matcher.DomainIncludes: {
|
||||||
|
domainPredicates.push(value.toLowerCase());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Matcher.TildesUsernameEquals: {
|
||||||
|
userPredicates.add(value.toLowerCase());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Matcher.TitleIncludes: {
|
||||||
|
titlePredicates.push(value.toLowerCase());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Matcher.UserLabelEquals: {
|
||||||
|
for (const userLabel of userLabels) {
|
||||||
|
if (value === userLabel.value.text) {
|
||||||
|
userPredicates.add(userLabel.value.username.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
console.warn(`Unknown HideTopicMatcher: ${matcher as string}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep a count of how many topics have been hidden.
|
||||||
|
let topicsHidden = 0;
|
||||||
|
|
||||||
|
// Shorthand to hide a topic and increment the count.
|
||||||
|
const hide = (topic: HTMLElement) => {
|
||||||
|
hideTopic(topic);
|
||||||
|
topicsHidden++;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const topic of topics) {
|
||||||
|
// First check the topic author.
|
||||||
|
const author = (topic.dataset.topicPostedBy ?? "<unknown>").toLowerCase();
|
||||||
|
if (userPredicates.has(author)) {
|
||||||
|
hide(topic);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second check the topic title.
|
||||||
|
const title = (
|
||||||
|
topic.querySelector(".topic-title")?.textContent ?? ""
|
||||||
|
).toLowerCase();
|
||||||
|
if (titlePredicates.some((value) => title.includes(value))) {
|
||||||
|
hide(topic);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third check the topic link.
|
||||||
|
const url = new URL(
|
||||||
|
topic.querySelector<HTMLAnchorElement>(".topic-title a")!.href,
|
||||||
|
);
|
||||||
|
if (domainPredicates.some((value) => url.hostname.includes(value))) {
|
||||||
|
hide(topic);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add the Hide Topics button if any topics have been hidden and if the
|
||||||
|
// button isn't already there.
|
||||||
|
if (
|
||||||
|
topicsHidden > 0 &&
|
||||||
|
document.querySelector("#trx-hide-topics-button") === null
|
||||||
|
) {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(<HideTopicsFeature hiddenTopicsCount={topicsHidden} />, container);
|
||||||
|
querySelector("#sidebar").insertAdjacentElement("beforeend", container);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Hide Topics: Initialized for ${topicsHidden} topics.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HideTopicsFeature extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hiddenTopicsCount: this.getHiddenTopics().length,
|
||||||
|
hidden: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getHiddenTopics = (): HTMLElement[] => {
|
||||||
|
return querySelectorAll("[data-trx-hide-topics]");
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleHidden = () => {
|
||||||
|
const {hidden} = this.state;
|
||||||
|
if (hidden) {
|
||||||
|
// Remove all the `<li>` spacers when unhiding the topics.
|
||||||
|
for (const spacer of querySelectorAll("[data-trx-hide-topics-spacer]")) {
|
||||||
|
spacer.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const topic of this.getHiddenTopics()) {
|
||||||
|
if (hidden) {
|
||||||
|
topic.classList.remove("trx-hidden");
|
||||||
|
} else {
|
||||||
|
hideTopic(topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({hidden: !hidden});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {hidden, hiddenTopicsCount} = this.state;
|
||||||
|
const pluralized = pluralize(hiddenTopicsCount, "topic");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
id="trx-hide-topics-button"
|
||||||
|
class="btn primary"
|
||||||
|
onClick={this.toggleHidden}
|
||||||
|
>
|
||||||
|
{hidden ? "Unhide" : "Hide"} {hiddenTopicsCount} {pluralized}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import {
|
||||||
JumpToNewCommentFeature,
|
JumpToNewCommentFeature,
|
||||||
UserLabelsFeature,
|
UserLabelsFeature,
|
||||||
runAnonymizeUsernamesFeature,
|
runAnonymizeUsernamesFeature,
|
||||||
|
runHideTopicsFeature,
|
||||||
runHideVotesFeature,
|
runHideVotesFeature,
|
||||||
runMarkdownToolbarFeature,
|
runMarkdownToolbarFeature,
|
||||||
runThemedLogoFeature,
|
runThemedLogoFeature,
|
||||||
|
@ -23,6 +24,7 @@ async function initialize() {
|
||||||
// them without having to change the hardcoded values.
|
// them without having to change the hardcoded values.
|
||||||
const usesKnownGroups = new Set<Feature>([Feature.Autocomplete]);
|
const usesKnownGroups = new Set<Feature>([Feature.Autocomplete]);
|
||||||
const knownGroups = await fromStorage(Data.KnownGroups);
|
const knownGroups = await fromStorage(Data.KnownGroups);
|
||||||
|
const userLabels = await fromStorage(Feature.UserLabels);
|
||||||
|
|
||||||
// Only when any of the features that uses this data are enabled, try to save
|
// Only when any of the features that uses this data are enabled, try to save
|
||||||
// the groups.
|
// the groups.
|
||||||
|
@ -64,6 +66,12 @@ async function initialize() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enabledFeatures.value.has(Feature.HideTopics)) {
|
||||||
|
observerFeatures.push(async () => {
|
||||||
|
await runHideTopicsFeature(userLabels);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (enabledFeatures.value.has(Feature.HideVotes)) {
|
if (enabledFeatures.value.has(Feature.HideVotes)) {
|
||||||
observerFeatures.push(async () => {
|
observerFeatures.push(async () => {
|
||||||
const data = await fromStorage(Feature.HideVotes);
|
const data = await fromStorage(Feature.HideVotes);
|
||||||
|
@ -96,7 +104,6 @@ async function initialize() {
|
||||||
// Object to hold the active components we are going to render.
|
// Object to hold the active components we are going to render.
|
||||||
const components: Record<string, JSX.Element | undefined> = {};
|
const components: Record<string, JSX.Element | undefined> = {};
|
||||||
|
|
||||||
const userLabels = await fromStorage(Feature.UserLabels);
|
|
||||||
if (enabledFeatures.value.has(Feature.Autocomplete)) {
|
if (enabledFeatures.value.has(Feature.Autocomplete)) {
|
||||||
components.autocomplete = (
|
components.autocomplete = (
|
||||||
<AutocompleteFeature
|
<AutocompleteFeature
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Scripts
|
// Scripts
|
||||||
@import "scripts/autocomplete";
|
@import "scripts/autocomplete";
|
||||||
@import "scripts/back-to-top";
|
@import "scripts/back-to-top";
|
||||||
|
@import "scripts/hide-topics";
|
||||||
@import "scripts/jump-to-new-comment";
|
@import "scripts/jump-to-new-comment";
|
||||||
@import "scripts/markdown-toolbar";
|
@import "scripts/markdown-toolbar";
|
||||||
@import "scripts/user-labels";
|
@import "scripts/user-labels";
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
[data-trx-hide-topics] {
|
||||||
|
border: 1px solid #f00;
|
||||||
|
|
||||||
|
// Remove the .is-topic-official margin-left rule, otherwise the border looks
|
||||||
|
// weird on official topics.
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
Loading…
Reference in New Issue