add connection status, version comparing, not hardlocking until config being sent (fixes #27) (related #23)

This commit is contained in:
Grant 2024-05-26 14:23:54 -06:00
parent eb73597667
commit c907d52027
12 changed files with 146 additions and 14 deletions

21
package-lock.json generated
View File

@ -12674,6 +12674,26 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-toastify": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz",
"integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==",
"dependencies": {
"clsx": "^2.1.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-toastify/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/react-zoom-pan-pinch": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.4.2.tgz",
@ -16005,6 +16025,7 @@
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-toastify": "^10.0.5",
"react-zoom-pan-pinch": "^3.4.1",
"socket.io-client": "^4.7.4"
},

View File

@ -29,6 +29,7 @@
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-toastify": "^10.0.5",
"react-zoom-pan-pinch": "^3.4.1",
"socket.io-client": "^4.7.4"
},

View File

@ -8,14 +8,37 @@ import { ToolbarWrapper } from "./Toolbar/ToolbarWrapper";
import React, { lazy, useEffect } from "react";
import { ChatContext } from "../contexts/ChatContext";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
const Chat = lazy(() => import("./Chat/Chat"));
console.log("Client init with version " + __COMMIT_HASH__);
const DynamicallyLoadChat = () => {
const { loadChat } = useAppContext();
return <React.Suspense>{loadChat && <Chat />}</React.Suspense>;
};
// get access to context data
const AppInner = () => {
return (
<>
<Header />
<CanvasWrapper />
<ToolbarWrapper />
{/* <DynamicallyLoadChat /> */}
<DebugModal />
<SettingsSidebar />
<ToastContainer position="top-left" />
</>
);
};
const App = () => {
useEffect(() => {
// detect auth callback for chat, regardless of it being loaded
@ -113,14 +136,7 @@ const App = () => {
<AppContext>
<ChatContext>
<TemplateContext>
<Header />
<CanvasWrapper />
<ToolbarWrapper />
{/* <DynamicallyLoadChat /> */}
<DebugModal />
<SettingsSidebar />
<AppInner />
</TemplateContext>
</ChatContext>
</AppContext>

View File

@ -10,11 +10,13 @@ import { Template } from "./Template";
import { IRouterData, Router } from "../lib/router";
export const CanvasWrapper = () => {
const { config } = useAppContext();
// to prevent safari from blurring things, use the zoom css property
return (
<main>
<PanZoomWrapper>
<Template />
{config && <Template />}
<CanvasInner />
</PanZoomWrapper>
</main>
@ -31,7 +33,7 @@ const CanvasInner = () => {
}, [PanZoom]);
useEffect(() => {
if (!config.canvas || !canvasRef.current) return;
if (!config?.canvas || !canvasRef.current) return;
const canvas = canvasRef.current!;
const canvasInstance = new Canvas(config, canvas, PanZoom);
const initAt = Date.now();

View File

@ -1,4 +1,4 @@
import { Button } from "@nextui-org/react";
import { Button, Card, CardBody } from "@nextui-org/react";
import { useAppContext } from "../contexts/AppContext";
import { User } from "./Header/User";
import { Debug } from "@sc07-canvas/lib/src/debug";
@ -13,12 +13,20 @@ const DynamicChat = () => {
};
export const Header = () => {
const { setSettingsSidebar } = useAppContext();
const { setSettingsSidebar, connected } = useAppContext();
return (
<header id="main-header">
<div></div>
<div className="spacer"></div>
{!connected && (
<div>
<Card>
<CardBody>Disconnected</CardBody>
</Card>
</div>
)}
<div className="spacer"></div>
<div className="box">
<User />
<Button onClick={() => setSettingsSidebar(true)}>Settings</Button>

View File

@ -1,3 +1,4 @@
import { useAppContext } from "../../contexts/AppContext";
import { CanvasMeta } from "./CanvasMeta";
import { Palette } from "./Palette";
import { UndoButton } from "./UndoButton";
@ -6,6 +7,10 @@ import { UndoButton } from "./UndoButton";
* Wrapper for everything aligned at the bottom of the screen
*/
export const ToolbarWrapper = () => {
const { config } = useAppContext();
if (!config) return <></>;
return (
<div id="toolbar">
<CanvasMeta />

View File

@ -13,6 +13,7 @@ import {
IPosition,
} from "@sc07-canvas/lib/src/net";
import Network from "../lib/network";
import { Spinner } from "@nextui-org/react";
const appContext = createContext<IAppContext>({} as any);
@ -23,6 +24,7 @@ export const AppContext = ({ children }: PropsWithChildren) => {
const [auth, setAuth] = useState<AuthSession>();
const [canvasPosition, setCanvasPosition] = useState<ICanvasPosition>();
const [cursorPosition, setCursorPosition] = useState<IPosition>();
const [connected, setConnected] = useState(false);
// --- settings ---
const [loadChat, _setLoadChat] = useState(false);
@ -43,7 +45,6 @@ export const AppContext = ({ children }: PropsWithChildren) => {
}
function handleConfig(config: ClientConfig) {
console.info("Server sent config", config);
setConfig(config);
}
@ -65,12 +66,23 @@ export const AppContext = ({ children }: PropsWithChildren) => {
}
}
function handleConnect() {
setConnected(true);
}
function handleDisconnect() {
setConnected(false);
}
Network.on("user", handleUser);
Network.on("config", handleConfig);
Network.waitFor("pixels").then(([data]) => handlePixels(data));
Network.on("pixels", handlePixels);
Network.on("undo", handleUndo);
Network.on("connected", handleConnect);
Network.on("disconnected", handleDisconnect);
Network.socket.connect();
loadSettings();
@ -79,6 +91,9 @@ export const AppContext = ({ children }: PropsWithChildren) => {
Network.off("user", handleUser);
Network.off("config", handleConfig);
Network.off("pixels", handlePixels);
Network.off("undo", handleUndo);
Network.off("connected", handleConnect);
Network.off("disconnected", handleDisconnect);
};
}, []);
@ -102,9 +117,15 @@ export const AppContext = ({ children }: PropsWithChildren) => {
undo,
loadChat,
setLoadChat,
connected,
}}
>
{config ? children : "Loading..."}
{!config && (
<div className="fixed top-0 left-0 w-full h-full z-[9999] backdrop-blur-sm bg-black/30 text-white flex items-center justify-center">
<Spinner label="Loading..." />
</div>
)}
{children}
</appContext.Provider>
);
};

View File

@ -7,8 +7,12 @@ import {
Pixel,
ServerToClientEvents,
} from "@sc07-canvas/lib/src/net";
import { toast } from "react-toastify";
export interface INetworkEvents {
connected: () => void;
disconnected: () => void;
user: (user: AuthSession) => void;
config: (user: ClientConfig) => void;
canvas: (pixels: string[]) => void;
@ -31,6 +35,7 @@ class Network extends EventEmitter<INetworkEvents> {
{
autoConnect: false,
withCredentials: true,
transports: ["polling"],
}
);
private online_count = 0;
@ -41,11 +46,40 @@ class Network extends EventEmitter<INetworkEvents> {
constructor() {
super();
this.socket.on("connect", () => {
console.log("Connected to server");
toast.success("Connected to server");
this.emit("connected");
});
this.socket.on("connect_error", (err) => {
// TODO: proper error handling
console.error("Failed to connect to server", err);
toast.error("Failed to connect: " + (err.message || err.name));
});
this.socket.on("disconnect", (reason, desc) => {
console.log("Disconnected from server", reason, desc);
toast.warn("Disconnected from server");
this.emit("disconnected");
});
this.socket.on("user", (user: AuthSession) => {
this.emit("user", user);
});
this.socket.on("config", (config) => {
console.info("Server sent config", config);
if (config.version !== __COMMIT_HASH__) {
toast.info("Client version does not match server, reloading...");
console.warn("Client version does not match server, reloading...", {
clientVersion: __COMMIT_HASH__,
serverVersion: config.version,
});
window.location.reload();
}
this.emit("config", config);
});

View File

@ -8,3 +8,5 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare const __COMMIT_HASH__: string;

View File

@ -1,5 +1,11 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import * as child from "child_process";
const commitHash = child
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
export default defineConfig({
root: "src",
@ -13,4 +19,7 @@ export default defineConfig({
include: "**/*.{jsx,tsx}",
}),
],
define: {
__COMMIT_HASH__: JSON.stringify(commitHash),
},
});

View File

@ -42,6 +42,7 @@ export interface IAppContext {
undo?: { available: true; expireAt: number };
loadChat: boolean;
setLoadChat: (v: boolean) => void;
connected: boolean;
}
export interface IPalleteContext {
@ -93,6 +94,10 @@ export type CanvasConfig = {
};
export type ClientConfig = {
/**
* Monolith git hash, if it doesn't match, client will reload
*/
version: string;
pallete: {
colors: PalleteColor[];
pixel_cooldown: number;

View File

@ -1,4 +1,5 @@
import http from "node:http";
import * as child from "node:child_process";
import {
ClientConfig,
ClientToServerEvents,
@ -15,6 +16,12 @@ import { Logger } from "./Logger";
import { Redis } from "./redis";
import { User } from "../models/User";
// maybe move to a constants file?
const commitHash = child
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
/**
* get socket.io server config, generated from environment vars
*/
@ -55,6 +62,7 @@ prisma.paletteColor
const getClientConfig = (): ClientConfig => {
return {
version: commitHash,
pallete: {
colors: PALLETE,
pixel_cooldown: PIXEL_TIMEOUT_MS,