From 04d3afaf7b8276e23d90e79c3db25e57ee630412 Mon Sep 17 00:00:00 2001 From: Hampus Date: Sat, 29 Aug 2026 22:46:30 +0200 Subject: [PATCH] fix(app): default voice shortcuts to global and add a toggle (#2097) --- .../app/keybindings/KeybindManager.ts | 6 + .../src/features/input/state/InputKeybind.ts | 35 +++- .../tabs/keybinds_tab/CustomKeybindRow.tsx | 41 +++-- .../tabs/keybinds_tab/DefaultShortcutRow.tsx | 149 +++++++++++------- 4 files changed, 155 insertions(+), 76 deletions(-) diff --git a/fluxer_app/src/features/app/keybindings/KeybindManager.ts b/fluxer_app/src/features/app/keybindings/KeybindManager.ts index 604f6f0ac..cb3420940 100644 --- a/fluxer_app/src/features/app/keybindings/KeybindManager.ts +++ b/fluxer_app/src/features/app/keybindings/KeybindManager.ts @@ -381,6 +381,12 @@ class KeybindManager { private async checkInputMonitoringPermission(): Promise { if (!isNativeMacOS()) return true; if (this.inputMonitoringHookStatus === 'granted') return true; + const electronApi = getElectronAPI(); + if (await electronApi?.checkInputMonitoringAccess?.()) { + NativePermission.setInputMonitoringStatus('granted'); + this.inputMonitoringHookStatus = 'granted'; + return true; + } const result = await ensureMacPermission('input-monitoring', {behavior: 'passive'}); switch (result) { case 'granted': diff --git a/fluxer_app/src/features/input/state/InputKeybind.ts b/fluxer_app/src/features/input/state/InputKeybind.ts index 761bb4021..e68670d2b 100644 --- a/fluxer_app/src/features/input/state/InputKeybind.ts +++ b/fluxer_app/src/features/input/state/InputKeybind.ts @@ -82,6 +82,7 @@ import { ZOOM_IN_DESCRIPTOR, ZOOM_OUT_DESCRIPTOR, } from '@app/features/input/state/input_keybind/shared'; +import AppStorage from '@app/features/platform/state/PersistentStorage'; import {awaitHydration, makePersistent} from '@app/features/platform/utils/MobXPersistence'; import {makeSyncedField} from '@app/features/user/state/SyncedField'; import UserSettings from '@app/features/user/state/UserSettings'; @@ -285,6 +286,8 @@ export interface CustomKeybindEntry { } const STORE_VERSION = 5 as const; + +const GLOBAL_KEYBIND_DEFAULT_MIGRATION_KEY = 'Keybind:globalDefaultMigration:v1'; const generateId = (): string => { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); @@ -748,7 +751,7 @@ const getDefaultKeybinds = ( { action: 'voice_toggle_mute', label: i18n._(TOGGLE_MUTE_DESCRIPTOR), - combo: {key: 'm', ctrlOrMeta: true, shift: true, global: false, enabled: true}, + combo: {key: 'm', ctrlOrMeta: true, shift: true, global: true, enabled: true}, allowGlobal: true, assignable: true, section: 'voice_and_video', @@ -756,7 +759,7 @@ const getDefaultKeybinds = ( { action: 'voice_toggle_deafen', label: i18n._(VOICE_TOGGLE_DEAFEN_DESCRIPTOR), - combo: {key: 'd', ctrlOrMeta: true, shift: true, global: false, enabled: true}, + combo: {key: 'd', ctrlOrMeta: true, shift: true, global: true, enabled: true}, allowGlobal: true, assignable: true, section: 'voice_and_video', @@ -1025,6 +1028,18 @@ class Keybind { }); } + private migrateGlobalCapableKeybindsToGlobal(): void { + if (AppStorage.getItem(GLOBAL_KEYBIND_DEFAULT_MIGRATION_KEY) === '1') return; + for (const entry of this.customKeybinds) { + if (entry.action == null) continue; + const config = this.getDefaultByAction(entry.action); + if (config?.allowGlobal && entry.combo.global !== true) { + entry.combo = {...entry.combo, global: true}; + } + } + AppStorage.setItem(GLOBAL_KEYBIND_DEFAULT_MIGRATION_KEY, '1'); + } + setI18n(i18n: I18n): void { this.i18n = i18n; if (this.initialized) return; @@ -1034,6 +1049,7 @@ class Keybind { if (!Array.isArray(this.customKeybinds)) { this.customKeybinds = []; } + this.migrateGlobalCapableKeybindsToGlobal(); this.initialized = true; }); }); @@ -1200,6 +1216,21 @@ class Keybind { return created; } + isActionGlobalCapable(action: KeybindCommand): boolean { + return this.getDefaultByAction(action)?.allowGlobal === true; + } + + isActionGlobal(action: KeybindCommand): boolean { + const combo = this.getPrimaryCustomKeybind(action)?.combo ?? this.getDefaultByAction(action)?.combo; + return combo?.global === true; + } + + setActionGlobal(action: KeybindCommand, global: boolean): void { + const baseCombo = this.getPrimaryCustomKeybind(action)?.combo ?? + this.getDefaultByAction(action)?.combo ?? {key: ''}; + this.setPrimaryCustomKeybindCombo(action, {...baseCombo, global}); + } + setTransmitMode(mode: TransmitMode): void { runInAction(() => { this.transmitMode = mode; diff --git a/fluxer_app/src/features/user/components/modals/tabs/keybinds_tab/CustomKeybindRow.tsx b/fluxer_app/src/features/user/components/modals/tabs/keybinds_tab/CustomKeybindRow.tsx index b205313a4..0f5e4abad 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/keybinds_tab/CustomKeybindRow.tsx +++ b/fluxer_app/src/features/user/components/modals/tabs/keybinds_tab/CustomKeybindRow.tsx @@ -7,6 +7,7 @@ import Keybind, { type KeyCombo, } from '@app/features/input/state/InputKeybind'; import {remFromPx} from '@app/features/theme/layout/RemFromPx'; +import {CheckboxItem} from '@app/features/ui/action_menu/ContextMenu'; import {MoreOptionsVerticalIcon} from '@app/features/ui/action_menu/ContextMenuIcons'; import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup'; import {MenuItem} from '@app/features/ui/action_menu/MenuItem'; @@ -68,19 +69,33 @@ export const CustomKeybindRow = observer( }; const openRowMenu = (event: React.MouseEvent) => { ContextMenuCommands.openFromEvent(event, ({onClose}) => ( - - } - danger - onClick={() => { - onClose(); - onRemove(); - }} - data-flx="user.keybinds-tab.open-row-menu.menu-item.close--2" - > - {i18n._(DELETE_SHORTCUT_DESCRIPTOR)} - - + <> + {entry.action && Keybind.isActionGlobalCapable(entry.action) ? ( + + Keybind.updateCustomKeybindCombo(entry.id, {...entry.combo, global: value})} + closeOnChange + data-flx="user.keybinds-tab.open-row-menu.checkbox-item.global" + > + Global shortcut + + + ) : null} + + } + danger + onClick={() => { + onClose(); + onRemove(); + }} + data-flx="user.keybinds-tab.open-row-menu.menu-item.close--2" + > + {i18n._(DELETE_SHORTCUT_DESCRIPTOR)} + + + )); }; return ( diff --git a/fluxer_app/src/features/user/components/modals/tabs/keybinds_tab/DefaultShortcutRow.tsx b/fluxer_app/src/features/user/components/modals/tabs/keybinds_tab/DefaultShortcutRow.tsx index 0f056d529..4edca2802 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/keybinds_tab/DefaultShortcutRow.tsx +++ b/fluxer_app/src/features/user/components/modals/tabs/keybinds_tab/DefaultShortcutRow.tsx @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import Keybind, {type KeybindCommand} from '@app/features/input/state/InputKeybind'; +import {CheckboxItem} from '@app/features/ui/action_menu/ContextMenu'; import { EditSimpleIcon, HideIcon, @@ -53,6 +54,10 @@ const MORE_SHORTCUT_OPTIONS_DESCRIPTOR = msg({ message: 'More shortcut options', comment: 'Short label in the keybinds tab. Keep it concise.', }); +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}> = ({ row, overriddenActions, @@ -74,97 +79,119 @@ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActi }); const allHaveCustom = perAction.every((p) => p.customBindings.length > 0); const noneHaveCustom = perAction.every((p) => p.customBindings.length === 0); - if (noneHaveCustom) { - return ( - - } - onClick={() => { - onClose(); - for (const {action} of perAction) { - Keybind.addCustomKeybindForAction(action.action); - } - }} - data-flx="user.keybinds-tab.open-row-menu.menu-item.close--3" - > - {i18n._(SET_CUSTOM_SHORTCUT_DESCRIPTOR)} - - } - 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)} - - - ); - } - if (allHaveCustom && perAction.length === 1) { - const {action, hasEnabledCustomBinding} = perAction[0]; - return ( - - {hasEnabledCustomBinding ? ( + const globalAction = + actions.length === 1 && Keybind.isActionGlobalCapable(actions[0].action) ? actions[0].action : null; + const globalGroup = globalAction ? ( + + Keybind.setActionGlobal(globalAction, value)} + closeOnChange + data-flx="user.keybinds-tab.open-row-menu.checkbox-item.global" + > + {i18n._(GLOBAL_SHORTCUT_DESCRIPTOR)} + + + ) : null; + const renderBody = () => { + if (noneHaveCustom) { + return ( + } - danger + icon={} onClick={() => { onClose(); - Keybind.removeCustomKeybindsForAction(action.action); + for (const {action} of perAction) { + Keybind.addCustomKeybindForAction(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--3" > - {i18n._(RESET_TO_BUILT_IN_DESCRIPTOR)} + {i18n._(SET_CUSTOM_SHORTCUT_DESCRIPTOR)} - ) : ( } + icon={} onClick={() => { onClose(); - Keybind.addCustomKeybindForAction(action.action); + for (const {action} of perAction) { + Keybind.addCustomKeybindForAction(action.action); + } }} - data-flx="user.keybinds-tab.open-row-menu.menu-item.close--6" + data-flx="user.keybinds-tab.open-row-menu.menu-item.close--4" > - {i18n._(ADD_ANOTHER_CUSTOM_SHORTCUT_DESCRIPTOR)} + {i18n._(DISABLE_BUILT_IN_SHORTCUT_DESCRIPTOR)} - )} - - ); - } - return ( - <> - {perAction.map(({action, label, customBindings, hasEnabledCustomBinding}) => ( - - {customBindings.length > 0 && hasEnabledCustomBinding ? ( + + ); + } + if (allHaveCustom && perAction.length === 1) { + const {action, hasEnabledCustomBinding} = perAction[0]; + return ( + + {hasEnabledCustomBinding ? ( } + icon={} danger onClick={() => { onClose(); Keybind.removeCustomKeybindsForAction(action.action); }} - data-flx="user.keybinds-tab.open-row-menu.menu-item.close--7" + data-flx="user.keybinds-tab.open-row-menu.menu-item.close--5" > - {i18n._(RESET_TO_BUILT_IN_2_DESCRIPTOR, {label})} + {i18n._(RESET_TO_BUILT_IN_DESCRIPTOR)} ) : ( } + icon={} onClick={() => { onClose(); Keybind.addCustomKeybindForAction(action.action); }} - data-flx="user.keybinds-tab.open-row-menu.menu-item.close--8" + data-flx="user.keybinds-tab.open-row-menu.menu-item.close--6" > - {i18n._(SET_CUSTOM_SHORTCUT_FOR_DESCRIPTOR, {label})} + {i18n._(ADD_ANOTHER_CUSTOM_SHORTCUT_DESCRIPTOR)} )} - ))} + ); + } + return ( + <> + {perAction.map(({action, label, customBindings, hasEnabledCustomBinding}) => ( + + {customBindings.length > 0 && hasEnabledCustomBinding ? ( + } + danger + onClick={() => { + onClose(); + Keybind.removeCustomKeybindsForAction(action.action); + }} + data-flx="user.keybinds-tab.open-row-menu.menu-item.close--7" + > + {i18n._(RESET_TO_BUILT_IN_2_DESCRIPTOR, {label})} + + ) : ( + } + onClick={() => { + onClose(); + Keybind.addCustomKeybindForAction(action.action); + }} + data-flx="user.keybinds-tab.open-row-menu.menu-item.close--8" + > + {i18n._(SET_CUSTOM_SHORTCUT_FOR_DESCRIPTOR, {label})} + + )} + + ))} + + ); + }; + return ( + <> + {globalGroup} + {renderBody()} ); });