chat initial (related #6)
This commit is contained in:
parent
5ba2eb0888
commit
732feacd5b
|
@ -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 <React.Suspense>{loadChat && <Chat />}</React.Suspense>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<AppContext>
|
||||
<TemplateContext>
|
||||
<Header />
|
||||
<CanvasWrapper />
|
||||
<ToolbarWrapper />
|
||||
<ChatContext>
|
||||
<TemplateContext>
|
||||
<Header />
|
||||
<CanvasWrapper />
|
||||
<ToolbarWrapper />
|
||||
|
||||
<DebugModal />
|
||||
<SettingsSidebar />
|
||||
</TemplateContext>
|
||||
{/* <DynamicallyLoadChat /> */}
|
||||
|
||||
<DebugModal />
|
||||
<SettingsSidebar />
|
||||
</TemplateContext>
|
||||
</ChatContext>
|
||||
</AppContext>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const Chat = () => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ position: "fixed", top: 0, left: 0, zIndex: 999 }}>
|
||||
chat
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
|
@ -0,0 +1,22 @@
|
|||
import { Button } from "@nextui-org/react";
|
||||
import { useChatContext } from "../../contexts/ChatContext";
|
||||
|
||||
const InnerChatSettings = () => {
|
||||
const { user, doLogin, doLogout } = useChatContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!user && <Button onClick={doLogin}>Login</Button>}
|
||||
{user && (
|
||||
<>
|
||||
<div className="flex gap-1">
|
||||
<div className="flex-grow">{user.userId}</div>
|
||||
<Button onClick={doLogout}>Logout</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InnerChatSettings;
|
|
@ -0,0 +1,19 @@
|
|||
import { Badge, Button } from "@nextui-org/react";
|
||||
import { useChatContext } from "../../contexts/ChatContext";
|
||||
|
||||
const OpenChatButton = () => {
|
||||
const { notificationCount } = useChatContext();
|
||||
|
||||
return (
|
||||
<Badge
|
||||
content={notificationCount}
|
||||
isInvisible={notificationCount === 0}
|
||||
color="danger"
|
||||
size="sm"
|
||||
>
|
||||
<Button>Chat</Button>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenChatButton;
|
|
@ -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 <React.Suspense>{loadChat && <OpenChatButton />}</React.Suspense>;
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
const { setSettingsSidebar } = useAppContext();
|
||||
|
@ -14,6 +23,7 @@ export const Header = () => {
|
|||
<User />
|
||||
<Button onClick={() => setSettingsSidebar(true)}>Settings</Button>
|
||||
<Button onClick={() => Debug.openDebugTools()}>debug</Button>
|
||||
<DynamicChat />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<header>
|
||||
<Switch size="sm" isSelected={loadChat} onValueChange={setLoadChat} />
|
||||
<h2>Chat</h2>
|
||||
</header>
|
||||
<section>
|
||||
<React.Suspense>{loadChat && <InnerChatSettings />}</React.Suspense>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 = () => {
|
|||
</header>
|
||||
<section>abc</section>
|
||||
<TemplateSettings />
|
||||
<ChatSettings />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -24,6 +24,9 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
|||
const [canvasPosition, setCanvasPosition] = useState<ICanvasPosition>();
|
||||
const [cursorPosition, setCursorPosition] = useState<IPosition>();
|
||||
|
||||
// --- 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 (
|
||||
<appContext.Provider
|
||||
value={{
|
||||
|
@ -82,6 +96,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
|||
settingsSidebar,
|
||||
setSettingsSidebar,
|
||||
undo,
|
||||
loadChat,
|
||||
setLoadChat,
|
||||
}}
|
||||
>
|
||||
{config ? children : "Loading..."}
|
||||
|
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
const chatContext = createContext<IChatContext>({} as any);
|
||||
|
||||
export const useChatContext = () => useContext(chatContext);
|
||||
|
||||
export const ChatContext = ({ children }: PropsWithChildren) => {
|
||||
const checkInterval = useRef<ReturnType<typeof setInterval>>();
|
||||
const checkNotifs = useRef<ReturnType<typeof setInterval>>();
|
||||
|
||||
const [user, setUser] = useState<IMatrixUser>();
|
||||
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 (
|
||||
<chatContext.Provider
|
||||
value={{ user, notificationCount: notifs, doLogin, doLogout }}
|
||||
>
|
||||
{children}
|
||||
</chatContext.Provider>
|
||||
);
|
||||
};
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_HOST: string;
|
||||
readonly VITE_MATRIX_HOST: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
|
Loading…
Reference in New Issue