Add the Hide Topics settings component.
This commit is contained in:
parent
67e796d12d
commit
7d480598da
|
@ -2,6 +2,7 @@ export {AboutSetting} from "./about.js";
|
|||
export {AnonymizeUsernamesSetting} from "./anonymize-usernames.js";
|
||||
export {AutocompleteSetting} from "./autocomplete.js";
|
||||
export {BackToTopSetting} from "./back-to-top.js";
|
||||
export {HideTopicsSetting} from "./hide-topics.js";
|
||||
export {HideVotesSetting} from "./hide-votes.js";
|
||||
export {JumpToNewCommentSetting} from "./jump-to-new-comment.js";
|
||||
export {MarkdownToolbarSetting} from "./markdown-toolbar.js";
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
import {Component} from "preact";
|
||||
import {
|
||||
createValueHideTopicPredicate,
|
||||
fromStorage,
|
||||
isHideTopicMatcher,
|
||||
Feature,
|
||||
HideTopicMatcher,
|
||||
type HideTopicPredicate,
|
||||
type HideTopicsData,
|
||||
} from "../../storage/exports.js";
|
||||
import {log} from "../../utilities/logging.js";
|
||||
import {Setting, type SettingProps} from "./index.js";
|
||||
|
||||
type State = {
|
||||
predicates: HideTopicsData;
|
||||
predicatesToRemove: HideTopicsData;
|
||||
unsavedPredicateIds: number[];
|
||||
};
|
||||
|
||||
export class HideTopicsSetting extends Component<SettingProps, State> {
|
||||
constructor(props: SettingProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
predicates: [],
|
||||
predicatesToRemove: [],
|
||||
unsavedPredicateIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.setState({
|
||||
predicates: await fromStorage(Feature.HideTopics),
|
||||
});
|
||||
}
|
||||
|
||||
newPredicate = async () => {
|
||||
const {predicates, unsavedPredicateIds} = this.state;
|
||||
predicates.sort((a, b) => b.value.id - a.value.id);
|
||||
const newId = (predicates[0]?.value.id ?? 0) + 1;
|
||||
predicates.push(
|
||||
await createValueHideTopicPredicate({
|
||||
id: newId,
|
||||
matcher: HideTopicMatcher.DomainIncludes,
|
||||
value: "example.org",
|
||||
}),
|
||||
);
|
||||
unsavedPredicateIds.push(newId);
|
||||
|
||||
this.setState({
|
||||
predicates,
|
||||
unsavedPredicateIds,
|
||||
});
|
||||
};
|
||||
|
||||
onInput = (event: Event, id: number, key: keyof HideTopicPredicate) => {
|
||||
const {predicates, unsavedPredicateIds} = this.state;
|
||||
const index = predicates.findIndex(({value}) => value.id === id);
|
||||
if (index === -1) {
|
||||
log(`Tried to edit unknown predicate with ID: ${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = (event.target as HTMLInputElement)!.value;
|
||||
switch (key) {
|
||||
case "matcher": {
|
||||
if (isHideTopicMatcher(newValue)) {
|
||||
predicates[index].value.matcher = newValue;
|
||||
} else {
|
||||
log(`Unknown HideTopicMatcher: ${newValue}`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "value": {
|
||||
predicates[index].value.value = newValue;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
log(`Can't edit predicate key: ${key}`, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unsavedPredicateIds.push(id);
|
||||
this.setState({predicates, unsavedPredicateIds});
|
||||
};
|
||||
|
||||
remove = (id: number) => {
|
||||
const {predicates, predicatesToRemove, unsavedPredicateIds} = this.state;
|
||||
const index = predicates.findIndex(({value}) => value.id === id);
|
||||
if (index === -1) {
|
||||
log(`Tried to remove unknown predicate with ID: ${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
predicatesToRemove.push(...predicates.splice(index, 1));
|
||||
unsavedPredicateIds.push(id);
|
||||
this.setState({predicates, predicatesToRemove, unsavedPredicateIds});
|
||||
};
|
||||
|
||||
save = async () => {
|
||||
const {predicates, predicatesToRemove} = this.state;
|
||||
for (const predicate of predicates) {
|
||||
await predicate.save();
|
||||
}
|
||||
|
||||
for (const predicate of predicatesToRemove) {
|
||||
await predicate.remove();
|
||||
}
|
||||
|
||||
this.setState({predicatesToRemove: [], unsavedPredicateIds: []});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {predicates, unsavedPredicateIds} = this.state;
|
||||
predicates.sort((a, b) => a.value.id - b.value.id);
|
||||
|
||||
const editors = predicates.map(({value: predicate}) => {
|
||||
const matcherHandler = (event: Event) => {
|
||||
this.onInput(event, predicate.id, "matcher");
|
||||
};
|
||||
|
||||
const valueHandler = (event: Event) => {
|
||||
this.onInput(event, predicate.id, "value");
|
||||
};
|
||||
|
||||
const removeHandler = () => {
|
||||
this.remove(predicate.id);
|
||||
};
|
||||
|
||||
const matcherOptions = Object.values(HideTopicMatcher).map((key) => (
|
||||
<option selected={predicate.matcher === key} value={key}>
|
||||
{key
|
||||
.replace(/-/g, " ")
|
||||
.replace(/(\b[a-z])/gi, (character) => character.toUpperCase())}
|
||||
</option>
|
||||
));
|
||||
|
||||
const hasUnsavedChanges = unsavedPredicateIds.includes(predicate.id)
|
||||
? "unsaved-changes"
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div class={`hide-topics-editor ${hasUnsavedChanges}`}>
|
||||
<select onChange={matcherHandler}>{matcherOptions}</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Value to match"
|
||||
value={predicate.value}
|
||||
onInput={valueHandler}
|
||||
/>
|
||||
<button class="button destructive" onClick={removeHandler}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const hasUnsavedChanges = unsavedPredicateIds.length > 0;
|
||||
return (
|
||||
<Setting {...this.props}>
|
||||
<p class="info">
|
||||
Hide topics from the topic listing matching custom predicates.
|
||||
<br />
|
||||
Topics will be hidden when any of your predicates match and you can
|
||||
unhide them by clicking a button that will appear at the bottom of the
|
||||
sidebar. The topics will then show themselves and have a red border.
|
||||
<br />
|
||||
For hiding topics with certain tags, Tildes can do that natively via
|
||||
your{" "}
|
||||
<a href="https://tildes.net/settings/filters">
|
||||
filtered tags settings
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<details class="hide-topics-matcher-explanation">
|
||||
<summary>Matcher explanations</summary>
|
||||
|
||||
<p>
|
||||
All matches are done without taking casing into account, so you
|
||||
don't have to care about upper or lowercase letters.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>Domain Includes</b> will match the domain of all topic links
|
||||
(including text topics). For example with the link{" "}
|
||||
<code>https://tildes.net/~tildes</code>, "tildes.net" is what will
|
||||
be matched against. If your value is included in the domain
|
||||
anywhere the topic will be hidden.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>Tildes Username Equals</b> will match the topic author's
|
||||
username. If your value is exactly the same as the topic author's
|
||||
the topic will be hidden.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>Title Includes</b> will match the topic title. If your value is
|
||||
anywhere in the title the topic will be hidden.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>User Label Equals</b> will match any user labels you have
|
||||
applied to the topic author. For example if you set a "Hide
|
||||
Topics" user labels matcher and then add a user label to someone
|
||||
with "Hide Topics" as the text, their topics will be hidden.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<div class="hide-topics-main-button-group">
|
||||
<button class="button" onClick={this.newPredicate}>
|
||||
New Predicate
|
||||
</button>
|
||||
<button
|
||||
class={`button ${hasUnsavedChanges ? "unsaved-changes" : ""}`}
|
||||
onClick={this.save}
|
||||
>
|
||||
Save{hasUnsavedChanges ? "*" : ""}
|
||||
</button>
|
||||
</div>
|
||||
<div class="hide-topics-predicate-editors">{editors}</div>
|
||||
</Setting>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import {
|
|||
AnonymizeUsernamesSetting,
|
||||
AutocompleteSetting,
|
||||
BackToTopSetting,
|
||||
HideTopicsSetting,
|
||||
HideVotesSetting,
|
||||
JumpToNewCommentSetting,
|
||||
MarkdownToolbarSetting,
|
||||
|
@ -42,6 +43,13 @@ export const features: FeatureData[] = [
|
|||
title: "Back To Top",
|
||||
component: BackToTopSetting,
|
||||
},
|
||||
{
|
||||
availableSince: new Date("2023-06-31"),
|
||||
index: 0,
|
||||
key: Feature.HideTopics,
|
||||
title: "Hide Topics",
|
||||
component: HideTopicsSetting,
|
||||
},
|
||||
{
|
||||
availableSince: new Date("2019-11-12"),
|
||||
index: 0,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
@import "button";
|
||||
@import "shared";
|
||||
@import "settings";
|
||||
@import "settings/hide-topics";
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
.setting {
|
||||
--unsaved-changes-color: var(--yellow);
|
||||
|
||||
.hide-topics-matcher-explanation {
|
||||
margin-bottom: 8px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 2rem;
|
||||
padding-bottom: 8px;
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hide-topics-main-button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
button.unsaved-changes {
|
||||
background-color: var(--unsaved-changes-color);
|
||||
}
|
||||
}
|
||||
|
||||
.hide-topics-predicate-editors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hide-topics-editor {
|
||||
--save-status-color: var(--blue);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto min-content;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--save-status-color);
|
||||
color: var(--foreground);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
select {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
&.unsaved-changes {
|
||||
--save-status-color: var(--unsaved-changes-color);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue