From 27eeb3acd245a5f97eb573668a93cba3c568c568 Mon Sep 17 00:00:00 2001
From: Paul <paulmakles@gmail.com>
Date: Fri, 18 Jun 2021 17:57:08 +0100
Subject: [PATCH] Add Redux and reducers. Load i18n files and add dayjs.

---
 index.html                            |   4 +-
 package.json                          |  10 +-
 src/app.tsx                           |  16 +-
 src/context/Locale.tsx                | 156 +++++++++-
 src/context/Theme.tsx                 | 164 +++++++---
 src/context/index.tsx                 |  15 +
 src/context/revoltjs/RevoltClient.tsx |  19 ++
 src/context/revoltjs/messages.ts      |  10 +
 src/lib/isTouchscreenDevice.ts        |   7 +
 src/main.tsx                          |   2 +-
 src/redux/State.tsx                   |  32 ++
 src/redux/connector.tsx               |  16 +
 src/redux/index.ts                    |  62 ++++
 src/redux/reducers/auth.ts            |  48 +++
 src/redux/reducers/drafts.ts          |  33 ++
 src/redux/reducers/experiments.ts     |  43 +++
 src/redux/reducers/index.ts           |  47 +++
 src/redux/reducers/locale.ts          |  50 ++++
 src/redux/reducers/queue.ts           | 103 +++++++
 src/redux/reducers/settings.ts        |  98 ++++++
 src/redux/reducers/sync.ts            |  85 ++++++
 src/redux/reducers/typing.ts          |  46 +++
 src/redux/reducers/unreads.ts         |  68 +++++
 ui/ui.tsx                             |  11 +-
 yarn.lock                             | 414 +++++++++++++++++++++++++-
 25 files changed, 1506 insertions(+), 53 deletions(-)
 create mode 100644 src/context/index.tsx
 create mode 100644 src/context/revoltjs/RevoltClient.tsx
 create mode 100644 src/context/revoltjs/messages.ts
 create mode 100644 src/lib/isTouchscreenDevice.ts
 create mode 100644 src/redux/State.tsx
 create mode 100644 src/redux/connector.tsx
 create mode 100644 src/redux/index.ts
 create mode 100644 src/redux/reducers/auth.ts
 create mode 100644 src/redux/reducers/drafts.ts
 create mode 100644 src/redux/reducers/experiments.ts
 create mode 100644 src/redux/reducers/index.ts
 create mode 100644 src/redux/reducers/locale.ts
 create mode 100644 src/redux/reducers/queue.ts
 create mode 100644 src/redux/reducers/settings.ts
 create mode 100644 src/redux/reducers/sync.ts
 create mode 100644 src/redux/reducers/typing.ts
 create mode 100644 src/redux/reducers/unreads.ts

diff --git a/index.html b/index.html
index 3eb390e..1885d9e 100644
--- a/index.html
+++ b/index.html
@@ -4,10 +4,10 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Vite App</title>
+    <title>REVOLT</title>
   </head>
   <body>
     <div id="app"></div>
-    <script type="module" src="/src/entrypoints/main.tsx"></script>
+    <script type="module" src="/src/main.tsx"></script>
   </body>
 </html>
diff --git a/package.json b/package.json
index d6845c4..b1b5dde 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "scripts": {
     "dev": "vite",
     "build": "rimraf build && tsc && vite build",
-    "serve": "vite preview",
+    "preview": "vite preview",
     "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
     "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'"
   },
@@ -30,14 +30,22 @@
     "@styled-icons/feather": "^10.34.0",
     "@types/node": "^15.12.3",
     "@types/preact-i18n": "^2.3.0",
+    "@types/react-helmet": "^6.1.1",
     "@types/styled-components": "^5.1.10",
     "@typescript-eslint/eslint-plugin": "^4.27.0",
     "@typescript-eslint/parser": "^4.27.0",
+    "dayjs": "^1.10.5",
     "eslint": "^7.28.0",
     "eslint-config-preact": "^1.1.4",
+    "localforage": "^1.9.0",
     "preact-i18n": "^2.4.0-preactx",
     "prettier": "^2.3.1",
+    "react-device-detect": "^1.17.0",
+    "react-helmet": "^6.1.0",
     "react-overlapping-panels": "1.1.2-patch.0",
+    "react-redux": "^7.2.4",
+    "redux": "^4.1.0",
+    "revolt.js": "4.2.0-alpha.3-patch.0",
     "rimraf": "^3.0.2",
     "sass": "^1.35.1",
     "styled-components": "^5.3.0",
diff --git a/src/app.tsx b/src/app.tsx
index f47efe3..9e16c37 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,7 +1,17 @@
+import { Text } from "preact-i18n";
+import Context from "./context";
+
+import dayjs from "dayjs";
+
+import localeData from 'dayjs/plugin/localeData';
+dayjs.extend(localeData)
+
 export function App() {
     return (
-        <>
-            <h1>REVOLT</h1>
-        </>
+        <Context>
+            <h1><Text id="general.about" /></h1>
+            <h3>{ dayjs.locale() }</h3>
+            <h2>{ dayjs.months() }</h2>
+        </Context>
     );
 }
diff --git a/src/context/Locale.tsx b/src/context/Locale.tsx
index 0403fed..c33aad8 100644
--- a/src/context/Locale.tsx
+++ b/src/context/Locale.tsx
@@ -1,10 +1,162 @@
 import { IntlProvider } from "preact-i18n";
+import { connectState } from "../redux/connector";
 import definition from "../../external/lang/en.json";
