diff --git a/package.json b/package.json index 25a45625..83f056bd 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,8 @@ "markdown-it-sub": "^1.0.0", "markdown-it-sup": "^1.0.0", "mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext", + "mobx": "^6.3.2", + "mobx-react-lite": "^3.2.0", "preact-context-menu": "^0.1.5", "preact-i18n": "^2.4.0-preactx", "prettier": "^2.3.1", diff --git a/src/context/index.tsx b/src/context/index.tsx index f7a3ccb6..e3c757e2 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -2,6 +2,7 @@ import { BrowserRouter as Router } from "react-router-dom"; import State from "../redux/State"; +import MobXState from "../mobx/State"; import { Children } from "../types/Preact"; import Locale from "./Locale"; import Settings from "./Settings"; @@ -14,17 +15,19 @@ export default function Context({ children }: { children: Children }) { return ( - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ); diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx index 6c8b3ace..27ed3193 100644 --- a/src/context/revoltjs/RevoltClient.tsx +++ b/src/context/revoltjs/RevoltClient.tsx @@ -14,6 +14,7 @@ import { AuthState } from "../../redux/reducers/auth"; import Preloader from "../../components/ui/Preloader"; +import { useData } from "../../mobx/State"; import { Children } from "../../types/Preact"; import { useIntermediate } from "../intermediate/Intermediate"; import { registerEvents, setReconnectDisallowed } from "./events"; @@ -157,9 +158,10 @@ function Context({ auth, children }: Props) { }; }, [client, auth.active]); + const store = useData(); useEffect( - () => registerEvents({ operations }, setStatus, client), - [client], + () => registerEvents({ operations }, setStatus, client, store), + [client, store], ); useEffect(() => { diff --git a/src/context/revoltjs/events.ts b/src/context/revoltjs/events.ts index 67f6267e..f6607334 100644 --- a/src/context/revoltjs/events.ts +++ b/src/context/revoltjs/events.ts @@ -5,6 +5,8 @@ import { StateUpdater } from "preact/hooks"; import { dispatch } from "../../redux"; +import { DataStore } from "../../mobx"; +import { useData } from "../../mobx/State"; import { ClientOperations, ClientStatus } from "./RevoltClient"; export var preventReconnect = false; @@ -18,6 +20,7 @@ export function registerEvents( { operations }: { operations: ClientOperations }, setStatus: StateUpdater, client: Client, + store: DataStore, ) { function attemptReconnect() { if (preventReconnect) return; @@ -45,6 +48,7 @@ export function registerEvents( }, packet: (packet: ClientboundNotification) => { + store.packet(packet); switch (packet.type) { case "ChannelStartTyping": { if (packet.user === client.user?._id) return; diff --git a/src/context/revoltjs/hooks.ts b/src/context/revoltjs/hooks.ts index 7a4ba41c..22a297c9 100644 --- a/src/context/revoltjs/hooks.ts +++ b/src/context/revoltjs/hooks.ts @@ -5,7 +5,7 @@ import Collection from "revolt.js/dist/maps/Collection"; import { useContext, useEffect, useState } from "preact/hooks"; -//#region Hooks v1 +//#region Hooks v1 (deprecated) import { AppContext } from "./RevoltClient"; export interface HookContext { @@ -238,7 +238,7 @@ export function useServerPermission(id: string, context?: HookContext) { } //#endregion -//#region Hooks v2 +//#region Hooks v2 (deprecated) type CollectionKeys = Exclude< keyof PickProperties>, undefined @@ -249,7 +249,7 @@ interface Depedency { id?: string; } -export function useData( +export function useDataDeprecated( cb: (client: Client) => T, dependencies: Depedency[], ): T { diff --git a/src/mobx/State.tsx b/src/mobx/State.tsx new file mode 100644 index 00000000..cd3b149a --- /dev/null +++ b/src/mobx/State.tsx @@ -0,0 +1,26 @@ +import { createContext } from "preact"; +import { useContext } from "preact/hooks"; + +import { DataStore } from "."; +import { Children } from "../types/Preact"; + +interface Props { + children: Children; +} + +export const DataContext = createContext(null!); + +// ! later we can do seamless account switching, by hooking this into Redux +// ! and monitoring changes to active account and hence swapping stores. +// although this may need more work since we need a Client per account too. +const store = new DataStore(); + +export default function StateLoader(props: Props) { + return ( + + {props.children} + + ); +} + +export const useData = () => useContext(DataContext); diff --git a/src/mobx/index.ts b/src/mobx/index.ts new file mode 100644 index 00000000..b41a4139 --- /dev/null +++ b/src/mobx/index.ts @@ -0,0 +1,95 @@ +import isEqual from "lodash.isequal"; +import { + makeAutoObservable, + observable, + autorun, + runInAction, + reaction, + makeObservable, + action, + extendObservable, +} from "mobx"; +import { Attachment, Users } from "revolt.js/dist/api/objects"; +import { RemoveUserField } from "revolt.js/dist/api/routes"; +import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; + +type Nullable = T | null; +function toNullable(data?: T) { + return typeof data === "undefined" ? null : data; +} + +class User { + _id: string; + username: string; + + avatar: Nullable; + badges: Nullable; + status: Nullable; + relationship: Nullable; + online: Nullable; + + constructor(data: Users.User) { + this._id = data._id; + this.username = data.username; + + this.avatar = toNullable(data.avatar); + this.badges = toNullable(data.badges); + this.status = toNullable(data.status); + this.relationship = toNullable(data.relationship); + this.online = toNullable(data.online); + + makeAutoObservable(this); + } + + @action update(data: Partial, clear?: RemoveUserField) { + const apply = (key: keyof Users.User) => { + // This code has been tested. + // @ts-expect-error + if (data[key] && !isEqual(this[key], data[key])) { + // @ts-expect-error + this[key] = data[key]; + } + }; + + switch (clear) { + case "Avatar": + this.avatar = null; + break; + case "StatusText": { + if (this.status) { + this.status.text = undefined; + } + } + } + + apply("avatar"); + apply("badges"); + apply("status"); + apply("relationship"); + apply("online"); + } +} + +export class DataStore { + @observable users = new Map(); + + constructor() { + makeAutoObservable(this); + } + + @action + packet(packet: ClientboundNotification) { + switch (packet.type) { + case "Ready": { + for (let user of packet.users) { + this.users.set(user._id, new User(user)); + } + break; + } + case "UserUpdate": { + this.users.get(packet.id)?.update(packet.data, packet.clear); + break; + } + } + } +} diff --git a/src/pages/developer/Developer.tsx b/src/pages/developer/Developer.tsx index 4c0f3cc0..562a41a4 100644 --- a/src/pages/developer/Developer.tsx +++ b/src/pages/developer/Developer.tsx @@ -1,4 +1,6 @@ import { Wrench } from "@styled-icons/boxicons-solid"; +import { isObservable, isObservableProp } from "mobx"; +import { observer } from "mobx-react-lite"; import { Channels } from "revolt.js/dist/api/objects"; import { useContext } from "preact/hooks"; @@ -7,10 +9,13 @@ import PaintCounter from "../../lib/PaintCounter"; import { TextReact } from "../../lib/i18n"; import { AppContext } from "../../context/revoltjs/RevoltClient"; -import { useData, useUserPermission } from "../../context/revoltjs/hooks"; +import { useUserPermission } from "../../context/revoltjs/hooks"; +import UserIcon from "../../components/common/user/UserIcon"; import Header from "../../components/ui/Header"; +import { useData } from "../../mobx/State"; + export default function Developer() { // const voice = useContext(VoiceContext); const client = useContext(AppContext); @@ -35,7 +40,10 @@ export default function Developer() { fields={{ provider: GAMING! }} /> - + + + +
{/* Voice Status: {VoiceStatus[voice.status]} @@ -55,29 +63,66 @@ export default function Developer() { ); } -function DataTest() { - const channel_id = ( - useContext(AppContext) - .channels.toArray() - .find((x) => x.channel_type === "Group") as Channels.GroupChannel - )._id; - - const data = useData( - (client) => { - return { - name: (client.channels.get(channel_id) as Channels.GroupChannel) - .name, - }; - }, - [{ key: "channels", id: channel_id }], - ); - +const ObserverTest = observer(() => { + const client = useContext(AppContext); + const store = useData(); return (
- Channel name: {data.name} -
+

+ username:{" "} + {store.users.get(client.user!._id)?.username ?? "no user!"} -

+

); -} +}); + +const ObserverTest2 = observer(() => { + const client = useContext(AppContext); + const store = useData(); + return ( +
+

+ status:{" "} + {JSON.stringify(store.users.get(client.user!._id)?.status) ?? + "none"} + +

+
+ ); +}); + +const ObserverTest3 = observer(() => { + const client = useContext(AppContext); + const store = useData(); + return ( +
+

+ avatar{" "} + + +

+
+ ); +}); + +const ObserverTest4 = observer(() => { + const client = useContext(AppContext); + const store = useData(); + return ( +
+

+ status text:{" "} + {JSON.stringify( + store.users.get(client.user!._id)?.status?.text, + ) ?? "none"} + +

+
+ ); +}); diff --git a/tsconfig.json b/tsconfig.json index 8f108763..722675db 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,23 @@ { - "compilerOptions": { - "target": "ESNext", - "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve", - "jsxFactory": "h", - "jsxFragmentFactory": "Fragment", - "types": [ - "vite-plugin-pwa/client" - ] - }, - "include": ["src", "ui/ui.tsx"] + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment", + "types": ["vite-plugin-pwa/client"], + "experimentalDecorators": true + }, + "include": ["src", "ui/ui.tsx"] } diff --git a/yarn.lock b/yarn.lock index e14cb3b2..f943b7e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3077,6 +3077,16 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mobx-react-lite@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz#331d7365a6b053378dfe9c087315b4e41c5df69f" + integrity sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g== + +mobx@^6.3.2: + version "6.3.2" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.2.tgz#125590961f702a572c139ab69392bea416d2e51b" + integrity sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"