Compare commits
No commits in common. "b8ff891c316c8ec57c071146787a19efc660a189" and "84c69eaab3864cfa6f0188c531888ea5bf88561b" have entirely different histories.
b8ff891c31
...
84c69eaab3
|
@ -1,70 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
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";
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
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 {
|
import {Feature, fromStorage, Data} from "../storage/exports.js";
|
||||||
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,
|
||||||
|
@ -112,11 +106,6 @@ 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()));
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ 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";
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -185,9 +185,9 @@ export class UsernameColorsSetting extends Component<SettingProps, State> {
|
||||||
You can enter multiple usernames separated by a comma if you want them
|
You can enter multiple usernames separated by a comma if you want them
|
||||||
to use the same color.
|
to use the same color.
|
||||||
<br />
|
<br />
|
||||||
If randomize is enabled then all usernames will be given a random
|
If randomize is selected then all usernames will be given a random
|
||||||
background color based on a hash of the username. Manually assigned
|
background color. This will not override colors you have manually
|
||||||
colors will be applied normally.
|
assigned.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="username-colors-controls">
|
<div class="username-colors-controls">
|
||||||
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
HideVotesSetting,
|
HideVotesSetting,
|
||||||
JumpToNewCommentSetting,
|
JumpToNewCommentSetting,
|
||||||
MarkdownToolbarSetting,
|
MarkdownToolbarSetting,
|
||||||
MiscellaneousSetting,
|
|
||||||
ThemedLogoSetting,
|
ThemedLogoSetting,
|
||||||
UserLabelsSetting,
|
UserLabelsSetting,
|
||||||
UsernameColorsSetting,
|
UsernameColorsSetting,
|
||||||
|
@ -72,13 +71,6 @@ 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,
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
@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%;
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,27 +10,18 @@ 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 feature names.
|
* Keys of miscellaneous data stored in WebExtension storage.
|
||||||
*/
|
|
||||||
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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, MiscellaneousFeature} from "./enums.js";
|
import {Data, Feature} 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,14 +51,6 @@ 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),
|
||||||
|
@ -93,7 +85,7 @@ export const storageValues = {
|
||||||
/**
|
/**
|
||||||
* Shorthand for the inferred shape of {@link storageValues}.
|
* Shorthand for the inferred shape of {@link storageValues}.
|
||||||
*/
|
*/
|
||||||
export type StorageValues = typeof storageValues;
|
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.
|
||||||
|
|
Loading…
Reference in New Issue