fix(app): default voice shortcuts to global and add a toggle (#2097)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-29 22:46:30 +02:00
1 parent dbc63e9ef7
commit 04d3afaf7b
4 files changed
+155 -76

No files matched your search

@@ -381,6 +381,12 @@ class KeybindManager {
private async checkInputMonitoringPermission(): Promise<boolean> { private async checkInputMonitoringPermission(): Promise<boolean> {
if (!isNativeMacOS()) return true; if (!isNativeMacOS()) return true;
if (this.inputMonitoringHookStatus === 'granted') 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'}); const result = await ensureMacPermission('input-monitoring', {behavior: 'passive'});
switch (result) { switch (result) {
case 'granted': case 'granted':
@@ -82,6 +82,7 @@ import {
ZOOM_IN_DESCRIPTOR, ZOOM_IN_DESCRIPTOR,
ZOOM_OUT_DESCRIPTOR, ZOOM_OUT_DESCRIPTOR,
} from '@app/features/input/state/input_keybind/shared'; } 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 {awaitHydration, makePersistent} from '@app/features/platform/utils/MobXPersistence';
import {makeSyncedField} from '@app/features/user/state/SyncedField'; import {makeSyncedField} from '@app/features/user/state/SyncedField';
import UserSettings from '@app/features/user/state/UserSettings'; import UserSettings from '@app/features/user/state/UserSettings';
@@ -285,6 +286,8 @@ export interface CustomKeybindEntry {
} }
const STORE_VERSION = 5 as const; const STORE_VERSION = 5 as const;
const GLOBAL_KEYBIND_DEFAULT_MIGRATION_KEY = 'Keybind:globalDefaultMigration:v1';
const generateId = (): string => { const generateId = (): string => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID(); return crypto.randomUUID();
@@ -748,7 +751,7 @@ const getDefaultKeybinds = (
{ {
action: 'voice_toggle_mute', action: 'voice_toggle_mute',
label: i18n._(TOGGLE_MUTE_DESCRIPTOR), 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, allowGlobal: true,
assignable: true, assignable: true,
section: 'voice_and_video', section: 'voice_and_video',
@@ -756,7 +759,7 @@ const getDefaultKeybinds = (
{ {
action: 'voice_toggle_deafen', action: 'voice_toggle_deafen',
label: i18n._(VOICE_TOGGLE_DEAFEN_DESCRIPTOR), 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, allowGlobal: true,
assignable: true, assignable: true,
section: 'voice_and_video', 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 { setI18n(i18n: I18n): void {
this.i18n = i18n; this.i18n = i18n;
if (this.initialized) return; if (this.initialized) return;
@@ -1034,6 +1049,7 @@ class Keybind {
if (!Array.isArray(this.customKeybinds)) { if (!Array.isArray(this.customKeybinds)) {
this.customKeybinds = []; this.customKeybinds = [];
} }
this.migrateGlobalCapableKeybindsToGlobal();
this.initialized = true; this.initialized = true;
}); });
}); });
@@ -1200,6 +1216,21 @@ class Keybind {
return created; 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 { setTransmitMode(mode: TransmitMode): void {
runInAction(() => { runInAction(() => {
this.transmitMode = mode; this.transmitMode = mode;
@@ -7,6 +7,7 @@ import Keybind, {
type KeyCombo, type KeyCombo,
} from '@app/features/input/state/InputKeybind'; } from '@app/features/input/state/InputKeybind';
import {remFromPx} from '@app/features/theme/layout/RemFromPx'; 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 {MoreOptionsVerticalIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup'; import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
import {MenuItem} from '@app/features/ui/action_menu/MenuItem'; import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
@@ -68,19 +69,33 @@ export const CustomKeybindRow = observer(
}; };
const openRowMenu = (event: React.MouseEvent<HTMLButtonElement>) => { const openRowMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
ContextMenuCommands.openFromEvent(event, ({onClose}) => ( ContextMenuCommands.openFromEvent(event, ({onClose}) => (
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group"> <>
<MenuItem {entry.action && Keybind.isActionGlobalCapable(entry.action) ? (
icon={<TrashIcon size={remFromPx(16)} data-flx="user.keybinds-tab.open-row-menu.trash-icon" />} <MenuGroup data-flx="user.keybinds-tab.open-row-menu.global-menu-group">
danger <CheckboxItem
onClick={() => { checked={entry.combo.global === true}
onClose(); onCheckedChange={(value) => Keybind.updateCustomKeybindCombo(entry.id, {...entry.combo, global: value})}
onRemove(); closeOnChange
}} data-flx="user.keybinds-tab.open-row-menu.checkbox-item.global"
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--2" >
> <Trans>Global shortcut</Trans>
{i18n._(DELETE_SHORTCUT_DESCRIPTOR)} </CheckboxItem>
</MenuItem> </MenuGroup>
</MenuGroup> ) : null}
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group">
<MenuItem
icon={<TrashIcon size={remFromPx(16)} data-flx="user.keybinds-tab.open-row-menu.trash-icon" />}
danger
onClick={() => {
onClose();
onRemove();
}}
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--2"
>
{i18n._(DELETE_SHORTCUT_DESCRIPTOR)}
</MenuItem>
</MenuGroup>
</>
)); ));
}; };
return ( return (
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import Keybind, {type KeybindCommand} from '@app/features/input/state/InputKeybind'; import Keybind, {type KeybindCommand} from '@app/features/input/state/InputKeybind';
import {CheckboxItem} from '@app/features/ui/action_menu/ContextMenu';
import { import {
EditSimpleIcon, EditSimpleIcon,
HideIcon, HideIcon,
@@ -53,6 +54,10 @@ const MORE_SHORTCUT_OPTIONS_DESCRIPTOR = msg({
message: 'More shortcut options', message: 'More shortcut options',
comment: 'Short label in the keybinds tab. Keep it concise.', 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<KeybindCommand>}> = ({ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActions: ReadonlySet<KeybindCommand>}> = ({
row, row,
overriddenActions, overriddenActions,
@@ -74,97 +79,119 @@ export const DefaultShortcutRow: React.FC<{row: ShortcutRowModel; overriddenActi
}); });
const allHaveCustom = perAction.every((p) => p.customBindings.length > 0); const allHaveCustom = perAction.every((p) => p.customBindings.length > 0);
const noneHaveCustom = perAction.every((p) => p.customBindings.length === 0); const noneHaveCustom = perAction.every((p) => p.customBindings.length === 0);
if (noneHaveCustom) { const globalAction =
return ( actions.length === 1 && Keybind.isActionGlobalCapable(actions[0].action) ? actions[0].action : null;
<MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--2"> const globalGroup = globalAction ? (
<MenuItem <MenuGroup data-flx="user.keybinds-tab.open-row-menu.global-menu-group">
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--2" />} <CheckboxItem
onClick={() => { checked={Keybind.isActionGlobal(globalAction)}
onClose(); onCheckedChange={(value) => Keybind.setActionGlobal(globalAction, value)}
for (const {action} of perAction) { closeOnChange
Keybind.addCustomKeybindForAction(action.action); data-flx="user.keybinds-tab.open-row-menu.checkbox-item.global"
} >
}} {i18n._(GLOBAL_SHORTCUT_DESCRIPTOR)}
data-flx="user.keybinds-tab.open-row-menu.menu-item.close--3" </CheckboxItem>
> </MenuGroup>
{i18n._(SET_CUSTOM_SHORTCUT_DESCRIPTOR)} ) : null;
</MenuItem> const renderBody = () => {
<MenuItem if (noneHaveCustom) {
icon={<HideIcon size={16} data-flx="user.keybinds-tab.open-row-menu.hide-icon--2" />} return (
onClick={() => { <MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--2">
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 ? (
<MenuItem <MenuItem
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon" />} icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--2" />}
danger
onClick={() => { onClick={() => {
onClose(); 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)}
</MenuItem> </MenuItem>
) : (
<MenuItem <MenuItem
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--3" />} icon={<HideIcon size={16} data-flx="user.keybinds-tab.open-row-menu.hide-icon--2" />}
onClick={() => { onClick={() => {
onClose(); 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)}
</MenuItem> </MenuItem>
)} </MenuGroup>
</MenuGroup> );
); }
} if (allHaveCustom && perAction.length === 1) {
return ( const {action, hasEnabledCustomBinding} = perAction[0];
<> return (
{perAction.map(({action, label, customBindings, hasEnabledCustomBinding}) => ( <MenuGroup data-flx="user.keybinds-tab.open-row-menu.menu-group--3">
<MenuGroup key={action.action} data-flx="user.keybinds-tab.open-row-menu.menu-group--4"> {hasEnabledCustomBinding ? (
{customBindings.length > 0 && hasEnabledCustomBinding ? (
<MenuItem <MenuItem
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon--2" />} icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon" />}
danger danger
onClick={() => { onClick={() => {
onClose(); onClose();
Keybind.removeCustomKeybindsForAction(action.action); 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)}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem <MenuItem
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--4" />} icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--3" />}
onClick={() => { onClick={() => {
onClose(); onClose();
Keybind.addCustomKeybindForAction(action.action); 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)}
</MenuItem> </MenuItem>
)} )}
</MenuGroup> </MenuGroup>
))} );
}
return (
<>
{perAction.map(({action, label, customBindings, hasEnabledCustomBinding}) => (
<MenuGroup key={action.action} data-flx="user.keybinds-tab.open-row-menu.menu-group--4">
{customBindings.length > 0 && hasEnabledCustomBinding ? (
<MenuItem
icon={<RetryIcon size={16} data-flx="user.keybinds-tab.open-row-menu.retry-icon--2" />}
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})}
</MenuItem>
) : (
<MenuItem
icon={<EditSimpleIcon size={16} data-flx="user.keybinds-tab.open-row-menu.edit-simple-icon--4" />}
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})}
</MenuItem>
)}
</MenuGroup>
))}
</>
);
};
return (
<>
{globalGroup}
{renderBody()}
</> </>
); );
}); });