+import { useEffect, useState } from "preact/hooks";
+
+import dayjs from "dayjs";
+import calendar from "dayjs/plugin/calendar";
+import update from "dayjs/plugin/updateLocale";
+import format from "dayjs/plugin/localizedFormat";
+dayjs.extend(calendar);
+dayjs.extend(format);
+dayjs.extend(update);
+
+export enum Language {
+    ENGLISH = "en",
+
+    ARABIC = "ar",
+    AZERBAIJANI = "az",
+    CZECH = "cs",
+    GERMAN = "de",
+    SPANISH = "es",
+    FINNISH = "fi",
+    FRENCH = "fr",
+    HINDI = "hi",
+    CROATIAN = "hr",
+    HUNGARIAN = "hu",
+    INDONESIAN = "id",
+    LITHUANIAN = "lt",
+    MACEDONIAN = "mk",
+    DUTCH = "nl",
+    POLISH = "pl",
+    PORTUGUESE_BRAZIL = "pt_BR",
+    ROMANIAN = "ro",
+    RUSSIAN = "ru",
+    SERBIAN = "sr",
+    SWEDISH = "sv",
+    TURKISH = "tr",
+    UKRANIAN = "uk",
+    CHINESE_SIMPLIFIED = "zh_Hans",
+
+    OWO = "owo",
+    PIRATE = "pr",
+    BOTTOM = "bottom",
+    PIGLATIN = "piglatin",
+    HARDCORE = "hardcore"
+}
+
+export interface LanguageEntry {
+    display: string;
+    emoji: string;
+    i18n: string;
+    dayjs?: string;
+    rtl?: boolean;
+}
+
+export const Languages: { [key in Language]: LanguageEntry } = {
+    en: {
+        display: "English (Traditional)",
+        emoji: "🇬🇧",
+        i18n: "en",
+        dayjs: "en-gb"
+    },
+
+    ar: { display: "عربي", emoji: "🇸🇦", i18n: "ar", rtl: true },
+    az: { display: "Azərbaycan dili", emoji: "🇦🇿", i18n: "az" },
+    cs: { display: "Čeština", emoji: "🇨🇿", i18n: "cs" },
+    de: { display: "Deutsch", emoji: "🇩🇪", i18n: "de" },
+    es: { display: "Español", emoji: "🇪🇸", i18n: "es" },
+    fi: { display: "suomi", emoji: "🇫🇮", i18n: "fi" },
+    fr: { display: "Français", emoji: "🇫🇷", i18n: "fr" },
+    hi: { display: "हिन्दी", emoji: "🇮🇳", i18n: "hi" },
+    hr: { display: "Hrvatski", emoji: "🇭🇷", i18n: "hr" },
+    hu: { display: "magyar", emoji: "🇭🇺", i18n: "hu" },
+    id: { display: "bahasa Indonesia", emoji: "🇮🇩", i18n: "id" },
+    lt: { display: "Lietuvių", emoji: "🇱🇹", i18n: "lt" },
+    mk: { display: "Македонски", emoji: "🇲🇰", i18n: "mk" },
+    nl: { display: "Nederlands", emoji: "🇳🇱", i18n: "nl" },
+    pl: { display: "Polski", emoji: "🇵🇱", i18n: "pl" },
+    pt_BR: {
+        display: "Português (do Brasil)",
+        emoji: "🇧🇷",
+        i18n: "pt_BR",
+        dayjs: "pt-br"
+    },
+    ro: { display: "Română", emoji: "🇷🇴", i18n: "ro" },
+    ru: { display: "Русский", emoji: "🇷🇺", i18n: "ru" },
+    sr: { display: "Српски", emoji: "🇷🇸", i18n: "sr" },
+    sv: { display: "Svenska", emoji: "🇸🇪", i18n: "sv" },
+    tr: { display: "Türkçe", emoji: "🇹🇷", i18n: "tr" },
+    uk: { display: "Українська", emoji: "🇺🇦", i18n: "uk" },
+    zh_Hans: {
+        display: "中文 (简体)",
+        emoji: "🇨🇳",
+        i18n: "zh_Hans",
+        dayjs: "zh"
+    },
+
+    owo: { display: "OwO", emoji: "🐱", i18n: "owo", dayjs: "en-gb" },
+    pr: { display: "Pirate", emoji: "🏴‍☠️", i18n: "pr", dayjs: "en-gb" },
+    bottom: { display: "Bottom", emoji: "🥺", i18n: "bottom", dayjs: "en-gb" },
+    piglatin: { display: "Pig Latin", emoji: "🐖", i18n: "piglatin", dayjs: "en-gb" },
+    hardcore: {
+        display: "Hardcore Mode",
+        emoji: "🔥",
+        i18n: "hardcore",
+        dayjs: "en-gb"
+    }
+};
 
 interface Props {
     children: JSX.Element | JSX.Element[];
+    locale: Language;
 }
 
