1
Fork 0

Add the Miscellaneous features category with the Comment Anchor Fix to start.

This commit is contained in:
Bauke 2023-07-11 19:07:13 +02:00
parent 84c69eaab3
commit 2666168b02
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
10 changed files with 246 additions and 4 deletions

View File

@ -0,0 +1,70 @@
import {log, pluralize} from "../../utilities/exports.js";
export function runCommentAnchorFixFeature(): void {
const count = commentAnchorFix();
if (count > 0) {
const pluralized = `${count} ${pluralize(count, "comment")}`;
log(`Comment Anchor Fix applied, uncollapsed ${pluralized}.`);
}
}
/**
* Apply the comment anchor fix, uncollapsing any collapsed comments and
* scrolling the linked comment into view.
*/
function commentAnchorFix(): number {
const anchor = window.location.hash;
// Linked comments follow the `#comment-<base 36 ID>` pattern.
if (!/^#comment-[a-z\d]+$/i.test(anchor)) {
return 0;
}
// Conveniently, the anchor including the leading hash is a valid CSS selector
// for the target comment, so we can directly select it.
const targetComment =
document.querySelector<HTMLElement>(anchor) ?? undefined;
if (targetComment === undefined) {
return 0;
}
const count = recursiveUncollapseParent(
// Start from the comment's first child so the target comment also has the
// collapse classes removed. The collapse styling accomodates for `:target`
// so this isn't technically necessary, but this way we make sure it stays
// uncollapsed even if the `:target` changes. It also makes the returned
// count correct as otherwise this comment wouldn't be included.
targetComment.firstElementChild ?? targetComment,
0,
);
if (count > 0) {
targetComment.scrollIntoView({behavior: "smooth"});
}
return count;
}
/**
* Recursively go up the chain of comments uncollapsing each one until it
* reaches the `ol#comments` list or no parents remain, returning how many
* comments have been uncollapsed.
* @param target The current target to select its parent from.
*/
function recursiveUncollapseParent(target: Element, count: number): number {
const parent = target.parentElement ?? undefined;
if (parent === undefined || parent.id === "comments") {
return count;
}
for (const collapsedClass of [
"is-comment-collapsed",
"is-comment-collapsed-individual",
]) {
if (parent.classList.contains(collapsedClass)) {
parent.classList.remove(collapsedClass);
count++;
}
}
return recursiveUncollapseParent(parent, count);
}

View File

@ -1,6 +1,7 @@
export * from "./anonymize-usernames.js"; export * from "./anonymize-usernames.js";
export * from "./autocomplete.js"; export * from "./autocomplete.js";
export * from "./back-to-top.js"; export * from "./back-to-top.js";
export * from "./comment-anchor-fix.js";
export * from "./hide-votes.js"; export * from "./hide-votes.js";
export * from "./jump-to-new-comment.js"; export * from "./jump-to-new-comment.js";
export * from "./markdown-toolbar.js"; export * from "./markdown-toolbar.js";

View File

@ -1,12 +1,18 @@
import {type JSX, render} from "preact"; import {type JSX, render} from "preact";
import {extractGroups, initializeGlobals, log} from "../utilities/exports.js"; import {extractGroups, initializeGlobals, log} from "../utilities/exports.js";
import {Feature, fromStorage, Data} from "../storage/exports.js"; import {
Data,
Feature,
MiscellaneousFeature,
fromStorage,
} from "../storage/exports.js";
import { import {
AutocompleteFeature, AutocompleteFeature,
BackToTopFeature, BackToTopFeature,
JumpToNewCommentFeature, JumpToNewCommentFeature,
UserLabelsFeature, UserLabelsFeature,
runAnonymizeUsernamesFeature, runAnonymizeUsernamesFeature,
runCommentAnchorFixFeature,
runHideTopicsFeature, runHideTopicsFeature,
runHideVotesFeature, runHideVotesFeature,
runMarkdownToolbarFeature, runMarkdownToolbarFeature,
@ -106,6 +112,11 @@ async function initialize() {
}); });
} }
const miscEnabled = await fromStorage(Data.MiscellaneousEnabledFeatures);
if (miscEnabled.value.has(MiscellaneousFeature.CommentAnchorFix)) {
runCommentAnchorFixFeature();
}
// Initialize all the observer-dependent features first. // Initialize all the observer-dependent features first.
await Promise.all(observerFeatures.map(async (feature) => feature())); await Promise.all(observerFeatures.map(async (feature) => feature()));

