mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-25 00:20: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)));
|
||||
}
|
||||
|
||||
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 { useContext } from "preact/hooks";
|
||||
|
||||
import Persistent from "./interfaces/Persistent";
|
||||
import Auth from "./stores/Auth";
|
||||
import Draft from "./stores/Draft";
|
||||
import Experiments from "./stores/Experiments";
|
||||
import Layout from "./stores/Layout";
|
||||
import LocaleOptions from "./stores/LocaleOptions";
|
||||
|
||||
interface StoreDefinition {
|
||||
id: string;
|
||||
instance: Record<string, unknown>;
|
||||
persistent: boolean;
|
||||
synced: boolean;
|
||||
global: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles global application state.
|
||||
*/
|
||||
|
@ -27,6 +21,8 @@ export default class State {
|
|||
experiments: Experiments;
|
||||
layout: Layout;
|
||||
|
||||
private persistent: [string, Persistent<unknown>][] = [];
|
||||
|
||||
/**
|
||||
* Construct new State.
|
||||
*/
|
||||
|
@ -38,6 +34,57 @@ export default class State {
|
|||
this.layout = new Layout();
|
||||
|
||||
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.
|
||||
*/
|
||||
export default interface Persistent<T> {
|
||||
export default interface Persistent<T> extends Store {
|
||||
/**
|
||||
* Override toJSON to serialise this data store.
|
||||
* This will also force all subclasses to implement this method.
|
||||
* Serialise this data store.
|
||||
*/
|
||||
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 Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
|
||||
interface Data {
|
||||
sessions: Record<string, Session>;
|
||||
|
@ -13,7 +14,7 @@ interface Data {
|
|||
* Handles account authentication, managing multiple
|
||||
* 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 current: Nullable<string>;
|
||||
|
||||
|
@ -26,10 +27,14 @@ export default class Auth implements Persistent<Data> {
|
|||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "auth";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
sessions: this.sessions,
|
||||
current: this.current,
|
||||
sessions: [...this.sessions],
|
||||
current: this.current ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
|
||||
interface Data {
|
||||
drafts: Record<string, string>;
|
||||
|
@ -9,7 +12,7 @@ interface Data {
|
|||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
|
@ -20,9 +23,13 @@ export default class Draft implements Persistent<Data> {
|
|||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "draft";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
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 Store from "../interfaces/Store";
|
||||
|
||||
/**
|
||||
* Union type of available experiments.
|
||||
|
@ -35,7 +42,7 @@ interface Data {
|
|||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
|
@ -43,12 +50,17 @@ export default class Experiments implements Persistent<Data> {
|
|||
*/
|
||||
constructor() {
|
||||
this.enabled = new ObservableSet();
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "experiments";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
enabled: this.enabled,
|
||||
enabled: [...this.enabled],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
|
||||
interface Data {
|
||||
lastSection?: "home" | "server";
|
||||
|
@ -14,7 +17,7 @@ interface Data {
|
|||
* Handles providing good UX experience on navigating
|
||||
* 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.
|
||||
* 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);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "layout";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
lastSection: this.lastSection,
|
||||
lastHomePath: this.lastHomePath,
|
||||
lastOpened: this.lastOpened,
|
||||
openSections: this.openSections,
|
||||
lastOpened: mapToRecord(this.lastOpened),
|
||||
openSections: mapToRecord(this.openSections),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { action, computed, makeAutoObservable } from "mobx";
|
|||
import { Language, Languages } from "../../context/Locale";
|
||||
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
|
||||
interface Data {
|
||||
lang: Language;
|
||||
|
@ -52,7 +53,7 @@ export function findLanguage(lang?: string): Language {
|
|||
* Handles providing good UX experience on navigating
|
||||
* 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;
|
||||
|
||||
/**
|
||||
|
@ -63,6 +64,10 @@ export default class LocaleOptions implements Persistent<Data> {
|
|||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "locale";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
lang: this.lang,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
import { Channel } from "revolt-api/types/Channels";
|
||||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
|
||||
/**
|
||||
* Possible notification states.
|
||||
|
@ -31,7 +34,7 @@ interface Data {
|
|||
/**
|
||||
* 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 channel: ObservableMap<string, string>;
|
||||
|
||||
|
@ -44,10 +47,14 @@ export default class NotificationOptions implements Persistent<Data> {
|
|||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "notifications";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
server: this.server,
|
||||
channel: this.channel,
|
||||
server: mapToRecord(this.server),
|
||||
channel: mapToRecord(this.channel),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -26,16 +26,19 @@ export default function StateLoader(props: Props) {
|
|||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
localForage.getItem("state").then((state) => {
|
||||
if (state !== null) {
|
||||
dispatch({ type: "__INIT", state: state as State });
|
||||
localForage.getItem("state").then((s) => {
|
||||
if (s !== null) {
|
||||
dispatch({ type: "__INIT", state: s as State });
|
||||
}
|
||||
|
||||
setLoaded(true);
|
||||
state.hydrate().then(() => setLoaded(true));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!loaded) return null;
|
||||
|
||||
useEffect(state.registerListeners);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<StateContextProvider value={state}>
|
||||
|
|
Loading…
Reference in a new issue