Skip to content

Commit cf976f9

Browse files
committed
Add optional keyboard hotkeys
1 parent 7724876 commit cf976f9

File tree

7 files changed

+99
-6
lines changed

7 files changed

+99
-6
lines changed

web/admin/assets/css/main.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@
4848
.meta-item {
4949
@apply inline-flex items-center gap-1;
5050
}
51+
52+
kbd {
53+
@apply border border-gray-300 dark:border-gray-700 rounded px-1 bg-gray-200 dark:bg-gray-800 shadow-sm dark:shadow-gray-800;
54+
}

web/admin/components/reject-reason-modal.vue

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<script setup lang="ts">
2+
import { useKeyboardShurtcuts } from "~/lib/settings";
3+
import { onDocumentEvent, shouldHandleKeypress } from "~/lib/util";
4+
25
defineProps<{
36
name: string;
47
}>();
5-
defineEmits<{
8+
const $emit = defineEmits<{
69
(event: "reject", reason: string): void;
710
(event: "cancel"): void;
811
}>();
@@ -33,6 +36,28 @@ function handleCheck(reason: string, value: boolean) {
3336
selectedReasons.value.delete(reason);
3437
}
3538
}
39+
40+
onDocumentEvent("keydown", (e: KeyboardEvent) => {
41+
if (!shouldHandleKeypress(e)) return;
42+
if (e.key === "Enter") {
43+
(document.querySelector("#reject") as HTMLButtonElement)?.click();
44+
return;
45+
}
46+
if (e.key === "Escape") {
47+
$emit("cancel");
48+
return;
49+
}
50+
if (!e.key.match(/^[0-9]$/)) return;
51+
const index = parseInt(e.key) - 1;
52+
const reason = reasons[index];
53+
console.log(reason);
54+
if (index === reasons.length) {
55+
showOther.value = true;
56+
(document.querySelector("#other") as HTMLInputElement)?.focus();
57+
} else if (reason) {
58+
handleCheck(reason, !selectedReasons.value.has(reason));
59+
}
60+
});
3661
</script>
3762

