Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -398,7 +399,7 @@ export function AppShell() {
}

function handleKeyDown(event: KeyboardEvent) {
if (!(event.metaKey || event.ctrlKey) || event.altKey) {
if (!hasPrimaryShortcutModifier(event) || event.altKey) {
return;
}

Expand Down Expand Up @@ -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;

Expand Down
5 changes: 1 addition & 4 deletions desktop/src/app/navigation/useBackForwardControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useRouterState,
} from "@tanstack/react-router";

import { isMacPlatform } from "@/shared/lib/platform";
import { trimMapToSize } from "@/shared/lib/trimMapToSize";

type RouterHistoryState = {
Expand All @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion desktop/src/app/useWebviewZoomShortcuts.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}

Expand Down
55 changes: 53 additions & 2 deletions desktop/src/features/messages/lib/useRichTextEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down
5 changes: 3 additions & 2 deletions desktop/src/features/search/useChannelFind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 3 additions & 5 deletions desktop/src/shared/lib/keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isMacPlatform } from "@/shared/lib/platform";

export type ShortcutCategory =
| "Navigation"
| "Messages"
Expand Down Expand Up @@ -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 changes: 31 additions & 0 deletions desktop/src/shared/lib/platform.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 2 additions & 1 deletion desktop/src/shared/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down
Loading