Add MobX store, create observable User.

This commit is contained in:
Paul 2021-07-29 12:41:28 +01:00
parent 781fa5de10
commit cf3930b094
10 changed files with 247 additions and 61 deletions

View file

@ -82,6 +82,8 @@
"markdown-it-sub": "^1.0.0", "markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0", "markdown-it-sup": "^1.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext", "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-context-menu": "^0.1.5",
"preact-i18n": "^2.4.0-preactx", "preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1", "prettier": "^2.3.1",

View file

@ -2,6 +2,7 @@ import { BrowserRouter as Router } from "react-router-dom";
import State from "../redux/State"; import State from "../redux/State";
import MobXState from "../mobx/State";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
import Locale from "./Locale"; import Locale from "./Locale";
import Settings from "./Settings"; import Settings from "./Settings";
@ -14,17 +15,19 @@ export default function Context({ children }: { children: Children }) {
return ( return (
<Router> <Router>
<State> <State>
<Theme> <MobXState>
<Settings> <Theme>
<Locale> <Settings>
<Intermediate> <Locale>
<Client> <Intermediate>
<Voice>{children}</Voice> <Client>
</Client> <Voice>{children}</Voice>
</Intermediate> </Client>
</Locale> </Intermediate>
</Settings> </Locale>
</Theme> </Settings>
</Theme>
</MobXState>
</State> </State>
</Router> </Router>
); );

View file

@ -14,6 +14,7 @@ import { AuthState } from "../../redux/reducers/auth";
import Preloader from "../../components/ui/Preloader"; import Preloader from "../../components/ui/Preloader";
import { useData } from "../../mobx/State";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
import { useIntermediate } from "../intermediate/Intermediate"; import { useIntermediate } from "../intermediate/Intermediate";
import { registerEvents, setReconnectDisallowed } from "./events"; import { registerEvents, setReconnectDisallowed } from "./events";
@ -157,9 +158,10 @@ function Context({ auth, children }: Props) {
}; };
}, [client, auth.active]); }, [client, auth.active]);
const store = useData();
useEffect( useEffect(
() => registerEvents({ operations }, setStatus, client), () => registerEvents({ operations }, setStatus, client, store),
[client], [client, store],
); );
useEffect(() => { useEffect(() => {

View file

@ -5,6 +5,8 @@ import { StateUpdater } from "preact/hooks";
import { dispatch } from "../../redux"; import { dispatch } from "../../redux";
import { DataStore } from "../../mobx";
import { useData } from "../../mobx/State";
import { ClientOperations, ClientStatus } from "./RevoltClient"; import { ClientOperations, ClientStatus } from "./RevoltClient";
export var preventReconnect = false; export var preventReconnect = false;
@ -18,6 +20,7 @@ export function registerEvents(
{ operations }: { operations: ClientOperations }, { operations }: { operations: ClientOperations },
setStatus: StateUpdater<ClientStatus>, setStatus: StateUpdater<ClientStatus>,
client: Client, client: Client,
store: DataStore,
) { ) {
function attemptReconnect() { function attemptReconnect() {
if (preventReconnect) return; if (preventReconnect) return;
@ -45,6 +48,7 @@ export function registerEvents(
}, },
packet: (packet: ClientboundNotification) => { packet: (packet: ClientboundNotification) => {
store.packet(packet);
switch (packet.type) { switch (packet.type) {
case "ChannelStartTyping": { case "ChannelStartTyping": {
if (packet.user === client.user?._id) return; if (packet.user === client.user?._id) return;

View file

@ -5,7 +5,7 @@ import Collection from "revolt.js/dist/maps/Collection";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
//#region Hooks v1 //#region Hooks v1 (deprecated)
import { AppContext } from "./RevoltClient"; import { AppContext } from "./RevoltClient";
export interface HookContext { export interface HookContext {
@ -238,7 +238,7 @@ export function useServerPermission(id: string, context?: HookContext) {
} }
//#endregion //#endregion
//#region Hooks v2 //#region Hooks v2 (deprecated)
type CollectionKeys = Exclude< type CollectionKeys = Exclude<
keyof PickProperties<Client, Collection<any>>, keyof PickProperties<Client, Collection<any>>,
undefined undefined
@ -249,7 +249,7 @@ interface Depedency {
id?: string; id?: string;
} }
export function useData<T>( export function useDataDeprecated<T>(
cb: (client: Client) => T, cb: (client: Client) => T,
dependencies: Depedency[], dependencies: Depedency[],
): T { ): T {

26
src/mobx/State.tsx Normal file
View file

@ -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<DataStore>(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 (
<DataContext.Provider value={store}>
{props.children}
</DataContext.Provider>
);
}
export const useData = () => useContext(DataContext);

95
src/mobx/index.ts Normal file
View file

@ -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> = T | null;
function toNullable<T>(data?: T) {
return typeof data === "undefined" ? null : data;
}
class User {
_id: string;
username: string;
avatar: Nullable<Attachment>;
badges: Nullable<number>;
status: Nullable<Users.Status>;
relationship: Nullable<Users.Relationship>;
online: Nullable<boolean>;
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<Users.User>, 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<string, User>();
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;
}
}
}
}

View file

@ -1,4 +1,6 @@
import { Wrench } from "@styled-icons/boxicons-solid"; 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 { Channels } from "revolt.js/dist/api/objects";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
@ -7,10 +9,13 @@ import PaintCounter from "../../lib/PaintCounter";
import { TextReact } from "../../lib/i18n"; import { TextReact } from "../../lib/i18n";
import { AppContext } from "../../context/revoltjs/RevoltClient"; 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 Header from "../../components/ui/Header";
import { useData } from "../../mobx/State";
export default function Developer() { export default function Developer() {
// const voice = useContext(VoiceContext); // const voice = useContext(VoiceContext);
const client = useContext(AppContext); const client = useContext(AppContext);
@ -35,7 +40,10 @@ export default function Developer() {
fields={{ provider: <b>GAMING!</b> }} fields={{ provider: <b>GAMING!</b> }}
/> />
</div> </div>
<DataTest /> <ObserverTest />
<ObserverTest2 />
<ObserverTest3 />
<ObserverTest4 />
<div style={{ padding: "16px" }}> <div style={{ padding: "16px" }}>
{/*<span> {/*<span>
<b>Voice Status:</b> {VoiceStatus[voice.status]} <b>Voice Status:</b> {VoiceStatus[voice.status]}
@ -55,29 +63,66 @@ export default function Developer() {
); );
} }
function DataTest() { const ObserverTest = observer(() => {
const channel_id = ( const client = useContext(AppContext);
useContext(AppContext) const store = useData();
.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 }],
);
return ( return (
<div style={{ padding: "16px" }}> <div style={{ padding: "16px" }}>
Channel name: {data.name} <p>
<div style={{ width: "24px" }}> username:{" "}
{store.users.get(client.user!._id)?.username ?? "no user!"}
<PaintCounter small /> <PaintCounter small />
</div> </p>
</div> </div>
); );
} });
const ObserverTest2 = observer(() => {
const client = useContext(AppContext);
const store = useData();
return (
<div style={{ padding: "16px" }}>
<p>
status:{" "}
{JSON.stringify(store.users.get(client.user!._id)?.status) ??
"none"}
<PaintCounter small />
</p>
</div>
);
});
const ObserverTest3 = observer(() => {
const client = useContext(AppContext);
const store = useData();
return (
<div style={{ padding: "16px" }}>
<p>
avatar{" "}
<UserIcon
size={64}
attachment={
store.users.get(client.user!._id)?.avatar ?? undefined
}
/>
<PaintCounter small />
</p>
</div>
);
});
const ObserverTest4 = observer(() => {
const client = useContext(AppContext);
const store = useData();
return (
<div style={{ padding: "16px" }}>
<p>
status text:{" "}
{JSON.stringify(
store.users.get(client.user!._id)?.status?.text,
) ?? "none"}
<PaintCounter small />
</p>
</div>
);
});

View file

@ -1,24 +1,23 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "preserve", "jsx": "preserve",
"jsxFactory": "h", "jsxFactory": "h",
"jsxFragmentFactory": "Fragment", "jsxFragmentFactory": "Fragment",
"types": [ "types": ["vite-plugin-pwa/client"],
"vite-plugin-pwa/client" "experimentalDecorators": true
] },
}, "include": ["src", "ui/ui.tsx"]
"include": ["src", "ui/ui.tsx"]
} }

View file

@ -3077,6 +3077,16 @@ minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 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: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"