diff --git a/packages/client/src/components/App.tsx b/packages/client/src/components/App.tsx
index 52e0849..263032e 100644
--- a/packages/client/src/components/App.tsx
+++ b/packages/client/src/components/App.tsx
@@ -17,6 +17,7 @@ import { KeybindModal } from "./KeybindModal";
import { ProfileModal } from "./Profile/ProfileModal";
import { WelcomeModal } from "./Welcome/WelcomeModal";
import { InfoSidebar } from "./Info/InfoSidebar";
+import { ModModal } from "./Moderation/ModModal";
const Chat = lazy(() => import("./Chat/Chat"));
@@ -148,6 +149,7 @@ const AppInner = () => {
+
>
diff --git a/packages/client/src/components/Moderation/ModModal.tsx b/packages/client/src/components/Moderation/ModModal.tsx
new file mode 100644
index 0000000..241d4c9
--- /dev/null
+++ b/packages/client/src/components/Moderation/ModModal.tsx
@@ -0,0 +1,64 @@
+import {
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ Switch,
+} from "@nextui-org/react";
+import { useAppContext } from "../../contexts/AppContext";
+import { useCallback, useEffect, useState } from "react";
+import { KeybindManager } from "../../lib/keybinds";
+import { Canvas } from "../../lib/canvas";
+
+export const ModModal = () => {
+ const { showModModal, setShowModModal, hasAdmin } = useAppContext();
+ const [bypassCooldown, setBypassCooldown_] = useState(false);
+
+ useEffect(() => {
+ setBypassCooldown_(Canvas.instance?.getCooldownBypass() || false);
+
+ const handleKeybind = () => {
+ if (!hasAdmin) {
+ console.warn("Unable to open mod menu; hasAdmin is not set");
+ return;
+ }
+
+ setShowModModal((m) => !m);
+ };
+
+ KeybindManager.on("TOGGLE_MOD_MENU", handleKeybind);
+
+ return () => {
+ KeybindManager.off("TOGGLE_MOD_MENU", handleKeybind);
+ };
+ }, []);
+
+ const setBypassCooldown = useCallback(
+ (value: boolean) => {
+ setBypassCooldown_(value);
+ Canvas.instance?.setCooldownBypass(value);
+ },
+ [setBypassCooldown_]
+ );
+
+ return (
+
+
+ {(onClose) => (
+ <>
+ Mod Menu
+
+
+ Bypass placement cooldown
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/packages/client/src/contexts/AppContext.tsx b/packages/client/src/contexts/AppContext.tsx
index 0af5145..ebc03c0 100644
--- a/packages/client/src/contexts/AppContext.tsx
+++ b/packages/client/src/contexts/AppContext.tsx
@@ -1,4 +1,4 @@
-import {
+import React, {
PropsWithChildren,
createContext,
useContext,
@@ -43,6 +43,8 @@ interface IAppContext {
setProfile: (v?: string) => void;
hasAdmin: boolean;
+ showModModal: boolean;
+ setShowModModal: React.Dispatch>;
}
interface ICanvasPosition {
@@ -119,6 +121,7 @@ export const AppContext = ({ children }: PropsWithChildren) => {
const [profile, setProfile] = useState();
const [hasAdmin, setHasAdmin] = useState(false);
+ const [showModModal, setShowModModal] = useState(false);
useEffect(() => {
function loadSettings() {
@@ -224,6 +227,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
setProfile,
infoSidebar,
setInfoSidebar,
+ showModModal,
+ setShowModModal,
}}
>
{!config && (
diff --git a/packages/client/src/lib/canvas.ts b/packages/client/src/lib/canvas.ts
index 7f44fa6..c34a6a5 100644
--- a/packages/client/src/lib/canvas.ts
+++ b/packages/client/src/lib/canvas.ts
@@ -39,6 +39,8 @@ export class Canvas extends EventEmitter {
} = {};
lastPlace: number | undefined;
+ private bypassCooldown = false;
+
constructor(canvas: HTMLCanvasElement, PanZoom: PanZoom) {
super();
Canvas.instance = this;
@@ -99,6 +101,14 @@ export class Canvas extends EventEmitter {
return this.PanZoom;
}
+ setCooldownBypass(value: boolean) {
+ this.bypassCooldown = value;
+ }
+
+ getCooldownBypass() {
+ return this.bypassCooldown;
+ }
+
getAllPixels() {
let pixels: {
x: number;
@@ -252,11 +262,15 @@ export class Canvas extends EventEmitter {
// }
Network.socket
- .emitWithAck("place", {
- x,
- y,
- color: this.Pallete.getSelectedColor()!.id,
- })
+ .emitWithAck(
+ "place",
+ {
+ x,
+ y,
+ color: this.Pallete.getSelectedColor()!.id,
+ },
+ this.bypassCooldown
+ )
.then((ack) => {
if (ack.success) {
this.lastPlace = Date.now();
diff --git a/packages/client/src/lib/keybinds.ts b/packages/client/src/lib/keybinds.ts
index 2c34a6f..538ea80 100644
--- a/packages/client/src/lib/keybinds.ts
+++ b/packages/client/src/lib/keybinds.ts
@@ -49,6 +49,11 @@ const KEYBINDS = enforceObjectType({
key: "KeyH",
},
],
+ TOGGLE_MOD_MENU: [
+ {
+ key: "KeyM",
+ },
+ ],
});
class KeybindManager_ extends EventEmitter<{
diff --git a/packages/lib/src/net.ts b/packages/lib/src/net.ts
index 94070df..5782785 100644
--- a/packages/lib/src/net.ts
+++ b/packages/lib/src/net.ts
@@ -27,6 +27,7 @@ export interface ServerToClientEvents {
export interface ClientToServerEvents {
place: (
pixel: Pixel,
+ bypassCooldown: boolean,
ack: (
_: PacketAck<
Pixel,
diff --git a/packages/server/src/lib/SocketServer.ts b/packages/server/src/lib/SocketServer.ts
index 8202f57..c74ec15 100644
--- a/packages/server/src/lib/SocketServer.ts
+++ b/packages/server/src/lib/SocketServer.ts
@@ -193,7 +193,7 @@ export class SocketServer {
});
});
- socket.on("place", async (pixel, ack) => {
+ socket.on("place", async (pixel, bypassCooldown, ack) => {
if (!user) {
ack({ success: false, error: "no_user" });
return;
@@ -212,7 +212,13 @@ export class SocketServer {
// force a user data update
await user.update(true);
- if (user.pixelStack < 1) {
+ if (bypassCooldown && !user.isModerator) {
+ // only moderators can do this
+ ack({ success: false, error: "invalid_pixel" });
+ return;
+ }
+
+ if (!bypassCooldown && user.pixelStack < 1) {
ack({ success: false, error: "pixel_cooldown" });
return;
}
diff --git a/packages/server/src/models/User.ts b/packages/server/src/models/User.ts
index 0fb3de4..643bada 100644
--- a/packages/server/src/models/User.ts
+++ b/packages/server/src/models/User.ts
@@ -60,6 +60,8 @@ export class User {
this.lastPixelTime = userData.lastPixelTime;
this.pixelStack = userData.pixelStack;
this.undoExpires = userData.undoExpires || undefined;
+ this.isAdmin = userData.isAdmin;
+ this.isModerator = userData.isModerator;
}
async modifyStack(modifyBy: number): Promise {