diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 00000000..312f7991
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "associatedIndex": 6
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1728462402451
+
+
+ 1728462402451
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ file://$PROJECT_DIR$/packages/revolt.js/src/events/EventClient.ts
+ 142
+
+
+
+ file://$PROJECT_DIR$/packages/client/components/ui/components/design/atoms/display/Time.tsx
+ 24
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/client/components/modal/index.tsx b/packages/client/components/modal/index.tsx
index 1ba8578d..8e7c5777 100644
--- a/packages/client/components/modal/index.tsx
+++ b/packages/client/components/modal/index.tsx
@@ -1,4 +1,4 @@
-import { For } from "solid-js";
+import { createEffect, For, onMount } from "solid-js";
import { SetStoreFunction, createStore } from "solid-js/store";
import type { MFA, MFATicket } from "revolt.js";
@@ -9,6 +9,7 @@ import "../ui/styled.d.ts";
import { RenderModal } from "./modals";
import { Modals } from "./types";
+import { registerKeybindWithPriority, unregisterKeybindWithPriority } from "../../src/shared/lib/priorityKeybind";
export type ActiveModal = {
/**
@@ -27,6 +28,15 @@ export type ActiveModal = {
props: Modals;
};
+/**
+ * Handle key press
+ * @param event Event
+ */
+function keyDown(event: KeyboardEvent) {
+ event.stopPropagation();
+ modalController.pop();
+}
+
/**
* Global modal controller for layering and displaying one or more modal to the user
*/
@@ -43,23 +53,6 @@ export class ModalController {
this.setModals = setModals;
this.pop = this.pop.bind(this);
-
- // TODO: this should instead work using some sort of priority queue system from a dedicated keybind handler
- // so that, for example, popping draft does not conflict with closing the current modal
- // example API: registerKeybind(key = 'Escape', priority = 20, fn = () => void)
- // => event.stopPropagation
-
- /**
- * Handle key press
- * @param event Event
- */
- function keyDown(event: KeyboardEvent) {
- if (event.key === "Escape") {
- modalController.pop();
- }
- }
-
- document.addEventListener("keydown", keyDown);
}
/**
@@ -201,6 +194,12 @@ export class ModalControllerExtended extends ModalController {
export const modalController = new ModalControllerExtended();
export function ModalRenderer() {
+ createEffect(() => {
+ if (modalController.modals.length === 0) return unregisterKeybindWithPriority(keyDown);
+
+ return registerKeybindWithPriority("Escape", keyDown, 1, "user-visible");
+ });
+
return (
{(entry) => }
diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx
index 10141ad8..1d97fc50 100644
--- a/packages/client/src/index.tsx
+++ b/packages/client/src/index.tsx
@@ -44,6 +44,7 @@ import { HomePage } from "./interface/Home";
import { ServerHome } from "./interface/ServerHome";
import { ChannelPage } from "./interface/channels/ChannelPage";
import "./sentry";
+import { registerKeybindsWithPriority } from "./shared/lib/priorityKeybind";
attachDevtoolsOverlay();
@@ -115,6 +116,8 @@ function MountContext(props: { children?: JSX.Element }) {
);
}
+registerKeybindsWithPriority();
+
render(
() => (
diff --git a/packages/client/src/interface/channels/text/Composition.tsx b/packages/client/src/interface/channels/text/Composition.tsx
index f6282854..81e01690 100644
--- a/packages/client/src/interface/channels/text/Composition.tsx
+++ b/packages/client/src/interface/channels/text/Composition.tsx
@@ -23,6 +23,7 @@ import {
MessageBox,
MessageReplyPreview,
} from "@revolt/ui";
+import { registerKeybindWithPriority, unregisterKeybindWithPriority } from "../../../shared/lib/priorityKeybind";
interface Props {
/**
@@ -288,8 +289,8 @@ export function MessageComposition(props: Props) {
}
// Bind onKeyDown to the document
- onMount(() => document.addEventListener("keydown", onKeyDown));
- onCleanup(() => document.removeEventListener("keydown", onKeyDown));
+ onMount(() => registerKeybindWithPriority("Escape", onKeyDown));
+ onCleanup(() => unregisterKeybindWithPriority(onKeyDown));
/**
* Handle files being added to the draft.
diff --git a/packages/client/src/shared/lib/priorityKeybind.ts b/packages/client/src/shared/lib/priorityKeybind.ts
new file mode 100644
index 00000000..e5bc4080
--- /dev/null
+++ b/packages/client/src/shared/lib/priorityKeybind.ts
@@ -0,0 +1,67 @@
+export type SchedulePriority =
+ | 'user-blocking'
+ | 'user-visible'
+ | 'background';
+
+type KeybindHandler = (e: KeyboardEvent) => void | Promise;
+
+const registeredHandlers = new Map();
+
+export function registerKeybindWithPriority(key: string, handler: KeybindHandler, priority = 0, schedule: SchedulePriority = 'user-blocking') {
+ registeredHandlers.set(handler, {
+ priority,
+ key,
+ schedule
+ })
+}
+
+export function unregisterKeybindWithPriority(handler: KeybindHandler) {
+ registeredHandlers.delete(handler);
+}
+
+function catchAll(e: KeyboardEvent) {
+ const entries = [...registeredHandlers.entries()];
+ const sorted = entries.toSorted(([_, { priority: a }], [__, { priority: b }]) => b - a);
+ let maxPrio = 0;
+
+ for (const [handler, { priority, key, schedule }] of sorted) {
+ maxPrio = Math.max(maxPrio, priority);
+ if (e.key !== key) return;
+
+ if (priority < maxPrio) return;
+
+ switch (schedule) {
+ case "user-blocking": {
+ queueMicrotask(() => handler(e));
+ break;
+ }
+ case "user-visible": {
+ setTimeout(() => handler(e), 0);
+ break;
+ }
+ case "background": {
+ requestIdleCallback(() => handler(e));
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Needs to be called if you want to register handlers on key down
+ */
+export function registerKeybindsWithPriority() {
+ document.addEventListener('keydown', catchAll)
+}
+
+/**
+ * Use whenever you need to clean up the registered handlers
+ */
+export function disposeKeybindsWithPriority() {
+ registeredHandlers.clear();
+ document.removeEventListener('keydown', catchAll)
+}