mirror of
https://github.com/block/buzz.git
synced 2026-11-08 02:11:36 +00:00
Use platform-native app shortcuts on macOS (#521)
This commit is contained in:
@@ -52,6 +52,7 @@ import { useDeferredStartup } from "@/shared/hooks/useDeferredStartup";
|
||||
import { joinChannel } from "@/shared/api/tauri";
|
||||
import type { Channel, RelayEvent, SearchHit } from "@/shared/api/types";
|
||||
import { ChannelNavigationProvider } from "@/shared/context/ChannelNavigationContext";
|
||||
import { hasPrimaryShortcutModifier } from "@/shared/lib/platform";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
SidebarInset,
|
||||
@@ -398,7 +399,7 @@ export function AppShell() {
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (!(event.metaKey || event.ctrlKey) || event.altKey) {
|
||||
if (!hasPrimaryShortcutModifier(event) || event.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -444,7 +445,7 @@ export function AppShell() {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
const isSettingsShortcut =
|
||||
(event.key === "," || event.code === "Comma") &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
hasPrimaryShortcutModifier(event) &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useRouterState,
|
||||
} from "@tanstack/react-router";
|
||||
|
||||
import { isMacPlatform } from "@/shared/lib/platform";
|
||||
import { trimMapToSize } from "@/shared/lib/trimMapToSize";
|
||||
|
||||
type RouterHistoryState = {
|
||||
@@ -26,10 +27,6 @@ function isEditableTarget(target: EventTarget | null): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isMacPlatform() {
|
||||
return window.navigator.platform.toLowerCase().includes("mac");
|
||||
}
|
||||
|
||||
export function useBackForwardControls() {
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { getCurrentWebview } from "@tauri-apps/api/webview";
|
||||
|
||||
import { hasPrimaryShortcutModifier } from "@/shared/lib/platform";
|
||||
|
||||
const DEFAULT_ZOOM_FACTOR = 1;
|
||||
const MIN_ZOOM_FACTOR = 0.2;
|
||||
const MAX_ZOOM_FACTOR = 10;
|
||||
@@ -9,7 +11,7 @@ const ZOOM_STEP = 0.2;
|
||||
type ZoomAction = "increase" | "decrease" | "reset";
|
||||
|
||||
function getZoomAction(event: KeyboardEvent): ZoomAction | null {
|
||||
if (!(event.metaKey || event.ctrlKey) || event.altKey) {
|
||||
if (!hasPrimaryShortcutModifier(event) || event.altKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import { useEditor, type Editor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { TextSelection } from "@tiptap/pm/state";
|
||||
import { Extension, type KeyboardShortcutCommand } from "@tiptap/core";
|
||||
import { Selection, TextSelection } from "@tiptap/pm/state";
|
||||
|
||||
import { isMacPlatform } from "@/shared/lib/platform";
|
||||
|
||||
import {
|
||||
MentionHighlightExtension,
|
||||
@@ -73,6 +75,55 @@ export function useRichTextEditor({
|
||||
// below with custom options (autolink, openOnClick, etc.).
|
||||
link: false,
|
||||
}),
|
||||
// macOS text fields traditionally support a small set of Emacs-style
|
||||
// Control shortcuts. ProseMirror already handles Ctrl-A/E/H/D on macOS;
|
||||
// these fill in the common movement and kill-line gaps for the composer.
|
||||
Extension.create({
|
||||
name: "macEmacsTextShortcuts",
|
||||
addKeyboardShortcuts() {
|
||||
const shortcuts: Record<string, KeyboardShortcutCommand> = {};
|
||||
if (!isMacPlatform()) {
|
||||
return shortcuts;
|
||||
}
|
||||
|
||||
return {
|
||||
"Ctrl-b": ({ editor: ed }) => {
|
||||
const { empty, from } = ed.state.selection;
|
||||
if (!empty || from <= 0) return false;
|
||||
return ed.commands.setTextSelection(from - 1);
|
||||
},
|
||||
"Ctrl-f": ({ editor: ed }) => {
|
||||
const { empty, from } = ed.state.selection;
|
||||
if (!empty || from >= ed.state.doc.content.size) return false;
|
||||
return ed.commands.setTextSelection(from + 1);
|
||||
},
|
||||
"Ctrl-k": ({ editor: ed }) => {
|
||||
const { state, view } = ed;
|
||||
const { $from, empty, from, to } = state.selection;
|
||||
|
||||
if (!empty) {
|
||||
return ed.commands.deleteSelection();
|
||||
}
|
||||
|
||||
const blockEnd = $from.end();
|
||||
if (from < blockEnd) {
|
||||
return ed.commands.deleteRange({ from, to: blockEnd });
|
||||
}
|
||||
|
||||
const nextSelection = Selection.findFrom(
|
||||
state.doc.resolve(to),
|
||||
1,
|
||||
true,
|
||||
);
|
||||
if (!nextSelection) return false;
|
||||
|
||||
const transaction = state.tr.delete(to, nextSelection.from);
|
||||
view.dispatch(transaction.scrollIntoView());
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
// Shift+Enter inside lists/blockquotes: split the node instead of
|
||||
// inserting a hard break so continuation lines keep their formatting.
|
||||
Extension.create({
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { useSearchMessagesQuery } from "@/features/search/hooks";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import { hasPrimaryShortcutModifier } from "@/shared/lib/platform";
|
||||
|
||||
const MIN_QUERY_LENGTH = 2;
|
||||
const DEBOUNCE_MS = 300;
|
||||
@@ -119,11 +120,11 @@ export function useChannelFind({ channelId, messages }: UseChannelFindOptions) {
|
||||
);
|
||||
}, [matchedIds.length]);
|
||||
|
||||
// Register CMD+F keyboard shortcut.
|
||||
// Register platform-standard find shortcut (⌘F on macOS, Ctrl+F elsewhere).
|
||||
React.useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
hasPrimaryShortcutModifier(event) &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
event.key.toLowerCase() === "f"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isMacPlatform } from "@/shared/lib/platform";
|
||||
|
||||
export type ShortcutCategory =
|
||||
| "Navigation"
|
||||
| "Messages"
|
||||
@@ -220,10 +222,6 @@ export function getShortcutsByCategory(): Map<
|
||||
return map;
|
||||
}
|
||||
|
||||
function isMac(): boolean {
|
||||
return /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
export function getPlatformKeys(shortcut: KeyboardShortcut): string {
|
||||
return isMac() ? shortcut.keys : shortcut.keysWindows;
|
||||
return isMacPlatform() ? shortcut.keys : shortcut.keysWindows;
|
||||
}
|
||||
|
||||
31
desktop/src/shared/lib/platform.ts
Normal file
31
desktop/src/shared/lib/platform.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
type ModifierKeyboardEvent = Pick<
|
||||
KeyboardEvent,
|
||||
"altKey" | "ctrlKey" | "metaKey" | "shiftKey"
|
||||
>;
|
||||
|
||||
/** Returns true on macOS/iOS-style Apple platforms. */
|
||||
export function isMacPlatform(): boolean {
|
||||
if (typeof navigator === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* The platform's normal application-shortcut modifier:
|
||||
* - macOS: Command (Meta)
|
||||
* - Windows/Linux: Control
|
||||
*
|
||||
* On macOS this intentionally rejects Control so native Emacs-style text
|
||||
* editing shortcuts (Ctrl-A/E/B/F/K/etc.) are left available to text fields.
|
||||
*/
|
||||
export function hasPrimaryShortcutModifier(
|
||||
event: ModifierKeyboardEvent,
|
||||
): boolean {
|
||||
if (isMacPlatform()) {
|
||||
return event.metaKey && !event.ctrlKey;
|
||||
}
|
||||
|
||||
return event.ctrlKey && !event.metaKey;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { hasPrimaryShortcutModifier } from "@/shared/lib/platform";
|
||||
import { useIsMobile } from "@/shared/hooks/use-mobile";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
@@ -105,7 +106,7 @@ const SidebarProvider = React.forwardRef<
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
hasPrimaryShortcutModifier(event)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
|
||||
Reference in New Issue
Block a user