From 78d97b52e3ff43f52fe7d8cfaaccb698a984676c Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 19 Jun 2024 15:38:53 -0600 Subject: [PATCH] add mod menu & ignore place limit (fixes #14) --- packages/client/src/components/App.tsx | 2 + .../src/components/Moderation/ModModal.tsx | 64 +++++++++++++++++++ packages/client/src/contexts/AppContext.tsx | 7 +- packages/client/src/lib/canvas.ts | 24 +++++-- packages/client/src/lib/keybinds.ts | 5 ++ packages/lib/src/net.ts | 1 + packages/server/src/lib/SocketServer.ts | 10 ++- packages/server/src/models/User.ts | 2 + 8 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 packages/client/src/components/Moderation/ModModal.tsx 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 {