diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index b72bfd4dc..a50397145 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,11 @@ # Changelog +## [v2.24.1] + +### Fixes + +- Fixed Firefox web paste to use the browser's native clipboard shortcuts and context menu [#3349](https://github.com/Automattic/simplenote-electron/pull/3349) + ## [v2.24.0] ### New Features diff --git a/lib/note-content-editor.tsx b/lib/note-content-editor.tsx index f591075d2..5d64f7ba9 100644 --- a/lib/note-content-editor.tsx +++ b/lib/note-content-editor.tsx @@ -26,7 +26,7 @@ import { getNotePosition, setNotePosition, } from './utils/note-scroll-position'; -import { isMac, isSafari } from './utils/platform'; +import { useMonacoContextMenu, useMonacoPasteShortcut } from './utils/platform'; import { withCheckboxCharacters, withCheckboxSyntax, @@ -648,28 +648,30 @@ class NoteContentEditor extends Component { }, }); - /* remove unwanted context menu items */ - // see https://github.com/Microsoft/monaco-editor/issues/1058#issuecomment-468681208 - const idsToRemove = [ - 'editor.action.changeAll', - 'editor.action.quickCommand', - ]; + if (useMonacoContextMenu) { + /* remove unwanted context menu items */ + // see https://github.com/Microsoft/monaco-editor/issues/1058#issuecomment-468681208 + const idsToRemove = [ + 'editor.action.changeAll', + 'editor.action.quickCommand', + ]; - const contextmenu = this.editor.getContribution( - 'editor.contrib.contextmenu' - ); + const contextmenu = this.editor.getContribution( + 'editor.contrib.contextmenu' + ); - // @ts-ignore undocumented internals - const realMethod = contextmenu._getMenuActions; + // @ts-ignore undocumented internals + const realMethod = contextmenu._getMenuActions; - // @ts-ignore undocumented internals - contextmenu._getMenuActions = function (...args) { - const items = realMethod.apply(contextmenu, args); + // @ts-ignore undocumented internals + contextmenu._getMenuActions = function (...args) { + const items = realMethod.apply(contextmenu, args); - return items.filter(function (item: Editor.IActionDescriptor) { - return !idsToRemove.includes(item.id); - }); - }; + return items.filter(function (item: Editor.IActionDescriptor) { + return !idsToRemove.includes(item.id); + }); + }; + } // remove some default keybindings // @see https://github.com/microsoft/monaco-editor/issues/3623#issuecomment-1472578786 @@ -697,10 +699,8 @@ class NoteContentEditor extends Component { // disable editor keybindings for Electron since it is handled by editorCommand // doing it this way will always show the keyboard hint in the context menu! - editor.createContextKey( - 'allowBrowserKeybinding', - window.electron ? false : true - ); + const allowBrowserKeybinding = !window.electron; + editor.createContextKey('allowBrowserKeybinding', allowBrowserKeybinding); editor.addAction({ id: 'context_undo', @@ -729,8 +729,8 @@ class NoteContentEditor extends Component { }, }); - // re-add keybindings for cut/copy/paste so they show labels - monaco.editor.addKeybindingRules([ + // Firefox web is more reliable when the browser handles clipboard UI natively. + const clipboardKeybindingRules = [ { keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyX, command: 'editor.action.clipboardCutAction', @@ -741,12 +741,15 @@ class NoteContentEditor extends Component { command: 'editor.action.clipboardCopyAction', when: 'allowBrowserKeybinding && editorTextFocus', }, - { + ]; + if (useMonacoPasteShortcut) { + clipboardKeybindingRules.push({ keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyV, command: 'editor.action.clipboardPasteAction', when: 'allowBrowserKeybinding && editorTextFocus', - }, - ]); + }); + } + monaco.editor.addKeybindingRules(clipboardKeybindingRules); // cancel selection that bubbles up to clear any search terms editor.addAction({ @@ -1212,6 +1215,7 @@ class NoteContentEditor extends Component { // @ts-ignore, @see https://github.com/microsoft/monaco-editor/issues/3829 'bracketPairColorization.enabled': false, codeLens: false, + contextmenu: useMonacoContextMenu, folding: false, fontFamily: '"Simplenote Tasks", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif', diff --git a/lib/utils/platform.test.ts b/lib/utils/platform.test.ts new file mode 100644 index 000000000..cef5e69aa --- /dev/null +++ b/lib/utils/platform.test.ts @@ -0,0 +1,63 @@ +import { + canUseMonacoContextMenu, + canUseMonacoPasteShortcut, + isFirefoxUserAgent, + isSafariUserAgent, +} from './platform'; + +const firefoxDesktopUserAgent = + 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0'; + +const firefoxIosUserAgent = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/150.0 Mobile/15E148 Safari/605.1.15'; + +const chromeDesktopUserAgent = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36'; + +const safariDesktopUserAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15'; + +describe('platform user-agent detection', () => { + it('detects Firefox user agents', () => { + expect(isFirefoxUserAgent(firefoxDesktopUserAgent)).toBe(true); + }); + + it('detects Firefox for iOS user agents', () => { + expect(isFirefoxUserAgent(firefoxIosUserAgent)).toBe(true); + }); + + it('does not detect Chromium as Firefox', () => { + expect(isFirefoxUserAgent(chromeDesktopUserAgent)).toBe(false); + }); + + it('keeps Safari detection separate from Chromium', () => { + expect(isSafariUserAgent(safariDesktopUserAgent)).toBe(true); + expect(isSafariUserAgent(chromeDesktopUserAgent)).toBe(false); + }); + + it('disables Monaco paste shortcut in Firefox web mode', () => { + expect(canUseMonacoPasteShortcut(false, firefoxDesktopUserAgent)).toBe( + false + ); + }); + + it('keeps Monaco paste shortcut enabled in Electron mode', () => { + expect(canUseMonacoPasteShortcut(true, firefoxDesktopUserAgent)).toBe(true); + }); + + it('keeps Monaco paste shortcut enabled outside Firefox web mode', () => { + expect(canUseMonacoPasteShortcut(false, chromeDesktopUserAgent)).toBe(true); + }); + + it('disables Monaco context menu in Firefox web mode', () => { + expect(canUseMonacoContextMenu(false, firefoxDesktopUserAgent)).toBe(false); + }); + + it('keeps Monaco context menu enabled in Electron mode', () => { + expect(canUseMonacoContextMenu(true, firefoxDesktopUserAgent)).toBe(true); + }); + + it('keeps Monaco context menu enabled outside Firefox web mode', () => { + expect(canUseMonacoContextMenu(false, chromeDesktopUserAgent)).toBe(true); + }); +}); diff --git a/lib/utils/platform.ts b/lib/utils/platform.ts index b4141f40d..00b210953 100644 --- a/lib/utils/platform.ts +++ b/lib/utils/platform.ts @@ -7,7 +7,35 @@ export const isMac = isElectron export const CmdOrCtrl = isElectron && isMac ? 'Cmd' : 'Ctrl'; -export const isSafari = /^((?!chrome|android).)*safari/i.test( +export const isSafariUserAgent = (userAgent: string) => + /^((?!chrome|android).)*safari/i.test(userAgent); + +export const isFirefoxUserAgent = (userAgent: string) => + /(firefox|fxios|librewolf|iceweasel)/i.test(userAgent); + +export const isSafari = isSafariUserAgent(window.navigator.userAgent); + +export const isFirefox = isFirefoxUserAgent(window.navigator.userAgent); + +const shouldUseNativeBrowserClipboardUi = ( + useElectron: boolean, + userAgent: string +) => !useElectron && isFirefoxUserAgent(userAgent); + +export const canUseMonacoPasteShortcut = ( + useElectron: boolean, + userAgent: string +) => !shouldUseNativeBrowserClipboardUi(useElectron, userAgent); + +export const useMonacoPasteShortcut = canUseMonacoPasteShortcut( + isElectron, + window.navigator.userAgent +); + +export const canUseMonacoContextMenu = canUseMonacoPasteShortcut; + +export const useMonacoContextMenu = canUseMonacoContextMenu( + isElectron, window.navigator.userAgent );