diff --git a/README.md b/README.md index 46af2ef8fb8459fd9609fc7e9302f1c59ab86d35..46d72efe27b536bedc504479d399b4cd7d9c8aac 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ - + +Canvas Logo # Canvas +
[![Support](https://img.shields.io/badge/support-donate-healthiness)](https://tips.sc07.com) @@ -24,8 +26,8 @@ Inspired by Reddit's r/Place event from 2017, Canvas is a collaborative pixel ca ## Screenshots - - +![Screenshot of Canvas 2025 zoomed out](./_readme/canvas2025.full.png){width=500} +![Screenshot of Canvas 2025 zoomed in to the sc07 logo](./_readme/canvas2025.zoom.png){width=500} ## Roadmap @@ -68,8 +70,14 @@ Details on contributing can be found in the [CONTRIBUTING.md file](./CONTRIBUTIN Canvas is licened under the MIT license see `LICENSE` +Artwork, images, emotes, logos, and other assets may be subject to separate licenses. + +See the license files within their respective directories. + --- +
- + +
diff --git a/eslint.config.mjs b/eslint.config.mjs index 33086d094f7c97d7fc4a4e946dcf03a2ae082696..8343d1e58b2692e233f2b1d8ea58de8c06372f97 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -8,8 +8,8 @@ export default defineConfig( globalIgnores([ "**/node_modules", "packages/**/dist", - "packages/**/*", - "!packages/server/**/*", + "packages/admin", + "packages/chat", ]), // -- global config -- @@ -28,8 +28,13 @@ export default defineConfig( // -- client -- { - files: ["packages/client/src"], + files: ["packages/client/src/**/*.{ts,tsx,js,jsx}"], plugins: { react }, + settings: { + react: { + version: "detect", + }, + }, rules: { "no-unused-vars": "off", "@typescript-eslint/no-namespace": "off", @@ -46,12 +51,20 @@ export default defineConfig( ignoreRestSiblings: true, }, ], + + "react/function-component-definition": [ + "error", + { + namedComponents: "arrow-function", + unnamedComponents: "arrow-function", + }, + ], }, }, // -- server -- { - files: ["packages/server/src"], + files: ["packages/server/src/**/*.{ts,js}"], rules: { "no-console": "error", "no-unused-vars": "off", diff --git a/package.json b/package.json index db5eb011229404f8a89975aed8330e900e19ea05..11f1867b2e094c7b66475ad233348a22a795814d 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "eslint": "^9.39.2", "eslint-config-react-app": "^7.0.1", "eslint-formatter-gitlab": "^7.0.1", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-simple-import-sort": "^12.1.1", "jest-fetch-mock": "^3.0.3", diff --git a/packages/client/src/Moderator/ModCanvasOverlay.tsx b/packages/client/src/Moderator/ModCanvasOverlay.tsx index 77bb6806ef43560cbf0596cfacb19b5d3e3bd6f5..7714fa2ffc8e664c409c6e071bd689ec8537245c 100644 --- a/packages/client/src/Moderator/ModCanvasOverlay.tsx +++ b/packages/client/src/Moderator/ModCanvasOverlay.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; -import { KeybindManager } from "../lib/keybinds"; + import { CanvasCore } from "../lib/canvas"; +import { KeybindManager } from "../lib/keybinds"; import { useModerator } from "./Moderator"; export const ModCanvasOverlay = () => { diff --git a/packages/client/src/Moderator/ModSidebar.tsx b/packages/client/src/Moderator/ModSidebar.tsx index 2cc65af695bcaa7c76a78d62f1fae5bf36056d02..6ecc316ffdf796845b1b341e98fdee7c781414e8 100644 --- a/packages/client/src/Moderator/ModSidebar.tsx +++ b/packages/client/src/Moderator/ModSidebar.tsx @@ -1,15 +1,16 @@ -import { Hammer } from "lucide-react"; -import { SidebarBase } from "../components/SidebarBase"; -import { useModerator } from "./Moderator"; import { Alert, Chip } from "@heroui/react"; -import { KeybindManager } from "../lib/keybinds"; -import { Keybind } from "../components/KeybindModal"; -import { useCallback, useState } from "react"; -import { handleError, oapi } from "../lib/utils"; import { EError, type RestAPI } from "@sc07-canvas/lib"; -import { UserCard } from "../components/Profile/UserCard"; +import { Hammer } from "lucide-react"; +import { useCallback, useState } from "react"; import { toast } from "react-toastify"; + import { Button } from "../components/core/Button"; +import { Keybind } from "../components/KeybindModal"; +import { UserCard } from "../components/Profile/UserCard"; +import { SidebarBase } from "../components/SidebarBase"; +import { KeybindManager } from "../lib/keybinds"; +import { handleError, oapi } from "../lib/utils"; +import { useModerator } from "./Moderator"; export const ModSidebar = () => { const { state, dispatch } = useModerator(); diff --git a/packages/client/src/Moderator/Moderator.tsx b/packages/client/src/Moderator/Moderator.tsx index c0495d78072370e9c5ce5c5ffc219533dd31d318..d0a9215d0edcf3546bcac73e34a674d5579d72a2 100644 --- a/packages/client/src/Moderator/Moderator.tsx +++ b/packages/client/src/Moderator/Moderator.tsx @@ -1,9 +1,18 @@ -import React, { createContext, useContext, useEffect, useReducer } from "react"; -import { UserModSidebar } from "./UserModSidebar"; -import { KeybindManager } from "../lib/keybinds"; -import { ModSidebar } from "./ModSidebar"; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useLayoutEffect, + useReducer, +} from "react"; + +import { usePanel } from "../contexts/PanelContext"; import { useHasRole } from "../hooks/useHasRole"; +import { KeybindManager } from "../lib/keybinds"; import { ModeratorModule } from "./ModeratorModule"; +import { ModSidebar } from "./ModSidebar"; +import { UserModSidebar } from "./UserModSidebar"; ModeratorModule.get(); // initialize @@ -40,18 +49,21 @@ export const useModerator = () => useContext(context); export const ModeratorContext = ({ children }: React.PropsWithChildren) => { const isMod = useHasRole("MOD"); + const Panel = usePanel(); - const [state, dispatch] = useReducer( + const [state, reducerDispatch] = useReducer( (state, data) => { switch (data.action) { case "INSPECT_USER": return { ...state, inspectUser: data.user, + showMenu: data.user ? false : state.showMenu, }; case "OPEN_MENU": return { ...state, + inspectUser: undefined, showMenu: true, }; case "CLOSE_MENU": @@ -105,6 +117,29 @@ export const ModeratorContext = ({ children }: React.PropsWithChildren) => { } ); + const dispatch = useCallback( + (action: ModerationActions) => { + if ( + action.action === "OPEN_MENU" || + (action.action === "INSPECT_USER" && action.user) + ) { + Panel.dispatch(["info", undefined]); + } + reducerDispatch(action); + }, + [Panel.dispatch], + ); + + useLayoutEffect(() => { + if (!Panel.state.info) return; + + if (state.showMenu) { + reducerDispatch({ action: "CLOSE_MENU" }); + } else if (state.inspectUser) { + reducerDispatch({ action: "INSPECT_USER" }); + } + }, [Panel.state.info, state.inspectUser, state.showMenu]); + useEffect(() => { if (isMod) { ModeratorModule.get().connect(); diff --git a/packages/client/src/Moderator/UserModSidebar.tsx b/packages/client/src/Moderator/UserModSidebar.tsx index da3f027e3d855186c7792354d00adda18de49ddb..d171febdd57d00f535285ee1ae6e7f50b6c8ff46 100644 --- a/packages/client/src/Moderator/UserModSidebar.tsx +++ b/packages/client/src/Moderator/UserModSidebar.tsx @@ -1,9 +1,3 @@ -import { Hammer } from "lucide-react"; -import { SidebarBase } from "../components/SidebarBase"; -import { ModeratorUtils, useModerator } from "./Moderator"; -import { useCallback, useEffect, useState } from "react"; -import { handleError, oapi, useQuery } from "../lib/utils"; -import { UserCard } from "../components/Profile/UserCard"; import { Accordion, AccordionItem, @@ -13,10 +7,17 @@ import { Link, } from "@heroui/react"; import { CalendarDateTime, parseDateTime } from "@internationalized/date"; -import { toast } from "react-toastify"; import { EError } from "@sc07-canvas/lib"; -import { ModeratorModule } from "./ModeratorModule"; +import { Hammer } from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import { toast } from "react-toastify"; + import { Button } from "../components/core/Button"; +import { UserCard } from "../components/Profile/UserCard"; +import { SidebarBase } from "../components/SidebarBase"; +import { handleError, oapi, useQuery } from "../lib/utils"; +import { ModeratorUtils, useModerator } from "./Moderator"; +import { ModeratorModule } from "./ModeratorModule"; export const UserModSidebar = () => { const { state, dispatch } = useModerator(); diff --git a/packages/client/src/assets/reactions/LICENSE.txt b/packages/client/src/assets/reactions/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..66de5c5d2c1edec69ef896d813320cc09e352c67 --- /dev/null +++ b/packages/client/src/assets/reactions/LICENSE.txt @@ -0,0 +1,9 @@ +Copyright © 2026 Rincs. + +These emote assets are not licensed under the MIT License. + +They are included in this repository with permission from the copyright holder +for use in this project only. + +No permission is granted to copy, modify, redistribute, or use these assets +outside of this project without permission from the copyright holder. \ No newline at end of file diff --git a/packages/client/src/assets/reactions/awe.png b/packages/client/src/assets/reactions/awe.png new file mode 100644 index 0000000000000000000000000000000000000000..e7d7db5cfb419d7b9641ed272bd9460ee217dc60 Binary files /dev/null and b/packages/client/src/assets/reactions/awe.png differ diff --git a/packages/client/src/assets/reactions/bongo.gif b/packages/client/src/assets/reactions/bongo.gif new file mode 100644 index 0000000000000000000000000000000000000000..7341737a15d0915e382ba0fe628001fc28b7f209 Binary files /dev/null and b/packages/client/src/assets/reactions/bongo.gif differ diff --git a/packages/client/src/assets/reactions/cry.png b/packages/client/src/assets/reactions/cry.png new file mode 100644 index 0000000000000000000000000000000000000000..a2237772115d9abeee763ca8ffbbaf3fda1ea244 Binary files /dev/null and b/packages/client/src/assets/reactions/cry.png differ diff --git a/packages/client/src/assets/reactions/cute.png b/packages/client/src/assets/reactions/cute.png new file mode 100644 index 0000000000000000000000000000000000000000..1fa1bc1e3c232edd6eddb8e979d4d7a1830fa73b Binary files /dev/null and b/packages/client/src/assets/reactions/cute.png differ diff --git a/packages/client/src/assets/reactions/jam.gif b/packages/client/src/assets/reactions/jam.gif new file mode 100644 index 0000000000000000000000000000000000000000..309ac28454cc7c5d1898a919d2c8ac0ef6435480 Binary files /dev/null and b/packages/client/src/assets/reactions/jam.gif differ diff --git a/packages/client/src/assets/reactions/love.png b/packages/client/src/assets/reactions/love.png new file mode 100644 index 0000000000000000000000000000000000000000..0282ba9e1d572dbfabe259807e1d2cc3ff596610 Binary files /dev/null and b/packages/client/src/assets/reactions/love.png differ diff --git a/packages/client/src/assets/reactions/pog.png b/packages/client/src/assets/reactions/pog.png new file mode 100644 index 0000000000000000000000000000000000000000..c081c2b561319c958d2d9f5b3bdc2169a6b2f33a Binary files /dev/null and b/packages/client/src/assets/reactions/pog.png differ diff --git a/packages/client/src/assets/reactions/shocked.png b/packages/client/src/assets/reactions/shocked.png new file mode 100644 index 0000000000000000000000000000000000000000..9e0f059084b49e2e54317423157c2adf70cc6c33 Binary files /dev/null and b/packages/client/src/assets/reactions/shocked.png differ diff --git a/packages/client/src/assets/reactions/work.png b/packages/client/src/assets/reactions/work.png new file mode 100644 index 0000000000000000000000000000000000000000..3ee567073b050a6d797a0cf8eed2fe48f2723b93 Binary files /dev/null and b/packages/client/src/assets/reactions/work.png differ diff --git a/packages/client/src/assets/reactions/yay.png b/packages/client/src/assets/reactions/yay.png new file mode 100644 index 0000000000000000000000000000000000000000..51b5628ae977ce00077c1a46ac4fc6f77d49aa12 Binary files /dev/null and b/packages/client/src/assets/reactions/yay.png differ diff --git a/packages/client/src/components/App.tsx b/packages/client/src/components/App.tsx index dc0769cab44827b9c56f5e259dc76b154a44293f..178382f1c5cc74bb1072b1adf75fcd79011cf2cf 100644 --- a/packages/client/src/components/App.tsx +++ b/packages/client/src/components/App.tsx @@ -1,32 +1,34 @@ -import { Header } from "./Header/Header"; +import "react-toastify/dist/ReactToastify.css"; + import { useEffect, useState } from "react"; -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 { SWRConfig } from "swr"; -import "react-toastify/dist/ReactToastify.css"; -import { AuthErrors } from "./AuthErrors"; +import AudioContext from "@/contexts/AudioContext"; +import { CaptureContext } from "@/contexts/CaptureContext"; +import PanelContext, { usePanel } from "@/contexts/PanelContext"; + +import { AppContext, useAppContext } from "../contexts/AppContext"; +import { DialogContext } from "../contexts/DialogContext"; +import { TemplateContext } from "../contexts/TemplateContext"; import { KeybindManager } from "../lib/keybinds"; -import { PixelWhoisSidebar } from "./PixelSidebar/PixelWhoisSidebar"; -import { KeybindModal } from "./KeybindModal"; -import { ProfileModal } from "./Profile/ProfileModal"; -import { InfoSidebar } from "./Info/InfoSidebar"; -import { DynamicModals } from "./DynamicModals"; -import { ToastWrapper } from "./ToastWrapper"; import { Moderator, ModeratorContext } from "../Moderator/Moderator"; -import { DialogContext } from "../contexts/DialogContext"; +import { AuthErrors } from "./AuthErrors"; +import { CanvasWrapper } from "./CanvasWrapper"; import { ChatInfoDialog } from "./Chat/ChatInfoDialog"; -import { LoginModal } from "./Login/LoginModal"; import { Chat, ChatContext } from "./Chat/utils"; -import { EventInfoModal } from "./EventInfo/EventInfo"; -import AudioContext from "@/contexts/AudioContext"; -import PanelContext, { usePanel } from "@/contexts/PanelContext"; -import { CaptureContext } from "@/contexts/CaptureContext"; import { ColorPickFeedback } from "./ColorPickFeedback"; +import { DebugModal } from "./Debug/DebugModal"; +import { DynamicModals } from "./DynamicModals"; +import { EventInfoModal } from "./EventInfo/EventInfo"; +import { Header } from "./Header/Header"; +import { InfoSidebar } from "./Info/InfoSidebar"; +import { KeybindModal } from "./KeybindModal"; +import { LoginModal } from "./Login/LoginModal"; +import { PixelWhoisSidebar } from "./PixelSidebar/PixelWhoisSidebar"; +import { ProfileModal } from "./Profile/ProfileModal"; +import { SettingsSidebar } from "./Settings/SettingsSidebar"; +import { ToastWrapper } from "./ToastWrapper"; +import { ToolbarWrapper } from "./Toolbar/ToolbarWrapper"; console.log("Client init with version " + __COMMIT_HASH__); diff --git a/packages/client/src/components/AuthErrors.tsx b/packages/client/src/components/AuthErrors.tsx index 6b997a91869a2d8dc2d1085d8e4ec0f12f8df6fe..b8aaf601799bd409e2fccbdc16b324683b46d43d 100644 --- a/packages/client/src/components/AuthErrors.tsx +++ b/packages/client/src/components/AuthErrors.tsx @@ -7,6 +7,7 @@ import { ModalHeader, } from "@heroui/react"; import { useEffect, useState } from "react"; + import { Button } from "./core/Button"; const Params = { diff --git a/packages/client/src/components/CanvasWrapper.tsx b/packages/client/src/components/CanvasWrapper.tsx index c4247d99f8f7da73ab767eda188995799363f88e..b231cc708933604d3acb2262edaece503b3f9217 100644 --- a/packages/client/src/components/CanvasWrapper.tsx +++ b/packages/client/src/components/CanvasWrapper.tsx @@ -1,3 +1,11 @@ +import { type IPosition } from "@sc07-canvas/lib/net"; +import { + MOUSE_LONG_PRESS_DELAY, + PanZoomWrapper, + RendererContext, + type ViewportMoveEvent, +} from "@sc07-canvas/lib/renderer"; +import throttle from "lodash.throttle"; import { type CSSProperties, useCallback, @@ -6,41 +14,39 @@ import { useRef, useState, } from "react"; -import { CanvasCore } from "../lib/canvas"; + +import { useCaptureContext } from "@/contexts/CaptureContext"; +import { usePanel } from "@/contexts/PanelContext"; + import { useAppContext } from "../contexts/AppContext"; -import { - MOUSE_LONG_PRESS_DELAY, - PanZoomWrapper, - RendererContext, - type ViewportMoveEvent, -} from "@sc07-canvas/lib/renderer"; -import throttle from "lodash.throttle"; -import { type IPosition } from "@sc07-canvas/lib/net"; -import { Template } from "./Templating/Template"; -import { Template as TemplateCl } from "../lib/template"; -import { type IRouterData, Router } from "../lib/router"; -import { KeybindManager } from "../lib/keybinds"; -import { HeatmapOverlay } from "./Overlay/HeatmapOverlay"; import { useTemplateContext } from "../contexts/TemplateContext"; -import { PixelPulses } from "./Overlay/PixelPulses"; -import { CanvasUtils } from "../lib/canvas.utils"; -import { ModCanvasOverlay } from "../Moderator/ModCanvasOverlay"; import { useHasRole } from "../hooks/useHasRole"; -import { GridOverlay } from "./Overlay/GridOverlay"; +import { useIsMobile } from "../hooks/useIsMobile"; +import { CanvasCore } from "../lib/canvas"; +import { CanvasUtils } from "../lib/canvas.utils"; +import { KeybindManager } from "../lib/keybinds"; import { PaletteLib } from "../lib/palette"; +import { type IRouterData, Router } from "../lib/router"; +import { Template as TemplateCl } from "../lib/template"; import { getRenderer } from "../lib/utils"; -import { usePanel } from "@/contexts/PanelContext"; -import { useCaptureContext } from "@/contexts/CaptureContext"; +import { ModCanvasOverlay } from "../Moderator/ModCanvasOverlay"; import { showColorPickFeedback, showPixelPlacedFeedback, } from "./ColorPickFeedback"; +import { CursorPresenceLayer } from "./CursorPresenceLayer"; +import { GridOverlay } from "./Overlay/GridOverlay"; +import { HeatmapOverlay } from "./Overlay/HeatmapOverlay"; +import { PixelPulses } from "./Overlay/PixelPulses"; +import { Template } from "./Templating/Template"; const KEYBOARD_ZOOM_STEP = 1; const KEYBOARD_ZOOM_SMALL_STEP = 0.35; +const INSPECT_END_ANIMATION_DURATION = 180; export const CanvasWrapper = () => { const hasMod = useHasRole("MOD"); + const isMobile = useIsMobile(); const { config } = useAppContext(); const getInitialPosition = useCallback< @@ -88,63 +94,158 @@ export const CanvasWrapper = () => { {config &&