diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 1fa4b8add..15abb649d 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -908,17 +908,29 @@ function MarkdownLink({ } return ( - - {children} - + + + {children} + + ); } +const MARKDOWN_LINK_ANCHOR_STYLE: React.CSSProperties = { + display: "contents", + color: "inherit", + textDecoration: "none", +}; + +function preventAnchorNavigation(event: React.MouseEvent): void { + event.preventDefault(); +} + function getInlineCodeAutoLinkUrl( markdownParser: ReturnType, content: string, diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index c4c6f2fd2..009b00fd8 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -23,6 +23,7 @@ import { setupWindowResizeEvents, setupDefaultContextMenu, setupDragDropPrevention, + buildStandardContextMenuItems, } from "./window/window-manager.js"; import { registerDialogHandlers } from "./features/dialogs.js"; import { @@ -150,10 +151,7 @@ function showBrowserWebviewContextMenu( params: Electron.ContextMenuParams, ): void { const menu = Menu.buildFromTemplate([ - { role: "copy", enabled: params.selectionText.length > 0 }, - { role: "paste" }, - { type: "separator" }, - { role: "selectAll" }, + ...buildStandardContextMenuItems(contents, params), ...(app.isPackaged ? [] : [ diff --git a/packages/desktop/src/window/window-manager.ts b/packages/desktop/src/window/window-manager.ts index 35c1e1cb5..176ef6c90 100644 --- a/packages/desktop/src/window/window-manager.ts +++ b/packages/desktop/src/window/window-manager.ts @@ -1,4 +1,14 @@ -import { app, BrowserWindow, Menu, ipcMain, nativeTheme } from "electron"; +import { + app, + BrowserWindow, + Menu, + type MenuItemConstructorOptions, + type WebContents, + clipboard, + ipcMain, + nativeTheme, + shell, +} from "electron"; export function readBadgeCount(input: unknown): number { if (typeof input !== "number" || !Number.isSafeInteger(input) || input < 0) { @@ -272,14 +282,76 @@ export function setupDarwinPaintRefresh(win: BrowserWindow): void { }); } +export function buildStandardContextMenuItems( + contents: WebContents, + params: Electron.ContextMenuParams, +): MenuItemConstructorOptions[] { + const items: MenuItemConstructorOptions[] = []; + + if (params.misspelledWord) { + if (params.dictionarySuggestions.length > 0) { + for (const suggestion of params.dictionarySuggestions) { + items.push({ + label: suggestion, + click: () => contents.replaceMisspelling(suggestion), + }); + } + } else { + items.push({ label: "No suggestions", enabled: false }); + } + items.push({ type: "separator" }); + items.push({ + label: "Add to Dictionary", + click: () => contents.session.addWordToSpellCheckerDictionary(params.misspelledWord), + }); + items.push({ type: "separator" }); + } + + if (params.linkURL && /^https?:/i.test(params.linkURL)) { + items.push({ + label: "Open Link in Browser", + click: () => { + void shell.openExternal(params.linkURL); + }, + }); + items.push({ + label: "Copy Link Address", + click: () => clipboard.writeText(params.linkURL), + }); + items.push({ type: "separator" }); + } + + if (params.hasImageContents && params.srcURL) { + items.push({ + label: "Copy Image", + click: () => contents.copyImageAt(params.x, params.y), + }); + items.push({ + label: "Save Image As…", + click: () => contents.downloadURL(params.srcURL), + }); + items.push({ type: "separator" }); + } + + if (params.isEditable) { + items.push({ role: "cut", enabled: params.editFlags.canCut }); + items.push({ role: "copy", enabled: params.editFlags.canCopy }); + items.push({ role: "paste", enabled: params.editFlags.canPaste }); + items.push({ type: "separator" }); + items.push({ role: "selectAll" }); + } else { + items.push({ role: "copy", enabled: params.selectionText.length > 0 }); + items.push({ role: "paste" }); + items.push({ type: "separator" }); + items.push({ role: "selectAll" }); + } + + return items; +} + export function setupDefaultContextMenu(win: BrowserWindow): void { win.webContents.on("context-menu", (_event, params) => { - const menu = Menu.buildFromTemplate([ - { role: "copy", enabled: params.selectionText.length > 0 }, - { role: "paste" }, - { type: "separator" }, - { role: "selectAll" }, - ]); + const menu = Menu.buildFromTemplate(buildStandardContextMenuItems(win.webContents, params)); menu.popup({ window: win }); }); }