chat initial (related #6)
This commit is contained in:
parent
5ba2eb0888
commit
732feacd5b
|
@ -1,22 +1,128 @@
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
import { AppContext } from "../contexts/AppContext";
|
import { AppContext, useAppContext } from "../contexts/AppContext";
|
||||||
import { CanvasWrapper } from "./CanvasWrapper";
|
import { CanvasWrapper } from "./CanvasWrapper";
|
||||||
import { TemplateContext } from "../contexts/TemplateContext";
|
import { TemplateContext } from "../contexts/TemplateContext";
|
||||||
import { SettingsSidebar } from "./Settings/SettingsSidebar";
|
import { SettingsSidebar } from "./Settings/SettingsSidebar";
|
||||||
import { DebugModal } from "./Debug/DebugModal";
|
import { DebugModal } from "./Debug/DebugModal";
|
||||||
import { ToolbarWrapper } from "./Toolbar/ToolbarWrapper";
|
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 = () => {
|
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 (
|
return (
|
||||||
<AppContext>
|
<AppContext>
|
||||||
<TemplateContext>
|
<ChatContext>
|
||||||
<Header />
|
<TemplateContext>
|
||||||
<CanvasWrapper />
|
<Header />
|
||||||
<ToolbarWrapper />
|
<CanvasWrapper />
|
||||||
|
<ToolbarWrapper />
|
||||||
|
|
||||||
<DebugModal />
|
{/* <DynamicallyLoadChat /> */}
|
||||||
<SettingsSidebar />
|
|
||||||
</TemplateContext>
|
<DebugModal />
|
||||||
|
<SettingsSidebar />
|
||||||
|
</TemplateContext>
|
||||||
|
</ChatContext>
|
||||||
</AppContext>
|
</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 { useAppContext } from "../contexts/AppContext";
|
||||||
import { User } from "./Header/User";
|
import { User } from "./Header/User";
|
||||||
import { Debug } from "@sc07-canvas/lib/src/debug";
|
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 = () => {
|
export const Header = () => {
|
||||||
const { setSettingsSidebar } = useAppContext();
|
const { setSettingsSidebar } = useAppContext();
|
||||||
|
@ -14,6 +23,7 @@ export const Header = () => {
|
||||||
<User />
|
<User />
|
||||||
<Button onClick={() => setSettingsSidebar(true)}>Settings</Button>
|
<Button onClick={() => setSettingsSidebar(true)}>Settings</Button>
|
||||||
<Button onClick={() => Debug.openDebugTools()}>debug</Button>
|
<Button onClick={() => Debug.openDebugTools()}>debug</Button>
|
||||||
|
<DynamicChat />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 { TemplateSettings } from "./TemplateSettings";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
|
import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
|
||||||
|
import { ChatSettings } from "./ChatSettings";
|
||||||
|
|
||||||
export const SettingsSidebar = () => {
|
export const SettingsSidebar = () => {
|
||||||
const { settingsSidebar, setSettingsSidebar } = useAppContext();
|
const { settingsSidebar, setSettingsSidebar } = useAppContext();
|
||||||
|
@ -21,6 +22,7 @@ export const SettingsSidebar = () => {
|
||||||
</header>
|
</header>
|
||||||
<section>abc</section>
|
<section>abc</section>
|
||||||
<TemplateSettings />
|
<TemplateSettings />
|
||||||
|
<ChatSettings />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,9 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
||||||
const [canvasPosition, setCanvasPosition] = useState<ICanvasPosition>();
|
const [canvasPosition, setCanvasPosition] = useState<ICanvasPosition>();
|
||||||
const [cursorPosition, setCursorPosition] = useState<IPosition>();
|
const [cursorPosition, setCursorPosition] = useState<IPosition>();
|
||||||
|
|
||||||
|
// --- settings ---
|
||||||
|
const [loadChat, _setLoadChat] = useState(false);
|
||||||
|
|
||||||
const [pixels, setPixels] = useState({ available: 0 });
|
const [pixels, setPixels] = useState({ available: 0 });
|
||||||
const [undo, setUndo] = useState<{ available: true; expireAt: number }>();
|
const [undo, setUndo] = useState<{ available: true; expireAt: number }>();
|
||||||
|
|
||||||
|
@ -31,6 +34,10 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
||||||
const [settingsSidebar, setSettingsSidebar] = useState(false);
|
const [settingsSidebar, setSettingsSidebar] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
function loadSettings() {
|
||||||
|
setLoadChat(localStorage.getItem("matrix.enable") === "true");
|
||||||
|
}
|
||||||
|
|
||||||
function handleConfig(config: ClientConfig) {
|
function handleConfig(config: ClientConfig) {
|
||||||
console.info("Server sent config", config);
|
console.info("Server sent config", config);
|
||||||
setConfig(config);
|
setConfig(config);
|
||||||
|
@ -62,6 +69,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
||||||
|
|
||||||
Network.socket.connect();
|
Network.socket.connect();
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Network.off("user", handleUser);
|
Network.off("user", handleUser);
|
||||||
Network.off("config", handleConfig);
|
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 (
|
return (
|
||||||
<appContext.Provider
|
<appContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -82,6 +96,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
||||||
settingsSidebar,
|
settingsSidebar,
|
||||||
setSettingsSidebar,
|
setSettingsSidebar,
|
||||||
undo,
|
undo,
|
||||||
|
loadChat,
|
||||||
|
setLoadChat,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config ? children : "Loading..."}
|
{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 {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_HOST: string;
|
readonly VITE_API_HOST: string;
|
||||||
|
readonly VITE_MATRIX_HOST: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|
Loading…
Reference in New Issue