1
Fork 0

Compare commits

...

2 Commits

11 changed files with 249 additions and 7 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 "./autocomplete.js";
export * from "./back-to-top.js";
export * from "./comment-anchor-fix.js";
export * from "./hide-votes.js";
export * from "./jump-to-new-comment.js";
export * from "./markdown-toolbar.js";

View File

@ -1,12 +1,18 @@
import {type JSX, render} from "preact";
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 {
AutocompleteFeature,
BackToTopFeature,
JumpToNewCommentFeature,
UserLabelsFeature,
runAnonymizeUsernamesFeature,
runCommentAnchorFixFeature,
runHideTopicsFeature,
runHideVotesFeature,
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.
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 {JumpToNewCommentSetting} from "./jump-to-new-comment.js";
export {MarkdownToolbarSetting} from "./markdown-toolbar.js";
export {MiscellaneousSetting} from "./miscellaneous.js";
export {ThemedLogoSetting} from "./themed-logo.js";
export {UserLabelsSetting} from "./user-labels.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

@ -185,9 +185,9 @@ export class UsernameColorsSetting extends Component<SettingProps, State> {
You can enter multiple usernames separated by a comma if you want them
to use the same color.
<br />
If randomize is selected then all usernames will be given a random
background color. This will not override colors you have manually
assigned.
If randomize is enabled then all usernames will be given a random
background color based on a hash of the username. Manually assigned
colors will be applied normally.
</p>
<div class="username-colors-controls">

View File

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

View File

@ -6,6 +6,7 @@
@import "shared";
@import "settings";
@import "settings/hide-topics";
@import "settings/miscellaneous-features";
html {
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",
JumpToNewComment = "jump-to-new-comment",
MarkdownToolbar = "markdown-toolbar",
Miscellaneous = "miscellaneous-features",
ThemedLogo = "themed-logo",
UserLabels = "user-labels",
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 {
EnabledFeatures = "enabled-features",
KnownGroups = "known-groups",
LatestActiveFeatureTab = "latest-active-feature-tab",
MiscellaneousEnabledFeatures = "miscellaneous-enabled-features",
RandomizeUsernameColors = "randomize-username-colors",
Version = "data-version",
}

View File

@ -1,6 +1,6 @@
import {createValue, type Value} from "@holllo/webextension-storage";
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 {defaultKnownGroups} from "./known-groups.js";
import {collectUsernameColors} from "./username-color.js";
@ -51,6 +51,14 @@ export const storageValues = {
value: Feature.Debug,
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({
deserialize: (input) => JSON.parse(input) as string,
serialize: (input) => JSON.stringify(input),
@ -85,7 +93,7 @@ export const 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.