diff --git a/src/apps/chat/components/StatusBar.tsx b/src/apps/chat/components/StatusBar.tsx
index ab68ed0e7..b618d3aad 100644
--- a/src/apps/chat/components/StatusBar.tsx
+++ b/src/apps/chat/components/StatusBar.tsx
@@ -10,6 +10,7 @@ import { ShortcutKey, ShortcutObject } from '~/common/components/shortcuts/useGl
import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { useGlobalShortcutsStore } from '~/common/components/shortcuts/store-global-shortcuts';
+import { useShortcutsPreferencesStore, shortcutFingerprint } from '~/common/components/shortcuts/store-shortcuts-preferences';
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
import { useUXLabsStore } from '~/common/stores/store-ux-labs';
@@ -176,9 +177,12 @@ export function StatusBar(props: { toggleMinimized?: () => void, isMinimized?: b
// external state
const labsShowShortcutBar = useUXLabsStore(state => state.labsShowShortcutBar);
+ const disabledShortcuts = useShortcutsPreferencesStore(state => state.disabledShortcuts);
const shortcuts = useGlobalShortcutsStore(useShallow(state => {
- // get visible shortcuts
- let visibleShortcuts = !labsShowShortcutBar ? [] : state.getAllShortcuts().filter(shortcut => !!shortcut.description);
+ // get visible shortcuts, excluding user-denied ones
+ let visibleShortcuts = !labsShowShortcutBar ? [] : state.getAllShortcuts().filter(shortcut =>
+ !!shortcut.description && !disabledShortcuts.includes(shortcutFingerprint(shortcut)),
+ );
// filter by highest level if levels are present
const maxLevel = Math.max(...visibleShortcuts.map(s => s.level ?? 0));
diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx
index 7b51702b8..d9a16bdcb 100644
--- a/src/apps/chat/components/composer/Composer.tsx
+++ b/src/apps/chat/components/composer/Composer.tsx
@@ -26,6 +26,7 @@ import { ConversationsManager } from '~/common/chat-overlay/ConversationsManager
import { DMessageId, DMessageMetadata, DMetaReferenceItem, messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
import { PhPaintBrush } from '~/common/components/icons/phosphor/PhPaintBrush';
import { ShortcutKey, ShortcutObject, useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts';
+import { isShortcutDenied } from '~/common/components/shortcuts/store-shortcuts-preferences';
import { addSnackbar } from '~/common/components/snackbar/useSnackbarsStore';
import { animationEnterBelow } from '~/common/util/animUtils';
import { browserSpeechRecognitionCapability, PLACEHOLDER_INTERIM_TRANSCRIPT, SpeechResult, useSpeechRecognition } from '~/common/components/speechrecognition/useSpeechRecognition';
@@ -545,16 +546,20 @@ export function Composer(props: {
// Alt (Windows) or Option (Mac) + Enter: append the message instead of sending it
if (e.altKey && !e.metaKey && !e.ctrlKey) {
- if (await handleSendAction('append-user', composeText)) // 'alt+enter' -> write
- e.stopPropagation();
- return e.preventDefault();
+ if (!isShortcutDenied({ key: 'Enter', alt: true })) {
+ if (await handleSendAction('append-user', composeText)) // 'alt+enter' -> write
+ e.stopPropagation();
+ return e.preventDefault();
+ }
}
// Ctrl (Windows) or Command (Mac) + Enter: send for beaming
if (e.ctrlKey && !e.metaKey && !e.altKey) {
- if (await handleSendAction('beam-content', composeText)) // 'ctrl+enter' -> beam
- e.stopPropagation();
- return e.preventDefault();
+ if (!isShortcutDenied({ key: 'Enter', ctrl: true })) {
+ if (await handleSendAction('beam-content', composeText)) // 'ctrl+enter' -> beam
+ e.stopPropagation();
+ return e.preventDefault();
+ }
}
// Shift: toggles the 'enter is newline'
diff --git a/src/apps/settings-modal/ShortcutsModal.tsx b/src/apps/settings-modal/ShortcutsModal.tsx
index 925f0c383..63c27617d 100644
--- a/src/apps/settings-modal/ShortcutsModal.tsx
+++ b/src/apps/settings-modal/ShortcutsModal.tsx
@@ -1,10 +1,13 @@
import * as React from 'react';
-import { Box, Chip, Divider, Typography } from '@mui/joy';
+import { Box, Chip, Divider, IconButton, Tooltip, Typography } from '@mui/joy';
+import BlockIcon from '@mui/icons-material/Block';
+import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import { GoodModal } from '~/common/components/modals/GoodModal';
import type { ShortcutDefinition } from '~/common/components/shortcuts/useGlobalShortcuts';
import { shortcutsCatalog } from '~/common/components/shortcuts/shortcutsCatalog';
+import { shortcutFingerprint, useShortcutsPreferencesStore } from '~/common/components/shortcuts/store-shortcuts-preferences';
import { useGlobalShortcutsStore } from '~/common/components/shortcuts/store-global-shortcuts';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { Is } from '~/common/util/pwaUtils';
@@ -36,6 +39,11 @@ const _styles = {
justifyContent: 'space-between',
gap: 1,
},
+ rowLeft: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: 1,
+ },
keys: {
display: 'flex',
gap: 0.5,
@@ -111,6 +119,42 @@ function ShortcutKeyCombo(props: { def: ShortcutDefinition }) {
}
+function ShortcutRow(props: { item: typeof shortcutsCatalog[number]['items'][number], active: boolean }) {
+ const { item, active } = props;
+
+ const fp = shortcutFingerprint(item);
+ const isDenied = useShortcutsPreferencesStore((state) => state.disabledShortcuts.includes(fp));
+
+ const handleToggle = React.useCallback(() => {
+ useShortcutsPreferencesStore.getState().toggleShortcutDisabled(fp);
+ }, [fp]);
+
+ const effectivelyActive = active && !isDenied;
+
+ return (
+
+
+
+
+ {isDenied ? : }
+
+
+
+
+
+ {item.description}
+
+
+ );
+}
+
+
export function ShortcutsModal(props: { onClose: () => void }) {
// external state
@@ -130,14 +174,7 @@ export function ShortcutsModal(props: { onClose: () => void }) {
{category.items.map((item, i) => {
const active = _isActive(item, activeFingerprints);
- return (
-
-
-
- {item.description}
-
-
- );
+ return ;
})}
))}
diff --git a/src/common/components/shortcuts/globalShortcutsHandler.ts b/src/common/components/shortcuts/globalShortcutsHandler.ts
index 355e83507..9319c545b 100644
--- a/src/common/components/shortcuts/globalShortcutsHandler.ts
+++ b/src/common/components/shortcuts/globalShortcutsHandler.ts
@@ -1,4 +1,5 @@
import { useGlobalShortcutsStore } from './store-global-shortcuts';
+import { isShortcutDenied } from './store-shortcuts-preferences';
export function ensureGlobalShortcutHandler() {
@@ -41,6 +42,10 @@ function _handleGlobalShortcutKeyDown(event: KeyboardEvent) {
if (shortcut.skipIfInput && _isTextInputFocused())
continue;
+ // Skip if the shortcut is on the user's denylist
+ if (isShortcutDenied(shortcut))
+ continue;
+
// Execute the action (and prevent the default browser action)
event.preventDefault();
event.stopPropagation();
diff --git a/src/common/components/shortcuts/store-shortcuts-preferences.ts b/src/common/components/shortcuts/store-shortcuts-preferences.ts
new file mode 100644
index 000000000..230ae312e
--- /dev/null
+++ b/src/common/components/shortcuts/store-shortcuts-preferences.ts
@@ -0,0 +1,43 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+import type { ShortcutDefinition } from './useGlobalShortcuts';
+
+
+export function shortcutFingerprint(def: Pick): string {
+ return `${def.key.toLowerCase()}:${!!def.ctrl}:${!!def.shift}:${!!def.alt}`;
+}
+
+
+interface ShortcutsPreferencesStore {
+ disabledShortcuts: string[];
+ toggleShortcutDisabled: (fingerprint: string) => void;
+}
+
+export const useShortcutsPreferencesStore = create()(
+ persist(
+ (set) => ({
+
+ disabledShortcuts: [],
+
+ toggleShortcutDisabled: (fingerprint: string) =>
+ set((state) => {
+ const idx = state.disabledShortcuts.indexOf(fingerprint);
+ if (idx >= 0)
+ return { disabledShortcuts: state.disabledShortcuts.filter((f) => f !== fingerprint) };
+ return { disabledShortcuts: [...state.disabledShortcuts, fingerprint] };
+ }),
+
+ }),
+ {
+ name: 'app-shortcuts-preferences',
+ version: 1,
+ },
+ ),
+);
+
+
+export function isShortcutDenied(def: Pick): boolean {
+ const fp = shortcutFingerprint(def);
+ return useShortcutsPreferencesStore.getState().disabledShortcuts.includes(fp);
+}