diff --git a/source/content-scripts/features/autocomplete.tsx b/source/content-scripts/features/autocomplete.tsx index 45ecebf..2ec60d0 100644 --- a/source/content-scripts/features/autocomplete.tsx +++ b/source/content-scripts/features/autocomplete.tsx @@ -40,7 +40,7 @@ export class AutocompleteFeature extends Component { return value.textContent!.replace(/^@/, "").toLowerCase(); }), - ...props.userLabels.map((value) => value.username), + ...props.userLabels.map(({value}) => value.username), ].sort((a, b) => a.localeCompare(b)); this.state = { diff --git a/source/content-scripts/features/user-labels.tsx b/source/content-scripts/features/user-labels.tsx index 1492d66..7ec5f8f 100644 --- a/source/content-scripts/features/user-labels.tsx +++ b/source/content-scripts/features/user-labels.tsx @@ -1,6 +1,10 @@ import debounce from "debounce"; import {Component, render} from "preact"; -import {type UserLabelsData, saveUserLabels} from "../../storage/common.js"; +import { + type UserLabelsData, + createValueUserLabel, + saveUserLabels, +} from "../../storage/common.js"; import { createElementFromString, isColorBright, @@ -62,17 +66,17 @@ export class UserLabelsFeature extends Component { const sortedLabels = userLabels.sort((a, b): number => { if (inTopicListing) { // If we're in the topic listing sort with highest priority first. - if (a.priority !== b.priority) { - return b.priority - a.priority; + if (a.value.priority !== b.value.priority) { + return b.value.priority - a.value.priority; } - } else if (a.priority !== b.priority) { + } else if (a.value.priority !== b.value.priority) { // If we're not in the topic listing, sort with lowest priority first. // We will add elements backwards, so the first label will be // behind all the other labels. - return a.priority - b.priority; + return a.value.priority - b.value.priority; } - return b.text.localeCompare(a.text); + return b.value.text.localeCompare(a.value.text); }); for (const element of elements) { @@ -85,7 +89,7 @@ export class UserLabelsFeature extends Component { } const userLabels = sortedLabels.filter( - (value) => + ({value}) => value.username.toLowerCase() === username && (onlyID === undefined ? true : value.id === onlyID), ); @@ -121,23 +125,26 @@ export class UserLabelsFeature extends Component { } for (const userLabel of userLabels) { - const bright = isColorBright(userLabel.color.trim()) + const bright = isColorBright(userLabel.value.color.trim()) ? "trx-bright" : ""; const label = createElementFromString(` - ${userLabel.text} + ${userLabel.value.text} `); label.addEventListener("click", (event: MouseEvent) => { - this.editLabelHandler(event, userLabel.id); + this.editLabelHandler(event, userLabel.value.id); }); element.after(label); - label.setAttribute("style", `background-color: ${userLabel.color};`); + label.setAttribute( + "style", + `background-color: ${userLabel.value.color};`, + ); // If we're in the topic listing, stop after adding 1 label. if (inTopicListing) { @@ -181,7 +188,7 @@ export class UserLabelsFeature extends Component { if (this.state.target === target && !this.state.hidden) { this.hide(); } else { - const label = this.props.userLabels.find((value) => value.id === id); + const label = this.props.userLabels.find(({value}) => value.id === id); if (label === undefined) { log( "User Labels: Tried to edit label with ID that could not be found.", @@ -193,7 +200,11 @@ export class UserLabelsFeature extends Component { this.setState({ hidden: false, target, - ...label, + color: label.value.color, + id: label.value.id, + priority: label.value.priority, + text: label.value.text, + username: label.value.username, }); } }; @@ -233,28 +244,33 @@ export class UserLabelsFeature extends Component { if (id === undefined) { let newId = 1; if (userLabels.length > 0) { - newId = userLabels.sort((a, b) => b.id - a.id)[0].id + 1; + newId = + userLabels.sort((a, b) => b.value.id - a.value.id)[0].value.id + 1; } - userLabels.push({ - color, - id: newId, - priority, - text, - username, - }); + userLabels.push( + await createValueUserLabel({ + color, + id: newId, + priority, + text, + username, + }), + ); this.addLabelsToUsernames(querySelectorAll(".link-user"), newId); } else { - const index = userLabels.findIndex((value) => value.id === id); + const index = userLabels.findIndex(({value}) => value.id === id); userLabels.splice(index, 1); - userLabels.push({ - id, - color, - priority, - text, - username, - }); + userLabels.push( + await createValueUserLabel({ + id, + color, + priority, + text, + username, + }), + ); const elements = querySelectorAll(`[data-trx-label-id="${id}"]`); const bright = isColorBright(color); @@ -283,7 +299,7 @@ export class UserLabelsFeature extends Component { } const {userLabels} = this.props; - const index = userLabels.findIndex((value) => value.id === id); + const index = userLabels.findIndex(({value}) => value.id === id); if (index === undefined) { log( `User Labels: Tried to remove label with ID ${id} that could not be found.`, diff --git a/source/options/user-label-editor.tsx b/source/options/user-label-editor.tsx index 88d1044..c4f725f 100644 --- a/source/options/user-label-editor.tsx +++ b/source/options/user-label-editor.tsx @@ -10,6 +10,7 @@ import { type UserLabel, fromStorage, Feature, + createValueUserLabel, saveUserLabels, } from "../storage/common.js"; import "../scss/index.scss"; @@ -42,28 +43,31 @@ class App extends Component { }; } - addNewLabel = () => { + addNewLabel = async () => { const {newLabelUsername, userLabels} = this.state; if (!isValidTildesUsername(newLabelUsername)) { return; } const existingUserLabel = userLabels.find( - ({username}) => username.toLowerCase() === newLabelUsername.toLowerCase(), + ({value: {username}}) => + username.toLowerCase() === newLabelUsername.toLowerCase(), ); let id = 1; if (userLabels.length > 0) { - id = userLabels.sort((a, b) => b.id - a.id)[0].id + 1; + id = userLabels.sort((a, b) => b.value.id - a.value.id)[0].value.id + 1; } - userLabels.push({ - color: "#ff00ff", - id, - priority: 0, - text: "New Label", - username: existingUserLabel?.username ?? newLabelUsername, - }); + userLabels.push( + await createValueUserLabel({ + color: "#ff00ff", + id, + priority: 0, + text: "New Label", + username: existingUserLabel?.value.username ?? newLabelUsername, + }), + ); this.setState({userLabels}); }; @@ -73,7 +77,9 @@ class App extends Component { }; editUserLabel = (event: Event, targetId: number, key: keyof UserLabel) => { - const index = this.state.userLabels.findIndex(({id}) => id === targetId); + const index = this.state.userLabels.findIndex( + ({value: {id}}) => id === targetId, + ); if (index === -1) { log(`Tried to edit UserLabel with unknown ID: ${targetId}`); return; @@ -82,9 +88,9 @@ class App extends Component { const newValue = (event.target as HTMLInputElement).value; // eslint-disable-next-line unicorn/prefer-ternary if (key === "id" || key === "priority") { - this.state.userLabels[index][key] = Number(newValue); + this.state.userLabels[index].value[key] = Number(newValue); } else { - this.state.userLabels[index][key] = newValue; + this.state.userLabels[index].value[key] = newValue; } this.setState({ @@ -94,7 +100,9 @@ class App extends Component { }; removeUserLabel = (targetId: number) => { - const userLabels = this.state.userLabels.filter(({id}) => id !== targetId); + const userLabels = this.state.userLabels.filter( + ({value: {id}}) => id !== targetId, + ); this.setState({ hasUnsavedChanges: true, userLabels, @@ -109,13 +117,14 @@ class App extends Component { render() { const {hasUnsavedChanges, newLabelUsername, userLabels} = this.state; - userLabels.sort((a, b) => a.username.localeCompare(b.username)); + userLabels.sort((a, b) => a.value.username.localeCompare(b.value.username)); const labelGroups = new Map(); for (const label of userLabels) { - const group = labelGroups.get(label.username) ?? []; - group.push(label); - labelGroups.set(label.username, group); + const username = label.value.username.toLowerCase(); + const group = labelGroups.get(username) ?? []; + group.push(label.value); + labelGroups.set(username, group); } const labels: JSX.Element[] = []; diff --git a/source/storage/common.ts b/source/storage/common.ts index c84fe63..54ea869 100644 --- a/source/storage/common.ts +++ b/source/storage/common.ts @@ -1,4 +1,4 @@ -import {createValue} from "@holllo/webextension-storage"; +import {createValue, type Value} from "@holllo/webextension-storage"; import browser from "webextension-polyfill"; export enum Feature { @@ -36,7 +36,7 @@ export type UserLabel = { username: string; }; -export type UserLabelsData = UserLabel[]; +export type UserLabelsData = Array>; export type UsernameColor = { color: string; @@ -46,6 +46,21 @@ export type UsernameColor = { export type UsernameColorsData = UsernameColor[]; +/** + * Create a {@link Value}-wrapped {@link UserLabel}. + */ +export async function createValueUserLabel( + userLabel: UserLabel, +): Promise { + return createValue({ + deserialize: (input) => JSON.parse(input) as UserLabel, + serialize: (input) => JSON.stringify(input), + key: `${Feature.UserLabels}-${userLabel.id}`, + value: userLabel, + storage: browser.storage.sync, + }); +} + /** * Get all user labels from storage and combine them into a single array. */ @@ -57,7 +72,9 @@ export async function collectUserLabels(): Promise { continue; } - userLabels.push(JSON.parse(value as string) as UserLabel); + userLabels.push( + await createValueUserLabel(JSON.parse(value as string) as UserLabel), + ); } return userLabels; @@ -74,19 +91,8 @@ export async function collectUserLabels(): Promise { export async function saveUserLabels( userLabels: UserLabelsData, ): Promise { - const storage = await browser.storage.sync.get(); - for (const key of Object.keys(storage)) { - if (!key.startsWith(Feature.UserLabels)) { - continue; - } - - await browser.storage.sync.remove(key); - } - for (const label of userLabels) { - await browser.storage.sync.set({ - [`${Feature.UserLabels}-${label.id}`]: JSON.stringify(label), - }); + await label.save(); } } diff --git a/source/storage/migrations/migrations.ts b/source/storage/migrations/migrations.ts index f312ad7..f8a1205 100644 --- a/source/storage/migrations/migrations.ts +++ b/source/storage/migrations/migrations.ts @@ -1,7 +1,13 @@ import {setup} from "@holllo/test"; import {type Migration} from "@holllo/migration-helper"; import browser from "webextension-polyfill"; -import {Data, Feature, fromStorage, saveUserLabels} from "../common.js"; +import { + Data, + Feature, + createValueUserLabel, + fromStorage, + saveUserLabels, +} from "../common.js"; import {v112DeserializeData, v112Sample, type V112Settings} from "./v1-1-2.js"; export const migrations: Array> = [ @@ -11,7 +17,13 @@ export const migrations: Array> = [ const deserialized = v112DeserializeData(data); data.data.userLabels = deserialized.userLabels; data.data.usernameColors = deserialized.usernameColors; - await saveUserLabels(data.data.userLabels); + + const userLabels = []; + for (const userLabel of data.data.userLabels) { + userLabels.push(await createValueUserLabel(userLabel)); + } + + await saveUserLabels(userLabels); const hideVotes = await fromStorage(Feature.HideVotes); hideVotes.value = {