pixel whois, keybinds & fix coords not showing on hover (CanvasMeta) (related #11)
This commit is contained in:
parent
28cadf07ee
commit
6f7aad5da8
|
@ -11,6 +11,8 @@ import { ChatContext } from "../contexts/ChatContext";
|
|||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { AuthErrors } from "./AuthErrors";
|
||||
import "../lib/keybinds";
|
||||
import { PixelWhoisSidebar } from "./PixelWhoisSidebar";
|
||||
|
||||
const Chat = lazy(() => import("./Chat/Chat"));
|
||||
|
||||
|
@ -34,6 +36,7 @@ const AppInner = () => {
|
|||
|
||||
<DebugModal />
|
||||
<SettingsSidebar />
|
||||
<PixelWhoisSidebar />
|
||||
<AuthErrors />
|
||||
|
||||
<ToastContainer position="top-left" />
|
||||
|
|
|
@ -8,6 +8,7 @@ import throttle from "lodash.throttle";
|
|||
import { IPosition } from "@sc07-canvas/lib/src/net";
|
||||
import { Template } from "./Template";
|
||||
import { IRouterData, Router } from "../lib/router";
|
||||
import { KeybindManager } from "../lib/keybinds";
|
||||
|
||||
export const CanvasWrapper = () => {
|
||||
const { config } = useAppContext();
|
||||
|
@ -26,14 +27,64 @@ export const CanvasWrapper = () => {
|
|||
const CanvasInner = () => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>();
|
||||
const canvas = useRef<Canvas>();
|
||||
const { config, setCanvasPosition, setCursorPosition } = useAppContext();
|
||||
const { config, setCanvasPosition, setCursorPosition, setPixelWhois } =
|
||||
useAppContext();
|
||||
const PanZoom = useContext(RendererContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return;
|
||||
canvas.current = new Canvas(canvasRef.current!, PanZoom);
|
||||
|
||||
const handlePixelWhois = ({
|
||||
clientX,
|
||||
clientY,
|
||||
}: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
}) => {
|
||||
if (!canvas.current) {
|
||||
console.warn(
|
||||
"[CanvasWrapper#handlePixelWhois] canvas instance does not exist"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const [x, y] = canvas.current.screenToPos(clientX, clientY);
|
||||
if (x < 0 || y < 0) return; // discard if out of bounds
|
||||
|
||||
// canvas size can dynamically change, so we need to check the current config
|
||||
// we're depending on canvas.instance's config so we don't have to use a react dependency
|
||||
if (canvas.current.hasConfig()) {
|
||||
const {
|
||||
canvas: {
|
||||
size: [width, height],
|
||||
},
|
||||
} = canvas.current.getConfig();
|
||||
|
||||
if (x >= width || y >= height) return; // out of bounds
|
||||
} else {
|
||||
// although this should never happen, log it
|
||||
console.warn(
|
||||
"[CanvasWrapper#handlePixelWhois] canvas config is not available yet"
|
||||
);
|
||||
}
|
||||
|
||||
// .......
|
||||
// .......
|
||||
// .......
|
||||
// ...x...
|
||||
// .......
|
||||
// .......
|
||||
// .......
|
||||
const surrounding = canvas.current.getSurroundingPixels(x, y, 3);
|
||||
|
||||
setPixelWhois({ x, y, surrounding });
|
||||
};
|
||||
|
||||
KeybindManager.on("PIXEL_WHOIS", handlePixelWhois);
|
||||
|
||||
return () => {
|
||||
KeybindManager.off("PIXEL_WHOIS", handlePixelWhois);
|
||||
canvas.current!.destroy();
|
||||
};
|
||||
}, [PanZoom, setCursorPosition]);
|
||||
|
@ -139,6 +190,19 @@ const CanvasInner = () => {
|
|||
);
|
||||
}
|
||||
|
||||
if (canvas.current) {
|
||||
const pos = canvas.current?.panZoomTransformToCanvas();
|
||||
setCanvasPosition({
|
||||
x: pos.canvasX,
|
||||
y: pos.canvasY,
|
||||
zoom: state.scale >> 0,
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
"[CanvasWrapper] handleViewportMove has no canvas instance"
|
||||
);
|
||||
}
|
||||
|
||||
Router.queueUpdate();
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
import { Button, Spinner } from "@nextui-org/react";
|
||||
import { useAppContext } from "../contexts/AppContext";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ComponentPropsWithoutRef, useEffect, useRef, useState } from "react";
|
||||
import { api } from "../lib/utils";
|
||||
import { UserCard } from "./Profile/UserCard";
|
||||
|
||||
interface IPixel {
|
||||
userId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
color: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface IUser {
|
||||
sub: string;
|
||||
display_name?: string;
|
||||
picture_url?: string;
|
||||
profile_url?: string;
|
||||
isAdmin: boolean;
|
||||
isModerator: boolean;
|
||||
}
|
||||
|
||||
interface IInstance {
|
||||
hostname: string;
|
||||
name?: string;
|
||||
logo_url?: string;
|
||||
banner_url?: string;
|
||||
}
|
||||
|
||||
export const PixelWhoisSidebar = () => {
|
||||
const { pixelWhois, setPixelWhois } = useAppContext();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [whois, setWhois] = useState<{
|
||||
pixel: IPixel;
|
||||
otherPixels: number;
|
||||
user: IUser | null;
|
||||
instance: IInstance | null;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!pixelWhois) return;
|
||||
setLoading(true);
|
||||
setWhois(undefined);
|
||||
|
||||
api<
|
||||
{
|
||||
pixel: IPixel;
|
||||
otherPixels: number;
|
||||
user: IUser | null;
|
||||
instance: IInstance | null;
|
||||
},
|
||||
"no_pixel"
|
||||
>(`/api/canvas/pixel/${pixelWhois.x}/${pixelWhois.y}`)
|
||||
.then(({ status, data }) => {
|
||||
if (status === 200) {
|
||||
if (data.success) {
|
||||
setWhois({
|
||||
pixel: data.pixel,
|
||||
otherPixels: data.otherPixels,
|
||||
user: data.user,
|
||||
instance: data.instance,
|
||||
});
|
||||
} else {
|
||||
// error wahhhhhh
|
||||
}
|
||||
} else {
|
||||
// error wahhhh
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [pixelWhois]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sidebar sidebar-right"
|
||||
style={{ ...(pixelWhois ? {} : { display: "none" }) }}
|
||||
>
|
||||
{loading && (
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-black/25 flex justify-center items-center z-50">
|
||||
<div className="flex flex-col bg-white p-5 rounded-lg gap-5">
|
||||
<Spinner />
|
||||
Loading
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<header>
|
||||
<h1>Pixel Whois</h1>
|
||||
<div className="flex-grow"></div>
|
||||
<Button size="sm" isIconOnly onClick={() => setPixelWhois(undefined)}>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</Button>
|
||||
</header>
|
||||
<div className="w-full h-52 bg-gray-200 flex justify-center items-center">
|
||||
<div className="w-[128px] h-[128px] bg-white">
|
||||
<SmallCanvas
|
||||
surrounding={pixelWhois?.surrounding}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<section>{whois?.user && <UserCard user={whois.user} />}</section>
|
||||
<section>
|
||||
<table className="w-full">
|
||||
<tr>
|
||||
<th>Placed At</th>
|
||||
<td>{whois?.pixel.createdAt?.toString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Covered Pixels</th>
|
||||
<td>{whois?.otherPixels}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SmallCanvas = ({
|
||||
surrounding,
|
||||
...props
|
||||
}: {
|
||||
surrounding: string[][] | undefined;
|
||||
} & ComponentPropsWithoutRef<"canvas">) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) {
|
||||
console.warn("[SmallCanvas] canvasRef unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvasRef.current.getContext("2d");
|
||||
if (!ctx) {
|
||||
console.warn("[SmallCanvas] canvas context unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||
|
||||
ctx.fillStyle = "rgba(0,0,0,0.2)";
|
||||
ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||
|
||||
if (surrounding) {
|
||||
const PIXEL_WIDTH = canvasRef.current.width / surrounding[0].length;
|
||||
const middle: [x: number, y: number] = [
|
||||
Math.floor(surrounding[0].length / 2),
|
||||
Math.floor(surrounding.length / 2),
|
||||
];
|
||||
|
||||
for (let y = 0; y < surrounding.length; y++) {
|
||||
for (let x = 0; x < surrounding[y].length; x++) {
|
||||
let color = surrounding[y][x];
|
||||
ctx.beginPath();
|
||||
ctx.rect(x * PIXEL_WIDTH, y * PIXEL_WIDTH, PIXEL_WIDTH, PIXEL_WIDTH);
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(
|
||||
middle[0] * PIXEL_WIDTH,
|
||||
middle[1] * PIXEL_WIDTH,
|
||||
PIXEL_WIDTH,
|
||||
PIXEL_WIDTH
|
||||
);
|
||||
ctx.strokeStyle = "#f00";
|
||||
ctx.lineWidth = 4;
|
||||
ctx.stroke();
|
||||
}
|
||||
}, [surrounding]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
width={300}
|
||||
height={300}
|
||||
ref={(r) => (canvasRef.current = r)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
import { faMessage, faWarning, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, Link, Spinner } from "@nextui-org/react";
|
||||
import { MouseEvent, useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
interface IUser {
|
||||
sub: string;
|
||||
display_name?: string;
|
||||
picture_url?: string;
|
||||
profile_url?: string;
|
||||
isAdmin: boolean;
|
||||
isModerator: boolean;
|
||||
}
|
||||
|
||||
const MATRIX_HOST = import.meta.env.VITE_MATRIX_HOST!; // eg aftermath.gg
|
||||
const ELEMENT_HOST = import.meta.env.VITE_ELEMENT_HOST!; // eg https://chat.fediverse.events
|
||||
|
||||
const getMatrixLink = (user: IUser) => {
|
||||
return `${ELEMENT_HOST}/#/user/@${user.sub.replace("@", "=40")}:${MATRIX_HOST}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Small UserCard that shows profile picture, display name, full username, message box and (currently unused) view profile button
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export const UserCard = ({ user }: { user: IUser }) => {
|
||||
const [messageStatus, setMessageStatus] = useState<
|
||||
"loading" | "no_account" | "has_account" | "error"
|
||||
>("loading");
|
||||
|
||||
useEffect(() => {
|
||||
setMessageStatus("loading");
|
||||
|
||||
fetch(
|
||||
`https://${MATRIX_HOST}/_matrix/client/v3/profile/${encodeURIComponent(`@${user.sub.replace("@", "=40")}:${MATRIX_HOST}`)}`
|
||||
)
|
||||
.then((req) => {
|
||||
if (req.status === 200) {
|
||||
setMessageStatus("has_account");
|
||||
} else {
|
||||
setMessageStatus("no_account");
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(
|
||||
"Error while getting Matrix account details for " + user.sub,
|
||||
e
|
||||
);
|
||||
setMessageStatus("error");
|
||||
toast.error(
|
||||
"Error while getting Matrix account details for " + user.sub
|
||||
);
|
||||
});
|
||||
}, [user]);
|
||||
|
||||
const handleMatrixClick = (e: MouseEvent) => {
|
||||
if (messageStatus === "no_account") {
|
||||
e.preventDefault();
|
||||
|
||||
toast.info("This user has not setup chat yet, you cannot message them");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-row gap-2">
|
||||
<img
|
||||
src={user?.picture_url}
|
||||
alt={`${user?.sub}'s profile`}
|
||||
className="w-12 h-12"
|
||||
/>
|
||||
<div className="flex flex-col gap-0.25 grow">
|
||||
<span>{user?.display_name}</span>
|
||||
<span className="text-sm">{user?.sub}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
isIconOnly
|
||||
as={Link}
|
||||
href={getMatrixLink(user)}
|
||||
target="_blank"
|
||||
onClick={handleMatrixClick}
|
||||
>
|
||||
{messageStatus === "loading" ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={messageStatus === "error" ? faWarning : faMessage}
|
||||
color="inherit"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm">View Profile</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -35,6 +35,11 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
|||
|
||||
// overlays visible
|
||||
const [settingsSidebar, setSettingsSidebar] = useState(false);
|
||||
const [pixelWhois, setPixelWhois] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
surrounding: string[][];
|
||||
}>();
|
||||
|
||||
const [hasAdmin, setHasAdmin] = useState(false);
|
||||
|
||||
|
@ -130,6 +135,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
|
|||
setLoadChat,
|
||||
connected,
|
||||
hasAdmin,
|
||||
pixelWhois,
|
||||
setPixelWhois,
|
||||
}}
|
||||
>
|
||||
{!config && (
|
||||
|
|
|
@ -88,9 +88,51 @@ export class Canvas extends EventEmitter<CanvasEvents> {
|
|||
return !!this.config;
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nearby pixels
|
||||
* @param x
|
||||
* @param y
|
||||
* @param around (x,y) +- around
|
||||
*/
|
||||
getSurroundingPixels(x: number, y: number, around: number = 3) {
|
||||
let pixels = [];
|
||||
|
||||
for (let offsetY = 0; offsetY <= around + 1; offsetY++) {
|
||||
let arr = [];
|
||||
for (let offsetX = 0; offsetX <= around + 1; offsetX++) {
|
||||
let targetX = x + (offsetX - around + 1);
|
||||
let targetY = y + (offsetY - around + 1);
|
||||
let pixel = this.pixels[targetX + "_" + targetY];
|
||||
|
||||
if (pixel) {
|
||||
arr.push("#" + (this.Pallete.getColor(pixel.color)?.hex || "ffffff"));
|
||||
} else {
|
||||
arr.push("transparent");
|
||||
}
|
||||
}
|
||||
pixels.push(arr);
|
||||
}
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
handleMouseDown(e: ClickEvent) {
|
||||
const [x, y] = this.screenToPos(e.clientX, e.clientY);
|
||||
this.place(x, y);
|
||||
if (!e.alt && !e.ctrl && !e.meta && !e.shift && e.button === "LCLICK") {
|
||||
const [x, y] = this.screenToPos(e.clientX, e.clientY);
|
||||
this.place(x, y);
|
||||
} else {
|
||||
// KeybindManager.handleInteraction({
|
||||
// key: e.button,
|
||||
// alt: e.alt,
|
||||
// ctrl: e.ctrl,
|
||||
// meta: e.meta,
|
||||
// shift: e.meta
|
||||
// }, )
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove(e: HoverEvent) {
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
import EventEmitter from "eventemitter3";
|
||||
import { EnforceObjectType } from "./utils";
|
||||
|
||||
interface IKeybind {
|
||||
key: KeyboardEvent["code"] | "LCLICK" | "RCLICK" | "MCLICK";
|
||||
|
||||
alt?: boolean;
|
||||
ctrl?: boolean;
|
||||
meta?: boolean;
|
||||
shift?: boolean;
|
||||
}
|
||||
|
||||
interface EmittedKeybind {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
}
|
||||
|
||||
export const enforceObjectType: EnforceObjectType<IKeybind> = (v) => v;
|
||||
|
||||
const KEYBINDS = enforceObjectType({
|
||||
PIXEL_WHOIS: {
|
||||
key: "LCLICK",
|
||||
shift: true,
|
||||
},
|
||||
});
|
||||
|
||||
class KeybindManager_ extends EventEmitter<{
|
||||
[k in keyof typeof KEYBINDS]: (args: EmittedKeybind) => void;
|
||||
}> {
|
||||
constructor() {
|
||||
super();
|
||||
// setup listeners
|
||||
|
||||
document.addEventListener("keyup", this.handleKeyup);
|
||||
document.addEventListener("click", this.handleClick);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// remove listeners
|
||||
// this is global and doesn't depend on any elements, so this shouldn't need to be called
|
||||
}
|
||||
|
||||
handleKeyup = (e: KeyboardEvent) => {
|
||||
// discard if in an input element
|
||||
|
||||
const blacklistedElements = ["INPUT"];
|
||||
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (blacklistedElements.indexOf(e.target.tagName) > -1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let isHandled = this.handleInteraction(
|
||||
{
|
||||
key: e.code,
|
||||
alt: e.altKey,
|
||||
ctrl: e.ctrlKey,
|
||||
meta: e.metaKey,
|
||||
shift: e.shiftKey,
|
||||
},
|
||||
{
|
||||
clientX: -1,
|
||||
clientY: -1,
|
||||
}
|
||||
);
|
||||
|
||||
if (isHandled) e.preventDefault();
|
||||
};
|
||||
|
||||
handleClick = (e: MouseEvent) => {
|
||||
let button: "LCLICK" | "RCLICK" | "MCLICK" = ["LCLICK", "MCLICK", "RCLICK"][
|
||||
e.button
|
||||
] as any;
|
||||
|
||||
let isHandled = this.handleInteraction(
|
||||
{
|
||||
key: button,
|
||||
alt: e.altKey,
|
||||
ctrl: e.ctrlKey,
|
||||
meta: e.metaKey,
|
||||
shift: e.shiftKey,
|
||||
},
|
||||
{
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
}
|
||||
);
|
||||
|
||||
if (isHandled) e.preventDefault();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle interaction
|
||||
* @param key
|
||||
* @returns if handled
|
||||
*/
|
||||
handleInteraction(key: IKeybind, emit: EmittedKeybind): boolean {
|
||||
let isHandled = false;
|
||||
|
||||
for (const [name_, keybind] of Object.entries(KEYBINDS)) {
|
||||
const name: keyof typeof KEYBINDS = name_ as any;
|
||||
|
||||
if (keybind.key !== key.key) continue;
|
||||
if (typeof keybind.alt !== "undefined" && keybind.alt !== key.alt)
|
||||
continue;
|
||||
if (typeof keybind.ctrl !== "undefined" && keybind.ctrl !== key.ctrl)
|
||||
continue;
|
||||
if (typeof keybind.meta !== "undefined" && keybind.meta !== key.meta)
|
||||
continue;
|
||||
if (typeof keybind.shift !== "undefined" && keybind.shift !== key.shift)
|
||||
continue;
|
||||
|
||||
this.emit(name, emit);
|
||||
isHandled = true;
|
||||
}
|
||||
|
||||
return isHandled;
|
||||
}
|
||||
|
||||
getKeybind(key: keyof typeof KEYBINDS) {
|
||||
return KEYBINDS[key];
|
||||
}
|
||||
}
|
||||
|
||||
export const KeybindManager = new KeybindManager_();
|
|
@ -1,11 +1,11 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const api = async <T = any>(
|
||||
export const api = async <T = unknown, Error = string>(
|
||||
endpoint: string,
|
||||
method: "GET" | "POST" = "GET",
|
||||
body?: unknown
|
||||
): Promise<{
|
||||
status: number;
|
||||
data: ({ success: true } & T) | { success: false; error: string };
|
||||
data: ({ success: true } & T) | { success: false; error: Error };
|
||||
}> => {
|
||||
const API_HOST = import.meta.env.VITE_API_HOST || "";
|
||||
|
||||
|
@ -32,3 +32,7 @@ export const api = async <T = any>(
|
|||
data,
|
||||
};
|
||||
};
|
||||
|
||||
export type EnforceObjectType<T> = <V extends { [k: string]: T }>(
|
||||
v: V
|
||||
) => { [k in keyof V]: T };
|
||||
|
|
|
@ -44,6 +44,9 @@ export interface IAppContext {
|
|||
setLoadChat: (v: boolean) => void;
|
||||
connected: boolean;
|
||||
|
||||
pixelWhois?: { x: number; y: number; surrounding: string[][] };
|
||||
setPixelWhois: (v: this["pixelWhois"]) => void;
|
||||
|
||||
hasAdmin: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -89,8 +89,15 @@ interface ISetup {
|
|||
|
||||
// TODO: move these event interfaces out
|
||||
export interface ClickEvent {
|
||||
button: "LCLICK" | "MCLICK" | "RCLICK";
|
||||
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
|
||||
alt: boolean;
|
||||
ctrl: boolean;
|
||||
meta: boolean;
|
||||
shift: boolean;
|
||||
}
|
||||
|
||||
export interface HoverEvent {
|
||||
|
@ -596,8 +603,15 @@ export class PanZoom extends EventEmitter<PanZoomEvents> {
|
|||
// difference from the start position to the up position is very very slow,
|
||||
// so it's most likely intended to be a click
|
||||
this.emit("click", {
|
||||
button: ["LCLICK", "MCLICK", "RCLICK"][e.button] as any,
|
||||
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
|
||||
alt: e.altKey,
|
||||
ctrl: e.ctrlKey,
|
||||
meta: e.metaKey,
|
||||
shift: e.shiftKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -190,4 +190,48 @@ app.get("/callback", async (req, res) => {
|
|||
res.redirect("/");
|
||||
});
|
||||
|
||||
// TODO: Ratelimiting #40
|
||||
app.get("/canvas/pixel/:x/:y", async (req, res) => {
|
||||
const x = parseInt(req.params.x);
|
||||
const y = parseInt(req.params.y);
|
||||
|
||||
if (isNaN(x) || isNaN(y)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "x or y is not a number" });
|
||||
}
|
||||
|
||||
const pixel = await Canvas.getPixel(x, y);
|
||||
if (!pixel) {
|
||||
return res.json({ success: false, error: "no_pixel" });
|
||||
}
|
||||
|
||||
const otherPixels = await prisma.pixel.count({ where: { x, y } });
|
||||
|
||||
const user = await prisma.user.findFirst({ where: { sub: pixel.userId } });
|
||||
const instance = await prisma.instance.findFirst({
|
||||
where: { hostname: pixel.userId.split("@")[1] },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
pixel,
|
||||
otherPixels: otherPixels - 1,
|
||||
user: user && {
|
||||
sub: user.sub,
|
||||
display_name: user.display_name,
|
||||
picture_url: user.picture_url,
|
||||
profile_url: user.profile_url,
|
||||
isAdmin: user.isAdmin,
|
||||
isModerator: user.isModerator,
|
||||
},
|
||||
instance: instance && {
|
||||
hostname: instance.hostname,
|
||||
name: instance.name,
|
||||
logo_url: instance.logo_url,
|
||||
banner_url: instance.banner_url,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
|
|
@ -151,6 +151,15 @@ class Canvas {
|
|||
return await this.canvasToRedis();
|
||||
}
|
||||
|
||||
async getPixel(x: number, y: number) {
|
||||
return await prisma.pixel.findFirst({
|
||||
where: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async setPixel(user: { sub: string }, x: number, y: number, hex: string) {
|
||||
const redis = await Redis.getClient();
|
||||
|
||||
|
|
Loading…
Reference in New Issue