refactor: ♻️ Rewrite settings backend

This commit is contained in:
Jesse Wierzbinski 2024-07-21 18:53:16 +02:00
parent 80b1fc87f7
commit 78e4fa0f04
No known key found for this signature in database
6 changed files with 133 additions and 126 deletions

View file

@ -36,14 +36,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Switch } from "@ark-ui/vue"; import { Switch } from "@ark-ui/vue";
import { type Setting, type SettingIds, SettingType } from "~/settings"; import { type SettingIds, SettingType } from "~/settings";
const props = defineProps<{ const props = defineProps<{
setting: Setting; id: SettingIds;
}>(); }>();
const checked = ref(!!props.setting.value);
const setting = useSetting(props.setting.id as SettingIds); const setting = useSetting(props.id);
const checked = ref(setting.value.value as boolean);
watch(checked, (c) => { watch(checked, (c) => {
setting.value.value = c; setting.value.value = c;

View file

@ -1,6 +1,7 @@
<template> <template>
<HoverCard.Root :positioning="{ <HoverCard.Root :positioning="{
placement: 'bottom', placement: 'bottom',
strategy: 'fixed',
}" v-if="isEnabled.value"> }" v-if="isEnabled.value">
<HoverCard.Trigger :as-child="true"> <HoverCard.Trigger :as-child="true">
<slot /> <slot />

View file

@ -1,6 +1,6 @@
import type { Account, Emoji } from "@lysand-org/client/types"; import type { Account, Emoji } from "@lysand-org/client/types";
import { renderToString } from "vue/server-renderer"; import { renderToString } from "vue/server-renderer";
import { SettingIds, type Settings, getSettingById } from "~/settings"; import { SettingIds, type Settings } from "~/settings";
import MentionComponent from "../components/social-elements/notes/mention.vue"; import MentionComponent from "../components/social-elements/notes/mention.vue";
/** /**
@ -13,7 +13,7 @@ export const useParsedContent = (
content: MaybeRef<string>, content: MaybeRef<string>,
emojis: MaybeRef<Emoji[]>, emojis: MaybeRef<Emoji[]>,
mentions: MaybeRef<Account[]> = ref([]), mentions: MaybeRef<Account[]> = ref([]),
settings: MaybeRef<Settings> = ref([]), settings: MaybeRef<Settings | null> = ref(null),
): Ref<string | null> => { ): Ref<string | null> => {
const result = ref(null as string | null); const result = ref(null as string | null);
@ -27,10 +27,8 @@ export const useParsedContent = (
const contentHtml = document.createElement("div"); const contentHtml = document.createElement("div");
contentHtml.innerHTML = toValue(content); contentHtml.innerHTML = toValue(content);
const shouldRenderEmoji = getSettingById( const shouldRenderEmoji =
toValue(settings), toValue(settings)?.[SettingIds.CustomEmojis].value;
SettingIds.CustomEmojis,
)?.value;
// Replace emoji shortcodes with images // Replace emoji shortcodes with images
if (shouldRenderEmoji) { if (shouldRenderEmoji) {

View file

@ -3,7 +3,7 @@ import {
type Setting, type Setting,
type SettingIds, type SettingIds,
type Settings, type Settings,
parseFromJson, mergeSettings,
settings as settingsJson, settings as settingsJson,
} from "~/settings"; } from "~/settings";
@ -13,18 +13,17 @@ const useSettings = () => {
read(raw) { read(raw) {
const json = StorageSerializers.object.read(raw); const json = StorageSerializers.object.read(raw);
return parseFromJson(json); return mergeSettings(json);
}, },
write(value) { write(value) {
// key/value, with key being id and value being the value const json = Object.fromEntries(
const json = value.reduce( Object.entries(value).map(([key, value]) => [
(acc, setting) => { key,
acc[setting.id] = setting.value; value.value,
return acc; ]),
},
{} as Record<string, unknown>,
); );
// flatMap object values to .value
return StorageSerializers.object.write(json); return StorageSerializers.object.write(json);
}, },
}, },
@ -33,24 +32,21 @@ const useSettings = () => {
export const settings = useSettings(); export const settings = useSettings();
export const useSetting = <T extends Setting = Setting>(id: SettingIds) => { export const useSetting = <Id extends SettingIds>(
const setting: Ref<T> = ref<T>( id: Id,
settings.value.find((s) => s.id === id) as T, ): Ref<Settings[Id]> => {
) as unknown as Ref<T>; const setting = ref(settings.value[id]) as Ref<Settings[Id]>;
watch(settings, (newSettings) => { watch(settings, (newSettings) => {
setting.value = newSettings.find((s) => s.id === id) as T; setting.value = newSettings[id];
}); });
watch(setting, (newSetting) => { watch(setting, (newSetting) => {
settings.value = settings.value.map((s) => settings.value = {
s.id === id ? newSetting : s, ...settings.value,
) as Settings; [id]: newSetting,
};
}); });
return setting; return setting;
}; };
export const getSetting = <T extends Setting = Setting>(id: SettingIds) => {
return settingsJson.find((s) => s.id === id) as T;
};

View file

@ -1,22 +1,19 @@
<template> <template>
<SettingsSidebar> <SettingsSidebar>
<template #behaviour> <template #behaviour>
<Renderer :setting="setting" v-for="setting of getSettingsForPath( <Renderer :id="id" v-for="id of (Object.keys(getSettingsForPage(
settings,
SettingPages.Behaviour, SettingPages.Behaviour,
)" :key="setting.id" /> )) as SettingIds[])" :key="id" />
</template> </template>
<template #appearance> <template #appearance>
<Renderer :setting="setting" v-for="setting of getSettingsForPath( <Renderer :id="id" v-for="id of (Object.keys(getSettingsForPage(
settings,
SettingPages.Appearance, SettingPages.Appearance,
)" :key="setting.id" /> )) as SettingIds[])" :key="id" />
</template> </template>
<template #advanced> <template #advanced>
<Renderer :setting="setting" v-for="setting of getSettingsForPath( <Renderer :id="id" v-for="id of (Object.keys(getSettingsForPage(
settings,
SettingPages.Advanced, SettingPages.Advanced,
)" :key="setting.id" /> )) as SettingIds[])" :key="id" />
</template> </template>
<template #account> <template #account>
<ProfileEditor /> <ProfileEditor />
@ -28,7 +25,7 @@
import ProfileEditor from "~/components/settings/profile-editor.vue"; import ProfileEditor from "~/components/settings/profile-editor.vue";
import Renderer from "~/components/settings/renderer.vue"; import Renderer from "~/components/settings/renderer.vue";
import SettingsSidebar from "~/components/sidebars/settings-sidebar.vue"; import SettingsSidebar from "~/components/sidebars/settings-sidebar.vue";
import { SettingPages, getSettingsForPath } from "~/settings"; import { SettingIds, SettingPages, getSettingsForPage } from "~/settings";
definePageMeta({ definePageMeta({
layout: "app", layout: "app",

View file

@ -7,27 +7,51 @@ export enum SettingType {
Code = "code", Code = "code",
} }
export type Setting<T = SettingType> = { export type Setting = {
id: string;
title: string; title: string;
description: string; description: string;
notImplemented?: boolean; notImplemented?: boolean;
type: T; type: SettingType;
value: T extends SettingType.String | SettingType.Code value: unknown;
? string page: SettingPages;
: T extends SettingType.Boolean };
? boolean
: T extends SettingType.Float | SettingType.Integer export type StringSetting = Setting & {
? number type: SettingType.String;
: T extends SettingType.Enum value: string;
? string };
: never;
min?: T extends SettingType.Float | SettingType.Integer ? number : never; export type BooleanSetting = Setting & {
max?: T extends SettingType.Float | SettingType.Integer ? number : never; type: SettingType.Boolean;
step?: T extends SettingType.Float | SettingType.Integer ? number : never; value: boolean;
options?: T extends SettingType.Enum ? string[] : never; };
language?: T extends SettingType.Code ? string : never;
path: SettingPages; export type EnumSetting = Setting & {
type: SettingType.Enum;
value: string;
options: string[];
};
export type FloatSetting = Setting & {
type: SettingType.Float;
value: number;
min: number;
max: number;
step: number;
};
export type IntegerSetting = Setting & {
type: SettingType.Integer;
value: number;
min: number;
max: number;
step: number;
};
export type CodeSetting = Setting & {
type: SettingType.Code;
value: string;
language: string;
}; };
export enum SettingPages { export enum SettingPages {
@ -37,27 +61,6 @@ export enum SettingPages {
Appearance = "appearance", Appearance = "appearance",
} }
export const getSettingsForPath = (
settingsToFilterIn: Settings,
path: SettingPages,
) => settingsToFilterIn.filter((setting) => setting.path === path);
export const getSettingById = (settingsToFilterIn: Settings, id: SettingIds) =>
settingsToFilterIn.find((setting) => setting.id === id);
export const parseFromJson = (json: Record<string, unknown>) => {
const finalSettings = structuredClone(settings);
// Override the default values with the values from the JSON except for the user value
for (const setting of finalSettings) {
if (setting.id in json) {
setting.value = json[setting.id] as (typeof setting)["value"];
}
}
return finalSettings;
};
export enum SettingIds { export enum SettingIds {
Mfm = "mfm", Mfm = "mfm",
CustomCSS = "custom-css", CustomCSS = "custom-css",
@ -72,103 +75,115 @@ export enum SettingIds {
ConfirmFavourite = "confirm-favourite", ConfirmFavourite = "confirm-favourite",
} }
export const settings = [ export const settings: Record<SettingIds, Setting> = {
{ [SettingIds.Mfm]: {
id: SettingIds.Mfm,
title: "Render MFM", title: "Render MFM",
description: "Render Misskey-Flavoured Markdown", description: "Render Misskey-Flavoured Markdown",
type: SettingType.Boolean, type: SettingType.Boolean,
value: false, value: false,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
notImplemented: true, notImplemented: true,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
{ [SettingIds.CustomCSS]: {
id: SettingIds.CustomCSS,
title: "Custom CSS", title: "Custom CSS",
description: "Custom CSS for the UI", description: "Custom CSS for the UI",
type: SettingType.Code, type: SettingType.Code,
value: "", value: "",
language: "css", language: "css",
path: SettingPages.Appearance, page: SettingPages.Appearance,
} as Setting<SettingType.Code>, } as CodeSetting,
{ [SettingIds.Theme]: {
id: SettingIds.Theme,
title: "Theme", title: "Theme",
description: "UI theme", description: "UI theme",
type: SettingType.Enum, type: SettingType.Enum,
value: "dark", value: "dark",
options: ["light", "dark"], options: ["light", "dark"],
path: SettingPages.Appearance, page: SettingPages.Appearance,
} as Setting<SettingType.Enum>, } as EnumSetting,
{ [SettingIds.CustomEmojis]: {
id: SettingIds.CustomEmojis,
title: "Render Custom Emojis", title: "Render Custom Emojis",
description: "Render custom emojis", description: "Render custom emojis",
type: SettingType.Boolean, type: SettingType.Boolean,
value: true, value: true,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
{ [SettingIds.ShowContentWarning]: {
id: SettingIds.ShowContentWarning,
title: "Show Content Warning", title: "Show Content Warning",
description: "Show content warnings on notes marked sensitive/spoiler", description: "Show content warnings on notes marked sensitive/spoiler",
type: SettingType.Boolean, type: SettingType.Boolean,
value: true, value: true,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
{ [SettingIds.PopupAvatarHover]: {
id: SettingIds.PopupAvatarHover,
title: "Popup Profile Hover", title: "Popup Profile Hover",
description: "Show profile popup when hovering over a user's avatar", description: "Show profile popup when hovering over a user's avatar",
type: SettingType.Boolean, type: SettingType.Boolean,
value: true, value: true,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
{ [SettingIds.InfiniteScroll]: {
id: SettingIds.InfiniteScroll,
title: "Infinite Scroll", title: "Infinite Scroll",
description: description:
"Automatically load more notes when reaching the bottom of the page", "Automatically load more notes when reaching the bottom of the page",
type: SettingType.Boolean, type: SettingType.Boolean,
value: true, value: true,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
{ [SettingIds.ConfirmDelete]: {
id: SettingIds.ConfirmDelete,
title: "Confirm Delete", title: "Confirm Delete",
description: "Confirm before deleting a note", description: "Confirm before deleting a note",
type: SettingType.Boolean, type: SettingType.Boolean,
value: false, value: false,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
notImplemented: true, notImplemented: true,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
{ [SettingIds.ConfirmFollow]: {
id: SettingIds.ConfirmFollow,
title: "Confirm Follow", title: "Confirm Follow",
description: "Confirm before following/unfollowing a user", description: "Confirm before following/unfollowing a user",
type: SettingType.Boolean, type: SettingType.Boolean,
value: false, value: false,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
notImplemented: true, notImplemented: true,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
{ [SettingIds.ConfirmReblog]: {
id: SettingIds.ConfirmReblog,
title: "Confirm Reblog", title: "Confirm Reblog",
description: "Confirm before reblogging a note", description: "Confirm before reblogging a note",
type: SettingType.Boolean, type: SettingType.Boolean,
value: false, value: false,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
notImplemented: true, notImplemented: true,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
{ [SettingIds.ConfirmFavourite]: {
id: SettingIds.ConfirmFavourite,
title: "Confirm Favourite", title: "Confirm Favourite",
description: "Confirm before favouriting a note", description: "Confirm before favouriting a note",
type: SettingType.Boolean, type: SettingType.Boolean,
value: false, value: false,
path: SettingPages.Behaviour, page: SettingPages.Behaviour,
notImplemented: true, notImplemented: true,
} as Setting<SettingType.Boolean>, } as BooleanSetting,
]; };
export const getSettingsForPage = (page: SettingPages): Partial<Settings> => {
return Object.fromEntries(
Object.entries(settings).filter(([, setting]) => setting.page === page),
);
};
/**
* Merge a partly defined Settings object with the default settings
* Useful when there is an update to the settings in the backend
*/
export const mergeSettings = (
settingsToMerge: Record<SettingIds, Setting["value"]>,
): Settings => {
const finalSettings = structuredClone(settings);
for (const [key, value] of Object.entries(settingsToMerge)) {
if (key in settings) {
finalSettings[key as SettingIds].value = value;
}
}
return finalSettings;
};
export type Settings = typeof settings; export type Settings = typeof settings;