feat(mobx): start work on migrations

This commit is contained in:
Paul Makles 2021-12-20 12:01:45 +00:00
parent 89dda8fe82
commit 68578d2620
8 changed files with 105 additions and 99 deletions

View file

@ -34,7 +34,13 @@ import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
export const ThemeBaseSelectorShim = observer(() => { export const ThemeBaseSelectorShim = observer(() => {
const theme = useApplicationState().settings.theme; const theme = useApplicationState().settings.theme;
return ( return (
<ThemeBaseSelector value={theme.getBase()} setValue={theme.setBase} /> <ThemeBaseSelector
value={theme.isModified() ? undefined : theme.getBase()}
setValue={(base) => {
theme.setBase(base);
theme.reset();
}}
/>
); );
}); });
@ -97,10 +103,6 @@ export const ThemeCustomCSSShim = observer(() => {
); );
}); });
export const ThemeImporterShim = observer(() => {
return <a></a>;
});
/** /**
* Component providing a way to switch between compact and normal message view. * Component providing a way to switch between compact and normal message view.
*/ */

View file

@ -92,7 +92,7 @@ export function EmojiSelector({ value, setValue }: Props) {
<div <div
class="button" class="button"
onClick={() => setValue("mutant")} onClick={() => setValue("mutant")}
data-active={value === "mutant"}> data-active={!value || value === "mutant"}>
<img <img
loading="eager" loading="eager"
src={mutantSVG} src={mutantSVG}

View file

@ -0,0 +1,11 @@
import Store from "./Store";
/**
* A data store which is migrated forwards.
*/
export default interface Migrate<K extends string> extends Store {
/**
* Migrate this data store.
*/
migrate(key: K, data: Record<string, unknown>, rev: number): void;
}

View file

@ -1,76 +0,0 @@
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
import { mapToRecord } from "../../lib/conversion";
import { StoredTheme } from "../../redux/reducers/themes";
import Persistent from "../interfaces/Persistent";
import Store from "../interfaces/Store";
interface Data {
themes: Record<string, StoredTheme>;
}
/**
* Cache data store for temporary, long-lived data.
*/
export default class Cache implements Store, Persistent<Data> {
private themes: ObservableMap<string, StoredTheme>;
/**
* Construct new Cache store.
*/
constructor() {
this.themes = new ObservableMap();
makeAutoObservable(this);
}
get id() {
return "draft";
}
toJSON() {
return {
themes: JSON.parse(JSON.stringify(mapToRecord(this.themes))),
};
}
@action hydrate(data: Data) {
Object.keys(data.themes).forEach((key) =>
this.themes.set(key, data.themes[key]),
);
}
/**
* Cache a given theme.
* @param theme Theme
*/
@action cacheTheme(theme: StoredTheme) {
this.themes.set(theme.slug, theme);
}
/**
* Remove a cached theme.
* @param slug String
*/
@action removeTheme(slug: string) {
this.themes.delete(slug);
}
/**
* Get a cached theme by its slug.
* @param slug Theme slug
* @returns Theme, if found
*/
@computed getTheme(slug: string) {
return this.themes.get(slug);
}
/**
* Get all cached themes.
* @returns Themes
*/
@computed getThemes() {
return [...this.themes.values()];
}
}

View file

@ -50,7 +50,6 @@ export default class Experiments implements Store, Persistent<Data> {
*/ */
constructor() { constructor() {
this.enabled = new ObservableSet(); this.enabled = new ObservableSet();
makeAutoObservable(this); makeAutoObservable(this);
} }

70
src/mobx/stores/Sync.ts Normal file
View file

@ -0,0 +1,70 @@
import {
action,
computed,
makeAutoObservable,
ObservableMap,
ObservableSet,
} from "mobx";
import { Client } from "revolt.js";
import { mapToRecord } from "../../lib/conversion";
import Persistent from "../interfaces/Persistent";
import Store from "../interfaces/Store";
export type SyncKeys = "theme" | "appearance" | "locale" | "notifications";
export const SYNC_KEYS: SyncKeys[] = [
"theme",
"appearance",
"locale",
"notifications",
];
interface Data {
disabled: SyncKeys[];
}
/**
* Handles syncing settings data.
*/
export default class Sync implements Store, Persistent<Data> {
private disabled: ObservableSet<SyncKeys>;
/**
* Construct new Sync store.
*/
constructor() {
this.disabled = new ObservableSet();
makeAutoObservable(this);
this.isEnabled = this.isEnabled.bind(this);
}
get id() {
return "sync";
}
toJSON() {
return {
enabled: [...this.disabled],
};
}
@action hydrate(data: Data) {
if (data.disabled) {
for (const key of data.disabled) {
this.disabled.add(key as SyncKeys);
}
}
}
@computed isEnabled(key: SyncKeys) {
return !this.disabled.has(key);
}
async pull(client: Client) {
const data = await client.syncFetchSettings(
SYNC_KEYS.filter(this.isEnabled),
);
}
}

View file

@ -41,7 +41,9 @@ export default class STheme {
); );
} }
@action hydrate(data: Partial<Theme>) { @action hydrate(data: Partial<Theme>, resetCSS = false) {
if (resetCSS) this.setCSS();
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
const value = data[key as keyof Theme] as string; const value = data[key as keyof Theme] as string;
switch (key) { switch (key) {
@ -137,8 +139,8 @@ export default class STheme {
); );
} }
@action setCSS(value: string) { @action setCSS(value?: string) {
if (value.length > 0) { if (value && value.length > 0) {
this.settings.set("appearance:theme:css", value); this.settings.set("appearance:theme:css", value);
} else { } else {
this.settings.remove("appearance:theme:css"); this.settings.remove("appearance:theme:css");
@ -153,6 +155,13 @@ export default class STheme {
return this.settings.get("appearance:theme:css"); return this.settings.get("appearance:theme:css");
} }
@computed isModified() {
return (
Object.keys(this.settings.get("appearance:theme:overrides") ?? {})
.length > 0
);
}
@action setBase(base?: "light" | "dark") { @action setBase(base?: "light" | "dark") {
if (base) { if (base) {
this.settings.set("appearance:theme:base", base); this.settings.set("appearance:theme:base", base);

View file

@ -2,6 +2,7 @@ import styled from "styled-components";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useApplicationState } from "../../../mobx/State";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme"; import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme";
@ -149,6 +150,8 @@ export function ThemeShop() {
>(null); >(null);
const [themeData, setThemeData] = useState<Record<string, Theme>>({}); const [themeData, setThemeData] = useState<Record<string, Theme>>({});
const themes = useApplicationState().settings.theme;
async function fetchThemeList() { async function fetchThemeList() {
const manifest = await fetchManifest(); const manifest = await fetchManifest();
setThemeList( setThemeList(
@ -190,19 +193,7 @@ export function ThemeShop() {
<button <button
class="preview" class="preview"
onClick={() => { onClick={() => {
dispatch({ themes.hydrate(themeData[slug], true);
type: "THEMES_SET_THEME",
theme: {
slug,
meta: theme,
theme: themeData[slug]
}
})
dispatch({
type: "SETTINGS_SET_THEME",
theme: { base: slug },
});
}}> }}>
<ThemePreview slug={slug} theme={themeData[slug]} /> <ThemePreview slug={slug} theme={themeData[slug]} />
</button> </button>