Skip to content

Commit

Permalink
feat(keybinds): Priority queue for modal keybinds
Browse files Browse the repository at this point in the history
Found a random todo
  • Loading branch information
Drevoed committed Oct 10, 2024
1 parent ddaedef commit f9035bb
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 20 deletions.
142 changes: 142 additions & 0 deletions .idea/workspace.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 17 additions & 18 deletions packages/client/components/modal/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 = {
/**
Expand All @@ -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
*/
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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 (
<For each={modalController.modals}>
{(entry) => <RenderModal {...entry} />}
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -115,6 +116,8 @@ function MountContext(props: { children?: JSX.Element }) {
);
}

registerKeybindsWithPriority();

render(
() => (
<Router root={MountContext}>
Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/interface/channels/text/Composition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MessageBox,
MessageReplyPreview,
} from "@revolt/ui";
import { registerKeybindWithPriority, unregisterKeybindWithPriority } from "../../../shared/lib/priorityKeybind";

interface Props {
/**
Expand Down Expand Up @@ -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.
Expand Down
67 changes: 67 additions & 0 deletions packages/client/src/shared/lib/priorityKeybind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export type SchedulePriority =
| 'user-blocking'
| 'user-visible'
| 'background';

type KeybindHandler = (e: KeyboardEvent) => void | Promise<void>;

const registeredHandlers = new Map<KeybindHandler, {
priority: number;
schedule: SchedulePriority;
key: string;
}>();

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)
}

0 comments on commit f9035bb

Please sign in to comment.