3863
<template>
@@ -52,14 +77,16 @@ function handleCheck(reason: string, value: boolean) {
5277
<input
5378
:id="`reason-${idx}`"
5479
type="checkbox"
80+
:checked="selectedReasons.has(reason)"
5581
@input="
5682
handleCheck(reason, ($event.target as HTMLInputElement).checked)
5783
"
5884
/>
5985
<label
60-
class="w-full h-full cursor-pointer py-1.5"
86+
class="w-full h-full cursor-pointer py-1.5 flex items-center gap-1"
6187
:for="`reason-${idx}`"
6288
>
89+
<kbd v-if="useKeyboardShurtcuts" class="text-xs">{{ idx + 1 }}</kbd>
6390
{{ reason }}
6491
</label>
6592
</li>
@@ -69,15 +96,19 @@ function handleCheck(reason: string, value: boolean) {
6996
<input id="reason-other" v-model="showOther" type="checkbox" />
7097
<label
7198
id="other-label"
72-
class="w-full h-full cursor-pointer py-1.5"
99+
class="w-full h-full cursor-pointer py-1.5 flex items-center gap-1"
73100
for="reason-other"
74101
>
102+
<kbd v-if="useKeyboardShurtcuts" class="text-xs">{{
103+
reasons.length + 1
104+
}}</kbd>
75105
Other
76106
</label>
77107
</li>
78108
<li v-if="showOther">
79109
<input
80110
v-model="other"
111+
id="other"
81112
type="text"
82113
aria-labelledby="other-label"
83114
placeholder="Type a reason..."
@@ -95,6 +126,7 @@ function handleCheck(reason: string, value: boolean) {
95126
</button>
96127

97128
<button
129+
id="reject"
98130
class="py-1 px-2 mr-1 bg-red-500 dark:bg-red-600 hover:bg-red-600 dark:hover:bg-red-700 disabled:bg-red-400 disabled:dark:bg-red-500 rounded-lg disabled:cursor-not-allowed"
99131
:disabled="!selectedReasonsText"
100132
@click="$emit('reject', selectedReasonsText)"

web/admin/components/user/queue-actions.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { showQueueActionConfirmation } from "~/lib/settings";
33
import { ApprovalQueueAction } from "../../../proto/bff/v1/moderation_service_pb";
4+
import { onDocumentEvent, shouldHandleKeypress } from "~/lib/util";
45
56
const showRejectModal = ref(false);
67
const loading = ref(false);
@@ -87,6 +88,17 @@ function promptReject() {
8788
}
8889
showRejectModal.value = true;
8990
}
91+
92+
onDocumentEvent("keydown", (e: KeyboardEvent) => {
93+
if (!shouldHandleKeypress(e)) return;
94+
if (e.key === "a") {
95+
accept();
96+
} else if (e.key === "r") {
97+
showRejectModal.value = true;
98+
} else if (e.key === "h") {
99+
holdBack();
100+
}
101+
});
90102
</script>
91103

92104
<template>

web/admin/lib/settings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const SHOW_QUEUE_ACTION_CONFIRMATION = "bff-show-queue-action-confirmation";
22
const BLUR_NSFW_POST_MEDIA = "bff-blur-nsfw-post-media";
3+
const USE_KEYBOARD_SHORTCUTS = "bff-use-keyboard-shortcuts";
34

45
function defineLocalStorageRef(key: string): Ref<boolean> {
56
const r = useState(key, () => localStorage.getItem(key) === "true");
@@ -18,3 +19,6 @@ export const showQueueActionConfirmation = defineLocalStorageRef(
1819
SHOW_QUEUE_ACTION_CONFIRMATION
1920
);
2021
export const blurNsfwPostMedia = defineLocalStorageRef(BLUR_NSFW_POST_MEDIA);
22+
export const useKeyboardShurtcuts = defineLocalStorageRef(
23+
USE_KEYBOARD_SHORTCUTS
24+
);

web/admin/lib/util.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,26 @@ export function chunk<T>(a: Array<T>, max: number): Array<Array<T>> {
4040
}
4141
return result;
4242
}
43+
44+
export function onDocumentEvent<K extends keyof DocumentEventMap>(
45+
type: K,
46+
listener: (this: Document, ev: DocumentEventMap[K]) => any,
47+
options?: boolean | AddEventListenerOptions
48+
): void {
49+
onMounted(() => {
50+
document.addEventListener(type, listener, options);
51+
});
52+
onBeforeUnmount(() => {
53+
document.removeEventListener(type, listener);
54+
});
55+
}
56+
57+
export function shouldHandleKeypress(e: KeyboardEvent): boolean {
58+
return (
59+
!e.metaKey &&
60+
!e.altKey &&
61+
!e.ctrlKey &&
62+
!e.shiftKey &&
63+
!document.querySelector(":focus")
64+
);
65+
}

web/admin/pages/index.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ function didToProfile(did: string): ProfileViewDetailed | undefined {
6363
function selectRandomActor() {
6464
if (!queues.value) return;
6565
const queue = queues.value[currentQueue.value];
66-
67-
randomActor.value = queue[Math.floor(Math.random() * queue.length)] as Actor;
66+
randomActor.value = queue.toSorted((a, b) => {
67+
const ageA = a.createdAt?.toDate().getTime() || 0;
68+
const ageB = b.createdAt?.toDate().getTime() || 0
69+
return ageA - ageB;
70+
})[0] as Actor
6871
}
6972
7073
async function rejectAllDeleted() {

web/admin/pages/settings.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<script setup lang="ts">
2-
import { showQueueActionConfirmation, blurNsfwPostMedia } from "~/lib/settings";
2+
import {
3+
showQueueActionConfirmation,
4+
blurNsfwPostMedia,
5+
useKeyboardShurtcuts,
6+
} from "~/lib/settings";
37
import { logout } from "~/lib/auth";
48
</script>
59

@@ -29,6 +33,17 @@ import { logout } from "~/lib/auth";
2933
>Blur NSFW post media (images/video).</label
3034
>
3135
</div>
36+
<div class="flex gap-2 mb-4">
37+
<input
38+
id="use-keyboard-shortcuts"
39+
v-model="useKeyboardShurtcuts"
40+
type="checkbox"
41+
/>
42+
<label for="buse-keyboard-shortcuts"
43+
>Use keyboard shortcuts: <kbd>A</kbd> for approve, <kbd>R</kbd> for
44+
reject, and <kbd>H</kbd> for hold back</label
45+
>
46+
</div>
3247

3348
<button class="text-white bg-gray-700 px-2 py-1 rounded-lg" @click="logout">
3449
Logout

0 commit comments

Comments
 (0)