View File

@ -6,6 +6,7 @@ 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";
export {MiscellaneousSetting} from "./miscellaneous.js";
export {ThemedLogoSetting} from "./themed-logo.js"; export {ThemedLogoSetting} from "./themed-logo.js";
export {UserLabelsSetting} from "./user-labels.js"; export {UserLabelsSetting} from "./user-labels.js";
export {UsernameColorsSetting} from "./username-colors.js"; export {UsernameColorsSetting} from "./username-colors.js";

View File

@ -0,0 +1,100 @@
import {Component, type JSX} from "preact";
import {
type StorageValues,
fromStorage,
Data,
MiscellaneousFeature,
} from "../../storage/exports.js";
import {Setting, type SettingProps} from "./index.js";
type State = {
enabledFeatures: Awaited<StorageValues[Data.MiscellaneousEnabledFeatures]>;
};
function FeatureDescription({
feature,
}: {
feature: MiscellaneousFeature;
}): JSX.Element {
if (feature === MiscellaneousFeature.CommentAnchorFix) {
return (
<p class="description">
Uncollapses the linked-to comment if it is collapsed,{" "}
<a href="https://gitlab.com/tildes/tildes/-/issues/256">#256</a>.
</p>
);
}
return <></>;
}
export class MiscellaneousSetting extends Component<SettingProps, State> {
constructor(props: SettingProps) {
super(props);
this.state = {
enabledFeatures: undefined!,
};
}
async componentDidMount() {
this.setState({
enabledFeatures: await fromStorage(Data.MiscellaneousEnabledFeatures),
});
}
toggleFeature = async (feature: MiscellaneousFeature) => {
const {enabledFeatures} = this.state;
if (enabledFeatures.value.has(feature)) {
enabledFeatures.value.delete(feature);
} else {
enabledFeatures.value.add(feature);
}
this.setState({enabledFeatures});
await enabledFeatures.save();
};
render() {
const {enabledFeatures} = this.state;
if (enabledFeatures === undefined) {
return <></>;
}
const checkboxes = Object.values(MiscellaneousFeature).map((feature) => {
const enabled = enabledFeatures.value.has(feature);
const clickHandler = async () => {
await this.toggleFeature(feature);
};
return (
<li class={enabled ? "enabled" : ""}>
<label for={feature}>
<input
type="checkbox"
id={feature}
name={feature}
checked={enabled}
onClick={clickHandler}
/>
{feature
.replace(/-/g, " ")
.replace(/(\b[a-z])/gi, (character) => character.toUpperCase())}
</label>
<FeatureDescription feature={feature} />
</li>
);
});
return (
<Setting {...this.props}>
<p class="info">
Miscellaneous features and fixes, each one can be toggled individually
by checking their respective checkbox.
</p>
<ul class="miscellaneous-features-list">{checkboxes}</ul>
</Setting>
);
}
}

View File

