mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 16:16:49 +00:00
fix: give "Show the app" a key the composer doesn't use, and refuse the app's own keys for global shortcuts
This commit is contained in:
@@ -5,16 +5,10 @@ import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getDesktopBridge } from "@/lib/desktop/bridge";
|
||||
import type { DesktopOsDefaultAppStatus, DesktopOsSettings as Settings, DesktopOsStatus } from "@/lib/desktop/types";
|
||||
import { describeAccelerator, showShortcutDefault } from "@/lib/desktop/shortcut-keys";
|
||||
import { DesktopQuickComposeSetting } from "./desktop-quick-compose-setting";
|
||||
import { SettingItem, SettingsSection, ToggleSwitch } from "./settings-section";
|
||||
|
||||
/** On Windows and Linux this is what a press of the keys shows; macOS maps it to Cmd. */
|
||||
const SHOW_SHORTCUT = "CommandOrControl+Shift+B";
|
||||
|
||||
function shortcutLabel(platform: string | undefined): string {
|
||||
return platform === "darwin" ? "⇧⌘B" : "Ctrl+Shift+B";
|
||||
}
|
||||
|
||||
/**
|
||||
* The desktop app's own settings: only where there's an app to set. A browser never sees
|
||||
* them, and they render nothing until the bridge has answered. The General page has how
|
||||
@@ -46,6 +40,8 @@ export function DesktopOsSettings({ part = "app" }: { part?: "app" | "notificati
|
||||
|
||||
if (!os || !status) return null;
|
||||
const { settings } = status;
|
||||
const platform = bridge?.platform;
|
||||
const showKeys = describeAccelerator(settings.showShortcut ?? showShortcutDefault(platform), platform);
|
||||
const change = (patch: Partial<Settings>) => {
|
||||
void os.setSettings(patch).then(setStatus);
|
||||
};
|
||||
@@ -103,12 +99,12 @@ export function DesktopOsSettings({ part = "app" }: { part?: "app" | "notificati
|
||||
<SettingItem
|
||||
label={t("show_shortcut_label")}
|
||||
description={settings.showShortcut && !status.shortcutActive
|
||||
? t("show_shortcut_taken", { keys: shortcutLabel(bridge?.platform) })
|
||||
: t("show_shortcut_description", { keys: shortcutLabel(bridge?.platform) })}
|
||||
? t("show_shortcut_taken", { keys: showKeys })
|
||||
: t("show_shortcut_description", { keys: showKeys })}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={!!settings.showShortcut}
|
||||
onChange={(on) => change({ showShortcut: on ? SHOW_SHORTCUT : null })}
|
||||
onChange={(on) => change({ showShortcut: on ? showShortcutDefault(platform) : null })}
|
||||
/>
|
||||
</SettingItem>
|
||||
{status.quickCompose && (
|
||||
|
||||
@@ -431,6 +431,17 @@ describe('the settings section', () => {
|
||||
await waitFor(() => expect(os.openDefaultAppSettings).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('switching "Show the app" on takes a key the composer doesn\'t use', async () => {
|
||||
const { DesktopOsSettings } = await import('@/components/settings/desktop-os-settings');
|
||||
const os = bridgeFor(status());
|
||||
setBridge(os as never);
|
||||
render(<DesktopOsSettings />);
|
||||
expect(await screen.findByText('show_shortcut_label')).toBeInTheDocument();
|
||||
// Close to tray, start with the computer, then this one.
|
||||
fireEvent.click(screen.getAllByRole('switch')[2]);
|
||||
await waitFor(() => expect(os.setSettings).toHaveBeenCalledWith({ showShortcut: 'CommandOrControl+Alt+F11' }));
|
||||
});
|
||||
|
||||
it('says when another app holds the shortcut', async () => {
|
||||
const { DesktopOsSettings } = await import('@/components/settings/desktop-os-settings');
|
||||
setBridge(bridgeFor(status({ showShortcut: 'CommandOrControl+Shift+B' })) as never);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describeAccelerator, readShortcutPress, shortcutProblem, type ShortcutKeyEvent } from '../shortcut-keys';
|
||||
import { describeAccelerator, readShortcutPress, shortcutProblem, showShortcutDefault, type ShortcutKeyEvent } from '../shortcut-keys';
|
||||
|
||||
const press = (key: string, code: string, modifiers: Partial<ShortcutKeyEvent> = {}): ShortcutKeyEvent => ({
|
||||
key,
|
||||
@@ -108,4 +108,17 @@ describe('shortcutProblem', () => {
|
||||
expect(shortcutProblem('CommandOrControl+Control+N', 'win32')).toBe('incomplete');
|
||||
expect(shortcutProblem('CommandOrControl+Control+N', 'darwin')).toBeNull();
|
||||
});
|
||||
|
||||
it('refuses the app\'s own keys: a global one would take them from the composer and the menus', () => {
|
||||
for (const keys of ['CommandOrControl+Shift+B', 'CommandOrControl+Shift+C', 'CommandOrControl+Shift+Enter', 'CommandOrControl+Shift+M', 'CommandOrControl+Shift+N', 'CommandOrControl+Shift+S', 'CommandOrControl+Shift+R', 'CommandOrControl+Shift+F']) {
|
||||
for (const platform of ['win32', 'darwin', 'linux']) expect(shortcutProblem(keys, platform), `${keys} on ${platform}`).toBe('reserved');
|
||||
}
|
||||
expect(readShortcutPress(press('B', 'KeyB', { ctrlKey: true, shiftKey: true }), 'win32')).toEqual({ refused: 'reserved' });
|
||||
});
|
||||
|
||||
it('"Show the app" by default is none of them, and on Windows types nothing with AltGr', () => {
|
||||
for (const platform of ['win32', 'darwin', 'linux']) expect(shortcutProblem(showShortcutDefault(platform), platform)).toBeNull();
|
||||
expect(showShortcutDefault('win32')).toBe('CommandOrControl+Alt+F11');
|
||||
expect(showShortcutDefault('linux')).toBe('CommandOrControl+Alt+B');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
*/
|
||||
export const QUICK_COMPOSE_DEFAULT = 'CommandOrControl+Alt+N';
|
||||
|
||||
/**
|
||||
* What "Show the app with a keyboard shortcut" takes when switched on: Ctrl+Alt+B (⌥⌘B), and
|
||||
* on Windows Ctrl+Alt+F11, since Ctrl+Alt is AltGr there and B types on many layouts. The
|
||||
* shell's showShortcutBuiltIn, which also moves anyone still on the old Ctrl+Shift+B to it.
|
||||
*/
|
||||
export function showShortcutDefault(platform: string | undefined): string {
|
||||
return platform === 'win32' ? 'CommandOrControl+Alt+F11' : 'CommandOrControl+Alt+B';
|
||||
}
|
||||
|
||||
const MAC_SYMBOLS: Record<string, string> = {
|
||||
control: '⌃',
|
||||
ctrl: '⌃',
|
||||
@@ -67,6 +76,12 @@ export interface ShortcutKeyEvent {
|
||||
* shell's. `cmd` is Cmd on a Mac and Ctrl elsewhere.
|
||||
*/
|
||||
const RESERVED = new Set(['cmd+shift+A', 'cmd+shift+C', 'cmd+shift+V', 'cmd+shift+X', 'cmd+shift+Z']);
|
||||
/**
|
||||
* The app's own keys with two modifiers, which a global key would take from Bulwark itself:
|
||||
* the composer's Cc, Bcc and schedule send, and the menus' new message, new window, Save as
|
||||
* PDF, reply all and forward. Same list as APP_KEYS in the shell's os-rules.ts.
|
||||
*/
|
||||
const APP_KEYS = new Set(['cmd+shift+B', 'cmd+shift+C', 'cmd+shift+ENTER', 'cmd+shift+M', 'cmd+shift+N', 'cmd+shift+S', 'cmd+shift+R', 'cmd+shift+F']);
|
||||
const RESERVED_ON_MAC = new Set([
|
||||
'cmd+shift+3', 'cmd+shift+4', 'cmd+shift+5', 'cmd+shift+Q',
|
||||
'cmd+ctrl+Q', 'cmd+ctrl+F',
|
||||
@@ -119,7 +134,7 @@ export function shortcutProblem(accelerator: string, platform: string | undefine
|
||||
if (modifiers.has('super')) return 'reserved';
|
||||
if (modifiers.size < 2 || !(modifiers.has('cmd') || modifiers.has('ctrl'))) return 'incomplete';
|
||||
const named = `${[...modifiers].sort().join('+')}+${key}`;
|
||||
return RESERVED.has(named) || (mac && RESERVED_ON_MAC.has(named)) ? 'reserved' : null;
|
||||
return RESERVED.has(named) || APP_KEYS.has(`${[...modifiers].sort().join('+')}+${key.toUpperCase()}`) || (mac && RESERVED_ON_MAC.has(named)) ? 'reserved' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user