fix(app): make disabling built-in shortcuts take effect live (#3096)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-01 20:37:28 +02:00
1 parent 2df82b2b5e
commit 9a074adb11
45 files changed
+2476 -1424

No files matched your search

@@ -63,6 +63,22 @@ const CUSTOM_KEYBIND_ACTION_LABEL_DESCRIPTORS: Partial<Record<KeybindCommand, Me
message: 'Move item down',
comment: 'Label in the keybinds tab action dropdown. Distinguishes up from down.',
}),
message_reply_prev: msg({
message: 'Reply to message above',
comment: 'Label in the keybinds tab action dropdown. Distinguishes above from below.',
}),
message_reply_next: msg({
message: 'Reply to message below',
comment: 'Label in the keybinds tab action dropdown. Distinguishes above from below.',
}),
message_edit_prev: msg({
message: 'Edit your message above',
comment: 'Label in the keybinds tab action dropdown. Distinguishes above from below.',
}),
message_edit_next: msg({
message: 'Edit your message below',
comment: 'Label in the keybinds tab action dropdown. Distinguishes above from below.',
}),
};
export function getCustomKeybindActionLabel(i18n: I18n, action: KeybindCommand, fallbackLabel?: string): string {
@@ -2,6 +2,7 @@
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
import Keybind from '@app/features/input/state/InputKeybind';
import {isBuiltinDisableMarker} from '@app/features/input/state/KeybindResolution';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {RetryIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
import {Button} from '@app/features/ui/button/Button';
@@ -57,7 +58,11 @@ const SHORTCUTS_ARE_PAUSED_WHILE_RECORDING_DESCRIPTOR = msg({
});
export const CustomKeybindsList: React.FC<{searchQuery: string}> = observer(({searchQuery}) => {
const {i18n} = useLingui();
const customKeybinds = Keybind.getCustomKeybinds();
const allCustomKeybinds = Keybind.getCustomKeybinds();
const customKeybinds = useMemo(
() => allCustomKeybinds.filter((entry) => !isBuiltinDisableMarker(entry)),
[allCustomKeybinds],
);
const syncAcrossDevices = Keybind.getSyncAcrossDevices();
const disableBuiltinKeybinds = Keybind.getDisableBuiltinKeybinds();
const labelByAction = getCustomKeybindActionLabelMap(i18n, Keybind.getDefaults());
@@ -110,7 +115,7 @@ export const CustomKeybindsList: React.FC<{searchQuery: string}> = observer(({se
)),
);
};
const hasAnyOverrides = customKeybinds.length > 0 || disableBuiltinKeybinds;
const hasAnyOverrides = allCustomKeybinds.length > 0 || disableBuiltinKeybinds;
return (
<div className={styles.customSection} data-flx="user.keybinds-tab.custom-keybinds-list.custom-section">
<div className={styles.customHeader} data-flx="user.keybinds-tab.custom-keybinds-list.custom-header">
@@ -5,6 +5,7 @@ import Keybind, {
type KeybindConfig,
type KeybindSection,
} from '@app/features/input/state/InputKeybind';
import {isActiveCustomKeybind, isBuiltinDisableMarker} from '@app/features/input/state/KeybindResolution';
import styles from '@app/features/user/components/modals/tabs/KeybindsTab.module.css';
import {DefaultShortcutRow} from '@app/features/user/components/modals/tabs/keybinds_tab/DefaultShortcutRow';
import {
@@ -52,13 +53,13 @@ export const DefaultKeybindsList: React.FC<{searchQuery: string}> = observer(({s
const {i18n} = useLingui();
const defaults = Keybind.getDefaults();
const customKeybinds = Keybind.getCustomKeybinds();
const overriddenActions = useMemo(() => {
const set = new Set<KeybindCommand>();
for (const entry of customKeybinds) {
if (entry.action) set.add(entry.action);
}
return set;
}, [customKeybinds]);
const overriddenActions = new Set<KeybindCommand>();
const disabledActions = new Set<KeybindCommand>();
for (const entry of customKeybinds) {
if (!entry.action) continue;
if (isActiveCustomKeybind(entry)) overriddenActions.add(entry.action);
else if (isBuiltinDisableMarker(entry)) disabledActions.add(entry.action);
}
const normalized = normalizeQuery(searchQuery);
const sectionLabels: Record<KeybindSection, string> = {
defaults: i18n._(DEFAULT_SHORTCUTS_DESCRIPTOR),
@@ -124,6 +125,7 @@ export const DefaultKeybindsList: React.FC<{searchQuery: string}> = observer(({s
key={Array.isArray(row) ? `${row[0].action}-${row[1].action}-${idx}` : `${row.action}-${idx}`}
row={row}
overriddenActions={overriddenActions}
disabledActions={disabledActions}
data-flx="user.keybinds-tab.default-keybinds-list.default-shortcut-row"
/>
))}
@@ -5,9 +5,10 @@ import {OVERRIDDEN_CHIP_STYLE} from '@app/features/user/components/modals/tabs/k
import {Trans} from '@lingui/react/macro';
import type React from 'react';
export const DefaultShortcutChipList: React.FC<{chips: Array<string>; overridden?: boolean}> = ({
export const DefaultShortcutChipList: React.FC<{chips: Array<string>; overridden?: boolean; disabled?: boolean}> = ({
chips,
overridden,
disabled,
}) => (
<div className={styles.defaultChips} data-flx="user.keybinds-tab.default-shortcut-chip-list.default-chips">
{chips.length === 0 ? (
@@ -22,7 +23,7 @@ export const DefaultShortcutChipList: React.FC<{chips: Array<string>; overridden
<span
key={`${idx}-${chip}`}
className={styles.defaultChip}
style={overridden ? OVERRIDDEN_CHIP_STYLE : undefined}
style={overridden || disabled ? OVERRIDDEN_CHIP_STYLE : undefined}
data-flx="user.keybinds-tab.default-shortcut-chip-list.default-chip"
>
{chip}
@@ -36,6 +37,13 @@ export const DefaultShortcutChipList: React.FC<{chips: Array<string>; overridden
>
<Trans>Overridden</Trans>
</span>
) : disabled ? (
<span
className={styles.defaultChipsEmpty}
data-flx="user.keybinds-tab.default-shortcut-chip-list.default-chips-empty--3"
>
<Trans comment="Status next to a built-in shortcut the user turned off in the keybinds tab.">Disabled</Trans>
</span>
) : null}
</div>
);
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import Keybind, {type KeybindCommand} from '@app/features/input/state/InputKeybind';
import {isBuiltinDisableMarker} from '@app/features/input/state/KeybindResolution';
import {CheckboxItem} from '@app/features/ui/action_menu/ContextMenu';
import {
EditSimpleIcon,
@@ -46,6 +47,11 @@ const RESET_TO_BUILT_IN_2_DESCRIPTOR = msg({
comment:
'Button or menu action label in the keybinds tab. Keep it concise. Preserve {label}; it is inserted by code.',
});
const RE_ENABLE_BUILT_IN_SHORTCUT_DESCRIPTOR = msg({
message: 'Re-enable built-in shortcut',
comment:
'Button or menu action label in the keybinds tab. Turns a disabled built-in shortcut back on. Keep it concise.',
});
const SET_CUSTOM_SHORTCUT_FOR_DESCRIPTOR = msg({
message: 'Set custom shortcut for "{label}"',
comment: 'Label in the keybinds tab. Preserve {label}; it is inserted by code.',
@@ -58,29 +64,47 @@ const GLOBAL_SHORTCUT_DESCRIPTOR = msg({
message: 'Global shortcut',
comment: 'Toggle in the keybinds tab. When on, the shortcut works even when Fluxer is not focused. Keep it concise.',
});
export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActions: ReadonlySet<KeybindCommand>}> = ({
row,
overriddenActions,
}) => {
export const DefaultShortcutRow: React.FC<{
row: ShortcutRowModel;
overriddenActions: ReadonlySet<KeybindCommand>;
disabledActions: ReadonlySet<KeybindCommand>;
}> = ({row, overriddenActions, disabledActions}) => {
const {i18n} = useLingui();
const actions = getRowActions(row);
const openRowMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
ContextMenuCommands.openFromEvent(event, ({onClose}) => {
const perAction = actions.map((action) => {
const customBindings = Keybind.getCustomKeybinds().filter((entry) => entry.action === action.action);
const hasEnabledCustomBinding = customBindings.some((entry) => entry.enabled);
return {
action,
label: getCustomKeybindActionLabel(i18n, action.action, action.label),
customBindings,
hasEnabledCustomBinding,
};
});
const allHaveCustom = perAction.every((p) => p.customBindings.length > 0);
const noneHaveCustom = perAction.every((p) => p.customBindings.length === 0);
const perAction = actions.map((action) => ({
action,
label: getCustomKeybindActionLabel(i18n, action.action, action.label),
hasCustomBinding: Keybind.getCustomKeybinds().some(
(entry) => entry.action === action.action && !isBuiltinDisableMarker(entry),
),
builtinDisabled: Keybind.isBuiltinDisabled(action.action),
}));
const noneHaveCustom = perAction.every((p) => !p.hasCustomBinding);
const anyBuiltinDisabled = perAction.some((p) => p.builtinDisabled);
const allBuiltinDisabled = perAction.every((p) => p.builtinDisabled);
const globalAction =
actions.length === 1 && Keybind.isActionGlobalCapable(actions[0].action) ? actions[0].action : null;
actions.length === 1 && !anyBuiltinDisabled && Keybind.isActionGlobalCapable(actions[0].action)
? actions[0].action
: null;
const reEnableGroup = anyBuiltinDisabled ? (
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.re-enable-menu-group">
<MenuItem
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon--3" />}
onClick={() => {
onClose();
for (const {action} of perAction) {
Keybind.enableBuiltinForAction(action.action);
}
}}
data-flx="user.keybinds-tab.open-row-menu.menu-item.re-enable-built-in"
>
{i18n._(RE_ENABLE_BUILT_IN_SHORTCUT_DESCRIPTOR)}
</MenuItem>
</MenuGroup>
) : null;
const globalGroup = globalAction ? (
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.global-menu-group">
<CheckboxItem
@@ -109,57 +133,56 @@ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActi
>
{i18n._(SET_CUSTOM_SHORTCUT_DESCRIPTOR)}
</MenuItem>
<MenuItem
icon={<HideIcon size={16} data-flx="user.keybinds-tab.open-row-menu.hide-icon--2" />}
onClick={() => {
onClose();
for (const {action} of perAction) {
Keybind.addCustomKeybindForAction(action.action);
}
}}
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--4"
>
{i18n._(DISABLE_BUILT_IN_SHORTCUT_DESCRIPTOR)}
</MenuItem>
</MenuGroup>
);
}
if (allHaveCustom && perAction.length === 1) {
const {action, hasEnabledCustomBinding} = perAction[0];
return (
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--3">
{hasEnabledCustomBinding ? (
{allBuiltinDisabled ? null : (
<MenuItem
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon" />}
danger
icon={<HideIcon size={16} data-flx="user.keybinds-tab.open-row-menu.hide-icon--2" />}
onClick={() => {
onClose();
Keybind.removeCustomKeybindsForAction(action.action);
for (const {action} of perAction) {
Keybind.disableBuiltinForAction(action.action);
}
}}
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--5"
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--4"
>
{i18n._(RESET_TO_BUILT_IN_DESCRIPTOR)}
</MenuItem>
) : (
<MenuItem
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--3" />}
onClick={() => {
onClose();
Keybind.addCustomKeybindForAction(action.action);
}}
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--6"
>
{i18n._(ADD_ANOTHER_CUSTOM_SHORTCUT_DESCRIPTOR)}
{i18n._(DISABLE_BUILT_IN_SHORTCUT_DESCRIPTOR)}
</MenuItem>
)}
</MenuGroup>
);
}
if (perAction.length === 1) {
const {action} = perAction[0];
return (
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--3">
<MenuItem
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon" />}
danger
onClick={() => {
onClose();
Keybind.removeCustomKeybindsForAction(action.action);
}}
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--5"
>
{i18n._(RESET_TO_BUILT_IN_DESCRIPTOR)}
</MenuItem>
<MenuItem
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--3" />}
onClick={() => {
onClose();
Keybind.addCustomKeybindForAction(action.action);
}}
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--6"
>
{i18n._(ADD_ANOTHER_CUSTOM_SHORTCUT_DESCRIPTOR)}
</MenuItem>
</MenuGroup>
);
}
return (
<>
{perAction.map(({action, label, customBindings, hasEnabledCustomBinding}) => (
{perAction.map(({action, label, hasCustomBinding}) => (
<MenuGroup key={action.action} data-flx="user.keybinds-tab.open-row-menu.menu-group--4">
{customBindings.length > 0 && hasEnabledCustomBinding ? (
{hasCustomBinding ? (
<MenuItem
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon--2" />}
danger
@@ -191,6 +214,7 @@ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActi
return (
<>
{globalGroup}
{reEnableGroup}
{renderBody()}
</>
);
@@ -213,11 +237,13 @@ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActi
<DefaultShortcutChipList
chips={chipsForDefaultEntry(i18n, a)}
overridden={overriddenActions.has(a.action)}
disabled={disabledActions.has(a.action)}
data-flx="user.keybinds-tab.default-shortcut-row.default-shortcut-chip-list"
/>
<DefaultShortcutChipList
chips={chipsForDefaultEntry(i18n, b)}
overridden={overriddenActions.has(b.action)}
disabled={disabledActions.has(b.action)}
data-flx="user.keybinds-tab.default-shortcut-row.default-shortcut-chip-list--2"
/>
</div>
@@ -250,6 +276,7 @@ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActi
<DefaultShortcutChipList
chips={chipsForDefaultEntry(i18n, entry)}
overridden={overriddenActions.has(entry.action)}
disabled={disabledActions.has(entry.action)}
data-flx="user.keybinds-tab.default-shortcut-row.default-shortcut-chip-list--3"
/>
<button