mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-09 08:43:37 -05:00
feat(mobx): add sync back (do not look at the code)
This commit is contained in:
parent
cc0e45526c
commit
e89bbb7455
18 changed files with 341 additions and 166 deletions
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
|
@ -10,8 +10,7 @@
|
||||||
"name": "Launch Chrome against localhost",
|
"name": "Launch Chrome against localhost",
|
||||||
"url": "http://local.revolt.chat:3000",
|
"url": "http://local.revolt.chat:3000",
|
||||||
"webRoot": "${workspaceFolder}",
|
"webRoot": "${workspaceFolder}",
|
||||||
"runtimeExecutable": "/usr/bin/chromium",
|
"runtimeExecutable": "/usr/bin/chromium"
|
||||||
"userDataDir": "${workspaceFolder}/.vscode/vscode-chrome-debug-userdatadir"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,7 @@
|
||||||
"eslint-config-preact": "^1.1.4",
|
"eslint-config-preact": "^1.1.4",
|
||||||
"eventemitter3": "^4.0.7",
|
"eventemitter3": "^4.0.7",
|
||||||
"highlight.js": "^11.0.1",
|
"highlight.js": "^11.0.1",
|
||||||
|
"json-stringify-deterministic": "^1.0.2",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"lodash.defaultsdeep": "^4.6.1",
|
"lodash.defaultsdeep": "^4.6.1",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { BrowserRouter as Router } from "react-router-dom";
|
import { BrowserRouter as Router } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { hydrateState } from "../mobx/State";
|
||||||
|
|
||||||
|
import Preloader from "../components/ui/Preloader";
|
||||||
|
|
||||||
import { Children } from "../types/Preact";
|
import { Children } from "../types/Preact";
|
||||||
import Locale from "./Locale";
|
import Locale from "./Locale";
|
||||||
import Theme from "./Theme";
|
import Theme from "./Theme";
|
||||||
|
@ -11,6 +17,14 @@ import Client from "./revoltjs/RevoltClient";
|
||||||
* @param param0 Provided children
|
* @param param0 Provided children
|
||||||
*/
|
*/
|
||||||
export default function Context({ children }: { children: Children }) {
|
export default function Context({ children }: { children: Children }) {
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
hydrateState().then(() => setReady(true));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!ready) return <Preloader type="spinner" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router basename={import.meta.env.BASE_URL}>
|
<Router basename={import.meta.env.BASE_URL}>
|
||||||
<Locale>
|
<Locale>
|
||||||
|
|
|
@ -1,138 +1,37 @@
|
||||||
/**
|
/**
|
||||||
* This file monitors changes to settings and syncs them to the server.
|
* This file monitors changes to settings and syncs them to the server.
|
||||||
*/
|
*/
|
||||||
|
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||||
|
|
||||||
/*type Props = {
|
import { useEffect } from "preact/hooks";
|
||||||
settings: Settings;
|
|
||||||
locale: Language;
|
|
||||||
sync: SyncOptions;
|
|
||||||
notifications: Notifications;
|
|
||||||
};
|
|
||||||
|
|
||||||
const lastValues: { [key in SyncKeys]?: unknown } = {};
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
export function mapSync(
|
import { useClient } from "./RevoltClient";
|
||||||
packet: UserSettings,
|
|
||||||
revision?: Record<string, number>,
|
|
||||||
) {
|
|
||||||
const update: { [key in SyncKeys]?: [number, SyncData[key]] } = {};
|
|
||||||
for (const key of Object.keys(packet)) {
|
|
||||||
const [timestamp, obj] = packet[key];
|
|
||||||
if (timestamp < (revision ?? {})[key] ?? 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let object;
|
export default function SyncManager() {
|
||||||
if (obj[0] === "{") {
|
const client = useClient();
|
||||||
object = JSON.parse(obj);
|
const state = useApplicationState();
|
||||||
} else {
|
|
||||||
object = obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastValues[key as SyncKeys] = object;
|
|
||||||
update[key as SyncKeys] = [timestamp, object];
|
|
||||||
}
|
|
||||||
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SyncManager(props: Props) {
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
const status = useContext(StatusContext);
|
|
||||||
|
|
||||||
|
// Sync settings from Revolt.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === ClientStatus.ONLINE) {
|
state.sync.pull(client);
|
||||||
client
|
}, [client]);
|
||||||
.syncFetchSettings(
|
|
||||||
DEFAULT_ENABLED_SYNC.filter(
|
|
||||||
(x) => !props.sync?.disabled?.includes(x),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.then((data) => {
|
|
||||||
dispatch({
|
|
||||||
type: "SYNC_UPDATE",
|
|
||||||
update: mapSync(data),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client
|
// Keep data synced.
|
||||||
.syncFetchUnreads()
|
useEffect(() => state.registerListeners(client), [client]);
|
||||||
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
|
|
||||||
}
|
|
||||||
}, [client, props.sync?.disabled, status]);
|
|
||||||
|
|
||||||
const syncChange = useCallback(
|
|
||||||
(key: SyncKeys, data: unknown) => {
|
|
||||||
const timestamp = +new Date();
|
|
||||||
dispatch({
|
|
||||||
type: "SYNC_SET_REVISION",
|
|
||||||
key,
|
|
||||||
timestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
client.syncSetSettings(
|
|
||||||
{
|
|
||||||
[key]: data as string,
|
|
||||||
},
|
|
||||||
timestamp,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[client],
|
|
||||||
);
|
|
||||||
|
|
||||||
const disabled = useMemo(
|
|
||||||
() => props.sync.disabled ?? [],
|
|
||||||
[props.sync.disabled],
|
|
||||||
);
|
|
||||||
for (const [key, object] of [
|
|
||||||
["appearance", props.settings.appearance],
|
|
||||||
["theme", props.settings.theme],
|
|
||||||
["locale", props.locale],
|
|
||||||
["notifications", props.notifications],
|
|
||||||
] as [SyncKeys, unknown][]) {
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
useEffect(() => {
|
|
||||||
if (disabled.indexOf(key) === -1) {
|
|
||||||
if (typeof lastValues[key] !== "undefined") {
|
|
||||||
if (!isEqual(lastValues[key], object)) {
|
|
||||||
syncChange(key, object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastValues[key] = object;
|
|
||||||
}, [key, syncChange, disabled, object]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Take data updates from Revolt.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onPacket(packet: ClientboundNotification) {
|
function onPacket(packet: ClientboundNotification) {
|
||||||
if (packet.type === "UserSettingsUpdate") {
|
if (packet.type === "UserSettingsUpdate") {
|
||||||
const update: { [key in SyncKeys]?: [number, SyncData[key]] } =
|
state.sync.apply(packet.update);
|
||||||
mapSync(packet.update, props.sync.revision);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "SYNC_UPDATE",
|
|
||||||
update,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.addListener("packet", onPacket);
|
client.addListener("packet", onPacket);
|
||||||
return () => client.removeListener("packet", onPacket);
|
return () => client.removeListener("packet", onPacket);
|
||||||
}, [client, disabled, props.sync]);
|
}, [client]);
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connectState(SyncManager, (state) => {
|
|
||||||
return {
|
|
||||||
settings: state.settings,
|
|
||||||
locale: state.locale,
|
|
||||||
sync: state.sync,
|
|
||||||
notifications: state.notifications,
|
|
||||||
};
|
|
||||||
});*/
|
|
||||||
|
|
||||||
export default function SyncManager() {
|
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { Client } from "revolt.js/dist";
|
import { Client } from "revolt.js/dist";
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
|
||||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
|
||||||
|
|
||||||
import { StateUpdater } from "preact/hooks";
|
import { StateUpdater } from "preact/hooks";
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
// @ts-expect-error No typings.
|
||||||
|
import stringify from "json-stringify-deterministic";
|
||||||
import localforage from "localforage";
|
import localforage from "localforage";
|
||||||
import { autorun, makeAutoObservable, reaction } from "mobx";
|
import { makeAutoObservable, reaction } from "mobx";
|
||||||
|
import { Client } from "revolt.js";
|
||||||
import { createContext } from "preact";
|
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import Persistent from "./interfaces/Persistent";
|
import Persistent from "./interfaces/Persistent";
|
||||||
|
import Syncable from "./interfaces/Syncable";
|
||||||
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";
|
||||||
|
@ -14,7 +15,11 @@ import MessageQueue from "./stores/MessageQueue";
|
||||||
import NotificationOptions from "./stores/NotificationOptions";
|
import NotificationOptions from "./stores/NotificationOptions";
|
||||||
import ServerConfig from "./stores/ServerConfig";
|
import ServerConfig from "./stores/ServerConfig";
|
||||||
import Settings from "./stores/Settings";
|
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.
|
* Handles global application state.
|
||||||
|
@ -32,6 +37,7 @@ export default class State {
|
||||||
sync: Sync;
|
sync: Sync;
|
||||||
|
|
||||||
private persistent: [string, Persistent<unknown>][] = [];
|
private persistent: [string, Persistent<unknown>][] = [];
|
||||||
|
private disabled: Set<string> = new Set();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct new State.
|
* Construct new State.
|
||||||
|
@ -46,11 +52,11 @@ export default class State {
|
||||||
this.notifications = new NotificationOptions();
|
this.notifications = new NotificationOptions();
|
||||||
this.queue = new MessageQueue();
|
this.queue = new MessageQueue();
|
||||||
this.settings = new Settings();
|
this.settings = new Settings();
|
||||||
this.sync = new Sync();
|
this.sync = new Sync(this);
|
||||||
|
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
this.registerListeners = this.registerListeners.bind(this);
|
|
||||||
this.register();
|
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.
|
* Register reaction listeners for persistent data stores.
|
||||||
* @returns Function to dispose of listeners
|
* @returns Function to dispose of listeners
|
||||||
*/
|
*/
|
||||||
registerListeners() {
|
registerListeners(client: Client) {
|
||||||
const listeners = this.persistent.map(([id, store]) => {
|
const listeners = this.persistent.map(([id, store]) => {
|
||||||
return reaction(
|
return reaction(
|
||||||
() => store.toJSON(),
|
() => stringify(store.toJSON()),
|
||||||
async (value) => {
|
async (value) => {
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.error("Failed to serialise!");
|
console.error("Failed to serialise!");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -110,10 +177,15 @@ export default class State {
|
||||||
* Load data stores from local storage.
|
* Load data stores from local storage.
|
||||||
*/
|
*/
|
||||||
async hydrate() {
|
async hydrate() {
|
||||||
for (const [id, store] of this.persistent) {
|
const sync = (await localforage.getItem("sync")) as DataSync;
|
||||||
const data = await localforage.getItem(id);
|
if (sync) {
|
||||||
if (typeof data === "object" && data !== null) {
|
const { revision } = sync;
|
||||||
store.hydrate(data);
|
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;
|
var state: State;
|
||||||
|
|
||||||
|
export async function hydrateState() {
|
||||||
|
state = new State();
|
||||||
|
await state.hydrate();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the application state
|
* Get the application state
|
||||||
* @returns Application state
|
* @returns Application state
|
||||||
*/
|
*/
|
||||||
export function useApplicationState() {
|
export function useApplicationState() {
|
||||||
if (!state) state = new State();
|
|
||||||
return 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.
|
* Hydrate this data store using given data.
|
||||||
* @param data 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;
|
active?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyMigrateAuth(auth: LegacyAuthState): DataAuth {
|
export function legacyMigrateAuth(auth: LegacyAuthState): DataAuth {
|
||||||
return {
|
return {
|
||||||
current: auth.active,
|
current: auth.active,
|
||||||
sessions: auth.accounts,
|
sessions: auth.accounts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyMigrateLocale(lang: Language): DataLocaleOptions {
|
export function legacyMigrateLocale(lang: Language): DataLocaleOptions {
|
||||||
return {
|
return {
|
||||||
lang,
|
lang,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyMigrateTheme(theme: LegacyThemeOptions): Partial<ISettings> {
|
export function legacyMigrateTheme(
|
||||||
|
theme: LegacyThemeOptions,
|
||||||
|
): Partial<ISettings> {
|
||||||
const { light, font, css, monospaceFont, ...variables } =
|
const { light, font, css, monospaceFont, ...variables } =
|
||||||
theme.custom ?? {};
|
theme.custom ?? {};
|
||||||
|
|
||||||
|
@ -90,7 +92,7 @@ function legacyMigrateTheme(theme: LegacyThemeOptions): Partial<ISettings> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyMigrateAppearance(
|
export function legacyMigrateAppearance(
|
||||||
appearance: LegacyAppearanceOptions,
|
appearance: LegacyAppearanceOptions,
|
||||||
): Partial<ISettings> {
|
): Partial<ISettings> {
|
||||||
return {
|
return {
|
||||||
|
@ -98,7 +100,7 @@ function legacyMigrateAppearance(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyMigrateNotification(
|
export function legacyMigrateNotification(
|
||||||
channel: LegacyNotifications,
|
channel: LegacyNotifications,
|
||||||
): DataNotificationOptions {
|
): DataNotificationOptions {
|
||||||
return {
|
return {
|
||||||
|
@ -106,7 +108,7 @@ function legacyMigrateNotification(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyMigrateSync(sync: LegacySyncOptions): DataSync {
|
export function legacyMigrateSync(sync: LegacySyncOptions): DataSync {
|
||||||
return {
|
return {
|
||||||
disabled: sync.disabled ?? [],
|
disabled: sync.disabled ?? [],
|
||||||
revision: {
|
revision: {
|
||||||
|
|
|
@ -30,6 +30,20 @@ export default class Auth implements Store, Persistent<Data> {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sessions = new ObservableMap();
|
this.sessions = new ObservableMap();
|
||||||
this.current = null;
|
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);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,15 @@ import { Server } from "revolt.js/dist/maps/Servers";
|
||||||
|
|
||||||
import { mapToRecord } from "../../lib/conversion";
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
|
import {
|
||||||
|
legacyMigrateNotification,
|
||||||
|
LegacyNotifications,
|
||||||
|
} from "../legacy/redux";
|
||||||
|
|
||||||
|
import { MIGRATIONS } from "../State";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
|
import Syncable from "../interfaces/Syncable";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Possible notification states.
|
* Possible notification states.
|
||||||
|
@ -42,7 +49,9 @@ export interface Data {
|
||||||
/**
|
/**
|
||||||
* Manages the user's notification preferences.
|
* 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 server: ObservableMap<string, NotificationState>;
|
||||||
private channel: ObservableMap<string, NotificationState>;
|
private channel: ObservableMap<string, NotificationState>;
|
||||||
|
|
||||||
|
@ -208,4 +217,18 @@ export default class NotificationOptions implements Store, Persistent<Data> {
|
||||||
|
|
||||||
return false;
|
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 { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
|
import {
|
||||||
|
LegacyAppearanceOptions,
|
||||||
|
legacyMigrateAppearance,
|
||||||
|
legacyMigrateTheme,
|
||||||
|
LegacyTheme,
|
||||||
|
LegacyThemeOptions,
|
||||||
|
} from "../legacy/redux";
|
||||||
|
|
||||||
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
|
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
|
||||||
|
|
||||||
import { EmojiPack } from "../../components/common/Emoji";
|
import { EmojiPack } from "../../components/common/Emoji";
|
||||||
|
|
||||||
|
import { MIGRATIONS } from "../State";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
|
import Syncable from "../interfaces/Syncable";
|
||||||
import SAudio, { SoundOptions } from "./helpers/SAudio";
|
import SAudio, { SoundOptions } from "./helpers/SAudio";
|
||||||
import SSecurity from "./helpers/SSecurity";
|
import SSecurity from "./helpers/SSecurity";
|
||||||
import STheme from "./helpers/STheme";
|
import STheme from "./helpers/STheme";
|
||||||
|
@ -32,7 +42,9 @@ export interface ISettings {
|
||||||
/**
|
/**
|
||||||
* Manages user settings.
|
* 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>;
|
private data: ObservableMap<string, unknown>;
|
||||||
|
|
||||||
theme: STheme;
|
theme: STheme;
|
||||||
|
@ -109,4 +121,60 @@ export default class Settings implements Store, Persistent<ISettings> {
|
||||||
@computed getUnchecked(key: string) {
|
@computed getUnchecked(key: string) {
|
||||||
return this.data.get(key);
|
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,
|
makeAutoObservable,
|
||||||
ObservableMap,
|
ObservableMap,
|
||||||
ObservableSet,
|
ObservableSet,
|
||||||
|
runInAction,
|
||||||
} from "mobx";
|
} from "mobx";
|
||||||
import { Client } from "revolt.js";
|
import { Client } from "revolt.js";
|
||||||
|
import { UserSettings } from "revolt.js/node_modules/revolt-api/types/Sync";
|
||||||
|
|
||||||
import { mapToRecord } from "../../lib/conversion";
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
|
import State from "../State";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
|
@ -32,13 +35,15 @@ export interface Data {
|
||||||
* Handles syncing settings data.
|
* Handles syncing settings data.
|
||||||
*/
|
*/
|
||||||
export default class Sync implements Store, Persistent<Data> {
|
export default class Sync implements Store, Persistent<Data> {
|
||||||
|
private state: State;
|
||||||
private disabled: ObservableSet<SyncKeys>;
|
private disabled: ObservableSet<SyncKeys>;
|
||||||
private revision: ObservableMap<SyncKeys, number>;
|
private revision: ObservableMap<string, number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct new Sync store.
|
* Construct new Sync store.
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor(state: State) {
|
||||||
|
this.state = state;
|
||||||
this.disabled = new ObservableSet();
|
this.disabled = new ObservableSet();
|
||||||
this.revision = new ObservableMap();
|
this.revision = new ObservableMap();
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
@ -62,6 +67,12 @@ export default class Sync implements Store, Persistent<Data> {
|
||||||
this.disabled.add(key as SyncKeys);
|
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) {
|
@action enable(key: SyncKeys) {
|
||||||
|
@ -81,12 +92,74 @@ export default class Sync implements Store, Persistent<Data> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed isEnabled(key: SyncKeys) {
|
@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) {
|
async pull(client: Client) {
|
||||||
const data = await client.syncFetchSettings(
|
const data = await client.syncFetchSettings(
|
||||||
SYNC_KEYS.filter(this.isEnabled),
|
SYNC_KEYS.filter(this.isEnabled),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.apply(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ export default class SSecurity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed isTrustedOrigin(origin: string) {
|
@computed isTrustedOrigin(origin: string) {
|
||||||
console.log(this.settings.get("security:trustedOrigins"), origin);
|
|
||||||
return this.settings.get("security:trustedOrigins")?.includes(origin);
|
return this.settings.get("security:trustedOrigins")?.includes(origin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Wrench } from "@styled-icons/boxicons-solid";
|
import { Wrench } from "@styled-icons/boxicons-solid";
|
||||||
|
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import PaintCounter from "../../lib/PaintCounter";
|
import PaintCounter from "../../lib/PaintCounter";
|
||||||
import { TextReact } from "../../lib/i18n";
|
import { TextReact } from "../../lib/i18n";
|
||||||
|
@ -16,10 +16,14 @@ export default function Developer() {
|
||||||
const userPermission = client.user!.permission;
|
const userPermission = client.user!.permission;
|
||||||
const [ping, setPing] = useState<undefined | number>(client.websocket.ping);
|
const [ping, setPing] = useState<undefined | number>(client.websocket.ping);
|
||||||
|
|
||||||
setInterval(
|
useEffect(() => {
|
||||||
() => setPing(client.websocket.ping),
|
const timer = setInterval(
|
||||||
client.options.heartbeat * 1e3,
|
() => setPing(client.websocket.ping),
|
||||||
);
|
client.options.heartbeat * 1e3,
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
import styles from "./Panes.module.scss";
|
import styles from "./Panes.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
@ -11,7 +13,7 @@ import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import Checkbox from "../../../components/ui/Checkbox";
|
import Checkbox from "../../../components/ui/Checkbox";
|
||||||
|
|
||||||
export function Notifications() {
|
export const Notifications = observer(() => {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
const { openScreen } = useIntermediate();
|
const { openScreen } = useIntermediate();
|
||||||
const settings = useApplicationState().settings;
|
const settings = useApplicationState().settings;
|
||||||
|
@ -118,4 +120,4 @@ export function Notifications() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
@ -2974,6 +2974,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||||
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
|
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
|
||||||
|
|
||||||
|
json-stringify-deterministic@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-stringify-deterministic/-/json-stringify-deterministic-1.0.2.tgz#fe72b5f49cc39c5f7c5fcb17d2fb06fca092e727"
|
||||||
|
integrity sha512-u6lgTmpDXjVJChV2pOcW7bQOOXrGZAHKzfOfJXQ4ktFrpdotKgaf1crqktYrVOe/Ul0Qrm/CMjWNGS+ErpngCg==
|
||||||
|
|
||||||
json5@^2.1.2:
|
json5@^2.1.2:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
|
||||||
|
|
Loading…
Reference in a new issue