1
Fork 0

Add the Hide Topics settings component.

This commit is contained in:
Bauke 2023-06-30 13:26:39 +02:00
parent 67e796d12d
commit 7d480598da
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
5 changed files with 313 additions and 0 deletions

View File

@ -2,6 +2,7 @@ export {AboutSetting} from "./about.js";
export {AnonymizeUsernamesSetting} from "./anonymize-usernames.js"; export {AnonymizeUsernamesSetting} from "./anonymize-usernames.js";
export {AutocompleteSetting} from "./autocomplete.js"; export {AutocompleteSetting} from "./autocomplete.js";
export {BackToTopSetting} from "./back-to-top.js"; export {BackToTopSetting} from "./back-to-top.js";
export {HideTopicsSetting} from "./hide-topics.js";
export {HideVotesSetting} from "./hide-votes.js"; export {HideVotesSetting} from "./hide-votes.js";
export {JumpToNewCommentSetting} from "./jump-to-new-comment.js"; export {JumpToNewCommentSetting} from "./jump-to-new-comment.js";
export {MarkdownToolbarSetting} from "./markdown-toolbar.js"; export {MarkdownToolbarSetting} from "./markdown-toolbar.js";

View File

@ -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>
);
}
}

View File

@ -4,6 +4,7 @@ import {
AnonymizeUsernamesSetting, AnonymizeUsernamesSetting,
AutocompleteSetting, AutocompleteSetting,
BackToTopSetting, BackToTopSetting,
HideTopicsSetting,
HideVotesSetting, HideVotesSetting,
JumpToNewCommentSetting, JumpToNewCommentSetting,
MarkdownToolbarSetting, MarkdownToolbarSetting,
@ -42,6 +43,13 @@ export const features: FeatureData[] = [
title: "Back To Top", title: "Back To Top",
component: BackToTopSetting, component: BackToTopSetting,
}, },
{
availableSince: new Date("2023-06-31"),
index: 0,
key: Feature.HideTopics,
title: "Hide Topics",
component: HideTopicsSetting,
},
{ {
availableSince: new Date("2019-11-12"), availableSince: new Date("2019-11-12"),
index: 0, index: 0,

View File

@ -5,6 +5,7 @@
@import "button"; @import "button";
@import "shared"; @import "shared";
@import "settings"; @import "settings";
@import "settings/hide-topics";
html { html {
font-size: 62.5%; font-size: 62.5%;

View File

@ -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);
}
}
}