diff --git a/packages/client/src/components/App.tsx b/packages/client/src/components/App.tsx
index e162521..4a7d188 100644
--- a/packages/client/src/components/App.tsx
+++ b/packages/client/src/components/App.tsx
@@ -1,22 +1,128 @@
import { Header } from "./Header";
-import { AppContext } from "../contexts/AppContext";
+import { AppContext, useAppContext } from "../contexts/AppContext";
import { CanvasWrapper } from "./CanvasWrapper";
import { TemplateContext } from "../contexts/TemplateContext";
import { SettingsSidebar } from "./Settings/SettingsSidebar";
import { DebugModal } from "./Debug/DebugModal";
import { ToolbarWrapper } from "./Toolbar/ToolbarWrapper";
+import React, { lazy, useEffect } from "react";
+import { ChatContext } from "../contexts/ChatContext";
+
+const Chat = lazy(() => import("./Chat/Chat"));
+
+const DynamicallyLoadChat = () => {
+ const { loadChat } = useAppContext();
+
+ return {loadChat && };
+};
const App = () => {
+ useEffect(() => {
+ // detect auth callback for chat, regardless of it being loaded
+ // callback token expires quickly, so we should exchange it as quick as possible
+ (async () => {
+ const params = new URLSearchParams(window.location.search);
+ if (params.has("loginToken")) {
+ // login button opens a new tab that redirects here
+ // if we're that tab, we should try to close this tab when we're done
+ // should work because this tab is opened by JS
+ const shouldCloseWindow =
+ window.location.pathname.startsWith("/chat_callback");
+
+ // token provided by matrix's /sso/redirect
+ const token = params.get("loginToken")!;
+
+ // immediately remove from url to prevent reloading
+ window.history.replaceState({}, "", "/");
+
+ const loginReq = await fetch(
+ `https://${import.meta.env.VITE_MATRIX_HOST}/_matrix/client/v3/login`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ type: "m.login.token",
+ token,
+ }),
+ }
+ );
+
+ const loginRes = await loginReq.json();
+
+ console.log("[Chat] Matrix login", loginReq.status);
+
+ switch (loginReq.status) {
+ case 200: {
+ // success
+ console.log("[Chat] Logged in successfully", loginRes);
+
+ localStorage.setItem(
+ "matrix.access_token",
+ loginRes.access_token + ""
+ );
+ localStorage.setItem("matrix.device_id", loginRes.device_id + "");
+ localStorage.setItem("matrix.user_id", loginRes.user_id + "");
+
+ if (shouldCloseWindow) {
+ console.log(
+ "[Chat] Path matches autoclose, attempting to close window..."
+ );
+ window.close();
+ alert("You can close this window and return to the other tab :)");
+ } else {
+ console.log(
+ "[Chat] Path doesn't match autoclose, not doing anything"
+ );
+ }
+ break;
+ }
+ case 400:
+ case 403:
+ console.log("[Chat] Matrix login", loginRes);
+ alert(
+ "[Chat] Failed to login\n" +
+ loginRes.errcode +
+ " " +
+ loginRes.error
+ );
+ break;
+ case 429:
+ alert(
+ "[Chat] Failed to login, ratelimited.\nTry again in " +
+ Math.floor(loginRes.retry_after_ms / 1000) +
+ "s\n" +
+ loginRes.errcode +
+ " " +
+ loginRes.error
+ );
+ break;
+ default:
+ alert(
+ "Error " +
+ loginReq.status +
+ " returned when trying to login to chat"
+ );
+ }
+ }
+ })();
+ }, []);
+
return (
-
-
-
-
+
+
+
+
+
-
-
-
+ {/* */}
+
+
+
+
+
);
};
diff --git a/packages/client/src/components/Chat/Chat.tsx b/packages/client/src/components/Chat/Chat.tsx
new file mode 100644
index 0000000..d270935
--- /dev/null
+++ b/packages/client/src/components/Chat/Chat.tsx
@@ -0,0 +1,13 @@
+import { useEffect, useRef, useState } from "react";
+
+const Chat = () => {
+ const ref = useRef(null);
+
+ return (
+
+ chat
+
+ );
+};
+
+export default Chat;
diff --git a/packages/client/src/components/Chat/InnerChatSettings.tsx b/packages/client/src/components/Chat/InnerChatSettings.tsx
new file mode 100644
index 0000000..090f6e4
--- /dev/null
+++ b/packages/client/src/components/Chat/InnerChatSettings.tsx
@@ -0,0 +1,22 @@
+import { Button } from "@nextui-org/react";
+import { useChatContext } from "../../contexts/ChatContext";
+
+const InnerChatSettings = () => {
+ const { user, doLogin, doLogout } = useChatContext();
+
+ return (
+ <>
+ {!user && }
+ {user && (
+ <>
+
+
{user.userId}
+
+
+ >
+ )}
+ >
+ );
+};
+
+export default InnerChatSettings;
diff --git a/packages/client/src/components/Chat/OpenChatButton.tsx b/packages/client/src/components/Chat/OpenChatButton.tsx
new file mode 100644
index 0000000..0d41b76
--- /dev/null
+++ b/packages/client/src/components/Chat/OpenChatButton.tsx
@@ -0,0 +1,19 @@
+import { Badge, Button } from "@nextui-org/react";
+import { useChatContext } from "../../contexts/ChatContext";
+
+const OpenChatButton = () => {
+ const { notificationCount } = useChatContext();
+
+ return (
+
+
+
+ );
+};
+
+export default OpenChatButton;
diff --git a/packages/client/src/components/Header.tsx b/packages/client/src/components/Header.tsx
index 2f2788e..aed29a4 100644
--- a/packages/client/src/components/Header.tsx
+++ b/packages/client/src/components/Header.tsx
@@ -2,6 +2,15 @@ import { Button } from "@nextui-org/react";
import { useAppContext } from "../contexts/AppContext";
import { User } from "./Header/User";
import { Debug } from "@sc07-canvas/lib/src/debug";
+import React, { lazy } from "react";
+
+const OpenChatButton = lazy(() => import("./Chat/OpenChatButton"));
+
+const DynamicChat = () => {
+ const { loadChat } = useAppContext();
+
+ return {loadChat && };
+};
export const Header = () => {
const { setSettingsSidebar } = useAppContext();
@@ -14,6 +23,7 @@ export const Header = () => {
+
);
diff --git a/packages/client/src/components/Settings/ChatSettings.tsx b/packages/client/src/components/Settings/ChatSettings.tsx
new file mode 100644
index 0000000..da65f53
--- /dev/null
+++ b/packages/client/src/components/Settings/ChatSettings.tsx
@@ -0,0 +1,21 @@
+import { Switch } from "@nextui-org/react";
+import { useAppContext } from "../../contexts/AppContext";
+import React, { lazy } from "react";
+
+const InnerChatSettings = lazy(() => import("../Chat/InnerChatSettings"));
+
+export const ChatSettings = () => {
+ const { loadChat, setLoadChat } = useAppContext();
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/packages/client/src/components/Settings/SettingsSidebar.tsx b/packages/client/src/components/Settings/SettingsSidebar.tsx
index 09cdf3a..adbfd76 100644
--- a/packages/client/src/components/Settings/SettingsSidebar.tsx
+++ b/packages/client/src/components/Settings/SettingsSidebar.tsx
@@ -3,6 +3,7 @@ import { useAppContext } from "../../contexts/AppContext";
import { TemplateSettings } from "./TemplateSettings";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
+import { ChatSettings } from "./ChatSettings";
export const SettingsSidebar = () => {
const { settingsSidebar, setSettingsSidebar } = useAppContext();
@@ -21,6 +22,7 @@ export const SettingsSidebar = () => {
+
);
};
diff --git a/packages/client/src/contexts/AppContext.tsx b/packages/client/src/contexts/AppContext.tsx
index 1f4b590..e14b9c5 100644
--- a/packages/client/src/contexts/AppContext.tsx
+++ b/packages/client/src/contexts/AppContext.tsx
@@ -24,6 +24,9 @@ export const AppContext = ({ children }: PropsWithChildren) => {
const [canvasPosition, setCanvasPosition] = useState();
const [cursorPosition, setCursorPosition] = useState();
+ // --- settings ---
+ const [loadChat, _setLoadChat] = useState(false);
+
const [pixels, setPixels] = useState({ available: 0 });
const [undo, setUndo] = useState<{ available: true; expireAt: number }>();
@@ -31,6 +34,10 @@ export const AppContext = ({ children }: PropsWithChildren) => {
const [settingsSidebar, setSettingsSidebar] = useState(false);
useEffect(() => {
+ function loadSettings() {
+ setLoadChat(localStorage.getItem("matrix.enable") === "true");
+ }
+
function handleConfig(config: ClientConfig) {
console.info("Server sent config", config);
setConfig(config);
@@ -62,6 +69,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
Network.socket.connect();
+ loadSettings();
+
return () => {
Network.off("user", handleUser);
Network.off("config", handleConfig);
@@ -69,6 +78,11 @@ export const AppContext = ({ children }: PropsWithChildren) => {
};
}, []);
+ const setLoadChat = (v: boolean) => {
+ _setLoadChat(v);
+ localStorage.setItem("matrix.enable", v ? "true" : "false");
+ };
+
return (
{
settingsSidebar,
setSettingsSidebar,
undo,
+ loadChat,
+ setLoadChat,
}}
>
{config ? children : "Loading..."}
diff --git a/packages/client/src/contexts/ChatContext.tsx b/packages/client/src/contexts/ChatContext.tsx
new file mode 100644
index 0000000..9e5adcf
--- /dev/null
+++ b/packages/client/src/contexts/ChatContext.tsx
@@ -0,0 +1,124 @@
+import {
+ PropsWithChildren,
+ createContext,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+
+interface IMatrixUser {
+ userId: string;
+}
+
+export interface IChatContext {
+ user?: IMatrixUser;
+ notificationCount: number;
+
+ doLogin: () => void;
+ doLogout: () => Promise;
+}
+
+const chatContext = createContext({} as any);
+
+export const useChatContext = () => useContext(chatContext);
+
+export const ChatContext = ({ children }: PropsWithChildren) => {
+ const checkInterval = useRef>();
+ const checkNotifs = useRef>();
+
+ const [user, setUser] = useState();
+ const [notifs, setNotifs] = useState(0);
+
+ const doLogin = () => {
+ const redirectUrl =
+ window.location.protocol + "//" + window.location.host + "/chat_callback";
+
+ window.addEventListener("focus", handleWindowFocus);
+ checkInterval.current = setInterval(checkForAccessToken, 500);
+
+ window.open(
+ `https://${import.meta.env.VITE_MATRIX_HOST}/_matrix/client/v3/login/sso/redirect?redirectUrl=${encodeURIComponent(redirectUrl)}`,
+ "_blank"
+ );
+ };
+
+ const doLogout = async () => {
+ await fetch(
+ `https://${import.meta.env.VITE_MATRIX_HOST}/_matrix/client/v3/logout`,
+ {
+ method: "POST",
+ headers: {
+ Authorization:
+ "Bearer " + localStorage.getItem("matrix.access_token"),
+ },
+ }
+ );
+
+ localStorage.removeItem("matrix.access_token");
+ localStorage.removeItem("matrix.device_id");
+ localStorage.removeItem("matrix.user_id");
+ setUser(undefined);
+ };
+
+ useEffect(() => {
+ checkForAccessToken();
+ checkNotifs.current = setInterval(checkForNotifs, 1000);
+
+ return () => {
+ window.removeEventListener("focus", handleWindowFocus);
+ if (checkInterval.current) clearInterval(checkInterval.current);
+ if (checkNotifs.current) clearInterval(checkNotifs.current);
+ };
+ }, []);
+
+ const handleWindowFocus = () => {
+ console.log("[Chat] Window has gained focus");
+
+ checkForAccessToken();
+ };
+
+ const checkForAccessToken = () => {
+ const accessToken = localStorage.getItem("matrix.access_token");
+ const deviceId = localStorage.getItem("matrix.device_id");
+ const userId = localStorage.getItem("matrix.user_id");
+
+ if (!accessToken || !deviceId || !userId) return;
+
+ // access token acquired
+
+ window.removeEventListener("focus", handleWindowFocus);
+ if (checkInterval.current) clearInterval(checkInterval.current);
+
+ console.log("[Chat] access token has been acquired");
+ setUser({ userId });
+ };
+
+ const checkForNotifs = async () => {
+ const accessToken = localStorage.getItem("matrix.access_token");
+ if (!accessToken) return;
+
+ const notifReq = await fetch(
+ `https://${import.meta.env.VITE_MATRIX_HOST}/_matrix/client/v3/notifications?limit=10`,
+ {
+ headers: {
+ Authorization: "Bearer " + accessToken,
+ },
+ }
+ );
+
+ const notifRes = await notifReq.json();
+
+ const notificationCount =
+ notifRes?.notifications?.filter((n: any) => !n.read).length || 0;
+ setNotifs(notificationCount);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/client/src/vite-env.d.ts b/packages/client/src/vite-env.d.ts
index 58fa559..ada34ba 100644
--- a/packages/client/src/vite-env.d.ts
+++ b/packages/client/src/vite-env.d.ts
@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_API_HOST: string;
+ readonly VITE_MATRIX_HOST: string;
}
interface ImportMeta {