mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-12 18:29:57 -05:00
feat(mobx): add persistence
This commit is contained in:
parent
2b55770ecc
commit
bc799931a8
12 changed files with 136 additions and 45 deletions
|
@ -7,3 +7,11 @@ export function urlBase64ToUint8Array(base64String: string) {
|
||||||
|
|
||||||
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
|
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapToRecord<K extends symbol | string | number, V>(
|
||||||
|
map: Map<K, V>,
|
||||||
|
) {
|
||||||
|
let record = {} as Record<K, V>;
|
||||||
|
map.forEach((v, k) => (record[k] = v));
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +1,16 @@
|
||||||
import { makeAutoObservable } from "mobx";
|
import localforage from "localforage";
|
||||||
|
import { autorun, makeAutoObservable, reaction } from "mobx";
|
||||||
|
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
|
import Persistent from "./interfaces/Persistent";
|
||||||
import Auth from "./stores/Auth";
|
import Auth from "./stores/Auth";
|
||||||
import Draft from "./stores/Draft";
|
import Draft from "./stores/Draft";
|
||||||
import Experiments from "./stores/Experiments";
|
import Experiments from "./stores/Experiments";
|
||||||
import Layout from "./stores/Layout";
|
import Layout from "./stores/Layout";
|
||||||
import LocaleOptions from "./stores/LocaleOptions";
|
import LocaleOptions from "./stores/LocaleOptions";
|
||||||
|
|
||||||
interface StoreDefinition {
|
|
||||||
id: string;
|
|
||||||
instance: Record<string, unknown>;
|
|
||||||
persistent: boolean;
|
|
||||||
synced: boolean;
|
|
||||||
global: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles global application state.
|
* Handles global application state.
|
||||||
*/
|
*/
|
||||||
|
@ -27,6 +21,8 @@ export default class State {
|
||||||
experiments: Experiments;
|
experiments: Experiments;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
|
|
||||||
|
private persistent: [string, Persistent<unknown>][] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct new State.
|
* Construct new State.
|
||||||
*/
|
*/
|
||||||
|
@ -38,6 +34,57 @@ export default class State {
|
||||||
this.layout = new Layout();
|
this.layout = new Layout();
|
||||||
|
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
this.registerListeners = this.registerListeners.bind(this);
|
||||||
|
this.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
private register() {
|
||||||
|
for (const key of Object.keys(this)) {
|
||||||
|
const obj = (
|
||||||
|
this as unknown as Record<string, Record<string, unknown>>
|
||||||
|
)[key];
|
||||||
|
|
||||||
|
// Check if this is an object.
|
||||||
|
if (typeof obj === "object") {
|
||||||
|
// Check if this is a Store.
|
||||||
|
if (typeof obj.id === "string") {
|
||||||
|
const id = obj.id;
|
||||||
|
|
||||||
|
// Check if this is a Persistent<T>
|
||||||
|
if (
|
||||||
|
typeof obj.hydrate === "function" &&
|
||||||
|
typeof obj.toJSON === "function"
|
||||||
|
) {
|
||||||
|
this.persistent.push([
|
||||||
|
id,
|
||||||
|
obj as unknown as Persistent<unknown>,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerListeners() {
|
||||||
|
const listeners = this.persistent.map(([id, store]) => {
|
||||||
|
return reaction(
|
||||||
|
() => store.toJSON(),
|
||||||
|
(value) => {
|
||||||
|
localforage.setItem(id, value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => listeners.forEach((x) => x());
|
||||||
|
}
|
||||||
|
|
||||||
|
async hydrate() {
|
||||||
|
for (const [id, store] of this.persistent) {
|
||||||
|
const data = await localforage.getItem(id);
|
||||||
|
if (typeof data === "object" && data !== null) {
|
||||||
|
store.hydrate(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
auth
|
|
||||||
drafts
|
|
||||||
experiments
|
|
||||||
last opened
|
|
||||||
locale
|
|
||||||
notifications
|
|
||||||
queue
|
|
||||||
section toggle
|
|
||||||
serevr config
|
|
||||||
settings
|
|
||||||
sync
|
|
||||||
themes
|
|
||||||
trusted links
|
|
||||||
unreads
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
import Store from "./Store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data store which is persistent and should cache its data locally.
|
* A data store which is persistent and should cache its data locally.
|
||||||
*/
|
*/
|
||||||
export default interface Persistent<T> {
|
export default interface Persistent<T> extends Store {
|
||||||
/**
|
/**
|
||||||
* Override toJSON to serialise this data store.
|
* Serialise this data store.
|
||||||
* This will also force all subclasses to implement this method.
|
|
||||||
*/
|
*/
|
||||||
toJSON(): unknown;
|
toJSON(): unknown;
|
||||||
|
|
||||||
|
|
3
src/mobx/interfaces/Store.ts
Normal file
3
src/mobx/interfaces/Store.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default interface Store {
|
||||||
|
get id(): string;
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { Session } from "revolt-api/types/Auth";
|
||||||
import { Nullable } from "revolt.js/dist/util/null";
|
import { Nullable } from "revolt.js/dist/util/null";
|
||||||
|
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
sessions: Record<string, Session>;
|
sessions: Record<string, Session>;
|
||||||
|
@ -13,7 +14,7 @@ interface Data {
|
||||||
* Handles account authentication, managing multiple
|
* Handles account authentication, managing multiple
|
||||||
* accounts and their sessions.
|
* accounts and their sessions.
|
||||||
*/
|
*/
|
||||||
export default class Auth implements Persistent<Data> {
|
export default class Auth implements Store, Persistent<Data> {
|
||||||
private sessions: ObservableMap<string, Session>;
|
private sessions: ObservableMap<string, Session>;
|
||||||
private current: Nullable<string>;
|
private current: Nullable<string>;
|
||||||
|
|
||||||
|
@ -26,10 +27,14 @@ export default class Auth implements Persistent<Data> {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "auth";
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
sessions: this.sessions,
|
sessions: [...this.sessions],
|
||||||
current: this.current,
|
current: this.current ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
|
|
||||||
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
drafts: Record<string, string>;
|
drafts: Record<string, string>;
|
||||||
|
@ -9,7 +12,7 @@ interface Data {
|
||||||
/**
|
/**
|
||||||
* Handles storing draft (currently being written) messages.
|
* Handles storing draft (currently being written) messages.
|
||||||
*/
|
*/
|
||||||
export default class Draft implements Persistent<Data> {
|
export default class Draft implements Store, Persistent<Data> {
|
||||||
private drafts: ObservableMap<string, string>;
|
private drafts: ObservableMap<string, string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,9 +23,13 @@ export default class Draft implements Persistent<Data> {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "draft";
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
drafts: this.drafts,
|
drafts: mapToRecord(this.drafts),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { action, computed, makeAutoObservable, ObservableSet } from "mobx";
|
import {
|
||||||
|
action,
|
||||||
|
autorun,
|
||||||
|
computed,
|
||||||
|
makeAutoObservable,
|
||||||
|
ObservableSet,
|
||||||
|
} from "mobx";
|
||||||
|
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Union type of available experiments.
|
* Union type of available experiments.
|
||||||
|
@ -35,7 +42,7 @@ interface Data {
|
||||||
/**
|
/**
|
||||||
* Handles enabling and disabling client experiments.
|
* Handles enabling and disabling client experiments.
|
||||||
*/
|
*/
|
||||||
export default class Experiments implements Persistent<Data> {
|
export default class Experiments implements Store, Persistent<Data> {
|
||||||
private enabled: ObservableSet<Experiment>;
|
private enabled: ObservableSet<Experiment>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,12 +50,17 @@ export default class Experiments implements Persistent<Data> {
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.enabled = new ObservableSet();
|
this.enabled = new ObservableSet();
|
||||||
|
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "experiments";
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
enabled: this.enabled,
|
enabled: [...this.enabled],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
|
|
||||||
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
lastSection?: "home" | "server";
|
lastSection?: "home" | "server";
|
||||||
|
@ -14,7 +17,7 @@ interface Data {
|
||||||
* Handles providing good UX experience on navigating
|
* Handles providing good UX experience on navigating
|
||||||
* back and forth between different parts of the app.
|
* back and forth between different parts of the app.
|
||||||
*/
|
*/
|
||||||
export default class Layout implements Persistent<Data> {
|
export default class Layout implements Store, Persistent<Data> {
|
||||||
/**
|
/**
|
||||||
* The last 'major section' that the user had open.
|
* The last 'major section' that the user had open.
|
||||||
* This is either the home tab or a channel ID (for a server channel).
|
* This is either the home tab or a channel ID (for a server channel).
|
||||||
|
@ -47,12 +50,16 @@ export default class Layout implements Persistent<Data> {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "layout";
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
lastSection: this.lastSection,
|
lastSection: this.lastSection,
|
||||||
lastHomePath: this.lastHomePath,
|
lastHomePath: this.lastHomePath,
|
||||||
lastOpened: this.lastOpened,
|
lastOpened: mapToRecord(this.lastOpened),
|
||||||
openSections: this.openSections,
|
openSections: mapToRecord(this.openSections),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { action, computed, makeAutoObservable } from "mobx";
|
||||||
import { Language, Languages } from "../../context/Locale";
|
import { Language, Languages } from "../../context/Locale";
|
||||||
|
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
lang: Language;
|
lang: Language;
|
||||||
|
@ -52,7 +53,7 @@ export function findLanguage(lang?: string): Language {
|
||||||
* Handles providing good UX experience on navigating
|
* Handles providing good UX experience on navigating
|
||||||
* back and forth between different parts of the app.
|
* back and forth between different parts of the app.
|
||||||
*/
|
*/
|
||||||
export default class LocaleOptions implements Persistent<Data> {
|
export default class LocaleOptions implements Store, Persistent<Data> {
|
||||||
private lang: Language;
|
private lang: Language;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,6 +64,10 @@ export default class LocaleOptions implements Persistent<Data> {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "locale";
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
lang: this.lang,
|
lang: this.lang,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
import { Channel } from "revolt-api/types/Channels";
|
import { Channel } from "revolt-api/types/Channels";
|
||||||
|
|
||||||
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Possible notification states.
|
* Possible notification states.
|
||||||
|
@ -31,7 +34,7 @@ interface Data {
|
||||||
/**
|
/**
|
||||||
* Manages the user's notification preferences.
|
* Manages the user's notification preferences.
|
||||||
*/
|
*/
|
||||||
export default class NotificationOptions implements Persistent<Data> {
|
export default class NotificationOptions implements Store, Persistent<Data> {
|
||||||
private server: ObservableMap<string, string>;
|
private server: ObservableMap<string, string>;
|
||||||
private channel: ObservableMap<string, string>;
|
private channel: ObservableMap<string, string>;
|
||||||
|
|
||||||
|
@ -44,10 +47,14 @@ export default class NotificationOptions implements Persistent<Data> {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return "notifications";
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
server: this.server,
|
server: mapToRecord(this.server),
|
||||||
channel: this.channel,
|
channel: mapToRecord(this.channel),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,16 +26,19 @@ export default function StateLoader(props: Props) {
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localForage.getItem("state").then((state) => {
|
localForage.getItem("state").then((s) => {
|
||||||
if (state !== null) {
|
if (s !== null) {
|
||||||
dispatch({ type: "__INIT", state: state as State });
|
dispatch({ type: "__INIT", state: s as State });
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoaded(true);
|
state.hydrate().then(() => setLoaded(true));
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!loaded) return null;
|
if (!loaded) return null;
|
||||||
|
|
||||||
|
useEffect(state.registerListeners);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<StateContextProvider value={state}>
|
<StateContextProvider value={state}>
|
||||||
|
|
Loading…
Reference in a new issue