forked from abner/for-legacy-web
feat(mobx): add sync back (do not look at the code)
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
// @ts-expect-error No typings.
|
||||
import stringify from "json-stringify-deterministic";
|
||||
import localforage from "localforage";
|
||||
import { autorun, makeAutoObservable, reaction } from "mobx";
|
||||
|
||||
import { createContext } from "preact";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { makeAutoObservable, reaction } from "mobx";
|
||||
import { Client } from "revolt.js";
|
||||
|
||||
import Persistent from "./interfaces/Persistent";
|
||||
import Syncable from "./interfaces/Syncable";
|
||||
import Auth from "./stores/Auth";
|
||||
import Draft from "./stores/Draft";
|
||||
import Experiments from "./stores/Experiments";
|
||||
@@ -14,7 +15,11 @@ import MessageQueue from "./stores/MessageQueue";
|
||||
import NotificationOptions from "./stores/NotificationOptions";
|
||||
import ServerConfig from "./stores/ServerConfig";
|
||||
import Settings from "./stores/Settings";
|
||||
import Sync from "./stores/Sync";
|
||||
import Sync, { Data as DataSync, SyncKeys } from "./stores/Sync";
|
||||
|
||||
export const MIGRATIONS = {
|
||||
REDUX: 1640305719826,
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles global application state.
|
||||
@@ -32,6 +37,7 @@ export default class State {
|
||||
sync: Sync;
|
||||
|
||||
private persistent: [string, Persistent<unknown>][] = [];
|
||||
private disabled: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* Construct new State.
|
||||
@@ -46,11 +52,11 @@ export default class State {
|
||||
this.notifications = new NotificationOptions();
|
||||
this.queue = new MessageQueue();
|
||||
this.settings = new Settings();
|
||||
this.sync = new Sync();
|
||||
this.sync = new Sync(this);
|
||||
|
||||
makeAutoObservable(this);
|
||||
this.registerListeners = this.registerListeners.bind(this);
|
||||
this.register();
|
||||
this.setDisabled = this.setDisabled.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,17 +89,78 @@ export default class State {
|
||||
}
|
||||
}
|
||||
|
||||
setDisabled(key: string) {
|
||||
this.disabled.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register reaction listeners for persistent data stores.
|
||||
* @returns Function to dispose of listeners
|
||||
*/
|
||||
registerListeners() {
|
||||
registerListeners(client: Client) {
|
||||
const listeners = this.persistent.map(([id, store]) => {
|
||||
return reaction(
|
||||
() => store.toJSON(),
|
||||
() => stringify(store.toJSON()),
|
||||
async (value) => {
|
||||
try {
|
||||
await localforage.setItem(id, value);
|
||||
await localforage.setItem(id, JSON.parse(value));
|
||||
if (id === "sync") return;
|
||||
|
||||
const revision = +new Date();
|
||||
switch (id) {
|
||||
case "settings": {
|
||||
const { appearance, theme } =
|
||||
this.settings.toSyncable();
|
||||
|
||||
const obj: Record<string, unknown> = {};
|
||||
if (this.sync.isEnabled("appearance")) {
|
||||
if (this.disabled.has("appearance")) {
|
||||
this.disabled.delete("appearance");
|
||||
} else {
|
||||
obj["appearance"] = appearance;
|
||||
this.sync.setRevision(
|
||||
"appearance",
|
||||
revision,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sync.isEnabled("theme")) {
|
||||
if (this.disabled.has("theme")) {
|
||||
this.disabled.delete("theme");
|
||||
} else {
|
||||
obj["theme"] = theme;
|
||||
this.sync.setRevision(
|
||||
"theme",
|
||||
revision,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(obj).length > 0) {
|
||||
client.syncSetSettings(
|
||||
obj as any,
|
||||
revision,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (this.sync.isEnabled(id as SyncKeys)) {
|
||||
if (this.disabled.has(id)) {
|
||||
this.disabled.delete(id);
|
||||
}
|
||||
|
||||
this.sync.setRevision(id, revision);
|
||||
client.syncSetSettings(
|
||||
(
|
||||
store as unknown as Syncable
|
||||
).toSyncable(),
|
||||
revision,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to serialise!");
|
||||
console.error(err);
|
||||
@@ -110,10 +177,15 @@ export default class State {
|
||||
* Load data stores from local storage.
|
||||
*/
|
||||
async hydrate() {
|
||||
for (const [id, store] of this.persistent) {
|
||||
const data = await localforage.getItem(id);
|
||||
if (typeof data === "object" && data !== null) {
|
||||
store.hydrate(data);
|
||||
const sync = (await localforage.getItem("sync")) as DataSync;
|
||||
if (sync) {
|
||||
const { revision } = sync;
|
||||
for (const [id, store] of this.persistent) {
|
||||
if (id === "sync") continue;
|
||||
const data = await localforage.getItem(id);
|
||||
if (typeof data === "object" && data !== null) {
|
||||
store.hydrate(data, revision[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,12 +193,16 @@ export default class State {
|
||||
|
||||
var state: State;
|
||||
|
||||
export async function hydrateState() {
|
||||
state = new State();
|
||||
await state.hydrate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application state
|
||||
* @returns Application state
|
||||
*/
|
||||
export function useApplicationState() {
|
||||
if (!state) state = new State();
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -13,5 +13,5 @@ export default interface Persistent<T> extends Store {
|
||||
* Hydrate this data store using given data.
|
||||
* @param data Given data
|
||||
*/
|
||||
hydrate(data: T): void;
|
||||
hydrate(data: T, revision: number): void;
|
||||
}
|
||||
|
||||
9
src/mobx/interfaces/Syncable.ts
Normal file
9
src/mobx/interfaces/Syncable.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import Store from "./Store";
|
||||
|
||||
/**
|
||||
* A data store which syncs data to Revolt.
|
||||
*/
|
||||
export default interface Syncable extends Store {
|
||||
apply(key: string, data: unknown, revision: number): void;
|
||||
toSyncable(): { [key: string]: object };
|
||||
}
|
||||
@@ -62,20 +62,22 @@ export interface LegacyAuthState {
|
||||
active?: string;
|
||||
}
|
||||
|
||||
function legacyMigrateAuth(auth: LegacyAuthState): DataAuth {
|
||||
export function legacyMigrateAuth(auth: LegacyAuthState): DataAuth {
|
||||
return {
|
||||
current: auth.active,
|
||||
sessions: auth.accounts,
|
||||
};
|
||||
}
|
||||
|
||||
function legacyMigrateLocale(lang: Language): DataLocaleOptions {
|
||||
export function legacyMigrateLocale(lang: Language): DataLocaleOptions {
|
||||
return {
|
||||
lang,
|
||||
};
|
||||
}
|
||||
|
||||
function legacyMigrateTheme(theme: LegacyThemeOptions): Partial<ISettings> {
|
||||
export function legacyMigrateTheme(
|
||||
theme: LegacyThemeOptions,
|
||||
): Partial<ISettings> {
|
||||
const { light, font, css, monospaceFont, ...variables } =
|
||||
theme.custom ?? {};
|
||||
|
||||
@@ -90,7 +92,7 @@ function legacyMigrateTheme(theme: LegacyThemeOptions): Partial<ISettings> {
|
||||
};
|
||||
}
|
||||
|
||||
function legacyMigrateAppearance(
|
||||
export function legacyMigrateAppearance(
|
||||
appearance: LegacyAppearanceOptions,
|
||||
): Partial<ISettings> {
|
||||
return {
|
||||
@@ -98,7 +100,7 @@ function legacyMigrateAppearance(
|
||||
};
|
||||
}
|
||||
|
||||
function legacyMigrateNotification(
|
||||
export function legacyMigrateNotification(
|
||||
channel: LegacyNotifications,
|
||||
): DataNotificationOptions {
|
||||
return {
|
||||
@@ -106,7 +108,7 @@ function legacyMigrateNotification(
|
||||
};
|
||||
}
|
||||
|
||||
function legacyMigrateSync(sync: LegacySyncOptions): DataSync {
|
||||
export function legacyMigrateSync(sync: LegacySyncOptions): DataSync {
|
||||
return {
|
||||
disabled: sync.disabled ?? [],
|
||||
revision: {
|
||||
|
||||
@@ -30,6 +30,20 @@ export default class Auth implements Store, Persistent<Data> {
|
||||
constructor() {
|
||||
this.sessions = new ObservableMap();
|
||||
this.current = null;
|
||||
|
||||
// Inject session token if it is provided.
|
||||
if (import.meta.env.VITE_SESSION_TOKEN) {
|
||||
this.sessions.set("0", {
|
||||
session: {
|
||||
name: "0",
|
||||
user_id: "0",
|
||||
token: import.meta.env.VITE_SESSION_TOKEN as string,
|
||||
},
|
||||
});
|
||||
|
||||
this.current = "0";
|
||||
}
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,15 @@ import { Server } from "revolt.js/dist/maps/Servers";
|
||||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
import {
|
||||
legacyMigrateNotification,
|
||||
LegacyNotifications,
|
||||
} from "../legacy/redux";
|
||||
|
||||
import { MIGRATIONS } from "../State";
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
import Syncable from "../interfaces/Syncable";
|
||||
|
||||
/**
|
||||
* Possible notification states.
|
||||
@@ -42,7 +49,9 @@ export interface Data {
|
||||
/**
|
||||
* Manages the user's notification preferences.
|
||||
*/
|
||||
export default class NotificationOptions implements Store, Persistent<Data> {
|
||||
export default class NotificationOptions
|
||||
implements Store, Persistent<Data>, Syncable
|
||||
{
|
||||
private server: ObservableMap<string, NotificationState>;
|
||||
private channel: ObservableMap<string, NotificationState>;
|
||||
|
||||
@@ -208,4 +217,18 @@ export default class NotificationOptions implements Store, Persistent<Data> {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@action apply(_key: "notifications", data: unknown, revision: number) {
|
||||
if (revision < MIGRATIONS.REDUX) {
|
||||
data = legacyMigrateNotification(data as LegacyNotifications);
|
||||
}
|
||||
|
||||
this.hydrate(data as Data);
|
||||
}
|
||||
|
||||
@computed toSyncable() {
|
||||
return {
|
||||
notifications: this.toJSON(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,22 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
import {
|
||||
LegacyAppearanceOptions,
|
||||
legacyMigrateAppearance,
|
||||
legacyMigrateTheme,
|
||||
LegacyTheme,
|
||||
LegacyThemeOptions,
|
||||
} from "../legacy/redux";
|
||||
|
||||
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
|
||||
|
||||
import { EmojiPack } from "../../components/common/Emoji";
|
||||
|
||||
import { MIGRATIONS } from "../State";
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
import Syncable from "../interfaces/Syncable";
|
||||
import SAudio, { SoundOptions } from "./helpers/SAudio";
|
||||
import SSecurity from "./helpers/SSecurity";
|
||||
import STheme from "./helpers/STheme";
|
||||
@@ -32,7 +42,9 @@ export interface ISettings {
|
||||
/**
|
||||
* Manages user settings.
|
||||
*/
|
||||
export default class Settings implements Store, Persistent<ISettings> {
|
||||
export default class Settings
|
||||
implements Store, Persistent<ISettings>, Syncable
|
||||
{
|
||||
private data: ObservableMap<string, unknown>;
|
||||
|
||||
theme: STheme;
|
||||
@@ -109,4 +121,60 @@ export default class Settings implements Store, Persistent<ISettings> {
|
||||
@computed getUnchecked(key: string) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
@action apply(
|
||||
key: "appearance" | "theme",
|
||||
data: unknown,
|
||||
revision: number,
|
||||
) {
|
||||
if (revision < MIGRATIONS.REDUX) {
|
||||
if (key === "appearance") {
|
||||
data = legacyMigrateAppearance(data as LegacyAppearanceOptions);
|
||||
} else {
|
||||
data = legacyMigrateTheme(data as LegacyThemeOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "appearance") {
|
||||
this.remove("appearance:emoji");
|
||||
} else {
|
||||
this.remove("appearance:ligatures");
|
||||
this.remove("appearance:theme:base");
|
||||
this.remove("appearance:theme:css");
|
||||
this.remove("appearance:theme:font");
|
||||
this.remove("appearance:theme:light");
|
||||
this.remove("appearance:theme:monoFont");
|
||||
this.remove("appearance:theme:overrides");
|
||||
}
|
||||
|
||||
this.hydrate(data as ISettings);
|
||||
}
|
||||
|
||||
@computed private pullKeys(keys: (keyof ISettings)[]) {
|
||||
const obj: Partial<ISettings> = {};
|
||||
keys.forEach((key) => {
|
||||
let value = this.get(key);
|
||||
if (!value) return;
|
||||
(obj as any)[key] = value;
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@computed toSyncable() {
|
||||
const data: Record<"appearance" | "theme", Partial<ISettings>> = {
|
||||
appearance: this.pullKeys(["appearance:emoji"]),
|
||||
theme: this.pullKeys([
|
||||
"appearance:ligatures",
|
||||
"appearance:theme:base",
|
||||
"appearance:theme:css",
|
||||
"appearance:theme:font",
|
||||
"appearance:theme:light",
|
||||
"appearance:theme:monoFont",
|
||||
"appearance:theme:overrides",
|
||||
]),
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import {
|
||||
makeAutoObservable,
|
||||
ObservableMap,
|
||||
ObservableSet,
|
||||
runInAction,
|
||||
} from "mobx";
|
||||
import { Client } from "revolt.js";
|
||||
import { UserSettings } from "revolt.js/node_modules/revolt-api/types/Sync";
|
||||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
import State from "../State";
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
|
||||
@@ -32,13 +35,15 @@ export interface Data {
|
||||
* Handles syncing settings data.
|
||||
*/
|
||||
export default class Sync implements Store, Persistent<Data> {
|
||||
private state: State;
|
||||
private disabled: ObservableSet<SyncKeys>;
|
||||
private revision: ObservableMap<SyncKeys, number>;
|
||||
private revision: ObservableMap<string, number>;
|
||||
|
||||
/**
|
||||
* Construct new Sync store.
|
||||
*/
|
||||
constructor() {
|
||||
constructor(state: State) {
|
||||
this.state = state;
|
||||
this.disabled = new ObservableSet();
|
||||
this.revision = new ObservableMap();
|
||||
makeAutoObservable(this);
|
||||
@@ -62,6 +67,12 @@ export default class Sync implements Store, Persistent<Data> {
|
||||
this.disabled.add(key as SyncKeys);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.revision) {
|
||||
for (const key of Object.keys(data.revision)) {
|
||||
this.setRevision(key, data.revision[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action enable(key: SyncKeys) {
|
||||
@@ -81,12 +92,74 @@ export default class Sync implements Store, Persistent<Data> {
|
||||
}
|
||||
|
||||
@computed isEnabled(key: SyncKeys) {
|
||||
return !this.disabled.has(key);
|
||||
return !this.disabled.has(key) && SYNC_KEYS.includes(key);
|
||||
}
|
||||
|
||||
@action setRevision(key: string, revision: number) {
|
||||
if (revision < (this.getRevision(key) ?? 0)) return;
|
||||
this.revision.set(key, revision);
|
||||
}
|
||||
|
||||
@computed getRevision(key: string) {
|
||||
return this.revision.get(key);
|
||||
}
|
||||
|
||||
@action apply(data: UserSettings) {
|
||||
const tryRead = (key: string) => {
|
||||
if (key in data) {
|
||||
const revision = data[key][0];
|
||||
if (revision <= (this.getRevision(key) ?? 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(data[key][1]);
|
||||
} catch (err) {
|
||||
parsed = data[key][1];
|
||||
}
|
||||
|
||||
return [revision, parsed];
|
||||
}
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
const appearance = tryRead("appearance");
|
||||
if (appearance) {
|
||||
this.state.setDisabled("appearance");
|
||||
this.state.settings.apply(
|
||||
"appearance",
|
||||
appearance[1],
|
||||
appearance[0],
|
||||
);
|
||||
this.setRevision("appearance", appearance[0]);
|
||||
}
|
||||
|
||||
const theme = tryRead("theme");
|
||||
if (theme) {
|
||||
this.state.setDisabled("theme");
|
||||
this.state.settings.apply("theme", theme[1], theme[0]);
|
||||
this.setRevision("theme", theme[0]);
|
||||
}
|
||||
|
||||
const notifications = tryRead("notifications");
|
||||
if (notifications) {
|
||||
this.state.setDisabled("notifications");
|
||||
this.state.notifications.apply(
|
||||
"notifications",
|
||||
notifications[1],
|
||||
notifications[0],
|
||||
);
|
||||
this.setRevision("notifications", notifications[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async pull(client: Client) {
|
||||
const data = await client.syncFetchSettings(
|
||||
SYNC_KEYS.filter(this.isEnabled),
|
||||
);
|
||||
|
||||
this.apply(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ export default class SSecurity {
|
||||
}
|
||||
|
||||
@computed isTrustedOrigin(origin: string) {
|
||||
console.log(this.settings.get("security:trustedOrigins"), origin);
|
||||
return this.settings.get("security:trustedOrigins")?.includes(origin);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user