-export default function Locale({ children }: Props) {
-    return <IntlProvider definition={definition}>{children}</IntlProvider>;
+function Locale({ children, locale }: Props) {
+    const [defns, setDefinition] = useState(definition);
+    const lang = Languages[locale];
+
+    useEffect(() => {
+        if (locale === 'en') {
+            setDefinition(definition);
+            dayjs.locale('en');
+            return;
+        }
+
+        if (lang.i18n === "hardcore") {
+            setDefinition({} as any);
+            return;
+        }
+
+        import(
+            `../../external/lang/${lang.i18n}.json`
+        ).then(async lang_file => {
+            let defn = lang_file.default;
+            let target = lang.dayjs ?? lang.i18n;
+            let dayjs_locale = await import(/* @vite-ignore */ `/node_modules/dayjs/esm/locale/${target}.js`);
+
+            if (defn.dayjs) {
+                dayjs.updateLocale(target, { calendar: defn.dayjs });
+            }
+
+            dayjs.locale(dayjs_locale.default);
+            setDefinition(defn);
+        });
+    }, [locale]);
+
+    useEffect(() => {
+        document.body.style.direction = lang.rtl ? "rtl" : "";
+    }, [ lang.rtl ]);
+
+    return <IntlProvider definition={defns}>{children}</IntlProvider>;
 }
+
+export default connectState<Omit<Props, 'locale'>>(
+    Locale,
+    state => {
+        return {
+            locale: state.locale
+        };
+    },
+    true
+);
diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx
index 4066eb9..76fb20e 100644
--- a/src/context/Theme.tsx
+++ b/src/context/Theme.tsx
@@ -1,41 +1,137 @@
+import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
 import { createGlobalStyle } from "styled-components";
+import { Children } from "../types/Preact";
+import { Helmet } from "react-helmet";
 
-// ! TEMP START
-const a = {
-    light: false,
-    accent: "#FD6671",
-    background: "#191919",
-    foreground: "#F6F6F6",
-    block: "#2D2D2D",
-    "message-box": "#363636",
-    mention: "rgba(251, 255, 0, 0.06)",
-    success: "#65E572",
-    warning: "#FAA352",
-    error: "#F06464",
-    hover: "rgba(0, 0, 0, 0.1)",
-    "sidebar-active": "#FD6671",
-    "scrollbar-thumb": "#CA525A",
-    "scrollbar-track": "transparent",
-    "primary-background": "#242424",
-    "primary-header": "#363636",
-    "secondary-background": "#1E1E1E",
-    "secondary-foreground": "#C8C8C8",
-    "secondary-header": "#2D2D2D",
-    "tertiary-background": "#4D4D4D",
-    "tertiary-foreground": "#848484",
-    "status-online": "#3ABF7E",
-    "status-away": "#F39F00",
-    "status-busy": "#F84848",
-    "status-streaming": "#977EFF",
-    "status-invisible": "#A5A5A5",
+export type Variables =
+    | "accent"
+    | "background"
+    | "foreground"
+    | "block"
+    | "message-box"
+    | "mention"
+    | "success"
+    | "warning"
+    | "error"
+    | "hover"
+    | "sidebar-active"
+    | "scrollbar-thumb"
+    | "scrollbar-track"
+    | "primary-background"
+    | "primary-header"
+    | "secondary-background"
+    | "secondary-foreground"
+    | "secondary-header"
+    | "tertiary-background"
+    | "tertiary-foreground"
+    | "status-online"
+    | "status-away"
+    | "status-busy"
+    | "status-streaming"
+    | "status-invisible";
+
+export type Theme = {
+    [variable in Variables]: string;
+} & {
+    light?: boolean;
+    css?: string;
+};
+
+export interface ThemeOptions {
+    preset?: string;
+    custom?: Partial<Theme>;
+}
+
+// Generated from https://gitlab.insrt.uk/revolt/community/themes
+export const PRESETS: { [key: string]: Theme } = {
+    light: {
+        light: true,
+        accent: "#FD6671",
+        background: "#F6F6F6",
+        foreground: "#101010",
+        block: "#414141",
+        "message-box": "#F1F1F1",
+        mention: "rgba(251, 255, 0, 0.40)",
+        success: "#65E572",
+        warning: "#FAA352",
+        error: "#F06464",
+        hover: "rgba(0, 0, 0, 0.2)",
+        "sidebar-active": "#FD6671",
+        "scrollbar-thumb": "#CA525A",
+        "scrollbar-track": "transparent",
+        "primary-background": "#FFFFFF",
+        "primary-header": "#F1F1F1",
+        "secondary-background": "#F1F1F1",
+        "secondary-foreground": "#888888",
+        "secondary-header": "#F1F1F1",
+        "tertiary-background": "#4D4D4D",
+        "tertiary-foreground": "#646464",
+        "status-online": "#3ABF7E",
+        "status-away": "#F39F00",
+        "status-busy": "#F84848",
+        "status-streaming": "#977EFF",
+        "status-invisible": "#A5A5A5",
+    },
+    dark: {
+        light: false,
+        accent: "#FD6671",
+        background: "#191919",
+        foreground: "#F6F6F6",
+        block: "#2D2D2D",
+        "message-box": "#363636",
+        mention: "rgba(251, 255, 0, 0.06)",
+        success: "#65E572",
+        warning: "#FAA352",
+        error: "#F06464",
+        hover: "rgba(0, 0, 0, 0.1)",
+        "sidebar-active": "#FD6671",
+        "scrollbar-thumb": "#CA525A",
+        "scrollbar-track": "transparent",
+        "primary-background": "#242424",
+        "primary-header": "#363636",
+        "secondary-background": "#1E1E1E",
+        "secondary-foreground": "#C8C8C8",
+        "secondary-header": "#2D2D2D",
+        "tertiary-background": "#4D4D4D",
+        "tertiary-foreground": "#848484",
+        "status-online": "#3ABF7E",
+        "status-away": "#F39F00",
+        "status-busy": "#F84848",
+        "status-streaming": "#977EFF",
+        "status-invisible": "#A5A5A5",
+    },
 };
 
-export const GlobalTheme = createGlobalStyle`
+const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
 :root {
-	${Object.keys(a).map((key) => {
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        return `--${key}: ${(a as any)[key]};`;
-    })}
+	${(props) =>
+        (Object.keys(props.theme) as Variables[]).map((key) => {
+            return `--${key}: ${props.theme[key]};`;
+        })}
 }
 `;
-// ! TEMP END
+
+interface Props {
+    children: Children;
+}
+
+export default function Theme(props: Props) {
+    const theme = PRESETS.dark;
+
+    return (
+        <>
+            <Helmet>
+                <meta
+                    name="theme-color"
+                    content={
+                        isTouchscreenDevice
+                            ? theme["primary-header"]
+                            : theme["tertiary-background"]
+                    }
+                />
+            </Helmet>
+            <GlobalTheme theme={theme} />
+            {props.children}
+        </>
+    );
+}
diff --git a/src/context/index.tsx b/src/context/index.tsx
new file mode 100644
index 0000000..95219c4
--- /dev/null
+++ b/src/context/index.tsx
@@ -0,0 +1,15 @@
+import State from "../redux/State";
+import { Children } from "../types/Preact";
+
+import Locale from "./Locale";
+import Theme from "./Theme";
+
+export default function Context({ children }: { children: Children }) {
+    return (
+        <State>
+            <Locale>
+                <Theme>{children}</Theme>
+            </Locale>
+        </State>
+    );
+}
diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx
new file mode 100644
index 0000000..fdf0f9b
--- /dev/null
+++ b/src/context/revoltjs/RevoltClient.tsx
@@ -0,0 +1,19 @@
+import { Client } from 'revolt.js';
+
+export enum ClientStatus {
+    LOADING,
+    READY,
+    OFFLINE,
+    DISCONNECTED,
+    CONNECTING,
+    RECONNECTING,
+    ONLINE
+}
+
+export const RevoltJSClient = new Client({
+    autoReconnect: false,
+    apiURL: process.env.API_SERVER,
+    debug: process.env.NODE_ENV === "development",
+    // Match sw.js#13
+    // db: new Db("state", 3, ["channels", "servers", "users", "members"])
+});
diff --git a/src/context/revoltjs/messages.ts b/src/context/revoltjs/messages.ts
new file mode 100644
index 0000000..8db3e42
--- /dev/null
+++ b/src/context/revoltjs/messages.ts
@@ -0,0 +1,10 @@
+import { Message } from "revolt.js/dist/api/objects";
+
+export type MessageObject = Omit<Message, "edited"> & { edited?: string };
+export function mapMessage(message: Partial<Message>) {
+    const { edited, ...msg } = message;
+    return {
+        ...msg,
+        edited: edited?.$date
+    } as MessageObject;
+}
diff --git a/src/lib/isTouchscreenDevice.ts b/src/lib/isTouchscreenDevice.ts
new file mode 100644
index 0000000..a91bb23
--- /dev/null
+++ b/src/lib/isTouchscreenDevice.ts
@@ -0,0 +1,7 @@
+import { isDesktop, isMobile, isTablet } from "react-device-detect";
+export const isTouchscreenDevice =
+    isDesktop && !isTablet
+        ? false
+        : (typeof window !== "undefined"
+              ? navigator.maxTouchPoints > 0
+              : false) || isMobile;
diff --git a/src/main.tsx b/src/main.tsx
index ea0c7df..0e16b26 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,5 +1,5 @@
 import { render } from "preact";
-import "../styles/index.scss";
+import "./styles/index.scss";
 import { App } from "./app";
 
 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
diff --git a/src/redux/State.tsx b/src/redux/State.tsx
new file mode 100644
index 0000000..2198529
--- /dev/null
+++ b/src/redux/State.tsx
@@ -0,0 +1,32 @@
+import { store } from ".";
+import localForage from "localforage";
+import { Provider } from 'react-redux';
+import { Children } from "../types/Preact";
+import { useEffect, useState } from "preact/hooks";
+
+async function loadState() {
+    const state = await localForage.getItem("state");
+    if (state) {
+        store.dispatch({ type: "__INIT", state });
+    }
+}
+
+interface Props {
+    children: Children
+}
+
+export default function State(props: Props) {
+    const [loaded, setLoaded] = useState(false);
+
+    useEffect(() => {
+        loadState().then(() => setLoaded(true));
+    }, []);
+
+    if (!loaded) return null;
+    
+    return (
+        <Provider store={store}>
+            { props.children }
+        </Provider>
+    )
+}
diff --git a/src/redux/connector.tsx b/src/redux/connector.tsx
new file mode 100644
index 0000000..3a90faa
--- /dev/null
+++ b/src/redux/connector.tsx
@@ -0,0 +1,16 @@
+import { State } from ".";
+import { h } from "preact";
+//import { memo } from "preact/compat";
+import { connect, ConnectedComponent } from "react-redux";
+
+export function connectState<T>(
+    component: (props: any) => h.JSX.Element | null,
+    mapKeys: (state: State, props: T) => any,
+    useDispatcher?: boolean
+): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
+    return (
+        useDispatcher
+            ? connect(mapKeys, dispatcher => { return { dispatcher } })
+            : connect(mapKeys)
+    )(component);//(memo(component));
+}
diff --git a/src/redux/index.ts b/src/redux/index.ts
new file mode 100644
index 0000000..06e58ea
--- /dev/null
+++ b/src/redux/index.ts
@@ -0,0 +1,62 @@
+import { createStore } from "redux";
+import rootReducer from "./reducers";
+import localForage from "localforage";
+
+import { Typing } from "./reducers/typing";
+import { Drafts } from "./reducers/drafts";
+import { AuthState } from "./reducers/auth";
+import { Language } from "../context/Locale";
+import { Unreads } from "./reducers/unreads";
+import { SyncOptions } from "./reducers/sync";
+import { Settings } from "./reducers/settings";
+import { QueuedMessage } from "./reducers/queue";
+import { ExperimentOptions } from "./reducers/experiments";
+
+export type State = {
+    locale: Language;
+    auth: AuthState;
+    settings: Settings;
+    unreads: Unreads;
+    queue: QueuedMessage[];
+    typing: Typing;
+    drafts: Drafts;
+    sync: SyncOptions;
+    experiments: ExperimentOptions;
+};
+
+export const store = createStore((state: any, action: any) => {
+    if (process.env.NODE_ENV === "development") {
+        console.debug("State Update:", action);
+    }
+
+    if (action.type === "__INIT") {
+        return action.state;
+    }
+
+    return rootReducer(state, action);
+});
+
+// Save state using localForage.
+store.subscribe(() => {
+    const {
+        locale,
+        auth,
+        settings,
+        unreads,
+        queue,
+        drafts,
+        sync,
+        experiments
+    } = store.getState() as State;
+    
+    localForage.setItem("state", {
+        locale,
+        auth,
+        settings,
+        unreads,
+        queue,
+        drafts,
+        sync,
+        experiments
+    });
+});
diff --git a/src/redux/reducers/auth.ts b/src/redux/reducers/auth.ts
new file mode 100644
index 0000000..9d5cbde
--- /dev/null
+++ b/src/redux/reducers/auth.ts
@@ -0,0 +1,48 @@
+import { Auth } from "revolt.js/dist/api/objects";
+
+export interface AuthState {
+    accounts: {
+        [key: string]: {
+            session: Auth.Session;
+        };
+    };
+    active?: string;
+}
+
+export type AuthAction =
+    | { type: undefined }
+    | {
+          type: "LOGIN";
+          session: Auth.Session;
+      }
+    | {
+          type: "LOGOUT";
+          user_id?: string;
+      };
+
+export function auth(
+    state = { accounts: {} } as AuthState,
+    action: AuthAction
+): AuthState {
+    switch (action.type) {
+        case "LOGIN":
+            return {
+                accounts: {
+                    ...state.accounts,
+                    [action.session.user_id]: {
+                        session: action.session
+                    }
+                },
+                active: action.session.user_id
+            };
+        case "LOGOUT":
+            const accounts = Object.assign({}, state.accounts);
+            action.user_id && delete accounts[action.user_id];
+
+            return {
+                accounts
+            };
+        default:
+            return state;
+    }
+}
diff --git a/src/redux/reducers/drafts.ts b/src/redux/reducers/drafts.ts
new file mode 100644
index 0000000..a000e41
--- /dev/null
+++ b/src/redux/reducers/drafts.ts
@@ -0,0 +1,33 @@
+export type Drafts = { [key: string]: string };
+
+export type DraftAction =
+    | { type: undefined }
+    | {
+          type: "SET_DRAFT";
+          channel: string;
+          content: string;
+      }
+    | {
+          type: "CLEAR_DRAFT";
+          channel: string;
+      }
+    | {
+          type: "RESET";
+      };
+
+export function drafts(state: Drafts = {}, action: DraftAction): Drafts {
+    switch (action.type) {
+        case "SET_DRAFT":
+            return {
+                ...state,
+                [action.channel]: action.content
+            };
+        case "CLEAR_DRAFT":
+            const { [action.channel]: _, ...newState } = state;
+            return newState;
+        case "RESET":
+            return {};
+        default:
+            return state;
+    }
+}
diff --git a/src/redux/reducers/experiments.ts b/src/redux/reducers/experiments.ts
new file mode 100644
index 0000000..89a13e0
--- /dev/null
+++ b/src/redux/reducers/experiments.ts
@@ -0,0 +1,43 @@
+export type Experiments = never;
+export const AVAILABLE_EXPERIMENTS: Experiments[] = [ ];
+
+export interface ExperimentOptions {
+    enabled?: Experiments[]
+}
+
+export type ExperimentsAction =
+    | { type: undefined }
+    | {
+          type: "EXPERIMENTS_ENABLE";
+          key: Experiments;
+      }
+    | {
+          type: "EXPERIMENTS_DISABLE";
+          key: Experiments;
+      };
+
+export function experiments(
+    state = {} as ExperimentOptions,
+    action: ExperimentsAction
+): ExperimentOptions {
+    switch (action.type) {
+        case "EXPERIMENTS_ENABLE":
+            return {
+                ...state,
+                enabled: [
+                    ...(state.enabled ?? [])
+                        .filter(x => AVAILABLE_EXPERIMENTS.includes(x))
+                        .filter(v => v !== action.key),
+                    action.key
+                ]
+            };
+        case "EXPERIMENTS_DISABLE":
+            return {
+                ...state,
+                enabled: state.enabled?.filter(v => v !== action.key)
+                    .filter(x => AVAILABLE_EXPERIMENTS.includes(x))
+            };
+        default:
+            return state;
+    }
+}
diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts
new file mode 100644
index 0000000..2dc3ce7
--- /dev/null
+++ b/src/redux/reducers/index.ts
@@ -0,0 +1,47 @@
+import { combineReducers } from "redux";
+
+import { settings, SettingsAction } from "./settings";
+import { locale, LocaleAction } from "./locale";
+import { auth, AuthAction } from "./auth";
+import { unreads, UnreadsAction } from "./unreads";
+import { queue, QueueAction } from "./queue";
+import { typing, TypingAction } from "./typing";
+import { drafts, DraftAction } from "./drafts";
+import { sync, SyncAction } from "./sync";
+import { experiments, ExperimentsAction } from "./experiments";
+
+export default combineReducers({
+    locale,
+    auth,
+    settings,
+    unreads,
+    queue,
+    typing,
+    drafts,
+    sync,
+    experiments
+});
+
+export type Action =
+    | LocaleAction
+    | AuthAction
+    | SettingsAction
+    | UnreadsAction
+    | QueueAction
+    | TypingAction
+    | DraftAction
+    | SyncAction
+    | ExperimentsAction
+    | { type: "__INIT"; state: any };
+
+export type WithDispatcher = { dispatcher: (action: Action) => void };
+
+export function filter(obj: any, keys: string[]) {
+    const newObj: any = {};
+    for (const key of keys) {
+        const v = obj[key];
+        if (v) newObj[key] = v;
+    }
+
+    return newObj;
+}
diff --git a/src/redux/reducers/locale.ts b/src/redux/reducers/locale.ts
new file mode 100644
index 0000000..b573e62
--- /dev/null
+++ b/src/redux/reducers/locale.ts
@@ -0,0 +1,50 @@
+import { Language } from "../../context/Locale";
+import { SyncData, SyncKeys, SyncUpdateAction } from "./sync";
+
+export type LocaleAction =
+    | { type: undefined }
+    | {
+          type: "SET_LOCALE";
+          locale: Language;
+      }
+    | SyncUpdateAction;
+
+export function findLanguage(lang?: string): Language {
+    if (!lang) {
+        if (typeof navigator === "undefined") {
+            lang = Language.ENGLISH;
+        } else {
+            lang = navigator.language;
+        }
+    }
+
+    const code = lang.replace("-", "_");
+
+    const short = code.split("_")[0];
+    for (const key of Object.keys(Language)) {
+        const value = (Language as any)[key];
+        if (value.startsWith(code)) {
+            return value;
+        }
+    }
+
+    for (const key of Object.keys(Language).reverse()) {
+        const value = (Language as any)[key];
+        if (value.startsWith(short)) {
+            return value;
+        }
+    }
+
+    return Language.ENGLISH;
+}
+
+export function locale(state = findLanguage(), action: LocaleAction): Language {
+    switch (action.type) {
+        case "SET_LOCALE":
+            return action.locale;
+        case "SYNC_UPDATE":
+            return (action.update.locale?.[1] ?? state) as Language;
+        default:
+            return state;
+    }
+}
diff --git a/src/redux/reducers/queue.ts b/src/redux/reducers/queue.ts
new file mode 100644
index 0000000..c2f0fdf
--- /dev/null
+++ b/src/redux/reducers/queue.ts
@@ -0,0 +1,103 @@
+import { MessageObject } from "../../context/revoltjs/messages";
+
+export enum QueueStatus {
+    SENDING = "sending",
+    ERRORED = "errored"
+}
+
+export interface QueuedMessage {
+    id: string;
+    channel: string;
+    data: MessageObject;
+    status: QueueStatus;
+    error?: string;
+}
+
+export type QueueAction =
+    | { type: undefined }
+    | {
+          type: "QUEUE_ADD";
+          nonce: string;
+          channel: string;
+          message: MessageObject;
+      }
+    | {
+          type: "QUEUE_FAIL";
+          nonce: string;
+          error: string;
+      }
+    | {
+          type: "QUEUE_START";
+          nonce: string;
+      }
+    | {
+          type: "QUEUE_REMOVE";
+          nonce: string;
+      }
+    | {
+          type: "QUEUE_DROP_ALL";
+      }
+    | {
+          type: "QUEUE_FAIL_ALL";
+      }
+    | {
+          type: "RESET";
+      };
+
+export function queue(
+    state: QueuedMessage[] = [],
+    action: QueueAction
+): QueuedMessage[] {
+    switch (action.type) {
+        case "QUEUE_ADD": {
+            return [
+                ...state.filter(x => x.id !== action.nonce),
+                {
+                    id: action.nonce,
+                    data: action.message,
+                    channel: action.channel,
+                    status: QueueStatus.SENDING
+                }
+            ];
+        }
+        case "QUEUE_FAIL": {
+            const entry = state.find(
+                x => x.id === action.nonce
+            ) as QueuedMessage;
+            return [
+                ...state.filter(x => x.id !== action.nonce),
+                {
+                    ...entry,
+                    status: QueueStatus.ERRORED,
+                    error: action.error
+                }
+            ];
+        }
+        case "QUEUE_START": {
+            const entry = state.find(
+                x => x.id === action.nonce
+            ) as QueuedMessage;
+            return [
+                ...state.filter(x => x.id !== action.nonce),
+                {
+                    ...entry,
+                    status: QueueStatus.SENDING
+                }
+            ];
+        }
+        case "QUEUE_REMOVE":
+            return state.filter(x => x.id !== action.nonce);
+        case "QUEUE_FAIL_ALL":
+            return state.map(x => {
+                return {
+                    ...x,
+                    status: QueueStatus.ERRORED
+                };
+            });
+        case "QUEUE_DROP_ALL":
+        case "RESET":
+            return [];
+        default:
+            return state;
+    }
+}
diff --git a/src/redux/reducers/settings.ts b/src/redux/reducers/settings.ts
new file mode 100644
index 0000000..744c328
--- /dev/null
+++ b/src/redux/reducers/settings.ts
@@ -0,0 +1,98 @@
+import { filter } from ".";
+import { SyncUpdateAction } from "./sync";
+import { Theme, ThemeOptions } from "../../context/Theme";
+
+export interface NotificationOptions {
+    desktopEnabled?: boolean;
+    soundEnabled?: boolean;
+    outgoingSoundEnabled?: boolean;
+}
+
+export type EmojiPacks = 'mutant' | 'twemoji' | 'noto' | 'openmoji';
+export interface AppearanceOptions {
+    emojiPack?: EmojiPacks
+}
+
+export interface Settings {
+    theme?: ThemeOptions;
+    appearance?: AppearanceOptions;
+    notification?: NotificationOptions;
+}
+
+export type SettingsAction =
+    | { type: undefined }
+    | {
+          type: "SETTINGS_SET_THEME";
+          theme: ThemeOptions;
+      }
+    | {
+          type: "SETTINGS_SET_THEME_OVERRIDE";
+          custom?: Partial<Theme>;
+      }
+    | {
+          type: "SETTINGS_SET_NOTIFICATION_OPTIONS";
+          options: NotificationOptions;
+      }
+    | {
+          type: "SETTINGS_SET_APPEARANCE";
+          options: Partial<AppearanceOptions>;
+      }
+    | SyncUpdateAction
+    | {
+          type: "RESET";
+      };
+
+export function settings(
+    state = {} as Settings,
+    action: SettingsAction
+): Settings {
+    // setEmojiPack(state.appearance?.emojiPack ?? 'mutant');
+
+    switch (action.type) {
+        case "SETTINGS_SET_THEME":
+            return {
+                ...state,
+                theme: {
+                    ...filter(state.theme, [ 'custom', 'preset' ]),
+                    ...action.theme,
+                }
+            };
+        case "SETTINGS_SET_THEME_OVERRIDE":
+            return {
+                ...state,
+                theme: {
+                    ...state.theme,
+                    custom: {
+                        ...state.theme?.custom,
+                        ...action.custom
+                    }
+                }
+            };
+        case "SETTINGS_SET_NOTIFICATION_OPTIONS":
+            return {
+                ...state,
+                notification: {
+                    ...state.notification,
+                    ...action.options
+                }
+            };
+        case "SETTINGS_SET_APPEARANCE":
+            return {
+                ...state,
+                appearance: {
+                    ...filter(state.appearance, [ 'emojiPack' ]),
+                    ...action.options
+                }
+            }
+        case "SYNC_UPDATE":
+            return {
+                ...state,
+                appearance: action.update.appearance?.[1] ?? state.appearance,
+                theme: action.update.theme?.[1] ?? state.theme
+            }
+        case "RESET":
+            return {};
+        default:
+            return state;
+    }
+}
diff --git a/src/redux/reducers/sync.ts b/src/redux/reducers/sync.ts
new file mode 100644
index 0000000..2edf5e9
--- /dev/null
+++ b/src/redux/reducers/sync.ts
@@ -0,0 +1,85 @@
+import { AppearanceOptions } from "./settings";
+import { Language } from "../../context/Locale";
+import { ThemeOptions } from "../../context/Theme";
+
+export type SyncKeys = 'theme' | 'appearance' | 'locale';
+
+export interface SyncData {
+    locale?: Language;
+    theme?: ThemeOptions;
+    appearance?: AppearanceOptions;
+}
+
+export const DEFAULT_ENABLED_SYNC: SyncKeys[] = [ 'theme', 'appearance', 'locale' ];
+export interface SyncOptions {
+    disabled?: SyncKeys[]
+    revision?: {
+        [key: string]: number
+    }
+}
+
+export type SyncUpdateAction = {
+    type: "SYNC_UPDATE";
+    update: { [key in SyncKeys]?: [ number, SyncData[key] ] }
+};
+
+export type SyncAction =
+    | { type: undefined }
+    | {
+          type: "SYNC_ENABLE_KEY";
+          key: SyncKeys;
+      }
+    | {
+          type: "SYNC_DISABLE_KEY";
+          key: SyncKeys;
+      }
+    | {
+          type: "SYNC_SET_REVISION";
+          key: SyncKeys;
+          timestamp: number;
+      }
+    | SyncUpdateAction;
+
+export function sync(
+    state = {} as SyncOptions,
+    action: SyncAction
+): SyncOptions {
+    switch (action.type) {
+        case "SYNC_DISABLE_KEY":
+            return {
+                ...state,
+                disabled: [
+                    ...(state.disabled ?? []).filter(v => v !== action.key),
+                    action.key
+                ]
+            };
+        case "SYNC_ENABLE_KEY":
+            return {
+                ...state,
+                disabled: state.disabled?.filter(v => v !== action.key)
+            };
+        case "SYNC_SET_REVISION":
+            return {
+                ...state,
+                revision: {
+                    ...state.revision,
+                    [action.key]: action.timestamp
+                }
+            };
+        case "SYNC_UPDATE":
+            const revision = { ...state.revision };
+            for (const key of Object.keys(action.update)) {
+                const value = action.update[key as SyncKeys];
+                if (value) {
+                    revision[key] = value[0];
+                }
+            }
+
+            return {
+                ...state,
+                revision
+            }
+        default:
+            return state;
+    }
+}
diff --git a/src/redux/reducers/typing.ts b/src/redux/reducers/typing.ts
new file mode 100644
index 0000000..b343621
--- /dev/null
+++ b/src/redux/reducers/typing.ts
@@ -0,0 +1,46 @@
+export type TypingUser = { id: string, started: number };
+export type Typing = { [key: string]: TypingUser[] };
+
+export type TypingAction =
+    | { type: undefined }
+    | {
+          type: "TYPING_START";
+          channel: string;
+          user: string;
+      }
+    | {
+          type: "TYPING_STOP";
+          channel: string;
+          user: string;
+      }
+    | {
+          type: "RESET";
+      };
+
+export function typing(state: Typing = {}, action: TypingAction): Typing {
+    switch (action.type) {
+        case "TYPING_START":
+            return {
+                ...state,
+                [action.channel]: [
+                    ...(state[action.channel] ?? []).filter(
+                        x => x.id !== action.user
+                    ),
+                    {
+                        id: action.user,
+                        started: + new Date()
+                    }
+                ]
+            };
+        case "TYPING_STOP":
+            return {
+                ...state,
+                [action.channel]:
+                    state[action.channel]?.filter(x => x.id !== action.user) ?? []
+            };
+        case "RESET":
+            return {};
+        default:
+            return state;
+    }
+}
diff --git a/src/redux/reducers/unreads.ts b/src/redux/reducers/unreads.ts
new file mode 100644
index 0000000..a81a55d
--- /dev/null
+++ b/src/redux/reducers/unreads.ts
@@ -0,0 +1,68 @@
+import { Sync } from "revolt.js/dist/api/objects";
+
+export interface Unreads {
+    [key: string]: Partial<Omit<Sync.ChannelUnread, '_id'>>;
+}
+
+export type UnreadsAction =
+    | { type: undefined }
+    | {
+        type: "UNREADS_MARK_READ";
+        channel: string;
+        message: string;
+        request: boolean;
+      }
+    | {
+        type: "UNREADS_SET";
+        unreads: Sync.ChannelUnread[];
+    }
+    | {
+        type: "UNREADS_MENTION";
+        channel: string;
+        message: string;
+    }
+    | {
+        type: "RESET";
+      };
+
+export function unreads(state = {}, action: UnreadsAction): Unreads {
+    switch (action.type) {
+        case "UNREADS_MARK_READ":
+            if (action.request) {
+                // client.req('PUT', `/channels/${action.channel}/ack/${action.message}` as '/channels/id/ack/id');
+            }
+
+            return {
+                ...state,
+                [action.channel]: {
+                    last_id: action.message
+                }
+            };
+        case "UNREADS_SET":
+            {
+                const obj: Unreads = {};
+                for (const entry of action.unreads) {
+                    const { _id, ...v } = entry;
+                    obj[_id.channel] = v;
+                }
+
+                return obj;
+            }
+        case "UNREADS_MENTION":
+            {
+                const obj = (state as any)[action.channel];
+
+                return {
+                    ...state,
+                    [action.channel]: {
+                        ...obj,
+                        mentions: [ ...(obj?.mentions ?? []), action.message ]
+                    }
+                }
+            }
+        case "RESET":
+            return {};
+        default:
+            return state;
+    }
+}
diff --git a/ui/ui.tsx b/ui/ui.tsx
index 0e642da..cf8f0a7 100644
--- a/ui/ui.tsx
+++ b/ui/ui.tsx
@@ -3,7 +3,7 @@ import styled from 'styled-components';
 import '../src/styles/index.scss'
 import { render } from 'preact'
 
-import { GlobalTheme } from '../src/context/Theme';
+import Theme from '../src/context/Theme';
 
 export const UIDemo = styled.div`
 	gap: 12px;
@@ -61,8 +61,9 @@ export function UI() {
 }
 
 render(<>
-    <GlobalTheme />
-    <UIDemo>
-        <UI />
-    </UIDemo>
+    <Theme>
+        <UIDemo>
+            <UI />
+        </UIDemo>
+    </Theme>
 </>, document.getElementById('app')!)
diff --git a/yarn.lock b/yarn.lock
index 07a04f0..bbbd0a3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -183,7 +183,7 @@
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.6.tgz#d85cc68ca3cac84eae384c06f032921f5227f4b2"
   integrity sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==
 
-"@babel/runtime@^7.10.5", "@babel/runtime@^7.14.0":
+"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.1", "@babel/runtime@^7.14.0", "@babel/runtime@^7.9.2":
   version "7.14.6"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
   integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
@@ -264,6 +264,26 @@
   resolved "https://registry.yarnpkg.com/@fontsource/open-sans/-/open-sans-4.4.5.tgz#07b31617e62ed753c94cabcf552ebaed4de497ce"
   integrity sha512-PDWEvO1/p8OAHHiielvEmwGXHNbZhrZn96ojV7+/mKgFu+cCUcGVJl9sFs97rCWLe3hKQsYLEsJs4EiLjwa+UQ==
 
+"@insertish/mutable@1.0.6":
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/@insertish/mutable/-/mutable-1.0.6.tgz#f42eaba8528ff68cc8065d51f9bbbd30a24f34de"
+  integrity sha512-FTaPbesmBwcr3iKfbA2udFto61/sL7rOiCM08vBbE2X0wC63nsvTos6gnkwa1Nwom1v15jjrc/4B0YqI3vbZ/Q==
+  dependencies:
+    "@insertish/zangodb" "^1.0.12"
+    eventemitter3 "^4.0.7"
+    lodash.isequal "^4.5.0"
+
+"@insertish/zangodb@1.0.12", "@insertish/zangodb@^1.0.12":
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/@insertish/zangodb/-/zangodb-1.0.12.tgz#25264ec065720fa43c7549ec7245e4f3839cb0ea"
+  integrity sha512-JlLI12Xqt1xvv/p2/AHs163ZYMZsB3sJyjB8yaAs6QcG0tyRBTIyxV5ISEAkAPo5kzlFza5z5oH82yQe/qw5RQ==
+  dependencies:
+    clone "^2.1.2"
+    deepmerge "^4.2.2"
+    memoizee "^0.4.15"
+    object-hash "^2.1.1"
+    q "^1.5.1"
+
 "@mdn/browser-compat-data@^2.0.7":
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-2.0.7.tgz#72ec37b9c1e00ce0b4e0309d753be18e2da12ee3"
@@ -360,7 +380,7 @@
     "@babel/runtime" "^7.10.5"
     "@emotion/is-prop-valid" "^0.8.7"
 
-"@types/hoist-non-react-statics@*":
+"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0":
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
   integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@@ -390,6 +410,23 @@
   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
   integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
 
+"@types/react-helmet@^6.1.1":
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.1.tgz#4fde22cbcaa1b461642e1d719cc6162d95acb110"
+  integrity sha512-VmSCMz6jp/06DABoY60vQa++h1YFt0PfAI23llxBJHbowqFgLUL0dhS1AQeVPNqYfRp9LAfokrfWACTNeobOrg==
+  dependencies:
+    "@types/react" "*"
+
+"@types/react-redux@^7.1.16":
+  version "7.1.16"
+  resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21"
+  integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==
+  dependencies:
+    "@types/hoist-non-react-statics" "^3.3.0"
+    "@types/react" "*"
+    hoist-non-react-statics "^3.3.0"
+    redux "^4.0.0"
+
 "@types/react@*":
   version "17.0.11"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451"
@@ -611,6 +648,13 @@ astral-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
+axios@^0.19.2:
+  version "0.19.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
+  integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+  dependencies:
+    follow-redirects "1.5.10"
+
 babel-eslint@^10.0.1:
   version "10.1.0"
   resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
@@ -739,6 +783,11 @@ chalk@^4.0.0:
   optionalDependencies:
     fsevents "~2.3.2"
 
+clone@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
 color-convert@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -785,7 +834,7 @@ core-js@^3.6.5:
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.14.0.tgz#62322b98c71cc2018b027971a69419e2425c2a6c"
   integrity sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==
 
-cross-spawn@^7.0.2:
+cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -813,6 +862,26 @@ csstype@^3.0.2:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
   integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
 
+d@1, d@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
+  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+  dependencies:
+    es5-ext "^0.10.50"
+    type "^1.0.1"
+
+dayjs@^1.10.5:
+  version "1.10.5"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986"
+  integrity sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g==
+
+debug@=3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+  dependencies:
+    ms "2.0.0"
+
 debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
@@ -825,6 +894,11 @@ deep-is@^0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
+deepmerge@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+  integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+
 define-properties@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -858,6 +932,11 @@ doctrine@^3.0.0:
   dependencies:
     esutils "^2.0.2"
 
+duplexer@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
+  integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
+
 electron-to-chromium@^1.3.723:
   version "1.3.752"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09"
@@ -906,6 +985,42 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
+  version "0.10.53"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
+  integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
+  dependencies:
+    es6-iterator "~2.0.3"
+    es6-symbol "~3.1.3"
+    next-tick "~1.0.0"
+
+es6-iterator@^2.0.3, es6-iterator@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
+  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
+  dependencies:
+    d "^1.0.1"
+    ext "^1.1.2"
+
+es6-weak-map@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53"
+  integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
+  dependencies:
+    d "1"
+    es5-ext "^0.10.46"
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.1"
+
 esbuild@^0.12.5:
   version "0.12.9"
   resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.9.tgz#bed4e7087c286cd81d975631f77d47feb1660070"
@@ -1106,6 +1221,44 @@ esutils@^2.0.2:
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
+event-emitter@^0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+  integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
+event-stream@=3.3.4:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
+  integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=
+  dependencies:
+    duplexer "~0.1.1"
+    from "~0"
+    map-stream "~0.1.0"
+    pause-stream "0.0.11"
+    split "0.3"
+    stream-combiner "~0.0.4"
+    through "~2.3.1"
+
+eventemitter3@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+exponential-backoff@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.0.tgz#9409c7e579131f8bd4b32d7d8094a911040f2e68"
+  integrity sha512-oBuz5SYz5zzyuHINoe9ooePwSu0xApKWgeNzok4hZ5YKXFh9zrQBEM15CXqoZkJJPuI2ArvqjPQd8UKJA753XA==
+
+ext@^1.1.2:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
+  integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
+  dependencies:
+    type "^2.0.0"
+
 extend@3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@@ -1180,6 +1333,18 @@ flatted@^3.1.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
   integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
 
+follow-redirects@1.5.10:
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+  dependencies:
+    debug "=3.1.0"
+
+from@~0:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
+  integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
+
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1284,7 +1449,7 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
-hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -1301,6 +1466,11 @@ ignore@^5.1.4:
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
   integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
 
+immediate@~3.0.5:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+  integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
+
 import-fresh@^3.0.0, import-fresh@^3.2.1:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -1404,6 +1574,11 @@ is-number@^7.0.0:
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
+is-promise@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
+  integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
 is-regex@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
@@ -1429,6 +1604,11 @@ isexe@^2.0.0:
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
 
+isomorphic-ws@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
+  integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
+
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -1490,6 +1670,20 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
+lie@3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
+  integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
+  dependencies:
+    immediate "~3.0.5"
+
+localforage@^1.9.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
+  integrity sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==
+  dependencies:
+    lie "3.1.1"
+
 locate-path@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
@@ -1502,6 +1696,16 @@ lodash.clonedeep@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
   integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
 
+lodash.defaultsdeep@^4.6.1:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
+  integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
+
+lodash.isequal@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
 lodash.memoize@4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -1536,6 +1740,32 @@ lru-cache@^6.0.0:
   dependencies:
     yallist "^4.0.0"
 
+lru-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
+  integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=
+  dependencies:
+    es5-ext "~0.10.2"
+
+map-stream@~0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
+  integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=
+
+memoizee@^0.4.15:
+  version "0.4.15"
+  resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72"
+  integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==
+  dependencies:
+    d "^1.0.1"
+    es5-ext "^0.10.53"
+    es6-weak-map "^2.0.3"
+    event-emitter "^0.3.5"
+    is-promise "^2.2.2"
+    lru-queue "^0.1.0"
+    next-tick "^1.1.0"
+    timers-ext "^0.1.7"
+
 merge2@^1.3.0:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@@ -1561,6 +1791,11 @@ minimist@^1.2.5:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
 ms@2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -1576,6 +1811,21 @@ natural-compare@^1.4.0:
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
+next-tick@1, next-tick@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
+  integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+
+next-tick@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+  integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
+
+node-cleanup@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
+  integrity sha1-esGavSl+Caf3KnFUXZUbUX5N3iw=
+
 node-releases@^1.1.71:
   version "1.1.73"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
@@ -1591,6 +1841,11 @@ object-assign@^4.1.1:
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
 
+object-hash@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
+  integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
+
 object-inspect@^1.10.3, object-inspect@^1.9.0:
   version "1.10.3"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
@@ -1709,6 +1964,13 @@ path-type@^4.0.0:
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
+pause-stream@0.0.11:
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
+  integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=
+  dependencies:
+    through "~2.3"
+
 picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
@@ -1770,17 +2032,51 @@ prop-types@^15.7.2:
     object-assign "^4.1.1"
     react-is "^16.8.1"
 
+ps-tree@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.2.0.tgz#5e7425b89508736cdd4f2224d028f7bb3f722ebd"
+  integrity sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==
+  dependencies:
+    event-stream "=3.3.4"
+
 punycode@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
+q@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+  integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
 queue-microtask@^1.2.2:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
-react-is@^16.7.0, react-is@^16.8.1:
+react-device-detect@^1.17.0:
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.17.0.tgz#a00b4fd6880cebfab3fd8a42a79dc0290cdddca9"
+  integrity sha512-bBblIStwpHmoS281JFIVqeimcN3LhpoP5YKDWzxQdBIUP8S2xPvHDgizLDhUq2ScguLfVPmwfF5y268EEQR60w==
+  dependencies:
+    ua-parser-js "^0.7.24"
+
+react-fast-compare@^3.1.1:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
+  integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
+
+react-helmet@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
+  integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==
+  dependencies:
+    object-assign "^4.1.1"
+    prop-types "^15.7.2"
+    react-fast-compare "^3.1.1"
+    react-side-effect "^2.1.0"
+
+react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -1790,6 +2086,23 @@ react-overlapping-panels@1.1.2-patch.0:
   resolved "https://registry.yarnpkg.com/react-overlapping-panels/-/react-overlapping-panels-1.1.2-patch.0.tgz#335649735c029d334daea19ef6e30efc76b128fd"
   integrity sha512-PaXxk5HxBMYg46iADGGhkgXqqweJWo7yjSeT4/o0Q3s6Q7pl7Rz23lM3oW2gdJHBDOs/zBpZ+ZIP4j6grQlCOA==
 
+react-redux@^7.2.4:
+  version "7.2.4"
+  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
+  integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
+  dependencies:
+    "@babel/runtime" "^7.12.1"
+    "@types/react-redux" "^7.1.16"
+    hoist-non-react-statics "^3.3.2"
+    loose-envify "^1.4.0"
+    prop-types "^15.7.2"
+    react-is "^16.13.1"
+
+react-side-effect@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3"
+  integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
+
 readdirp@~3.6.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -1797,6 +2110,13 @@ readdirp@~3.6.0:
   dependencies:
     picomatch "^2.2.1"
 
+redux@^4.0.0, redux@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4"
+  integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==
+  dependencies:
+    "@babel/runtime" "^7.9.2"
+
 regenerator-runtime@^0.13.4:
   version "0.13.7"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
@@ -1846,6 +2166,22 @@ reusify@^1.0.4:
   resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
+revolt.js@4.2.0-alpha.3-patch.0:
+  version "4.2.0-alpha.3-patch.0"
+  resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.2.0-alpha.3-patch.0.tgz#ca79731c2b2fa9a8dbfbc5c9f84bef6ee2759918"
+  integrity sha512-g4eXHDbQyjKEiDOjj+3BxbRwPuVfOCYsnVqOiOXoAib4k48c27N+ZU0apYV25/AzCvIoYGDtVfY3I33UkTl1Rw==
+  dependencies:
+    "@insertish/mutable" "1.0.6"
+    "@insertish/zangodb" "1.0.12"
+    axios "^0.19.2"
+    eventemitter3 "^4.0.7"
+    exponential-backoff "^3.1.0"
+    isomorphic-ws "^4.0.1"
+    lodash.defaultsdeep "^4.6.1"
+    tsc-watch "^4.1.0"
+    ulid "^2.3.0"
+    ws "^7.2.1"
+
 rimraf@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -1946,11 +2282,30 @@ source-map@^0.5.0:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
 
+split@0.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
+  integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=
+  dependencies:
+    through "2"
+
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
+stream-combiner@~0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
+  integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=
+  dependencies:
+    duplexer "~0.1.1"
+
+string-argv@^0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738"
+  integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA==
+
 string-width@^4.2.0:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
@@ -2049,6 +2404,19 @@ text-table@^0.2.0:
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
+through@2, through@~2.3, through@~2.3.1:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+  integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+timers-ext@^0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6"
+  integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==
+  dependencies:
+    es5-ext "~0.10.46"
+    next-tick "1"
+
 to-fast-properties@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -2061,6 +2429,17 @@ to-regex-range@^5.0.1:
   dependencies:
     is-number "^7.0.0"
 
+tsc-watch@^4.1.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/tsc-watch/-/tsc-watch-4.4.0.tgz#3ebbf1db54bcef6bfe534b330fa87284a4139320"
+  integrity sha512-+0Yey6ptOOXAnt44OKTk2/EnQnmA0auL7VWXm9d9abMS4tabt0Xdr9B4AK6OJbWAre9ZdLA81+Nk8sz9unptyA==
+  dependencies:
+    cross-spawn "^7.0.3"
+    node-cleanup "^2.1.2"
+    ps-tree "^1.2.0"
+    string-argv "^0.1.1"
+    strip-ansi "^6.0.0"
+
 tslib@^1.8.1:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -2085,11 +2464,31 @@ type-fest@^0.20.2:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
   integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
 
+type@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
+  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
+
+type@^2.0.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
+  integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
+
 typescript@^4.3.2:
   version "4.3.3"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.3.tgz#5401db69bd3203daf1851a1a74d199cb3112c11a"
   integrity sha512-rUvLW0WtF7PF2b9yenwWUi9Da9euvDRhmH7BLyBG4DCFfOJ850LGNknmRpp8Z8kXNUPObdZQEfKOiHtXuQHHKA==
 
+ua-parser-js@^0.7.24:
+  version "0.7.28"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
+  integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
+
+ulid@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f"
+  integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==
+
 unbox-primitive@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
@@ -2152,6 +2551,11 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
+ws@^7.2.1:
+  version "7.5.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691"
+  integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==
+
 yallist@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
-- 
GitLab