@ -8,6 +8,7 @@ import {
HideVotesSetting, HideVotesSetting,
JumpToNewCommentSetting, JumpToNewCommentSetting,
MarkdownToolbarSetting, MarkdownToolbarSetting,
MiscellaneousSetting,
ThemedLogoSetting, ThemedLogoSetting,
UserLabelsSetting, UserLabelsSetting,
UsernameColorsSetting, UsernameColorsSetting,
@ -71,6 +72,13 @@ export const features: FeatureData[] = [
title: "Markdown Toolbar", title: "Markdown Toolbar",
component: MarkdownToolbarSetting, component: MarkdownToolbarSetting,
}, },
{
availableSince: new Date("2023-07-16"),
index: 0,
key: Feature.Miscellaneous,
title: "Miscellaneous",
component: MiscellaneousSetting,
},
{ {
availableSince: new Date("2022-02-27"), availableSince: new Date("2022-02-27"),
index: 0, index: 0,

View File

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

View File

@ -0,0 +1,33 @@
.miscellaneous-features-list {
display: flex;
flex-direction: column;
gap: 8px;
list-style: none;
li {
--border-color: var(--red);
background-color: var(--background-primary);
border: 1px solid var(--border-color);
&.enabled {
--border-color: var(--green);
}
}
.description {
margin: 8px;
}
label {
align-items: center;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
display: flex;
padding: 8px;
}
input[type="checkbox"] {
margin-right: 1rem;
}
}

View File

@ -10,18 +10,27 @@ export enum Feature {
HideVotes = "hide-votes", HideVotes = "hide-votes",
JumpToNewComment = "jump-to-new-comment", JumpToNewComment = "jump-to-new-comment",
MarkdownToolbar = "markdown-toolbar", MarkdownToolbar = "markdown-toolbar",
Miscellaneous = "miscellaneous-features",
ThemedLogo = "themed-logo", ThemedLogo = "themed-logo",
UserLabels = "user-labels", UserLabels = "user-labels",
UsernameColors = "username-colors", UsernameColors = "username-colors",
} }
/** /**
* Keys of miscellaneous data stored in WebExtension storage. * Keys of miscellaneous feature names.
*/
export enum MiscellaneousFeature {
CommentAnchorFix = "comment-anchor-fix",
}
/**
* Keys of data stored in WebExtension storage.
*/ */
export enum Data { export enum Data {
EnabledFeatures = "enabled-features", EnabledFeatures = "enabled-features",
KnownGroups = "known-groups", KnownGroups = "known-groups",
LatestActiveFeatureTab = "latest-active-feature-tab", LatestActiveFeatureTab = "latest-active-feature-tab",
MiscellaneousEnabledFeatures = "miscellaneous-enabled-features",
RandomizeUsernameColors = "randomize-username-colors", RandomizeUsernameColors = "randomize-username-colors",
Version = "data-version", Version = "data-version",
} }

View File

@ -1,6 +1,6 @@
import {createValue, type Value} from "@holllo/webextension-storage"; import {createValue, type Value} from "@holllo/webextension-storage";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import {Data, Feature} from "./enums.js"; import {Data, Feature, MiscellaneousFeature} from "./enums.js";
import {collectHideTopicsData} from "./hide-topics.js"; import {collectHideTopicsData} from "./hide-topics.js";
import {defaultKnownGroups} from "./known-groups.js"; import {defaultKnownGroups} from "./known-groups.js";
import {collectUsernameColors} from "./username-color.js"; import {collectUsernameColors} from "./username-color.js";
@ -51,6 +51,14 @@ export const storageValues = {
value: Feature.Debug, value: Feature.Debug,
storage: browser.storage.sync, storage: browser.storage.sync,
}), }),
[Data.MiscellaneousEnabledFeatures]: createValue({
deserialize: (input) =>
new Set(JSON.parse(input) as MiscellaneousFeature[]),
serialize: (input) => JSON.stringify(Array.from(input)),
key: Data.MiscellaneousEnabledFeatures,
value: new Set([MiscellaneousFeature.CommentAnchorFix]),
storage: browser.storage.sync,
}),
[Data.Version]: createValue({ [Data.Version]: createValue({
deserialize: (input) => JSON.parse(input) as string, deserialize: (input) => JSON.parse(input) as string,
serialize: (input) => JSON.stringify(input), serialize: (input) => JSON.stringify(input),
@ -85,7 +93,7 @@ export const storageValues = {
/** /**
* Shorthand for the inferred shape of {@link storageValues}. * Shorthand for the inferred shape of {@link storageValues}.
*/ */
type StorageValues = typeof storageValues; export type StorageValues = typeof storageValues;
/** /**
* Return the {@link Value}-wrapped data associated with a particular key. * Return the {@link Value}-wrapped data associated with a particular key.